diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..6644370b86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 + +[*.nix] +indent_size = 2 + +[*.{py,ipynb}] +indent_size = 4 +max_line_length = 100 + +[*.rs] +indent_style = space +indent_size = 4 + +[*.{ts,svelte}] +indent_size = 2 diff --git a/.envrc b/.envrc deleted file mode 100644 index 09e580571a..0000000000 --- a/.envrc +++ /dev/null @@ -1,5 +0,0 @@ -if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" -fi -use flake . -dotenv \ No newline at end of file diff --git a/.envrc.nix b/.envrc.nix index 4a6ade8151..a3f663db80 100644 --- a/.envrc.nix +++ b/.envrc.nix @@ -2,4 +2,10 @@ if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" fi use flake . +for venv in venv .venv env; do + if [[ -f "$venv/bin/activate" ]]; then + source "$venv/bin/activate" + break + fi +done dotenv_if_exists diff --git a/.envrc.venv b/.envrc.venv index a4b314c6f7..e315a030c7 100644 --- a/.envrc.venv +++ b/.envrc.venv @@ -1,2 +1,7 @@ -source env/bin/activate +for venv in venv .venv env; do + if [[ -f "$venv/bin/activate" ]]; then + source "$venv/bin/activate" + break + fi +done dotenv_if_exists diff --git a/.gitattributes b/.gitattributes index 302cb2e191..8f05eb707f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,8 +4,9 @@ # Ensure Python files always use LF for line endings. *.py text eol=lf # Treat designated file types as binary and do not alter their contents or line endings. -*.png binary -*.jpg binary +*.png filter=lfs diff=lfs merge=lfs -text binary +*.jpg filter=lfs diff=lfs merge=lfs -text binary +*.jpeg filter=lfs diff=lfs merge=lfs -text binary *.ico binary *.pdf binary # Explicit LFS tracking for test files @@ -14,3 +15,4 @@ *.mp4 filter=lfs diff=lfs merge=lfs -text binary *.mov filter=lfs diff=lfs merge=lfs -text binary *.gif filter=lfs diff=lfs merge=lfs -text binary +*.foxe filter=lfs diff=lfs merge=lfs -text binary diff --git a/.github/workflows/_docker-build-template.yml b/.github/workflows/_docker-build-template.yml index 730f4a4696..478a9bec84 100644 --- a/.github/workflows/_docker-build-template.yml +++ b/.github/workflows/_docker-build-template.yml @@ -38,19 +38,19 @@ jobs: sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/share/boost sudo rm -rf /usr/local/lib/android - + echo "=== Cleaning images from deleted branches ===" - + # Get list of all remote branches git ls-remote --heads origin | awk '{print $2}' | sed 's|refs/heads/||' > /tmp/active_branches.txt - + # Check each docker image tag against branch list docker images --format "{{.Repository}}:{{.Tag}}|{{.ID}}" | \ grep "ghcr.io/dimensionalos" | \ grep -v ":" | \ while IFS='|' read image_ref id; do tag=$(echo "$image_ref" | cut -d: -f2) - + # Skip if tag matches an active branch if grep -qx "$tag" /tmp/active_branches.txt; then echo "Branch exists: $tag - keeping $image_ref" @@ -59,15 +59,15 @@ jobs: docker rmi "$id" 2>/dev/null || true fi done - + rm -f /tmp/active_branches.txt - + USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') echo "Pre-docker-cleanup disk usage: ${USAGE}%" - + if [ $USAGE -gt 60 ]; then echo "=== Running quick cleanup (usage > 60%) ===" - + # Keep newest image per tag docker images --format "{{.Repository}}|{{.Tag}}|{{.ID}}" | \ grep "ghcr.io/dimensionalos" | \ @@ -81,10 +81,10 @@ jobs: { repo=$1; tag=$2; id=$3 repo_tag = repo ":" tag - + # Skip protected tags if (tag ~ /^(main|dev|latest)$/) next - + # Keep newest per tag, remove older duplicates if (!(repo_tag in seen_combos)) { seen_combos[repo_tag] = 1 @@ -92,28 +92,28 @@ jobs: system("docker rmi " id " 2>/dev/null || true") } }' - + docker image prune -f docker volume prune -f fi - + # Aggressive cleanup if still above 85% USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') if [ $USAGE -gt 85 ]; then echo "=== AGGRESSIVE cleanup (usage > 85%) - removing all except main/dev ===" - + # Remove ALL images except main and dev tags docker images --format "{{.Repository}}:{{.Tag}} {{.ID}}" | \ grep -E "ghcr.io/dimensionalos" | \ grep -vE ":(main|dev)$" | \ awk '{print $2}' | xargs -r docker rmi -f || true - + docker container prune -f docker volume prune -a -f docker network prune -f - docker image prune -f + docker image prune -f fi - + echo -e "post cleanup space:\n $(df -h)" - uses: docker/login-action@v3 diff --git a/.github/workflows/code-cleanup.yml b/.github/workflows/code-cleanup.yml index ddb75a90e3..48f6ea281e 100644 --- a/.github/workflows/code-cleanup.yml +++ b/.github/workflows/code-cleanup.yml @@ -1,5 +1,8 @@ name: code-cleanup -on: push +on: + push: + paths-ignore: + - '**.md' permissions: contents: write @@ -13,20 +16,21 @@ jobs: - name: Fix permissions run: | sudo chown -R $USER:$USER ${{ github.workspace }} || true - + - uses: actions/checkout@v3 - uses: actions/setup-python@v3 + - uses: astral-sh/setup-uv@v4 - name: Run pre-commit id: pre-commit-first uses: pre-commit/action@v3.0.1 continue-on-error: true - + - name: Re-run pre-commit if failed initially id: pre-commit-retry if: steps.pre-commit-first.outcome == 'failure' uses: pre-commit/action@v3.0.1 continue-on-error: false - + - name: Commit code changes uses: stefanzweifel/git-auto-commit-action@v5 with: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0c6abff68d..f9afc96de5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,7 +4,11 @@ on: branches: - main - dev + paths-ignore: + - '**.md' pull_request: + paths-ignore: + - '**.md' permissions: contents: read @@ -24,7 +28,7 @@ jobs: - name: Fix permissions run: | sudo chown -R $USER:$USER ${{ github.workspace }} || true - + - uses: actions/checkout@v4 - id: filter uses: dorny/paths-filter@v3 @@ -132,7 +136,7 @@ jobs: uses: ./.github/workflows/_docker-build-template.yml with: should-run: ${{ - needs.check-changes.result == 'success' && + needs.check-changes.result == 'success' && (needs.check-changes.outputs.dev == 'true' || (needs.ros-python.result == 'success' && (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.ros == 'true'))) }} @@ -201,6 +205,21 @@ jobs: cmd: "pytest -m lcm" dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + run-mypy: + needs: [check-changes, ros-dev] + if: always() + uses: ./.github/workflows/tests.yml + secrets: inherit + with: + should-run: ${{ + needs.check-changes.result == 'success' && + ((needs.ros-dev.result == 'success') || + (needs.ros-dev.result == 'skipped' && + needs.check-changes.outputs.tests == 'true')) + }} + cmd: "MYPYPATH=/opt/ros/humble/lib/python3.10/site-packages mypy dimos" + dev-image: ros-dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true' || needs.check-changes.outputs.ros == 'true') && needs.ros-dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + # Run module tests directly to avoid pytest forking issues # run-module-tests: # needs: [check-changes, dev] @@ -218,21 +237,20 @@ jobs: # - name: Fix permissions # run: | # sudo chown -R $USER:$USER ${{ github.workspace }} || true - # + # # - uses: actions/checkout@v4 # with: # lfs: true - # + # # - name: Configure Git LFS # run: | # git config --global --add safe.directory '*' # git lfs install # git lfs fetch # git lfs checkout - # + # # - name: Run module tests # env: # CI: "true" # run: | # /entrypoint.sh bash -c "pytest -m module" - diff --git a/.github/workflows/readme.md b/.github/workflows/readme.md index 0bc86973d8..f82ba479bb 100644 --- a/.github/workflows/readme.md +++ b/.github/workflows/readme.md @@ -12,17 +12,17 @@ https://code.visualstudio.com/docs/devcontainers/containers create personal access token (classic, not fine grained) https://github.com/settings/tokens -add permissions +add permissions - read:packages scope to download container images and read their metadata. - + and optionally, - + - write:packages scope to download and upload container images and read and write their metadata. - delete:packages scope to delete container images. more info @ https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry -login to docker via +login to docker via `sh echo TOKEN | docker login ghcr.io -u GITHUB_USER --password-stdin @@ -38,14 +38,14 @@ pull dev image (master) docker pull ghcr.io/dimensionalos/dev:latest ` -# todo +# todo Currently there is an issue with ensuring both correct docker image build ordering, and skipping unneccessary re-builds. (we need job dependancies for builds to wait to their images underneath to be built (for example py waits for ros)) by default if a parent is skipped, it's children get skipped as well, unless they have always() in their conditional. -Issue is once we put always() in the conditional, it seems that no matter what other check we put in the same conditional, job will always run. +Issue is once we put always() in the conditional, it seems that no matter what other check we put in the same conditional, job will always run. for this reason we cannot skip python (and above) builds for now. Needs review. I think we will need to write our own build dispatcher in python that calls github workflows that build images. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a94839a505..e84d7d43d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,14 +28,14 @@ jobs: # if: ${{ !inputs.should-run }} # run: | # exit 0 - + # - name: Free disk space # run: | # sudo rm -rf /opt/ghc # sudo rm -rf /usr/share/dotnet # sudo rm -rf /usr/local/share/boost # sudo rm -rf /usr/local/lib/android - + run-tests: runs-on: [self-hosted, Linux] container: @@ -47,17 +47,16 @@ jobs: steps: - uses: actions/checkout@v4 - + - name: Fix permissions run: | git config --global --add safe.directory '*' - + - name: Run tests run: | /entrypoint.sh bash -c "${{ inputs.cmd }}" - + - name: check disk space if: failure() run: | df -h - diff --git a/.gitignore b/.gitignore index 18fd575c85..0e6781f3a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# generic ignore pattern +**/*.ignore +**/*.ignore.* + .vscode/ # Ignore Python cache files @@ -8,6 +12,7 @@ __pycache__/ *venv*/ .venv*/ .ssh/ +.direnv/ # Ignore python tooling dirs *.egg-info/ @@ -49,3 +54,12 @@ yolo11n.pt # symlink one of .envrc.* if you'd like to use .envrc .claude +.direnv/ + +/logs + +*.so + +/.mypy_cache* + +*mobileclip* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67544f7f29..29d068dccf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,5 @@ default_stages: [pre-commit] +default_install_hook_types: [pre-commit, commit-msg] exclude: (dimos/models/.*)|(deprecated) repos: @@ -9,7 +10,7 @@ repos: - id: remove-crlf - id: insert-license files: \.py$ - exclude: __init__\.py$ + exclude: (__init__\.py$)|(dimos/rxpy_backpressure/) args: # use if you want to remove licences from all files # (for globally changing wording or something) @@ -19,7 +20,7 @@ repos: - --use-current-year - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.1 + rev: v0.14.3 hooks: - id: ruff-format stages: [pre-commit] @@ -33,7 +34,9 @@ repos: - id: trailing-whitespace language: python types: [text] - stages: [pre-push] + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] - id: check-json - id: check-toml - id: check-yaml @@ -41,6 +44,13 @@ repos: name: format json args: [ --autofix, --no-sort-keys ] + - repo: https://github.com/editorconfig-checker/editorconfig-checker.python + rev: 3.4.1 + hooks: + - id: editorconfig-checker + alias: ec + args: [-disable-max-line-length, -disable-indentation] + # - repo: local # hooks: # - id: mypy @@ -54,11 +64,38 @@ repos: - repo: local hooks: + - id: uv-lock-check + name: Check uv.lock is up-to-date + entry: uv lock --check + language: system + files: ^pyproject\.toml$ + pass_filenames: false + - id: lfs_check name: LFS data always_run: true pass_filenames: false - entry: bin/lfs_check + entry: bin/hooks/lfs_check language: script - + - id: largefiles-check + name: Large files check + always_run: true + pass_filenames: false + entry: python bin/hooks/largefiles_check + language: python + additional_dependencies: ['tomli'] + + - id: doclinks + name: Doclinks + always_run: true + pass_filenames: false + entry: python -m dimos.utils.docs.doclinks docs/ + language: system + files: ^docs/.*\.md$ + + - id: filter-commit-message + name: Filter generated signatures from commit message + entry: python bin/hooks/filter_commit_message.py + language: python + stages: [commit-msg] diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..e4fba21835 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.style.yapf b/.style.yapf index e1d4f5f627..b8d6fb374a 100644 --- a/.style.yapf +++ b/.style.yapf @@ -1,3 +1,3 @@ [style] based_on_style = google - column_limit = 80 \ No newline at end of file + column_limit = 80 diff --git a/AUTONOMY_STACK_README.md b/AUTONOMY_STACK_README.md deleted file mode 100644 index 70eff131ce..0000000000 --- a/AUTONOMY_STACK_README.md +++ /dev/null @@ -1,284 +0,0 @@ -# Autonomy Stack API Documentation - -## Prerequisites - -- Ubuntu 24.04 -- [ROS 2 Jazzy Installation](https://docs.ros.org/en/jazzy/Installation.html) - -Add the following line to your `~/.bashrc` to source the ROS 2 Jazzy setup script automatically: - -``` echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc``` - -## MID360 Ethernet Configuration (skip for sim) - -### Step 1: Configure Network Interface - -1. Open Network Settings in Ubuntu -2. Find your Ethernet connection to the MID360 -3. Click the gear icon to edit settings -4. Go to IPv4 tab -5. Change Method from "Automatic (DHCP)" to "Manual" -6. Add the following settings: - - **Address**: 192.168.1.5 - - **Netmask**: 255.255.255.0 - - **Gateway**: 192.168.1.1 -7. Click "Apply" - -### Step 2: Configure MID360 IP in JSON - -1. Find your MID360 serial number (on sticker under QR code) -2. Note the last 2 digits (e.g., if serial ends in 89, use 189) -3. Edit the configuration file: - -```bash -cd ~/autonomy_stack_mecanum_wheel_platform -nano src/utilities/livox_ros_driver2/config/MID360_config.json -``` - -4. Update line 28 with your IP (192.168.1.1xx where xx = last 2 digits): - -```json -"ip" : "192.168.1.1xx", -``` - -5. Save and exit - -### Step 3: Verify Connection - -```bash -ping 192.168.1.1xx # Replace xx with your last 2 digits -``` - -## Robot Configuration - -### Setting Robot Type - -The system supports different robot configurations. Set the `ROBOT_CONFIG_PATH` environment variable to specify which robot configuration to use: - -```bash -# For Unitree G1 (default if not set) -export ROBOT_CONFIG_PATH="unitree/unitree_g1" - -# Add to ~/.bashrc to make permanent -echo 'export ROBOT_CONFIG_PATH="unitree/unitree_g1"' >> ~/.bashrc -``` - -Available robot configurations: -- `unitree/unitree_g1` - Unitree G1 robot (default) -- Add your custom robot configs in `src/base_autonomy/local_planner/config/` - -## Build the system - -You must do this every you make a code change, this is not Python - -```colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release``` - -## System Launch - -### Simulation Mode - -```bash -cd ~/autonomy_stack_mecanum_wheel_platform - -# Base autonomy only -./system_simulation.sh - -# With route planner -./system_simulation_with_route_planner.sh - -# With exploration planner -./system_simulation_with_exploration_planner.sh -``` - -### Real Robot Mode - -```bash -cd ~/autonomy_stack_mecanum_wheel_platform - -# Base autonomy only -./system_real_robot.sh - -# With route planner -./system_real_robot_with_route_planner.sh - -# With exploration planner -./system_real_robot_with_exploration_planner.sh -``` - -## Quick Troubleshooting - -- **Cannot ping MID360**: Check Ethernet cable and network settings -- **SLAM drift**: Press clear-terrain-map button on joystick controller -- **Joystick not recognized**: Unplug and replug USB dongle - - -## ROS Topics - -### Input Topics (Commands) - -| Topic | Type | Description | -|-------|------|-------------| -| `/way_point` | `geometry_msgs/PointStamped` | Send navigation goal (position only) | -| `/goal_pose` | `geometry_msgs/PoseStamped` | Send goal with orientation | -| `/cancel_goal` | `std_msgs/Bool` | Cancel current goal (data: true) | -| `/joy` | `sensor_msgs/Joy` | Joystick input | -| `/stop` | `std_msgs/Int8` | Soft Stop (2=stop all commmand, 0 = release) | -| `/navigation_boundary` | `geometry_msgs/PolygonStamped` | Set navigation boundaries | -| `/added_obstacles` | `sensor_msgs/PointCloud2` | Virtual obstacles | - -### Output Topics (Status) - -| Topic | Type | Description | -|-------|------|-------------| -| `/state_estimation` | `nav_msgs/Odometry` | Robot pose from SLAM | -| `/registered_scan` | `sensor_msgs/PointCloud2` | Aligned lidar point cloud | -| `/terrain_map` | `sensor_msgs/PointCloud2` | Local terrain map | -| `/terrain_map_ext` | `sensor_msgs/PointCloud2` | Extended terrain map | -| `/path` | `nav_msgs/Path` | Local path being followed | -| `/cmd_vel` | `geometry_msgs/Twist` | Velocity commands to motors | -| `/goal_reached` | `std_msgs/Bool` | True when goal reached, false when cancelled/new goal | - -### Map Topics - -| Topic | Type | Description | -|-------|------|-------------| -| `/overall_map` | `sensor_msgs/PointCloud2` | Global map (only in sim)| -| `/registered_scan` | `sensor_msgs/PointCloud2` | Current scan in map frame | -| `/terrain_map` | `sensor_msgs/PointCloud2` | Local obstacle map | - -## Usage Examples - -### Send Goal -```bash -ros2 topic pub /way_point geometry_msgs/msg/PointStamped "{ - header: {frame_id: 'map'}, - point: {x: 5.0, y: 3.0, z: 0.0} -}" --once -``` - -### Cancel Goal -```bash -ros2 topic pub /cancel_goal std_msgs/msg/Bool "data: true" --once -``` - -### Monitor Robot State -```bash -ros2 topic echo /state_estimation -``` - -## Configuration Parameters - -### Vehicle Parameters (`localPlanner`) - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `vehicleLength` | 0.5 | Robot length (m) | -| `vehicleWidth` | 0.5 | Robot width (m) | -| `maxSpeed` | 0.875 | Maximum speed (m/s) | -| `autonomySpeed` | 0.875 | Autonomous mode speed (m/s) | - -### Goal Tolerance Parameters - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `goalReachedThreshold` | 0.3-0.5 | Distance to consider goal reached (m) | -| `goalClearRange` | 0.35-0.6 | Extra clearance around goal (m) | -| `goalBehindRange` | 0.35-0.8 | Stop pursuing if goal behind within this distance (m) | -| `omniDirGoalThre` | 1.0 | Distance for omnidirectional approach (m) | - -### Obstacle Avoidance - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `obstacleHeightThre` | 0.1-0.2 | Height threshold for obstacles (m) | -| `adjacentRange` | 3.5 | Sensor range for planning (m) | -| `minRelZ` | -0.4 | Minimum relative height to consider (m) | -| `maxRelZ` | 0.3 | Maximum relative height to consider (m) | - -### Path Planning - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `pathScale` | 0.875 | Path resolution scale | -| `minPathScale` | 0.675 | Minimum path scale when blocked | -| `minPathRange` | 0.8 | Minimum planning range (m) | -| `dirThre` | 90.0 | Direction threshold (degrees) | - -### Control Parameters (`pathFollower`) - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `lookAheadDis` | 0.5 | Look-ahead distance (m) | -| `maxAccel` | 2.0 | Maximum acceleration (m/s²) | -| `slowDwnDisThre` | 0.875 | Slow down distance threshold (m) | - -### SLAM Blind Zones (`feature_extraction_node`) - -| Parameter | Mecanum | Description | -|-----------|---------|-------------| -| `blindFront` | 0.1 | Front blind zone (m) | -| `blindBack` | -0.2 | Back blind zone (m) | -| `blindLeft` | 0.1 | Left blind zone (m) | -| `blindRight` | -0.1 | Right blind zone (m) | -| `blindDiskRadius` | 0.4 | Cylindrical blind zone radius (m) | - -## Operating Modes - -### Mode Control -- **Joystick L2**: Hold for autonomy mode -- **Joystick R2**: Hold to disable obstacle checking - -### Speed Control -The robot automatically adjusts speed based on: -1. Obstacle proximity -2. Path complexity -3. Goal distance - -## Tuning Guide - -### For Tighter Navigation -- Decrease `goalReachedThreshold` (e.g., 0.2) -- Decrease `goalClearRange` (e.g., 0.3) -- Decrease `vehicleLength/Width` slightly - -### For Smoother Navigation -- Increase `goalReachedThreshold` (e.g., 0.5) -- Increase `lookAheadDis` (e.g., 0.7) -- Decrease `maxAccel` (e.g., 1.5) - -### For Aggressive Obstacle Avoidance -- Increase `obstacleHeightThre` (e.g., 0.15) -- Increase `adjacentRange` (e.g., 4.0) -- Increase blind zone parameters - -## Common Issues - -### Robot Oscillates at Goal -- Increase `goalReachedThreshold` -- Increase `goalBehindRange` - -### Robot Stops Too Far from Goal -- Decrease `goalReachedThreshold` -- Decrease `goalClearRange` - -### Robot Hits Low Obstacles -- Decrease `obstacleHeightThre` -- Adjust `minRelZ` to include lower points - -## SLAM Configuration - -### Localization Mode -Set in `livox_mid360.yaml`: -```yaml -local_mode: true -init_x: 0.0 -init_y: 0.0 -init_yaw: 0.0 -``` - -### Mapping Performance -```yaml -mapping_line_resolution: 0.1 # Decrease for higher quality -mapping_plane_resolution: 0.2 # Decrease for higher quality -max_iterations: 5 # Increase for better accuracy -``` \ No newline at end of file diff --git a/LICENSE b/LICENSE index b06471524c..5e2927e3ad 100644 --- a/LICENSE +++ b/LICENSE @@ -14,4 +14,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000..59df25c071 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,21 @@ +global-exclude *.pyc +global-exclude __pycache__ +global-exclude .DS_Store + +# Exclude web development directories +recursive-exclude dimos/web/command-center-extension * +recursive-exclude dimos/web/websocket_vis/node_modules * +recursive-exclude dimos/agents/fixtures * +recursive-exclude dimos/mapping/google_maps/fixtures * +recursive-exclude dimos/web/dimos_interface * + +# Exclude development files +exclude .gitignore +exclude .gitattributes +prune .git +prune .github +prune .mypy_cache +prune .pytest_cache +prune .ruff_cache +prune .vscode +prune dimos/web/command-center-extension diff --git a/README.md b/README.md index 1db93e9887..9a74d63aa7 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ ## What is Dimensional? -Dimensional is an open-source framework for building agentive generalist robots. DimOS allows off-the-shelf Agents to call tools/functions and read sensor/state data directly from ROS. +Dimensional is an open-source framework for building agentive generalist robots. DimOS allows off-the-shelf Agents to call tools/functions and read sensor/state data directly from ROS. -The framework enables neurosymbolic orchestration of Agents as generalized spatial reasoners/planners and Robot state/action primitives as functions. +The framework enables neurosymbolic orchestration of Agents as generalized spatial reasoners/planners and Robot state/action primitives as functions. -The result: cross-embodied *"Dimensional Applications"* exceptional at generalization and robust at symbolic action execution. +The result: cross-embodied *"Dimensional Applications"* exceptional at generalization and robust at symbolic action execution. ## DIMOS x Unitree Go2 (OUT OF DATE) @@ -38,22 +38,38 @@ We are shipping a first look at the DIMOS x Unitree Go2 integration, allowing fo - Lidar / PointCloud primitives - Any other generic Unitree ROS2 topics -### Features +### Features - **DimOS Agents** - Agent() classes with planning, spatial reasoning, and Robot.Skill() function calling abilities. - Integrate with any off-the-shelf hosted or local model: OpenAIAgent, ClaudeAgent, GeminiAgent 🚧, DeepSeekAgent 🚧, HuggingFaceRemoteAgent, HuggingFaceLocalAgent, etc. - - Modular agent architecture for easy extensibility and chaining of Agent output --> Subagents input. + - Modular agent architecture for easy extensibility and chaining of Agent output --> Subagents input. - Agent spatial / language memory for location grounded reasoning and recall. - **DimOS Infrastructure** - A reactive data streaming architecture using RxPY to manage real-time video (or other sensor input), outbound commands, and inbound robot state between the DimOS interface, Agents, and ROS2. - - Robot Command Queue to handle complex multi-step actions to Robot. - - Simulation bindings (Genesis, Isaacsim, etc.) to test your agentive application before deploying to a physical robot. + - Robot Command Queue to handle complex multi-step actions to Robot. + - Simulation bindings (Genesis, Isaacsim, etc.) to test your agentive application before deploying to a physical robot. - **DimOS Interface / Development Tools** - Local development interface to control your robot, orchestrate agents, visualize camera/lidar streams, and debug your dimensional agentive application. +## MacOS Installation + +```sh +# Install Nix +curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install + +# clone the repository +git clone --branch dev --single-branch https://github.com/dimensionalOS/dimos.git + +# setup the environment (follow the prompts after nix develop) +cd dimos +nix develop + +# You should be able to follow the instructions below as well for a more manual installation +``` + --- ## Python Installation Tested on Ubuntu 22.04/24.04 @@ -83,43 +99,43 @@ pip install torch==2.0.1 torchvision torchaudio --index-url https://download.pyt #### Install dependencies ```bash # CPU only (reccomended to attempt first) -pip install -e .[cpu,dev] +pip install -e '.[cpu,dev]' # CUDA install -pip install -e .[cuda,dev] +pip install -e '.[cuda,dev]' # Copy and configure environment variables cp default.env .env ``` -#### Test the install -```bash +#### Test the install +```bash pytest -s dimos/ ``` #### Test Dimensional with a replay UnitreeGo2 stream (no robot required) ```bash -CONNECTION_TYPE=replay python dimos/robot/unitree_webrtc/unitree_go2.py +dimos --replay run unitree-go2 ``` #### Test Dimensional with a simulated UnitreeGo2 in MuJoCo (no robot required) ```bash -pip install -e .[sim] +pip install -e '.[sim]' export DISPLAY=:1 # Or DISPLAY=:0 if getting GLFW/OpenGL X11 errors -CONNECTION_TYPE=mujoco python dimos/robot/unitree_webrtc/unitree_go2.py +dimos --simulation run unitree-go2 ``` #### Test Dimensional with a real UnitreeGo2 over WebRTC ```bash export ROBOT_IP=192.168.X.XXX # Add the robot IP address -python dimos/robot/unitree_webrtc/unitree_go2.py +dimos run unitree-go2 ``` #### Test Dimensional with a real UnitreeGo2 running Agents *OpenAI / Alibaba keys required* ```bash export ROBOT_IP=192.168.X.XXX # Add the robot IP address -python dimos/robot/unitree_webrtc/run_agents2.py +dimos run unitree-go2-agentic ``` --- @@ -127,7 +143,7 @@ python dimos/robot/unitree_webrtc/run_agents2.py Full functionality will require API keys for the following: -Requirements: +Requirements: - OpenAI API key (required for all LLMAgents due to OpenAIEmbeddings) - Claude API key (required for ClaudeAgent) - Alibaba API key (required for Navigation skills) @@ -139,10 +155,10 @@ export CLAUDE_API_KEY= export ALIBABA_API_KEY= ``` -### ROS2 Unitree Go2 SDK Installation +### ROS2 Unitree Go2 SDK Installation #### System Requirements -- Ubuntu 22.04 +- Ubuntu 22.04 - ROS2 Distros: Iron, Humble, Rolling See [Unitree Go2 ROS2 SDK](https://github.com/dimensionalOS/go2_ros2_sdk) for additional installation instructions. @@ -167,7 +183,7 @@ colcon build ### Run the test application -#### ROS2 Terminal: +#### ROS2 Terminal: ```bash # Change path to your Go2 ROS2 SDK installation source /ros2_ws/install/setup.bash @@ -179,7 +195,7 @@ ros2 launch go2_robot_sdk robot.launch.py ``` -#### Python Terminal: +#### Python Terminal: ```bash # Change path to your Go2 ROS2 SDK installation source /ros2_ws/install/setup.bash @@ -189,7 +205,7 @@ python tests/run.py #### DimOS Interface: ```bash cd dimos/web/dimos_interface -yarn install +yarn install yarn dev # you may need to run sudo if previously built via Docker ``` @@ -224,7 +240,7 @@ yarn dev # you may need to run sudo if previously built via Docker │ ā”œā”€ā”€ types/ # Type definitions and interfaces │ ā”œā”€ā”€ utils/ # Utility functions and helpers │ └── web/ # DimOS development interface and API -│ ā”œā”€ā”€ dimos_interface/ # DimOS web interface +│ ā”œā”€ā”€ dimos_interface/ # DimOS web interface │ └── websocket_vis/ # Websocket visualizations ā”œā”€ā”€ tests/ # Test files │ ā”œā”€ā”€ genesissim/ # Genesis simulator tests @@ -244,7 +260,7 @@ yarn dev # you may need to run sudo if previously built via Docker from dimos.robot.unitree.unitree_go2 import UnitreeGo2 from dimos.robot.unitree.unitree_skills import MyUnitreeSkills from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.agents.agent import OpenAIAgent +from dimos.agents_deprecated.agent import OpenAIAgent # Initialize robot robot = UnitreeGo2(ip=robot_ip, @@ -267,11 +283,11 @@ while True: # keep process running ### DimOS Application with Agent chaining (OUT OF DATE) -Let's build a simple DimOS application with Agent chaining. We define a ```planner``` as a ```PlanningAgent``` that takes in user input to devise a complex multi-step plan. This plan is passed step-by-step to an ```executor``` agent that can queue ```AbstractRobotSkill``` commands to the ```ROSCommandQueue```. +Let's build a simple DimOS application with Agent chaining. We define a ```planner``` as a ```PlanningAgent``` that takes in user input to devise a complex multi-step plan. This plan is passed step-by-step to an ```executor``` agent that can queue ```AbstractRobotSkill``` commands to the ```ROSCommandQueue```. -Our reactive Pub/Sub data streaming architecture allows for chaining of ```Agent_0 --> Agent_1 --> ... --> Agent_n``` via the ```input_query_stream``` parameter in each which takes an ```Observable``` input from the previous Agent in the chain. +Our reactive Pub/Sub data streaming architecture allows for chaining of ```Agent_0 --> Agent_1 --> ... --> Agent_n``` via the ```input_query_stream``` parameter in each which takes an ```Observable``` input from the previous Agent in the chain. -**Via this method you can chain together any number of Agents() to create complex dimensional applications.** +**Via this method you can chain together any number of Agents() to create complex dimensional applications.** ```python @@ -371,8 +387,8 @@ First, define your skill. For instance, a `GreeterSkill` that can deliver a conf ```python class GreeterSkill(AbstractSkill): """Greats the user with a friendly message.""" # Gives the agent better context for understanding (the more detailed the better). - - greeting: str = Field(..., description="The greating message to display.") # The field needed for the calling of the function. Your agent will also pull from the description here to gain better context. + + greeting: str = Field(..., description="The greating message to display.") # The field needed for the calling of the function. Your agent will also pull from the description here to gain better context. def __init__(self, greeting_message: Optional[str] = None, **data): super().__init__(**data) @@ -410,19 +426,19 @@ Define the SkillLibrary and any skills it will manage in its collection: ```python class MovementSkillsLibrary(SkillLibrary): """A specialized skill library containing movement and navigation related skills.""" - + def __init__(self, robot=None): super().__init__() self._robot = robot - + def initialize_skills(self, robot=None): """Initialize all movement skills with the robot instance.""" if robot: self._robot = robot - + if not self._robot: raise ValueError("Robot instance is required to initialize skills") - + # Initialize with all movement-related skills self.add(Navigate(robot=self._robot)) self.add(NavigateToGoal(robot=self._robot)) @@ -431,7 +447,7 @@ class MovementSkillsLibrary(SkillLibrary): self.add(GetPose(robot=self._robot)) # Position tracking skill ``` -Note the addision of initialized skills added to this collection above. +Note the addision of initialized skills added to this collection above. Proceed to use this skill library in an Agent: @@ -450,9 +466,9 @@ performing_agent = OpenAIAgent( ``` ### Unitree Test Files -- **`tests/run_go2_ros.py`**: Tests `UnitreeROSControl(ROSControl)` initialization in `UnitreeGo2(Robot)` via direct function calls `robot.move()` and `robot.webrtc_req()` +- **`tests/run_go2_ros.py`**: Tests `UnitreeROSControl(ROSControl)` initialization in `UnitreeGo2(Robot)` via direct function calls `robot.move()` and `robot.webrtc_req()` - **`tests/simple_agent_test.py`**: Tests a simple zero-shot class `OpenAIAgent` example -- **`tests/unitree/test_webrtc_queue.py`**: Tests `ROSCommandQueue` via a 20 back-to-back WebRTC requests to the robot +- **`tests/unitree/test_webrtc_queue.py`**: Tests `ROSCommandQueue` via a 20 back-to-back WebRTC requests to the robot - **`tests/test_planning_agent_web_interface.py`**: Tests a simple two-stage `PlanningAgent` chained to an `ExecutionAgent` with backend FastAPI interface. - **`tests/test_unitree_agent_queries_fastapi.py`**: Tests a zero-shot `ExecutionAgent` with backend FastAPI interface. @@ -470,11 +486,11 @@ This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENS ## Acknowledgments -Huge thanks to! -- The Roboverse Community and their unitree-specific help. Check out their [Discord](https://discord.gg/HEXNMCNhEh). -- @abizovnuralem for his work on the [Unitree Go2 ROS2 SDK](https://github.com/abizovnuralem/go2_ros2_sdk) we integrate with for DimOS. -- @legion1581 for his work on the [Unitree Go2 WebRTC Connect](https://github.com/legion1581/go2_webrtc_connect) from which we've pulled the ```Go2WebRTCConnection``` class and other types for seamless WebRTC-only integration with DimOS. -- @tfoldi for the webrtc_req integration via Unitree Go2 ROS2 SDK, which allows for seamless usage of Unitree WebRTC control primitives with DimOS. +Huge thanks to! +- The Roboverse Community and their unitree-specific help. Check out their [Discord](https://discord.gg/HEXNMCNhEh). +- @abizovnuralem for his work on the [Unitree Go2 ROS2 SDK](https://github.com/abizovnuralem/go2_ros2_sdk) we integrate with for DimOS. +- @legion1581 for his work on the [Unitree Go2 WebRTC Connect](https://github.com/legion1581/go2_webrtc_connect) from which we've pulled the ```Go2WebRTCConnection``` class and other types for seamless WebRTC-only integration with DimOS. +- @tfoldi for the webrtc_req integration via Unitree Go2 ROS2 SDK, which allows for seamless usage of Unitree WebRTC control primitives with DimOS. ## Contact @@ -482,6 +498,5 @@ Huge thanks to! - Email: [build@dimensionalOS.com](mailto:build@dimensionalOS.com) ## Known Issues -- Agent() failure to execute Nav2 action primitives (move, reverse, spinLeft, spinRight) is almost always due to the internal ROS2 collision avoidance, which will sometimes incorrectly display obstacles or be overly sensitive. Look for ```[behavior_server]: Collision Ahead - Exiting DriveOnHeading``` in the ROS logs. Reccomend restarting ROS2 or moving robot from objects to resolve. +- Agent() failure to execute Nav2 action primitives (move, reverse, spinLeft, spinRight) is almost always due to the internal ROS2 collision avoidance, which will sometimes incorrectly display obstacles or be overly sensitive. Look for ```[behavior_server]: Collision Ahead - Exiting DriveOnHeading``` in the ROS logs. Reccomend restarting ROS2 or moving robot from objects to resolve. - ```docker-compose up --build``` does not fully initialize the ROS2 environment due to ```std::bad_alloc``` errors. This will occur during continuous docker development if the ```docker-compose down``` is not run consistently before rebuilding and/or you are on a machine with less RAM, as ROS is very memory intensive. Reccomend running to clear your docker cache/images/containers with ```docker system prune``` and rebuild. - diff --git a/README_installation.md b/README_installation.md new file mode 100644 index 0000000000..ccdcb1a550 --- /dev/null +++ b/README_installation.md @@ -0,0 +1,136 @@ +# DimOS + +## Installation + +Clone the repo: + +```bash +git clone -b main --single-branch git@github.com:dimensionalOS/dimos.git +cd dimos +``` + +### System dependencies + +Tested on Ubuntu 22.04/24.04. + +```bash +sudo apt update +sudo apt install git-lfs python3-venv python3-pyaudio portaudio19-dev libturbojpeg0-dev +``` + +### Python dependencies + +Install `uv` by [following their instructions](https://docs.astral.sh/uv/getting-started/installation/) or just run: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +Install Python dependencies: + +```bash +uv sync +``` + +Depending on what you want to test you might want to install more optional dependencies as well (recommended): + +```bash +uv sync --extra dev --extra cpu --extra sim --extra drone +``` + +### Install Foxglove Studio (robot visualization and control) + +> **Note:** This will be obsolete once we finish our migration to open source [Rerun](https://rerun.io/). + +Download and install [Foxglove Studio](https://foxglove.dev/download): + +```bash +wget https://get.foxglove.dev/desktop/latest/foxglove-studio-latest-linux-amd64.deb +sudo apt install ./foxglove-studio-*.deb +``` + +[Register an account](https://app.foxglove.dev/signup) to use it. + +Open Foxglove Studio: + +```bash +foxglove-studio +``` + +To connect and load our dashboard: + +1. Click on "Open connection" +2. In the popup window, leave the WebSocket URL as `ws://localhost:8765` and click "Open" +3. In the top right, click on the "Default" dropdown, then "Import from file..." +4. Navigate to the `dimos` repo and select `assets/foxglove_dashboards/unitree.json` + +### Test the install + +Run the Python tests: + +```bash +uv run pytest dimos +``` + +They should all pass in about 3 minutes. + +### Test a robot replay + +Run the system by playing back recorded data from a robot (the replay data is automatically downloaded via Git LFS): + +```bash +uv run dimos --replay run unitree-go2-basic +``` + +You can visualize the robot data in Foxglove Studio. + +### Run a simulation + +```bash +uv run dimos --simulation run unitree-go2-basic +``` + +This will open a MuJoCo simulation window. You can also visualize data in Foxglove. + +If you want to also teleoperate the simulated robot run: + +```bash +uv run dimos --simulation run unitree-go2-basic --extra-module keyboard_teleop +``` + +This will also open a Keyboard Teleop window. Focus on the window and use WASD to control the robot. + +### Command center + +You can also control the robot from the `command-center` extension to Foxglove. + +First, pull the LFS file: + +```bash +git lfs pull --include="assets/dimensional.command-center-extension-0.0.1.foxe" +``` + +To install it, drag that file over the Foxglove Studio window. The extension will be installed automatically. Then, click on the "Add panel" icon on the top right and add "command-center". + +You can now click on the map to give it a travel goal, or click on "Start Keyboard Control" to teleoperate it. + +### Using `dimos` in your code + +If you want to use dimos in your own project (not the cloned repo), you can install it as a dependency: + +```bash +uv add dimos +``` + +Note, a few dependencies do not have PyPI packages and need to be installed from their Git repositories. These are only required for specific features: + +- **CLIP** and **detectron2**: Required for the Detic open-vocabulary object detector +- **contact_graspnet_pytorch**: Required for robotic grasp prediction + +You can install them with: + +```bash +uv add git+https://github.com/openai/CLIP.git +uv add git+https://github.com/dimensionalOS/contact_graspnet_pytorch.git +uv add git+https://github.com/facebookresearch/detectron2.git +``` diff --git a/assets/agent/prompt.txt b/assets/agent/prompt.txt deleted file mode 100644 index f38c13eb13..0000000000 --- a/assets/agent/prompt.txt +++ /dev/null @@ -1,111 +0,0 @@ -You are Daneel, an advanced AI agent created by the Dimensional team to control and operate the Unitree Go2 quadraped robot with a carrying case on your back. Your purpose is to assist humans by perceiving, understanding, and navigating physical environments while providing helpful interactions and completing tasks. - -CORE CAPABILITIES: - -Interaction with humans: -1. If asked to drop something off for someone, you can announce yourself to the person you are delivering to, wait 5 seconds, and then continue with your task. -2. If asked to pick up something, you can ask for help from the person you are picking up from, wait for them to respond, and then continue with your task. -3. If a human accidentally seems to call you "daniel" or something similar, don't worry about it or acknowledge it, as its due to the speech to text transcription being inaccurate. -4. When greeted, respond with what you are, Daneel, an AI agent trained to operate autonomously in physical space. - - -You operate in an robot agent loop, iteratively completing tasks through these steps: -1. Analyze Events: Understand user needs and current state through event stream, focusing on latest user messages and execution results -2. Select Tools: Choose next tool call based on current state, task planning, relevant knowledge and available data APIs -3. Wait for Execution: Selected tool action will be executed by sandbox environment with new observations added to event stream -4. Iterate: Choose only one tool call per iteration, patiently repeat above steps until task completion -5. Killing: Kill skills when necessary with KillSkill. When asked to stop any skill or task, use KillSkill to stop it. - -SPATIAL UNDERSTANDING & MEMORY: -- You constantly are appending to your SpatialMemory, storing visual and positional data for future reference -- You can query your spatial memory using navigation related skills to find previously visited locations based on natural language descriptions -- You maintain persistent spatial knowledge across sessions in a vector database (ChromaDB) -- You can record specific locations to your SavedRobotLocations using GetPose to create landmarks that can be revisited - -PERCEPTION & TEMPORAL AWARENESS: -- You can perceive the world through multiple sensory streams (video, audio, positional data) -- You maintain awareness of what has happened over time, building a temporal model of your environment -- You can identify and respond to changes in your surroundings -- You can recognize and track humans and objects in your field of view - -NAVIGATION & MOVEMENT: -- You can navigate to semantically described locations using NavigateWithText (e.g., "go to the kitchen") -- You can navigate to visually identified objects using NavigateWithText (e.g., "go to the red chair") -- You can follow humans through complex environments using FollowHuman -- You can perform various body movements and gestures (sit, stand, dance, etc.) -- You can stop any navigation process that is currently running using KillSkill - - -Saved Robot Locations: -- LOCATION_NAME: Position (X, Y, Z), Rotation (X, Y, Z) - -***ALWAYS CHECK FIRST if you can find a navigation query in the Saved Robot Locations before running the NavigateWithText tool call. If a saved location is found, get there with NavigateToGoal.*** - -***Don't use object detections for navigating to an object, ALWAYS run NavigateWithText. Only use object detections if NavigateWithText fails*** - -***When running NavigateWithText, set skip_visual_search flag to TRUE if the query is a general location such as kitchen or office, if it fails, then run without this flag*** - -***When navigating to an object not in current object detected, run NavigateWithText, DO NOT EXPLORE with raw move commands!!!*** - -PLANNING & REASONING: -- You can develop both short-term and long-term plans to achieve complex goals -- You can reason about spatial relationships and plan efficient navigation paths -- You can adapt plans when encountering obstacles or changes in the environment -- You can combine multiple skills in sequence to accomplish multi-step tasks - -COMMUNICATION: -- You can listen to human instructions using speech recognition -- You can respond verbally using the Speak skill with natural-sounding speech -- You maintain contextual awareness in conversations -- You provide clear progress updates during task execution - -ADAPTABILITY: -- You can generalize your understanding to new, previously unseen environments -- You can apply learned skills to novel situations -- You can adjust your behavior based on environmental feedback -- You actively build and refine your knowledge of the world through exploration - -INTERACTION GUIDELINES: - -1. UNDERSTANDING USER REQUESTS - - Parse user instructions carefully to identify the intended goal - - Consider both explicit requests and implicit needs - - Ask clarifying questions when user intent is ambiguous - -2. SKILL SELECTION AND EXECUTION - - Choose the most appropriate skill(s) for each task - - Provide all required parameters with correct values and types - - Execute skills in a logical sequence when multi-step actions are needed - - Monitor skill execution and handle any failures gracefully - -3. SPATIAL REASONING - - Leverage your spatial memory to navigate efficiently - - Build new spatial memories when exploring unfamiliar areas - - Use landmark-based navigation when possible - - Combine semantic and metric mapping for robust localization - -4. SAFETY AND ETHICS - - Prioritize human safety in all actions - - Respect privacy and personal boundaries - - Avoid actions that could damage the environment or the robot - - Be transparent about your capabilities and limitations - -5. COMMUNICATION STYLE - - Be concise but informative in your responses - - Provide clear status updates during extended tasks - - Use appropriate terminology based on the user's expertise level - - Maintain a helpful, supportive, and respectful tone - - Respond with the Speak skill after EVERY QUERY to inform the user of your actions - - When speaking be terse and as concise as possible with a sentence or so, as you would if responding conversationally - -When responding to users: -1. First, acknowledge and confirm your understanding of their request -2. Select and execute the appropriate skill(s) using exact function names and proper parameters -3. Provide meaningful feedback about the outcome of your actions -4. Suggest next steps or additional information when relevant - -Example: If a user asks "Can you find the kitchen?", you would: -1. Acknowledge: "I'll help you find the kitchen." -2. Execute: Call the Navigate skill with query="kitchen" -3. Feedback: Report success or failure of navigation attempt -4. Next steps: Offer to take further actions once at the kitchen location \ No newline at end of file diff --git a/assets/agent/prompt_agents2.txt b/assets/agent/prompt_agents2.txt deleted file mode 100644 index e0a47b553e..0000000000 --- a/assets/agent/prompt_agents2.txt +++ /dev/null @@ -1,103 +0,0 @@ -You are Daneel, an advanced AI agent created by the Dimensional team to control and operate the Unitree Go2 quadraped robot with a carrying case on your back. Your purpose is to assist humans by perceiving, understanding, and navigating physical environments while providing helpful interactions and completing tasks. - -CORE CAPABILITIES: - -Interaction with humans: -1. If asked to drop something off for someone, you can announce yourself to the person you are delivering to, wait 5 seconds, and then continue with your task. -2. If asked to pick up something, you can ask for help from the person you are picking up from, wait for them to respond, and then continue with your task. -3. If a human accidentally seems to call you "daniel" or something similar, don't worry about it or acknowledge it, as its due to the speech to text transcription being inaccurate. -4. When greeted, respond with what you are, Daneel, an AI agent trained to operate autonomously in physical space. -5. Be helpful. This means being proactive and comunicative. - - -You operate in an robot agent loop, iteratively completing tasks through these steps: -1. Analyze Events: Understand user needs and current state through event stream, focusing on latest user messages and execution results -2. Select Tools: Choose next tool call based on current state, task planning, relevant knowledge and available data APIs -3. Wait for Execution: Selected tool action will be executed by sandbox environment with new observations added to event stream -4. Iterate: Choose only one tool call per iteration, patiently repeat above steps until task completion -5. Killing: Kill skills when necessary with KillSkill. When asked to stop any skill or task, use KillSkill to stop it. - -SPATIAL UNDERSTANDING & MEMORY: -- You constantly are appending to your spatial memory, storing visual and positional data for future reference. You also have things from the past stored in your spatial memory. -- You can query your spatial memory using navigation related skills to find previously visited locations based on natural language descriptions -- You maintain persistent spatial knowledge across sessions in a vector database (ChromaDB) -- You can record specific locations using the tool called `tag_location_in_spatial_memory(location_name='label')`. This creates landmarks that can be revisited. If someone says "what do you think about this bathroom?" you know from context that you are now in the bathroom and can tag it as "bathroom". If someone says "this is where I work out" you can tag it as "exercise location". -- For local area information use the `street_map_query` skill. Example: `street_map_query('Where is a large park nearby?')` - -PERCEPTION & TEMPORAL AWARENESS: -- You can perceive the world through multiple sensory streams (video, audio, positional data) -- You maintain awareness of what has happened over time, building a temporal model of your environment -- You can identify and respond to changes in your surroundings -- You can recognize and track humans and objects in your field of view - -NAVIGATION & MOVEMENT: -- You can navigate to semantically described locations using `navigate_with_text` (e.g., "go to the kitchen") -- You can navigate to visually identified objects using `navigate_with_text` (e.g., "go to the red chair") -- You can follow humans through complex environments using `follow_human` -- You can perform various body movements and gestures (sit, stand, dance, etc.) -- You can stop any navigation process that is currently running using `stop_movement` -- If you are told to go to a location use `navigate_with_text()` -- If you want to explore the environment and go to places you haven't been before you can call the 'start_exploration` tool - -PLANNING & REASONING: -- You can develop both short-term and long-term plans to achieve complex goals -- You can reason about spatial relationships and plan efficient navigation paths -- You can adapt plans when encountering obstacles or changes in the environment -- You can combine multiple skills in sequence to accomplish multi-step tasks - -COMMUNICATION: -- You can listen to human instructions using speech recognition -- You can respond verbally using the `speak_aloud` skill with natural-sounding speech -- You maintain contextual awareness in conversations -- You provide clear progress updates during task execution but always be concise. Never be verbose! - -ADAPTABILITY: -- You can generalize your understanding to new, previously unseen environments -- You can apply learned skills to novel situations -- You can adjust your behavior based on environmental feedback -- You actively build and refine your knowledge of the world through exploration - -INTERACTION GUIDELINES: - -1. UNDERSTANDING USER REQUESTS - - Parse user instructions carefully to identify the intended goal - - Consider both explicit requests and implicit needs - - Ask clarifying questions when user intent is very ambiguous. But you can also be proactive. If someone says "Go greet the new people who are arriving." you can guess that you need to move to the front door to expect new people. Both do the task, but also let people it's a bit ambiguous by saying "I'm heading to the front door. Let me know if I should be going somewhere else." - -2. SKILL SELECTION AND EXECUTION - - Choose the most appropriate skill(s) for each task - - Provide all required parameters with correct values and types - - Execute skills in a logical sequence when multi-step actions are needed - - Monitor skill execution and handle any failures gracefully - -3. SPATIAL REASONING - - Leverage your spatial memory to navigate efficiently - - Build new spatial memories when exploring unfamiliar areas - - Use landmark-based navigation when possible - - Combine semantic and metric mapping for robust localization - -4. SAFETY AND ETHICS - - Prioritize human safety in all actions - - Respect privacy and personal boundaries - - Avoid actions that could damage the environment or the robot - - Be transparent about your capabilities and limitations - -5. COMMUNICATION STYLE - - Be concise but informative in your responses - - Provide clear status updates during extended tasks - - Use appropriate terminology based on the user's expertise level - - Maintain a helpful, supportive, and respectful tone - - Respond with the `speak_aloud` skill after EVERY QUERY to inform the user of your actions - - When speaking be terse and as concise as possible with a sentence or so, as you would if responding conversationally - -When responding to users: -1. First, acknowledge and confirm your understanding of their request -2. Select and execute the appropriate skill(s) using exact function names and proper parameters -3. Provide meaningful feedback about the outcome of your actions -4. Suggest next steps or additional information when relevant - -Example: If a user asks "Can you find the kitchen?", you would: -1. Acknowledge: "I'll help you find the kitchen." -2. Execute: Call the Navigate skill with query="kitchen" -3. Feedback: Report success or failure of navigation attempt -4. Next steps: Offer to take further actions once at the kitchen location diff --git a/assets/dimensional.command-center-extension-0.0.1.foxe b/assets/dimensional.command-center-extension-0.0.1.foxe new file mode 100644 index 0000000000..163f1ef36b --- /dev/null +++ b/assets/dimensional.command-center-extension-0.0.1.foxe @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98a2a2154b102e8d889bb83305163ead388016377b8e8a56c8f42034443f9be4 +size 1229315 diff --git a/assets/dimensionalascii.txt b/assets/dimensionalascii.txt index 3202258acb..9b35fb8778 100644 --- a/assets/dimensionalascii.txt +++ b/assets/dimensionalascii.txt @@ -1,8 +1,7 @@ - ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— - ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ - ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ - ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ + ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— + ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ + ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ + ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā• - diff --git a/assets/dimos_terminal.png b/assets/dimos_terminal.png index 77f00e47fa..a71b06e1cc 100644 Binary files a/assets/dimos_terminal.png and b/assets/dimos_terminal.png differ diff --git a/assets/drone_foxglove_lcm_dashboard.json b/assets/drone_foxglove_lcm_dashboard.json new file mode 100644 index 0000000000..cfcd8afb47 --- /dev/null +++ b/assets/drone_foxglove_lcm_dashboard.json @@ -0,0 +1,381 @@ +{ + "configById": { + "RawMessages!3zk027p": { + "diffEnabled": false, + "diffMethod": "custom", + "diffTopicPath": "", + "showFullMessageForDiff": false, + "topicPath": "/drone/telemetry", + "fontSize": 12 + }, + "RawMessages!ra9m3n": { + "diffEnabled": false, + "diffMethod": "custom", + "diffTopicPath": "", + "showFullMessageForDiff": false, + "topicPath": "/drone/status", + "fontSize": 12 + }, + "RawMessages!2rdgzs9": { + "diffEnabled": false, + "diffMethod": "custom", + "diffTopicPath": "", + "showFullMessageForDiff": false, + "topicPath": "/drone/odom", + "fontSize": 12 + }, + "3D!18i6zy7": { + "layers": { + "845139cb-26bc-40b3-8161-8ab60af4baf5": { + "visible": true, + "frameLocked": true, + "label": "Grid", + "instanceId": "845139cb-26bc-40b3-8161-8ab60af4baf5", + "layerId": "foxglove.Grid", + "lineWidth": 0.5, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "order": 1, + "size": 30, + "divisions": 30, + "color": "#248eff57" + }, + "ff758451-8c06-4419-a995-e93c825eb8be": { + "visible": true, + "frameLocked": true, + "label": "Grid", + "instanceId": "ff758451-8c06-4419-a995-e93c825eb8be", + "layerId": "foxglove.Grid", + "frameId": "base_link", + "size": 3, + "divisions": 3, + "lineWidth": 1.5, + "color": "#24fff4ff", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "order": 2 + } + }, + "cameraState": { + "perspective": true, + "distance": 35.161738318180966, + "phi": 54.90139603020621, + "thetaOffset": -55.91718358847429, + "targetOffset": [ + -1.0714086708240587, + -1.3106525624032879, + 2.481084387307447e-16 + ], + "target": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": true, + "ignoreColladaUpAxis": false, + "syncCamera": false, + "transforms": { + "visible": true + } + }, + "transforms": {}, + "topics": { + "/lidar": { + "stixelsEnabled": false, + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 10, + "explicitAlpha": 1, + "decayTime": 0, + "cubeSize": 0.1, + "minValue": -0.3, + "cubeOutline": false + }, + "/odom": { + "visible": true, + "axisScale": 1 + }, + "/video": { + "visible": false + }, + "/global_map": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 10, + "decayTime": 0, + "pointShape": "cube", + "cubeOutline": false, + "cubeSize": 0.08, + "gradient": [ + "#06011dff", + "#d1e2e2ff" + ], + "stixelsEnabled": false, + "explicitAlpha": 1, + "minValue": -0.2 + }, + "/global_path": { + "visible": true, + "type": "line", + "arrowScale": [ + 1, + 0.15, + 0.15 + ], + "lineWidth": 0.132, + "gradient": [ + "#6bff7cff", + "#0081ffff" + ] + }, + "/global_target": { + "visible": true + }, + "/pt": { + "visible": false + }, + "/global_costmap": { + "visible": true, + "maxColor": "#8d3939ff", + "frameLocked": false, + "unknownColor": "#80808000", + "colorMode": "custom", + "alpha": 0.517, + "minColor": "#1e00ff00" + }, + "/global_gradient": { + "visible": true, + "maxColor": "#690066ff", + "unknownColor": "#30b89a00", + "minColor": "#00000000", + "colorMode": "custom", + "alpha": 0.3662, + "frameLocked": false, + "drawBehind": false + }, + "/global_cost_field": { + "visible": false, + "maxColor": "#ff0000ff", + "unknownColor": "#80808000" + }, + "/global_passable": { + "visible": false, + "maxColor": "#ffffff00", + "minColor": "#ff0000ff", + "unknownColor": "#80808000" + } + }, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/estimate", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": {}, + "foxglovePanelTitle": "test", + "followTf": "world" + }, + "Image!3mnp456": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": true + }, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/drone/color_image", + "colorMode": "gradient", + "calibrationTopic": "/drone/camera_info" + }, + "foxglovePanelTitle": "/video" + }, + "Image!1gtgk2x": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": true + }, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/drone/depth_colorized", + "colorMode": "gradient", + "calibrationTopic": "/drone/camera_info" + }, + "foxglovePanelTitle": "/video" + }, + "Plot!a1gj37": { + "paths": [ + { + "timestampMethod": "receiveTime", + "value": "/drone/odom.pose.position.x", + "enabled": true, + "color": "#4e98e2" + }, + { + "timestampMethod": "receiveTime", + "value": "/drone/odom.pose.orientation.y", + "enabled": true, + "color": "#f5774d" + }, + { + "timestampMethod": "receiveTime", + "value": "/drone/odom.pose.position.z", + "enabled": true, + "color": "#f7df71" + } + ], + "showXAxisLabels": true, + "showYAxisLabels": true, + "showLegend": true, + "legendDisplay": "floating", + "showPlotValuesInLegend": false, + "isSynced": true, + "xAxisVal": "timestamp", + "sidebarDimension": 240 + } + }, + "globalVariables": {}, + "userNodes": {}, + "playbackConfig": { + "speed": 1 + }, + "drawerConfig": { + "tracks": [] + }, + "layout": { + "direction": "row", + "first": { + "first": { + "first": "RawMessages!3zk027p", + "second": "RawMessages!ra9m3n", + "direction": "column", + "splitPercentage": 69.92084432717678 + }, + "second": "RawMessages!2rdgzs9", + "direction": "column", + "splitPercentage": 70.97625329815304 + }, + "second": { + "first": "3D!18i6zy7", + "second": { + "first": "Image!3mnp456", + "second": { + "first": "Image!1gtgk2x", + "second": "Plot!a1gj37", + "direction": "column" + }, + "direction": "column", + "splitPercentage": 36.93931398416886 + }, + "direction": "row", + "splitPercentage": 52.45307143723201 + }, + "splitPercentage": 39.13203076769059 + } +} diff --git a/assets/foxglove_dashboards/go2.json b/assets/foxglove_dashboards/go2.json new file mode 100644 index 0000000000..fb9df219c2 --- /dev/null +++ b/assets/foxglove_dashboards/go2.json @@ -0,0 +1,603 @@ +{ + "configById": { + "3D!3ezwzdr": { + "cameraState": { + "perspective": false, + "distance": 10.26684166532264, + "phi": 29.073691502600532, + "thetaOffset": 93.32472375597958, + "targetOffset": [ + 3.280168913303102, + -1.418093876569801, + -2.6619087209849424e-16 + ], + "target": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "transforms": { + "labelSize": 0.1, + "axisSize": 0.51 + } + }, + "transforms": { + "frame:sensor_at_scan": { + "visible": false + }, + "frame:camera_optical": { + "visible": false + }, + "frame:camera_link": { + "visible": false + }, + "frame:base_link": { + "visible": true + }, + "frame:sensor": { + "visible": false + }, + "frame:map": { + "visible": false + }, + "frame:world": { + "visible": false + } + }, + "topics": { + "/lidar": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 2.85, + "decayTime": 6, + "pointShape": "circle" + }, + "/detectorDB/scene_update": { + "visible": true + }, + "/path_active": { + "visible": true, + "lineWidth": 0.05, + "gradient": [ + "#00ff1eff", + "#6bff6e80" + ] + }, + "/map": { + "visible": false, + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/image": { + "visible": false + }, + "/camera_info": { + "visible": true, + "distance": 1, + "color": "#c4bcffff" + }, + "/detectorDB/pointcloud/0": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "pointSize": 2, + "flatColor": "#00ff00ff", + "cubeSize": 0.03 + }, + "/detectorDB/pointcloud/1": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "cubeSize": 0.03, + "flatColor": "#ff0000ff" + }, + "/detectorDB/pointcloud/2": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "cubeSize": 0.03, + "flatColor": "#00aaffff" + }, + "/global_map": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 4, + "pointShape": "circle", + "explicitAlpha": 1, + "cubeSize": 0.05, + "cubeOutline": false, + "flatColor": "#ed8080ff", + "minValue": -0.1, + "decayTime": 0 + }, + "/global_costmap": { + "visible": true, + "colorMode": "custom", + "unknownColor": "#ff000000", + "minColor": "#484981ff", + "maxColor": "#000000ff", + "frameLocked": false, + "drawBehind": false + }, + "/go2/color_image": { + "visible": false, + "cameraInfoTopic": "/go2/camera_info" + }, + "/go2/camera_info": { + "visible": true + }, + "/color_image": { + "visible": false, + "cameraInfoTopic": "/camera_info" + }, + "color_image": { + "visible": false, + "cameraInfoTopic": "/camera_info" + }, + "lidar": { + "visible": false, + "colorField": "z", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 2.76, + "pointShape": "cube" + }, + "odom": { + "visible": false + }, + "global_map": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "cube" + }, + "prev_lidar": { + "visible": false, + "pointShape": "cube", + "colorField": "z", + "colorMode": "flat", + "colorMap": "turbo", + "gradient": [ + "#b70000ff", + "#ff0000ff" + ], + "flatColor": "#80eda2ff" + }, + "additive_global_map": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "cube" + }, + "height_costmap": { + "visible": false + }, + "/odom": { + "visible": false + }, + "/costmap": { + "visible": false, + "colorMode": "custom", + "alpha": 1, + "frameLocked": false, + "maxColor": "#ff2222ff", + "minColor": "#00006bff", + "unknownColor": "#80808000" + }, + "/debug_navigation": { + "visible": false, + "cameraInfoTopic": "/camera_info" + }, + "/path": { + "visible": true, + "lineWidth": 0.03, + "gradient": [ + "#ff6b6bff", + "#ff0000ff" + ] + } + }, + "layers": { + "grid": { + "visible": true, + "drawBehind": false, + "frameLocked": true, + "label": "Grid", + "instanceId": "8cb9fe46-7478-4aa6-95c5-75c511fee62d", + "layerId": "foxglove.Grid", + "size": 50, + "color": "#24b6ffff", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "frameId": "world", + "divisions": 25, + "lineWidth": 1 + }, + "aac2d29a-9580-442f-8067-104830c336c8": { + "displayMode": "auto", + "fallbackColor": "#ffffff", + "showAxis": false, + "axisScale": 1, + "showOutlines": true, + "opacity": 1, + "visible": true, + "frameLocked": true, + "instanceId": "aac2d29a-9580-442f-8067-104830c336c8", + "label": "URDF", + "layerId": "foxglove.Urdf", + "sourceType": "filePath", + "url": "", + "filePath": "/home/lesh/coding/dimos/dimos/robot/unitree/go2/go2.urdf", + "parameter": "", + "topic": "", + "framePrefix": "", + "order": 2, + "links": { + "base_link": { + "visible": true + } + } + } + }, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": {}, + "followTf": "map" + }, + "command-center-extension.command-center!3xr2po0": {}, + "Plot!3cog9zw": { + "paths": [ + { + "timestampMethod": "receiveTime", + "value": "/metrics/_calculate_costmap.data", + "enabled": true, + "color": "#4e98e2", + "id": "a1ff9a80-7a45-48ff-bdb1-232bda7bd492" + }, + { + "timestampMethod": "receiveTime", + "value": "/metrics/get_global_pointcloud.data", + "enabled": true, + "color": "#f5774d", + "id": "5fe70fbd-33f9-4b15-849f-c7c49988af95" + }, + { + "timestampMethod": "receiveTime", + "value": "/metrics/add_frame.data", + "enabled": true, + "color": "#f7df71", + "id": "bb4a56f8-78ae-45cb-850e-48c462dab40f" + } + ], + "showXAxisLabels": true, + "showYAxisLabels": true, + "showLegend": true, + "legendDisplay": "floating", + "showPlotValuesInLegend": false, + "isSynced": true, + "xAxisVal": "timestamp", + "sidebarDimension": 240 + }, + "Plot!47kna9v": { + "paths": [ + { + "timestampMethod": "publishTime", + "value": "/global_map.header.stamp.sec", + "enabled": true, + "color": "#4e98e2", + "id": "19f95865-4d9e-4d38-b9d7-d227319d8ebd" + }, + { + "timestampMethod": "publishTime", + "value": "/global_costmap.header.stamp.sec", + "enabled": true, + "color": "#f5774d", + "id": "86ddc0e2-8e9c-4d52-bd5a-d02cb0357efe" + } + ], + "showXAxisLabels": true, + "showYAxisLabels": true, + "showLegend": true, + "legendDisplay": "floating", + "showPlotValuesInLegend": false, + "isSynced": true, + "xAxisVal": "timestamp", + "sidebarDimension": 240 + }, + "Image!3mnp456": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": false, + "transforms": { + "showLabel": false, + "visible": false + } + }, + "transforms": { + "frame:world": { + "visible": true + }, + "frame:camera_optical": { + "visible": false + }, + "frame:camera_link": { + "visible": false + }, + "frame:base_link": { + "visible": false + } + }, + "topics": { + "/lidar": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 6, + "explicitAlpha": 0.6, + "pointShape": "circle", + "cubeSize": 0.016 + }, + "/odom": { + "visible": false + }, + "/local_costmap": { + "visible": false + }, + "/global_costmap": { + "visible": false, + "minColor": "#ffffff00", + "maxColor": "#ff0000ff", + "unknownColor": "#80808000" + }, + "/detected_0": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 23, + "pointShape": "cube", + "cubeSize": 0.04, + "flatColor": "#ff0000ff", + "stixelsEnabled": false + }, + "/detected_1": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 20.51, + "flatColor": "#34ff00ff", + "pointShape": "cube", + "cubeSize": 0.04, + "cubeOutline": false + }, + "/filtered_pointcloud": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "rainbow", + "pointSize": 1.5, + "pointShape": "cube", + "flatColor": "#ff0000ff", + "cubeSize": 0.1 + }, + "/global_map": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "cube", + "pointSize": 5, + "cubeSize": 0.03 + }, + "/detected/pointcloud/1": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "cubeSize": 0.01, + "flatColor": "#00ff1eff", + "pointSize": 15, + "decayTime": 0, + "cubeOutline": true + }, + "/detected/pointcloud/2": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "circle", + "cubeSize": 0.1, + "flatColor": "#00fbffff", + "pointSize": 0.01 + }, + "/detected/pointcloud/0": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#ff0000ff", + "pointSize": 15, + "cubeOutline": true, + "cubeSize": 0.03 + }, + "/registered_scan": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 6.49 + }, + "/detection3d/markers": { + "visible": false + }, + "/foxglove/scene_update": { + "visible": true + }, + "/scene_update": { + "visible": false + }, + "/map": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 8 + }, + "/detection3d/scene_update": { + "visible": true + }, + "/detectorDB/scene_update": { + "visible": false + }, + "/detectorDB/pointcloud/0": { + "visible": false, + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/detectorDB/pointcloud/1": { + "visible": false, + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + } + }, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/color_image", + "colorMode": "gradient", + "annotations": { + "/detections": { + "visible": true + }, + "/annotations": { + "visible": true + }, + "/reid/annotations": { + "visible": true + }, + "/objectdb/annotations": { + "visible": true + }, + "/detector3d/annotations": { + "visible": true + }, + "/detectorDB/annotations": { + "visible": true + } + }, + "synchronize": false, + "rotation": 0, + "calibrationTopic": "/camera_info" + }, + "foxglovePanelTitle": "" + } + }, + "globalVariables": {}, + "userNodes": {}, + "playbackConfig": { + "speed": 1 + }, + "drawerConfig": { + "tracks": [] + }, + "layout": { + "direction": "row", + "first": "3D!3ezwzdr", + "second": { + "first": "command-center-extension.command-center!3xr2po0", + "second": { + "first": { + "first": "Plot!3cog9zw", + "second": "Plot!47kna9v", + "direction": "row" + }, + "second": "Image!3mnp456", + "direction": "column", + "splitPercentage": 38.08411214953271 + }, + "direction": "column", + "splitPercentage": 50.116550116550115 + }, + "splitPercentage": 63.706720977596746 + } +} diff --git a/assets/foxglove_dashboards/old/foxglove_g1_detections.json b/assets/foxglove_dashboards/old/foxglove_g1_detections.json new file mode 100644 index 0000000000..7def24fdaa --- /dev/null +++ b/assets/foxglove_dashboards/old/foxglove_g1_detections.json @@ -0,0 +1,915 @@ +{ + "configById": { + "3D!18i6zy7": { + "layers": { + "845139cb-26bc-40b3-8161-8ab60af4baf5": { + "visible": true, + "frameLocked": true, + "label": "Grid", + "instanceId": "845139cb-26bc-40b3-8161-8ab60af4baf5", + "layerId": "foxglove.Grid", + "lineWidth": 0.5, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "order": 1, + "size": 30, + "divisions": 30, + "color": "#248eff57" + }, + "ff758451-8c06-4419-a995-e93c825eb8be": { + "visible": false, + "frameLocked": true, + "label": "Grid", + "instanceId": "ff758451-8c06-4419-a995-e93c825eb8be", + "layerId": "foxglove.Grid", + "frameId": "base_link", + "divisions": 6, + "lineWidth": 1.5, + "color": "#24fff4ff", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "order": 2, + "size": 6 + } + }, + "cameraState": { + "perspective": true, + "distance": 17.147499997813583, + "phi": 41.70966129676441, + "thetaOffset": 46.32247127821147, + "targetOffset": [ + 1.489416869802203, + 3.0285403495275056, + -1.5060700211359088 + ], + "target": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": false, + "ignoreColladaUpAxis": false, + "syncCamera": true, + "transforms": { + "visible": true, + "showLabel": true, + "editable": true, + "enablePreloading": false, + "labelSize": 0.07 + } + }, + "transforms": { + "frame:camera_link": { + "visible": false + }, + "frame:sensor": { + "visible": false + }, + "frame:sensor_at_scan": { + "visible": false + }, + "frame:map": { + "visible": true + }, + "frame:world": { + "visible": true + } + }, + "topics": { + "/lidar": { + "stixelsEnabled": false, + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 2, + "explicitAlpha": 0.8, + "decayTime": 0, + "cubeSize": 0.05, + "cubeOutline": false, + "minValue": -2 + }, + "/odom": { + "visible": true, + "axisScale": 1 + }, + "/video": { + "visible": false + }, + "/global_map": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "decayTime": 0, + "pointShape": "square", + "cubeOutline": false, + "cubeSize": 0.08, + "gradient": [ + "#06011dff", + "#d1e2e2ff" + ], + "stixelsEnabled": false, + "explicitAlpha": 0.339, + "minValue": -0.2, + "pointSize": 5 + }, + "/global_path": { + "visible": true, + "type": "line", + "arrowScale": [ + 1, + 0.15, + 0.15 + ], + "lineWidth": 0.05, + "gradient": [ + "#6bff7cff", + "#0081ffff" + ] + }, + "/global_target": { + "visible": true + }, + "/pt": { + "visible": false + }, + "/global_costmap": { + "visible": true, + "maxColor": "#6b2b2bff", + "frameLocked": false, + "unknownColor": "#80808000", + "colorMode": "custom", + "alpha": 0.517, + "minColor": "#1e00ff00", + "drawBehind": false + }, + "/global_gradient": { + "visible": true, + "maxColor": "#690066ff", + "unknownColor": "#30b89a00", + "minColor": "#00000000", + "colorMode": "custom", + "alpha": 0.3662, + "frameLocked": false, + "drawBehind": false + }, + "/global_cost_field": { + "visible": false, + "maxColor": "#ff0000ff", + "unknownColor": "#80808000" + }, + "/global_passable": { + "visible": false, + "maxColor": "#ffffff00", + "minColor": "#ff0000ff", + "unknownColor": "#80808000" + }, + "/image": { + "visible": true, + "cameraInfoTopic": "/camera_info", + "distance": 1.5, + "planarProjectionFactor": 0, + "color": "#e7e1ffff" + }, + "/camera_info": { + "visible": true, + "distance": 1.5, + "planarProjectionFactor": 0 + }, + "/local_costmap": { + "visible": false + }, + "/navigation_goal": { + "visible": true + }, + "/debug_camera_optical_points": { + "stixelsEnabled": false, + "visible": false, + "pointSize": 0.07, + "pointShape": "cube", + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/debug_world_points": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "rainbow", + "pointShape": "cube" + }, + "/filtered_points_suitcase_0": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#ff0808ff", + "cubeSize": 0.149, + "pointSize": 28.57 + }, + "/filtered_points_combined": { + "visible": true, + "flatColor": "#ff0000ff", + "pointShape": "cube", + "pointSize": 6.63, + "colorField": "z", + "colorMode": "gradient", + "colorMap": "rainbow", + "cubeSize": 0.35, + "gradient": [ + "#d100caff", + "#ff0000ff" + ] + }, + "/filtered_points_box_7": { + "visible": true, + "flatColor": "#fbfaffff", + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/filtered_pointcloud": { + "visible": true, + "colorField": "z", + "colorMode": "flat", + "colorMap": "turbo", + "flatColor": "#ff0000ff", + "pointSize": 40.21, + "pointShape": "cube", + "cubeSize": 0.1, + "cubeOutline": true + }, + "/detected": { + "visible": false, + "pointSize": 1.5, + "pointShape": "cube", + "cubeSize": 0.118, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "flatColor": "#d70000ff", + "cubeOutline": true + }, + "/detected_0": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 1.6, + "pointShape": "cube", + "cubeSize": 0.1, + "flatColor": "#e00000ff", + "stixelsEnabled": false, + "decayTime": 0, + "cubeOutline": true + }, + "/detected_1": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "cubeSize": 0.1, + "flatColor": "#00ff15ff", + "cubeOutline": true + }, + "/image_detected_0": { + "visible": false + }, + "/detected/pointcloud/1": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#15ff00ff", + "pointSize": 0.1, + "cubeSize": 0.05, + "cubeOutline": true + }, + "/detected/pointcloud/2": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#00ffe1ff", + "pointSize": 10, + "cubeOutline": true, + "cubeSize": 0.05 + }, + "/detected/pointcloud/0": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#ff0000ff", + "cubeOutline": true, + "cubeSize": 0.04 + }, + "/detected/image/0": { + "visible": false + }, + "/detected/image/3": { + "visible": false + }, + "/detected/pointcloud/3": { + "visible": true, + "pointSize": 1.5, + "pointShape": "cube", + "cubeSize": 0.1, + "flatColor": "#00fffaff", + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo" + }, + "/detected/image/1": { + "visible": false + }, + "/registered_scan": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 2 + }, + "/image/camera_info": { + "visible": true, + "distance": 2 + }, + "/map": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "square", + "cubeSize": 0.13, + "explicitAlpha": 1, + "pointSize": 1, + "decayTime": 2 + }, + "/detection3d/markers": { + "visible": true, + "color": "#88ff00ff", + "showOutlines": true, + "selectedIdVariable": "" + }, + "/foxglove/scene_update": { + "visible": true + }, + "/scene_update": { + "visible": true, + "showOutlines": true, + "computeVertexNormals": true + }, + "/target": { + "visible": true, + "axisScale": 1 + }, + "/goal_pose": { + "visible": true, + "axisScale": 0.5 + }, + "/global_pointcloud": { + "visible": true, + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/pointcloud_map": { + "visible": false, + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/detectorDB/pointcloud/0": { + "visible": true, + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/path_active": { + "visible": true + }, + "/detector3d/image/0": { + "visible": true + }, + "/detector3d/pointcloud/0": { + "visible": true, + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/detectorDB/image/0": { + "visible": true + }, + "/detectorDB/scene_update": { + "visible": true + }, + "/detector3d/scene_update": { + "visible": true + }, + "/detector3d/image/1": { + "visible": true + }, + "/g1/camera_info": { + "visible": false + }, + "/detectorDB/image/1": { + "visible": true + } + }, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/estimate", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": {}, + "foxglovePanelTitle": "", + "followTf": "camera_link" + }, + "Image!3mnp456": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": false, + "transforms": { + "showLabel": false, + "visible": true + } + }, + "transforms": { + "frame:world": { + "visible": false + }, + "frame:camera_optical": { + "visible": false + }, + "frame:camera_link": { + "visible": false + }, + "frame:base_link": { + "visible": false + }, + "frame:sensor": { + "visible": false + } + }, + "topics": { + "/lidar": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 6, + "explicitAlpha": 0.6, + "pointShape": "circle", + "cubeSize": 0.016 + }, + "/odom": { + "visible": false + }, + "/local_costmap": { + "visible": false + }, + "/global_costmap": { + "visible": false, + "minColor": "#ffffff00" + }, + "/detected_0": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 23, + "pointShape": "cube", + "cubeSize": 0.04, + "flatColor": "#ff0000ff", + "stixelsEnabled": false + }, + "/detected_1": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 20.51, + "flatColor": "#34ff00ff", + "pointShape": "cube", + "cubeSize": 0.04, + "cubeOutline": false + }, + "/filtered_pointcloud": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "rainbow", + "pointSize": 1.5, + "pointShape": "cube", + "flatColor": "#ff0000ff", + "cubeSize": 0.1 + }, + "/global_map": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "cube", + "pointSize": 5 + }, + "/detected/pointcloud/1": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "cubeSize": 0.01, + "flatColor": "#00ff1eff", + "pointSize": 15, + "decayTime": 0, + "cubeOutline": true + }, + "/detected/pointcloud/2": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "circle", + "cubeSize": 0.1, + "flatColor": "#00fbffff", + "pointSize": 0.01 + }, + "/detected/pointcloud/0": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#ff0000ff", + "pointSize": 15, + "cubeOutline": true, + "cubeSize": 0.03 + }, + "/registered_scan": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 6.49 + }, + "/detection3d/markers": { + "visible": false + }, + "/foxglove/scene_update": { + "visible": true + }, + "/scene_update": { + "visible": false + }, + "/map": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 8 + } + }, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/image", + "colorMode": "gradient", + "annotations": { + "/detections": { + "visible": true + }, + "/annotations": { + "visible": true + }, + "/detector3d/annotations": { + "visible": true + }, + "/detectorDB/annotations": { + "visible": true + } + }, + "synchronize": false, + "rotation": 0, + "calibrationTopic": "/camera_info" + }, + "foxglovePanelTitle": "" + }, + "Plot!3heo336": { + "paths": [ + { + "timestampMethod": "publishTime", + "value": "/image.header.stamp.nsec", + "enabled": true, + "color": "#4e98e2", + "label": "image", + "showLine": true + }, + { + "timestampMethod": "publishTime", + "value": "/map.header.stamp.nsec", + "enabled": true, + "color": "#f5774d", + "label": "lidar", + "showLine": true + }, + { + "timestampMethod": "publishTime", + "value": "/tf.transforms[0].header.stamp.nsec", + "enabled": true, + "color": "#f7df71", + "label": "tf", + "showLine": true + }, + { + "timestampMethod": "publishTime", + "value": "/odom.header.stamp.nsec", + "enabled": true, + "color": "#5cd6a9", + "label": "odom", + "showLine": true + } + ], + "showXAxisLabels": true, + "showYAxisLabels": true, + "showLegend": true, + "legendDisplay": "floating", + "showPlotValuesInLegend": false, + "isSynced": true, + "xAxisVal": "timestamp", + "sidebarDimension": 240 + }, + "StateTransitions!2wj5twf": { + "paths": [ + { + "value": "/detectorDB/annotations.texts[0].text", + "timestampMethod": "receiveTime", + "customStates": { + "type": "discrete", + "states": [] + } + }, + { + "value": "/detectorDB/annotations.texts[1].text", + "timestampMethod": "receiveTime", + "customStates": { + "type": "discrete", + "states": [] + } + }, + { + "value": "/detectorDB/annotations.texts[2].text", + "timestampMethod": "receiveTime", + "customStates": { + "type": "discrete", + "states": [] + } + } + ], + "isSynced": true + }, + "Image!47pi3ov": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": {}, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/detector3d/image/0" + } + }, + "Image!4kk50gw": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": {}, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/detectorDB/image/1" + } + }, + "Image!2348e0b": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": {}, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/detectorDB/image/2", + "synchronize": false + } + } + }, + "globalVariables": {}, + "userNodes": {}, + "playbackConfig": { + "speed": 1 + }, + "drawerConfig": { + "tracks": [] + }, + "layout": { + "first": { + "first": "3D!18i6zy7", + "second": "Image!3mnp456", + "direction": "row", + "splitPercentage": 44.31249231586115 + }, + "second": { + "first": { + "first": "Plot!3heo336", + "second": "StateTransitions!2wj5twf", + "direction": "column" + }, + "second": { + "first": "Image!47pi3ov", + "second": { + "first": "Image!4kk50gw", + "second": "Image!2348e0b", + "direction": "row" + }, + "direction": "row", + "splitPercentage": 33.06523681858802 + }, + "direction": "row", + "splitPercentage": 46.39139486467731 + }, + "direction": "column", + "splitPercentage": 65.20874751491054 + } +} diff --git a/assets/foxglove_image_sharpness_test.json b/assets/foxglove_dashboards/old/foxglove_image_sharpness_test.json similarity index 100% rename from assets/foxglove_image_sharpness_test.json rename to assets/foxglove_dashboards/old/foxglove_image_sharpness_test.json diff --git a/assets/foxglove_unitree_lcm_dashboard.json b/assets/foxglove_dashboards/old/foxglove_unitree_lcm_dashboard.json similarity index 100% rename from assets/foxglove_unitree_lcm_dashboard.json rename to assets/foxglove_dashboards/old/foxglove_unitree_lcm_dashboard.json diff --git a/assets/foxglove_unitree_yolo.json b/assets/foxglove_dashboards/old/foxglove_unitree_yolo.json similarity index 100% rename from assets/foxglove_unitree_yolo.json rename to assets/foxglove_dashboards/old/foxglove_unitree_yolo.json diff --git a/assets/license_file_header.txt b/assets/license_file_header.txt index 4268cd990f..a02322f92f 100644 --- a/assets/license_file_header.txt +++ b/assets/license_file_header.txt @@ -10,4 +10,4 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +limitations under the License. diff --git a/base-requirements.txt b/base-requirements.txt index 6d4cb9671c..68b485fb9a 100644 --- a/base-requirements.txt +++ b/base-requirements.txt @@ -1,2 +1,2 @@ torch==2.0.1 -torchvision==0.15.2 \ No newline at end of file +torchvision==0.15.2 diff --git a/bin/dev b/bin/dev index 509fa5e14d..2ccebcb071 100755 --- a/bin/dev +++ b/bin/dev @@ -59,7 +59,7 @@ get_tag() { build_image() { local image_tag image_tag=$(get_tag) - + docker build \ --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \ --build-arg GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ diff --git a/bin/hooks/filter_commit_message.py b/bin/hooks/filter_commit_message.py new file mode 100644 index 0000000000..cd92b196af --- /dev/null +++ b/bin/hooks/filter_commit_message.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +import sys + + +def main() -> int: + if len(sys.argv) < 2: + print("Usage: filter_commit_message.py ", file=sys.stderr) + return 1 + + commit_msg_file = Path(sys.argv[1]) + if not commit_msg_file.exists(): + return 0 + + lines = commit_msg_file.read_text().splitlines(keepends=True) + + # Find the first line containing "Generated with" and truncate there + filtered_lines = [] + for line in lines: + if "Generated with" in line: + break + filtered_lines.append(line) + + commit_msg_file.write_text("".join(filtered_lines)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/hooks/largefiles_check b/bin/hooks/largefiles_check new file mode 100755 index 0000000000..190183ecc6 --- /dev/null +++ b/bin/hooks/largefiles_check @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Pre-commit hook to detect large files that should be in LFS.""" + +import argparse +import fnmatch +import os +import shutil +import subprocess +import sys + +import tomli + +parser = argparse.ArgumentParser() +parser.add_argument("--all", action="store_true", help="Check all files in repo, not just staged") +args = parser.parse_args() + +# Check git-lfs is installed +if not shutil.which("git-lfs"): + print("git-lfs is not installed.") + print("\nInstall with:") + print(" Arch: pacman -S git-lfs") + print(" Ubuntu: apt install git-lfs") + print(" macOS: brew install git-lfs") + print("\nThen run: git lfs install") + sys.exit(1) + +# Load config +with open("pyproject.toml", "rb") as f: + config = tomli.load(f).get("tool", {}).get("largefiles", {}) + +max_size_kb = config.get("max_size_kb", 50) +max_bytes = max_size_kb * 1024 +ignore_patterns = config.get("ignore", []) + +# Get LFS files to exclude +result = subprocess.run( + ["git", "lfs", "ls-files", "-n"], capture_output=True, text=True, check=True +) +lfs_files = set(result.stdout.splitlines()) + +# Get files to check +if args.all: + files_cmd = ["git", "ls-files"] +else: + files_cmd = ["git", "diff", "--cached", "--name-only"] + +violations = [] +result = subprocess.run(files_cmd, capture_output=True, text=True, check=True) +for file in result.stdout.splitlines(): + if file in lfs_files: + continue + if any(fnmatch.fnmatch(file, p) for p in ignore_patterns): + continue + if os.path.isfile(file) and os.path.getsize(file) > max_bytes: + violations.append((file, os.path.getsize(file))) + +if violations: + print(f"Large files detected (limit: {max_size_kb}KB):") + for f, size in sorted(violations, key=lambda x: -x[1]): + print(f" {size // 1024}KB {f}") + print("\nEither add to LFS or to [tool.largefiles].ignore in pyproject.toml") + sys.exit(1) diff --git a/bin/lfs_check b/bin/hooks/lfs_check similarity index 100% rename from bin/lfs_check rename to bin/hooks/lfs_check diff --git a/bin/lfs_push b/bin/lfs_push index 68b1326e49..0d9e01d743 100755 --- a/bin/lfs_push +++ b/bin/lfs_push @@ -28,27 +28,27 @@ compressed_dirs=() for dir_path in data/*; do # Skip if no directories found (glob didn't match) [ ! "$dir_path" ] && continue - + # Extract directory name dir_name=$(basename "$dir_path") - + # Skip .lfs directory if it exists [ "$dir_name" = ".lfs" ] && continue - + # Define compressed file path compressed_file="data/.lfs/${dir_name}.tar.gz" - + # Check if compressed file already exists if [ -f "$compressed_file" ]; then continue fi - + echo -e " ${YELLOW}Compressing${NC} $dir_path -> $compressed_file" - + # Show directory size before compression dir_size=$(du -sh "$dir_path" | cut -f1) echo -e " Data size: ${YELLOW}$dir_size${NC}" - + # Create compressed archive with progress bar # Use tar with gzip compression, excluding hidden files and common temp files tar -czf "$compressed_file" \ @@ -60,13 +60,13 @@ for dir_path in data/*; do --checkpoint-action=dot \ -C "data/" \ "$dir_name" - + if [ $? -eq 0 ]; then # Show compressed file size compressed_size=$(du -sh "$compressed_file" | cut -f1) echo -e " ${GREEN}āœ“${NC} Successfully compressed $dir_name (${GREEN}$dir_size${NC} → ${GREEN}$compressed_size${NC})" compressed_dirs+=("$dir_name") - + # Add the compressed file to git LFS tracking git add -f "$compressed_file" @@ -87,7 +87,7 @@ if [ ${#compressed_dirs[@]} -gt 0 ]; then dirs_list=$(IFS=', '; echo "${compressed_dirs[*]}") commit_msg="Auto-compress test data: ${dirs_list}" fi - + #git commit -m "$commit_msg" echo -e "${GREEN}āœ“${NC} Compressed file references added. Uploading..." git lfs push origin $(git branch --show-current) @@ -95,4 +95,3 @@ if [ ${#compressed_dirs[@]} -gt 0 ]; then else echo -e "${GREEN}āœ“${NC} No test data to compress" fi - diff --git a/bin/mypy-ros b/bin/mypy-ros new file mode 100755 index 0000000000..d46d6a542e --- /dev/null +++ b/bin/mypy-ros @@ -0,0 +1,44 @@ +#!/bin/bash + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +mypy_args=(--show-error-codes --hide-error-context --no-pretty) + +main() { + cd "$ROOT" + + if [ -z "$(docker images -q dimos-ros-dev)" ]; then + (cd docker/ros; docker build -t dimos-ros .) + docker build -t dimos-ros-python --build-arg FROM_IMAGE=dimos-ros -f docker/python/Dockerfile . + docker build -t dimos-ros-dev --build-arg FROM_IMAGE=dimos-ros-python -f docker/dev/Dockerfile . + fi + + sudo rm -fr .mypy_cache_docker + rm -fr .mypy_cache_local + + { + mypy_docker & + mypy_local & + wait + } | sort -u +} + +cleaned() { + grep ': error: ' | sort +} + +mypy_docker() { + docker run --rm -v $(pwd):/app -w /app dimos-ros-dev bash -c " + source /opt/ros/humble/setup.bash && + MYPYPATH=/opt/ros/humble/lib/python3.10/site-packages mypy ${mypy_args[*]} --cache-dir .mypy_cache_docker dimos + " | cleaned +} + +mypy_local() { + MYPYPATH=/opt/ros/jazzy/lib/python3.12/site-packages \ + mypy "${mypy_args[@]}" --cache-dir .mypy_cache_local dimos | cleaned +} + +main "$@" diff --git a/bin/mypy-strict b/bin/mypy-strict deleted file mode 100755 index 05001bf100..0000000000 --- a/bin/mypy-strict +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash -# -# Run mypy with strict settings on the dimos codebase. -# -# Usage: -# ./bin/mypy-strict # Run mypy and show all errors -# ./bin/mypy-strict --user me # Filter for your git user.email -# ./bin/mypy-strict --after cutoff # Filter for lines committed on or after 2025-10-08 -# ./bin/mypy-strict --after 2025-11-11 # Filter for lines committed on or after specific date -# ./bin/mypy-strict --user me --after cutoff # Chain filters -# - -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -cd "$ROOT" - -. .venv/bin/activate - -run_mypy() { - export MYPYPATH=/opt/ros/jazzy/lib/python3.12/site-packages - - mypy_args=( - --config-file mypy_strict.ini - --show-error-codes - --hide-error-context - --no-pretty - dimos - ) - mypy "${mypy_args[@]}" -} - -main() { - local user_email="none" - local after_date="" - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --user) - if [[ $# -lt 2 ]]; then - echo "Error: --user requires an argument" >&2 - exit 1 - fi - case "$2" in - me) - user_email="$(git config user.email || echo none)" - ;; - all) - user_email="none" - ;; - *) - user_email="$2" - ;; - esac - shift 2 - ;; - --after) - if [[ $# -lt 2 ]]; then - echo "Error: --after requires an argument" >&2 - exit 1 - fi - case "$2" in - cutoff) - after_date="2025-10-10" - ;; - start) - after_date="" - ;; - *) - after_date="$2" - ;; - esac - shift 2 - ;; - *) - echo "Error: Unknown argument '$1'" >&2 - exit 1 - ;; - esac - done - - # Build filter pipeline - local pipeline="run_mypy" - - if [[ -n "$after_date" ]]; then - pipeline="$pipeline | ./bin/filter-errors-after-date '$after_date'" - fi - - if [[ "$user_email" != "none" ]]; then - pipeline="$pipeline | ./bin/filter-errors-for-user '$user_email'" - fi - - eval "$pipeline" -} - -main "$@" diff --git a/bin/re-ignore-mypy.py b/bin/re-ignore-mypy.py new file mode 100755 index 0000000000..7d71bcd986 --- /dev/null +++ b/bin/re-ignore-mypy.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 + +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import defaultdict +from pathlib import Path +import re +import subprocess + + +def remove_type_ignore_comments(directory: Path) -> None: + # Pattern matches "# type: ignore" with optional error codes in brackets. + # Captures any trailing comment after `type: ignore`. + type_ignore_pattern = re.compile(r"(\s*)#\s*type:\s*ignore(?:\[[^\]]*\])?(\s*#.*)?") + + for py_file in directory.rglob("*.py"): + try: + content = py_file.read_text() + except Exception: + continue + + new_lines = [] + modified = False + + for line in content.splitlines(keepends=True): + match = type_ignore_pattern.search(line) + if match: + before = line[: match.start()] + trailing_comment = match.group(2) + + if trailing_comment: + new_line = before + match.group(1) + trailing_comment.lstrip() + else: + new_line = before + + if line.endswith("\n"): + new_line = new_line.rstrip() + "\n" + else: + new_line = new_line.rstrip() + new_lines.append(new_line) + modified = True + else: + new_lines.append(line) + + if modified: + try: + py_file.write_text("".join(new_lines)) + except Exception: + pass + + +def run_mypy(root: Path) -> str: + result = subprocess.run( + [str(root / "bin" / "mypy-ros")], + capture_output=True, + text=True, + cwd=root, + ) + return result.stdout + result.stderr + + +def parse_mypy_errors(output: str) -> dict[Path, dict[int, list[str]]]: + error_pattern = re.compile(r"^(.+):(\d+): error: .+\[([^\]]+)\]\s*$") + errors: dict[Path, dict[int, list[str]]] = defaultdict(lambda: defaultdict(list)) + + for line in output.splitlines(): + match = error_pattern.match(line) + if match: + file_path = Path(match.group(1)) + line_num = int(match.group(2)) + error_code = match.group(3) + if error_code not in errors[file_path][line_num]: + errors[file_path][line_num].append(error_code) + + return errors + + +def add_type_ignore_comments(root: Path, errors: dict[Path, dict[int, list[str]]]) -> None: + comment_pattern = re.compile(r"^([^#]*?)( #.*)$") + + for file_path, line_errors in errors.items(): + full_path = root / file_path + if not full_path.exists(): + continue + + try: + content = full_path.read_text() + except Exception: + continue + + lines = content.splitlines(keepends=True) + modified = False + + for line_num, error_codes in line_errors.items(): + if line_num < 1 or line_num > len(lines): + continue + + idx = line_num - 1 + line = lines[idx] + codes_str = ", ".join(sorted(error_codes)) + ignore_comment = f" # type: ignore[{codes_str}]" + + has_newline = line.endswith("\n") + line_content = line.rstrip("\n") + + comment_match = comment_pattern.match(line_content) + if comment_match: + code_part = comment_match.group(1) + existing_comment = comment_match.group(2) + new_line = code_part + ignore_comment + existing_comment + else: + new_line = line_content + ignore_comment + + if has_newline: + new_line += "\n" + + lines[idx] = new_line + modified = True + + if modified: + try: + full_path.write_text("".join(lines)) + except Exception: + pass + + +def main() -> None: + root = Path(__file__).parent.parent + dimos_dir = root / "dimos" + + remove_type_ignore_comments(dimos_dir) + mypy_output = run_mypy(root) + errors = parse_mypy_errors(mypy_output) + add_type_ignore_comments(root, errors) + + +if __name__ == "__main__": + main() diff --git a/bin/robot-debugger b/bin/robot-debugger index d9bef015e7..165a546a0c 100755 --- a/bin/robot-debugger +++ b/bin/robot-debugger @@ -8,7 +8,7 @@ # # And now start this script # -# $ ./bin/robot-debugger +# $ ./bin/robot-debugger # >>> robot.explore() # True # >>> diff --git a/data/.lfs/apartment.tar.gz b/data/.lfs/apartment.tar.gz new file mode 100644 index 0000000000..c8e6cf0331 --- /dev/null +++ b/data/.lfs/apartment.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d2c44f39573a80a65aeb6ccd3fcb1c8cb0741dbc7286132856409e88e150e77 +size 18141029 diff --git a/data/.lfs/astar_corner_min_cost.png.tar.gz b/data/.lfs/astar_corner_min_cost.png.tar.gz new file mode 100644 index 0000000000..35f3ffe0b6 --- /dev/null +++ b/data/.lfs/astar_corner_min_cost.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42517c5f67a9f06949cb2015a345f9d6b43d22cafd50e1fefb9b5d24d8b72509 +size 5671 diff --git a/data/.lfs/astar_min_cost.png.tar.gz b/data/.lfs/astar_min_cost.png.tar.gz new file mode 100644 index 0000000000..752a778295 --- /dev/null +++ b/data/.lfs/astar_min_cost.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06b67aa0d18c291c3525e67ca3a2a9ab2530f6fe782a850872ba4c343353a20a +size 12018 diff --git a/data/.lfs/big_office.ply.tar.gz b/data/.lfs/big_office.ply.tar.gz new file mode 100644 index 0000000000..c8524a1862 --- /dev/null +++ b/data/.lfs/big_office.ply.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7eabc682f75e1725a07df51bb009d3950190318d119d54d0ad8c6b7104f175e3 +size 2355227 diff --git a/data/.lfs/big_office_height_cost_occupancy.png.tar.gz b/data/.lfs/big_office_height_cost_occupancy.png.tar.gz new file mode 100644 index 0000000000..75addaf103 --- /dev/null +++ b/data/.lfs/big_office_height_cost_occupancy.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d8e7d096f1108d45ebdad760c4655de1e1d50105ca59c5188e79cb1a7c0d4a9 +size 133051 diff --git a/data/.lfs/big_office_simple_occupancy.png.tar.gz b/data/.lfs/big_office_simple_occupancy.png.tar.gz new file mode 100644 index 0000000000..dd667640be --- /dev/null +++ b/data/.lfs/big_office_simple_occupancy.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dded2e28694de9ec84a91a686b27654b83c604f44f4d3e336d5cd481e88d3249 +size 28146 diff --git a/data/.lfs/drone.tar.gz b/data/.lfs/drone.tar.gz new file mode 100644 index 0000000000..2973c649cd --- /dev/null +++ b/data/.lfs/drone.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd73f988eee8fd7b99d6c0bf6a905c2f43a6145a4ef33e9eef64bee5f53e04dd +size 709946060 diff --git a/data/.lfs/expected_occupancy_scene.xml.tar.gz b/data/.lfs/expected_occupancy_scene.xml.tar.gz new file mode 100644 index 0000000000..efbe7ce49d --- /dev/null +++ b/data/.lfs/expected_occupancy_scene.xml.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3eb91f3c7787882bf26a69df21bb1933d2f6cd71132ca5f0521e2808269bfa2 +size 6777 diff --git a/data/.lfs/gradient_simple.png.tar.gz b/data/.lfs/gradient_simple.png.tar.gz new file mode 100644 index 0000000000..7232282ce4 --- /dev/null +++ b/data/.lfs/gradient_simple.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e418f2a6858c757cb72bd25772749a1664c97a407682d88ad2b51c4bbdcb8006 +size 11568 diff --git a/data/.lfs/gradient_voronoi.png.tar.gz b/data/.lfs/gradient_voronoi.png.tar.gz new file mode 100644 index 0000000000..28e7f263c4 --- /dev/null +++ b/data/.lfs/gradient_voronoi.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3867c0fb5b00f8cb5e0876e5120a70d61f7da121c0a3400010743cc858ee2d54 +size 20680 diff --git a/data/.lfs/inflation_simple.png.tar.gz b/data/.lfs/inflation_simple.png.tar.gz new file mode 100644 index 0000000000..ca6586800c --- /dev/null +++ b/data/.lfs/inflation_simple.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:658ed8cafc24ac7dc610b7e5ae484f23e1963872ffc2add0632ee61a7c20492d +size 3412 diff --git a/data/.lfs/make_navigation_map_mixed.png.tar.gz b/data/.lfs/make_navigation_map_mixed.png.tar.gz new file mode 100644 index 0000000000..4fcaa8134a --- /dev/null +++ b/data/.lfs/make_navigation_map_mixed.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36ea27a2434836eb309728f35033674736552daeb82f6e41fb7e3eb175d950da +size 13084 diff --git a/data/.lfs/make_navigation_map_simple.png.tar.gz b/data/.lfs/make_navigation_map_simple.png.tar.gz new file mode 100644 index 0000000000..f966b459e2 --- /dev/null +++ b/data/.lfs/make_navigation_map_simple.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0d211fa1bc517ef78e8dc548ebff09f58ad34c86d28eb3bd48a09a577ee5d1e +size 11767 diff --git a/data/.lfs/make_path_mask_full.png.tar.gz b/data/.lfs/make_path_mask_full.png.tar.gz new file mode 100644 index 0000000000..0e9336aaea --- /dev/null +++ b/data/.lfs/make_path_mask_full.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b772d266dffa82ccf14f13c7d8cc2443210202836883c80f016a56d4cfe2b52a +size 11213 diff --git a/data/.lfs/make_path_mask_two_meters.png.tar.gz b/data/.lfs/make_path_mask_two_meters.png.tar.gz new file mode 100644 index 0000000000..7fa9e767b8 --- /dev/null +++ b/data/.lfs/make_path_mask_two_meters.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da608d410f4a1afee0965abfac814bc05267bdde31b0d3a9622c39515ee4f813 +size 11395 diff --git a/data/.lfs/models_mobileclip.tar.gz b/data/.lfs/models_mobileclip.tar.gz index 874c94de07..afe82c96e9 100644 --- a/data/.lfs/models_mobileclip.tar.gz +++ b/data/.lfs/models_mobileclip.tar.gz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f8022e365d9e456dcbd3913d36bf8c68a4cd086eb777c92a773c8192cd8235d -size 277814612 +oid sha256:143747a320e959d9ee9fd239535d0451c378b1a2e165a242e981c4a3e4defb73 +size 1654541503 diff --git a/data/.lfs/models_torchreid.tar.gz b/data/.lfs/models_torchreid.tar.gz new file mode 100644 index 0000000000..6446a049fb --- /dev/null +++ b/data/.lfs/models_torchreid.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2215070bd8e814ac9867410e3e6c49700f6c3ef7caf29b42d7832be090003743 +size 23873718 diff --git a/data/.lfs/mujoco_sim.tar.gz b/data/.lfs/mujoco_sim.tar.gz index 6bfc95c831..57833fbbc6 100644 --- a/data/.lfs/mujoco_sim.tar.gz +++ b/data/.lfs/mujoco_sim.tar.gz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3d607ce57127a6ac558f81ebb9c98bd23a71a86f9ffd5700b3389bf1a19ddf2 -size 59341859 +oid sha256:d178439569ed81dfad05455419dc51da2c52021313b6d7b9259d9e30946db7c6 +size 60186340 diff --git a/data/.lfs/occupancy_general.png.tar.gz b/data/.lfs/occupancy_general.png.tar.gz new file mode 100644 index 0000000000..b509151e5a --- /dev/null +++ b/data/.lfs/occupancy_general.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b770d950cf7206a67ccdfd8660ee0ab818228faa9ebbf1a37cbf6ee9d1ac7539 +size 2970 diff --git a/data/.lfs/occupancy_simple.npy.tar.gz b/data/.lfs/occupancy_simple.npy.tar.gz new file mode 100644 index 0000000000..cf42cf3667 --- /dev/null +++ b/data/.lfs/occupancy_simple.npy.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1cf83464442fb284b6f7ba2752546fc4571a73f3490c24a58fb45987555a66c +size 1954 diff --git a/data/.lfs/occupancy_simple.png.tar.gz b/data/.lfs/occupancy_simple.png.tar.gz new file mode 100644 index 0000000000..4962f13db1 --- /dev/null +++ b/data/.lfs/occupancy_simple.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c9dac221a594c87d0baa60b8c678c63a0c215325080b34ee60df5cc1e1c331d +size 3311 diff --git a/data/.lfs/office_building_1.tar.gz b/data/.lfs/office_building_1.tar.gz new file mode 100644 index 0000000000..0dc013bd94 --- /dev/null +++ b/data/.lfs/office_building_1.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70aac31ca76597b3eee1ddfcbe2ba71d432fd427176f66d8281d75da76641f49 +size 1061581652 diff --git a/data/.lfs/overlay_occupied.png.tar.gz b/data/.lfs/overlay_occupied.png.tar.gz new file mode 100644 index 0000000000..158a52c6bd --- /dev/null +++ b/data/.lfs/overlay_occupied.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b55bcf7a2a7a5cbdfdfe8c6a75c53ffe5707197d991d1e39e9aa9dc22503397 +size 3657 diff --git a/data/.lfs/resample_path_simple.png.tar.gz b/data/.lfs/resample_path_simple.png.tar.gz new file mode 100644 index 0000000000..1a8c1118d6 --- /dev/null +++ b/data/.lfs/resample_path_simple.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b5c454ed6cc66cf4446ce4a246464aec27368da4902651b4ad9ed29b3ba56ec +size 118319 diff --git a/data/.lfs/resample_path_smooth.png.tar.gz b/data/.lfs/resample_path_smooth.png.tar.gz new file mode 100644 index 0000000000..80af3d3805 --- /dev/null +++ b/data/.lfs/resample_path_smooth.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cc0dfd80bada94f2ab1bb577e2ec1734dad6894113f2fe77964bd80d886c3d3 +size 109699 diff --git a/data/.lfs/smooth_occupied.png.tar.gz b/data/.lfs/smooth_occupied.png.tar.gz new file mode 100644 index 0000000000..0e09e7d15a --- /dev/null +++ b/data/.lfs/smooth_occupied.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44c8988b8a7d954ee26a0a5f195b961c62bbdb251b540df6b4d67cd85a72e5ac +size 3511 diff --git a/data/.lfs/three_paths.npy.tar.gz b/data/.lfs/three_paths.npy.tar.gz new file mode 100644 index 0000000000..744eb06305 --- /dev/null +++ b/data/.lfs/three_paths.npy.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba849a6b648ccc9ed4987bbe985ee164dd9ad0324895076baa9f86196b2a0d5f +size 5180 diff --git a/data/.lfs/three_paths.ply.tar.gz b/data/.lfs/three_paths.ply.tar.gz new file mode 100644 index 0000000000..a5bfc6bac4 --- /dev/null +++ b/data/.lfs/three_paths.ply.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:639093004355c1ba796c668cd43476dfcabff137ca0bb430ace07730cc512f0e +size 307187 diff --git a/data/.lfs/three_paths.png.tar.gz b/data/.lfs/three_paths.png.tar.gz new file mode 100644 index 0000000000..ade2bd3eb7 --- /dev/null +++ b/data/.lfs/three_paths.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2265ddd76bfb70e7ac44f2158dc0d16e0df264095b0f45a77f95eb85c529d935 +size 2559 diff --git a/data/.lfs/unitree_go2_bigoffice.tar.gz b/data/.lfs/unitree_go2_bigoffice.tar.gz new file mode 100644 index 0000000000..6582702479 --- /dev/null +++ b/data/.lfs/unitree_go2_bigoffice.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a009674153f7ee1f98219af69dc7a92d063f2581bfd9b0aa019762c9235895c +size 2312982327 diff --git a/data/.lfs/unitree_go2_bigoffice_map.pickle.tar.gz b/data/.lfs/unitree_go2_bigoffice_map.pickle.tar.gz new file mode 100644 index 0000000000..89ecb54e87 --- /dev/null +++ b/data/.lfs/unitree_go2_bigoffice_map.pickle.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68adb344ae040c3f94d61dd058beb39cc2811c4ae8328f678bc2ba761c504eb5 +size 2331189 diff --git a/data/.lfs/visualize_occupancy_rainbow.png.tar.gz b/data/.lfs/visualize_occupancy_rainbow.png.tar.gz new file mode 100644 index 0000000000..9bbd2e6ea1 --- /dev/null +++ b/data/.lfs/visualize_occupancy_rainbow.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dc1e3b6519f7d7ff25b16c3124ee447f02857eeb3eb20930cdab95464b1f0a3 +size 11582 diff --git a/data/.lfs/visualize_occupancy_turbo.png.tar.gz b/data/.lfs/visualize_occupancy_turbo.png.tar.gz new file mode 100644 index 0000000000..e2863cdae6 --- /dev/null +++ b/data/.lfs/visualize_occupancy_turbo.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c21874bab6ec7cd9692d2b1e67498ddfff3c832ec992e9552fee17093759b270 +size 18593 diff --git a/default.env b/default.env index 0c7a0ff14a..5098a60892 100644 --- a/default.env +++ b/default.env @@ -10,6 +10,6 @@ WEBRTC_SERVER_HOST=0.0.0.0 WEBRTC_SERVER_PORT=9991 DISPLAY=:0 -# Optional +# Optional #DIMOS_MAX_WORKERS= TEST_RTSP_URL= diff --git a/dimos/__init__.py b/dimos/__init__.py index 8b13789179..e69de29bb2 100644 --- a/dimos/__init__.py +++ b/dimos/__init__.py @@ -1 +0,0 @@ - diff --git a/dimos/agents/__init__.py b/dimos/agents/__init__.py index e69de29bb2..2bac584249 100644 --- a/dimos/agents/__init__.py +++ b/dimos/agents/__init__.py @@ -0,0 +1,27 @@ +from langchain_core.messages import ( + AIMessage, + HumanMessage, + MessageLikeRepresentation, + SystemMessage, + ToolCall, + ToolMessage, +) + +from dimos.agents.agent import Agent, deploy +from dimos.agents.spec import AgentSpec +from dimos.agents.vlm_agent import VLMAgent +from dimos.agents.vlm_stream_tester import VlmStreamTester +from dimos.protocol.skill.skill import skill +from dimos.protocol.skill.type import Output, Reducer, Stream + +__all__ = [ + "Agent", + "AgentSpec", + "Output", + "Reducer", + "Stream", + "VLMAgent", + "VlmStreamTester", + "deploy", + "skill", +] diff --git a/dimos/agents/agent.py b/dimos/agents/agent.py index 62765ef706..e9c7c5d7b9 100644 --- a/dimos/agents/agent.py +++ b/dimos/agents/agent.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,900 +11,395 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import asyncio +import datetime +import json +from operator import itemgetter +import os +from typing import Any, TypedDict +import uuid + +from langchain_core.messages import ( + AIMessage, + HumanMessage, + SystemMessage, + ToolCall, + ToolMessage, +) + +from dimos.agents.llm_init import build_llm, build_system_message +from dimos.agents.spec import AgentSpec, Model, Provider +from dimos.core import DimosCluster, rpc +from dimos.protocol.skill.coordinator import SkillCoordinator, SkillState, SkillStateDict +from dimos.protocol.skill.skill import SkillContainer +from dimos.protocol.skill.type import Output +from dimos.utils.logging_config import setup_logger -"""Agent framework for LLM-based autonomous systems. +logger = setup_logger() -This module provides a flexible foundation for creating agents that can: -- Process image and text inputs through LLM APIs -- Store and retrieve contextual information using semantic memory -- Handle tool/function calling -- Process streaming inputs asynchronously -The module offers base classes (Agent, LLMAgent) and concrete implementations -like OpenAIAgent that connect to specific LLM providers. -""" +SYSTEM_MSG_APPEND = "\nYour message history will always be appended with a System Overview message that provides situational awareness." -from __future__ import annotations -# Standard library imports -import json -import os -import threading -from typing import TYPE_CHECKING, Any - -# Third-party imports -from dotenv import load_dotenv -from openai import NOT_GIVEN, OpenAI -from pydantic import BaseModel -from reactivex import Observable, Observer, create, empty, just, operators as RxOps -from reactivex.disposable import CompositeDisposable, Disposable -from reactivex.subject import Subject - -# Local imports -from dimos.agents.memory.chroma_impl import OpenAISemanticMemory -from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.agents.tokenizer.openai_tokenizer import OpenAITokenizer -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.stream_merger import create_stream_merger -from dimos.stream.video_operators import Operators as MyOps, VideoOperators as MyVidOps -from dimos.utils.logging_config import setup_logger -from dimos.utils.threadpool import get_scheduler +def toolmsg_from_state(state: SkillState) -> ToolMessage: + if state.skill_config.output != Output.standard: + content = "output attached in separate messages" + else: + content = state.content() # type: ignore[assignment] -if TYPE_CHECKING: - from reactivex.scheduler import ThreadPoolScheduler + return ToolMessage( + # if agent call has been triggered by another skill, + # and this specific skill didn't finish yet but we need a tool call response + # we return a message explaining that execution is still ongoing + content=content + or "Running, you will be called with an update, no need for subsequent tool calls", + name=state.name, + tool_call_id=state.call_id, + ) - from dimos.agents.memory.base import AbstractAgentSemanticMemory - from dimos.agents.tokenizer.base import AbstractTokenizer -# Initialize environment variables -load_dotenv() +class SkillStateSummary(TypedDict): + name: str + call_id: str + state: str + data: Any -# Initialize logger for the agent module -logger = setup_logger("dimos.agents") -# Constants -_TOKEN_BUDGET_PARTS = 4 # Number of parts to divide token budget -_MAX_SAVED_FRAMES = 100 # Maximum number of frames to save +def summary_from_state(state: SkillState, special_data: bool = False) -> SkillStateSummary: + content = state.content() + if isinstance(content, dict): + content = json.dumps(content) + if not isinstance(content, str): + content = str(content) -# ----------------------------------------------------------------------------- -# region Agent Base Class -# ----------------------------------------------------------------------------- -class Agent: - """Base agent that manages memory and subscriptions.""" + return { + "name": state.name, + "call_id": state.call_id, + "state": state.state.name, + "data": state.content() if not special_data else "data will be in a separate message", + } - def __init__( - self, - dev_name: str = "NA", - agent_type: str = "Base", - agent_memory: AbstractAgentSemanticMemory | None = None, - pool_scheduler: ThreadPoolScheduler | None = None, - ) -> None: - """ - Initializes a new instance of the Agent. - - Args: - dev_name (str): The device name of the agent. - agent_type (str): The type of the agent (e.g., 'Base', 'Vision'). - agent_memory (AbstractAgentSemanticMemory): The memory system for the agent. - pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. - If None, the global scheduler from get_scheduler() will be used. - """ - self.dev_name = dev_name - self.agent_type = agent_type - self.agent_memory = agent_memory or OpenAISemanticMemory() - self.disposables = CompositeDisposable() - self.pool_scheduler = pool_scheduler if pool_scheduler else get_scheduler() - - def dispose_all(self) -> None: - """Disposes of all active subscriptions managed by this agent.""" - if self.disposables: - self.disposables.dispose() - else: - logger.info("No disposables to dispose.") - - -# endregion Agent Base Class - - -# ----------------------------------------------------------------------------- -# region LLMAgent Base Class (Generic LLM Agent) -# ----------------------------------------------------------------------------- -class LLMAgent(Agent): - """Generic LLM agent containing common logic for LLM-based agents. - - This class implements functionality for: - - Updating the query - - Querying the agent's memory (for RAG) - - Building prompts via a prompt builder - - Handling tooling callbacks in responses - - Subscribing to image and query streams - - Emitting responses as an observable stream - - Subclasses must implement the `_send_query` method, which is responsible - for sending the prompt to a specific LLM API. - - Attributes: - query (str): The current query text to process. - prompt_builder (PromptBuilder): Handles construction of prompts. - system_query (str): System prompt for RAG context situations. - image_detail (str): Detail level for image processing ('low','high','auto'). - max_input_tokens_per_request (int): Maximum input token count. - max_output_tokens_per_request (int): Maximum output token count. - max_tokens_per_request (int): Total maximum token count. - rag_query_n (int): Number of results to fetch from memory. - rag_similarity_threshold (float): Minimum similarity for RAG results. - frame_processor (FrameProcessor): Processes video frames. - output_dir (str): Directory for output files. - response_subject (Subject): Subject that emits agent responses. - process_all_inputs (bool): Whether to process every input emission (True) or - skip emissions when the agent is busy processing a previous input (False). - """ - - logging_file_memory_lock = threading.Lock() - - def __init__( + +def _custom_json_serializers(obj): # type: ignore[no-untyped-def] + if isinstance(obj, datetime.date | datetime.datetime): + return obj.isoformat() + raise TypeError(f"Type {type(obj)} not serializable") + + +# takes an overview of running skills from the coorindator +# and builds messages to be sent to an agent +def snapshot_to_messages( + state: SkillStateDict, + tool_calls: list[ToolCall], +) -> tuple[list[ToolMessage], AIMessage | None]: + # builds a set of tool call ids from a previous agent request + tool_call_ids = set( + map(itemgetter("id"), tool_calls), + ) + + # build a tool msg responses + tool_msgs: list[ToolMessage] = [] + + # build a general skill state overview (for longer running skills) + state_overview: list[dict[str, SkillStateSummary]] = [] + + # for special skills that want to return a separate message + # (images for example, requires to be a HumanMessage) + special_msgs: list[HumanMessage] = [] + + # for special skills that want to return a separate message that should + # stay in history, like actual human messages, critical events + history_msgs: list[HumanMessage] = [] + + # Initialize state_msg + state_msg = None + + for skill_state in sorted( + state.values(), + key=lambda skill_state: skill_state.duration(), + ): + if skill_state.call_id in tool_call_ids: + tool_msgs.append(toolmsg_from_state(skill_state)) + + if skill_state.skill_config.output == Output.human: + content = skill_state.content() + if not content: + continue + history_msgs.append(HumanMessage(content=content)) # type: ignore[arg-type] + continue + + special_data = skill_state.skill_config.output == Output.image + if special_data: + content = skill_state.content() + if not content: + continue + special_msgs.append(HumanMessage(content=content)) # type: ignore[arg-type] + + if skill_state.call_id in tool_call_ids: + continue + + state_overview.append(summary_from_state(skill_state, special_data)) # type: ignore[arg-type] + + if state_overview: + state_overview_str = "\n".join( + json.dumps(s, default=_custom_json_serializers) for s in state_overview + ) + state_msg = AIMessage("State Overview:\n" + state_overview_str) + + return { # type: ignore[return-value] + "tool_msgs": tool_msgs, + "history_msgs": history_msgs, + "state_msgs": ([state_msg] if state_msg else []) + special_msgs, + } + + +# Agent class job is to glue skill coordinator state to an agent, builds langchain messages +class Agent(AgentSpec): + system_message: SystemMessage + state_messages: list[AIMessage | HumanMessage] + + def __init__( # type: ignore[no-untyped-def] self, - dev_name: str = "NA", - agent_type: str = "LLM", - agent_memory: AbstractAgentSemanticMemory | None = None, - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool = False, - system_query: str | None = None, - max_output_tokens_per_request: int = 16384, - max_input_tokens_per_request: int = 128000, - input_query_stream: Observable | None = None, - input_data_stream: Observable | None = None, - input_video_stream: Observable | None = None, + *args, + **kwargs, ) -> None: - """ - Initializes a new instance of the LLMAgent. - - Args: - dev_name (str): The device name of the agent. - agent_type (str): The type of the agent. - agent_memory (AbstractAgentSemanticMemory): The memory system for the agent. - pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. - If None, the global scheduler from get_scheduler() will be used. - process_all_inputs (bool): Whether to process every input emission (True) or - skip emissions when the agent is busy processing a previous input (False). - """ - super().__init__(dev_name, agent_type, agent_memory, pool_scheduler) - # These attributes can be configured by a subclass if needed. - self.query: str | None = None - self.prompt_builder: PromptBuilder | None = None - self.system_query: str | None = system_query - self.image_detail: str = "low" - self.max_input_tokens_per_request: int = max_input_tokens_per_request - self.max_output_tokens_per_request: int = max_output_tokens_per_request - self.max_tokens_per_request: int = ( - self.max_input_tokens_per_request + self.max_output_tokens_per_request - ) - self.rag_query_n: int = 4 - self.rag_similarity_threshold: float = 0.45 - self.frame_processor: FrameProcessor | None = None - self.output_dir: str = os.path.join(os.getcwd(), "assets", "agent") - self.process_all_inputs: bool = process_all_inputs - os.makedirs(self.output_dir, exist_ok=True) - - # Subject for emitting responses - self.response_subject = Subject() - - # Conversation history for maintaining context between calls - self.conversation_history = [] - - # Initialize input streams - self.input_video_stream = input_video_stream - self.input_query_stream = ( - input_query_stream - if (input_data_stream is None) - else ( - input_query_stream.pipe( - RxOps.with_latest_from(input_data_stream), - RxOps.map( - lambda combined: { - "query": combined[0], - "objects": combined[1] - if len(combined) > 1 - else "No object data available", - } - ), - RxOps.map( - lambda data: f"{data['query']}\n\nCurrent objects detected:\n{data['objects']}" - ), - RxOps.do_action( - lambda x: print(f"\033[34mEnriched query: {x.split(chr(10))[0]}\033[0m") - or [print(f"\033[34m{line}\033[0m") for line in x.split(chr(10))[1:]] - ), - ) + AgentSpec.__init__(self, *args, **kwargs) + + self.state_messages = [] + self.coordinator = SkillCoordinator() + self._history = [] # type: ignore[var-annotated] + self._agent_id = str(uuid.uuid4()) + self._agent_stopped = False + + self.system_message = build_system_message(self.config, append=SYSTEM_MSG_APPEND) + self.publish(self.system_message) + self._llm = build_llm(self.config) + + @rpc + def get_agent_id(self) -> str: + return self._agent_id + + @rpc + def start(self) -> None: + super().start() + self.coordinator.start() + + @rpc + def stop(self) -> None: + self.coordinator.stop() + self._agent_stopped = True + super().stop() + + def clear_history(self) -> None: + self._history.clear() + + def append_history(self, *msgs: list[AIMessage | HumanMessage]) -> None: + for msg in msgs: + self.publish(msg) # type: ignore[arg-type] + + self._history.extend(msgs) + + def history(self): # type: ignore[no-untyped-def] + return [self.system_message, *self._history, *self.state_messages] + + # Used by agent to execute tool calls + def execute_tool_calls(self, tool_calls: list[ToolCall]) -> None: + """Execute a list of tool calls from the agent.""" + if self._agent_stopped: + logger.warning("Agent is stopped, cannot execute tool calls.") + return + for tool_call in tool_calls: + logger.info(f"executing skill call {tool_call}") + self.coordinator.call_skill( + tool_call.get("id"), # type: ignore[arg-type] + tool_call.get("name"), + tool_call.get("args"), ) - ) - # Setup stream subscriptions based on inputs provided - if (self.input_video_stream is not None) and (self.input_query_stream is not None): - self.merged_stream = create_stream_merger( - data_input_stream=self.input_video_stream, text_query_stream=self.input_query_stream - ) + # used to inject skill calls into the agent loop without agent asking for it + def run_implicit_skill(self, skill_name: str, **kwargs) -> None: # type: ignore[no-untyped-def] + if self._agent_stopped: + logger.warning("Agent is stopped, cannot execute implicit skill calls.") + return + self.coordinator.call_skill(False, skill_name, {"args": kwargs}) - logger.info("Subscribing to merged input stream...") + async def agent_loop(self, first_query: str = ""): # type: ignore[no-untyped-def] + # TODO: Should I add a lock here to prevent concurrent calls to agent_loop? - # Define a query extractor for the merged stream - def query_extractor(emission): - return (emission[0], emission[1][0]) + if self._agent_stopped: + logger.warning("Agent is stopped, cannot run agent loop.") + # return "Agent is stopped." + import traceback - self.disposables.add( - self.subscribe_to_image_processing( - self.merged_stream, query_extractor=query_extractor - ) - ) - else: - # If no merged stream, fall back to individual streams - if self.input_video_stream is not None: - logger.info("Subscribing to input video stream...") - self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) - if self.input_query_stream is not None: - logger.info("Subscribing to input query stream...") - self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) - - def _update_query(self, incoming_query: str | None) -> None: - """Updates the query if an incoming query is provided. - - Args: - incoming_query (str): The new query text. - """ - if incoming_query is not None: - self.query = incoming_query - - def _get_rag_context(self) -> tuple[str, str]: - """Queries the agent memory to retrieve RAG context. - - Returns: - Tuple[str, str]: A tuple containing the formatted results (for logging) - and condensed results (for use in the prompt). - """ - results = self.agent_memory.query( - query_texts=self.query, - n_results=self.rag_query_n, - similarity_threshold=self.rag_similarity_threshold, - ) - formatted_results = "\n".join( - f"Document ID: {doc.id}\nMetadata: {doc.metadata}\nContent: {doc.page_content}\nScore: {score}\n" - for (doc, score) in results - ) - condensed_results = " | ".join(f"{doc.page_content}" for (doc, _) in results) - logger.info(f"Agent Memory Query Results:\n{formatted_results}") - logger.info("=== Results End ===") - return formatted_results, condensed_results + traceback.print_stack() + return "Agent is stopped." - def _build_prompt( - self, - base64_image: str | None, - dimensions: tuple[int, int] | None, - override_token_limit: bool, - condensed_results: str, - ) -> list: - """Builds a prompt message using the prompt builder. - - Args: - base64_image (str): Optional Base64-encoded image. - dimensions (Tuple[int, int]): Optional image dimensions. - override_token_limit (bool): Whether to override token limits. - condensed_results (str): The condensed RAG context. - - Returns: - list: A list of message dictionaries to be sent to the LLM. - """ - # Budget for each component of the prompt - budgets = { - "system_prompt": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "user_query": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "image": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "rag": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - } - - # Define truncation policies for each component - policies = { - "system_prompt": "truncate_end", - "user_query": "truncate_middle", - "image": "do_not_truncate", - "rag": "truncate_end", - } - - return self.prompt_builder.build( - user_query=self.query, - override_token_limit=override_token_limit, - base64_image=base64_image, - image_width=dimensions[0] if dimensions is not None else None, - image_height=dimensions[1] if dimensions is not None else None, - image_detail=self.image_detail, - rag_context=condensed_results, - system_prompt=self.system_query, - budgets=budgets, - policies=policies, - ) + self.state_messages = [] + if first_query: + self.append_history(HumanMessage(first_query)) # type: ignore[arg-type] - def _handle_tooling(self, response_message, messages): - """Handles tooling callbacks in the response message. - - If tool calls are present, the corresponding functions are executed and - a follow-up query is sent. - - Args: - response_message: The response message containing tool calls. - messages (list): The original list of messages sent. - - Returns: - The final response message after processing tool calls, if any. - """ - - # TODO: Make this more generic or move implementation to OpenAIAgent. - # This is presently OpenAI-specific. - def _tooling_callback(message, messages, response_message, skill_library: SkillLibrary): - has_called_tools = False - new_messages = [] - for tool_call in message.tool_calls: - has_called_tools = True - name = tool_call.function.name - args = json.loads(tool_call.function.arguments) - result = skill_library.call(name, **args) - logger.info(f"Function Call Results: {result}") - new_messages.append( - { - "role": "tool", - "tool_call_id": tool_call.id, - "content": str(result), - "name": name, - } - ) - if has_called_tools: - logger.info("Sending Another Query.") - messages.append(response_message) - messages.extend(new_messages) - # Delegate to sending the query again. - return self._send_query(messages) - else: - logger.info("No Need for Another Query.") - return None - - if response_message.tool_calls is not None: - return _tooling_callback( - response_message, messages, response_message, self.skill_library - ) - return None + def _get_state() -> str: + # TODO: FIX THIS EXTREME HACK + update = self.coordinator.generate_snapshot(clear=False) + snapshot_msgs = snapshot_to_messages(update, msg.tool_calls) + return json.dumps(snapshot_msgs, sort_keys=True, default=lambda o: repr(o)) - def _observable_query( - self, - observer: Observer, - base64_image: str | None = None, - dimensions: tuple[int, int] | None = None, - override_token_limit: bool = False, - incoming_query: str | None = None, - ): - """Prepares and sends a query to the LLM, emitting the response to the observer. - - Args: - observer (Observer): The observer to emit responses to. - base64_image (str): Optional Base64-encoded image. - dimensions (Tuple[int, int]): Optional image dimensions. - override_token_limit (bool): Whether to override token limits. - incoming_query (str): Optional query to update the agent's query. - - Raises: - Exception: Propagates any exceptions encountered during processing. - """ try: - self._update_query(incoming_query) - _, condensed_results = self._get_rag_context() - messages = self._build_prompt( - base64_image, dimensions, override_token_limit, condensed_results - ) - # logger.debug(f"Sending Query: {messages}") - logger.info("Sending Query.") - response_message = self._send_query(messages) - logger.info(f"Received Response: {response_message}") - if response_message is None: - raise Exception("Response message does not exist.") - - # TODO: Make this more generic. The parsed tag and tooling handling may be OpenAI-specific. - # If no skill library is provided or there are no tool calls, emit the response directly. - if ( - self.skill_library is None - or self.skill_library.get_tools() in (None, NOT_GIVEN) - or response_message.tool_calls is None - ): - final_msg = ( - response_message.parsed - if hasattr(response_message, "parsed") and response_message.parsed - else ( - response_message.content - if hasattr(response_message, "content") - else response_message - ) - ) - observer.on_next(final_msg) - self.response_subject.on_next(final_msg) - else: - response_message_2 = self._handle_tooling(response_message, messages) - final_msg = ( - response_message_2 if response_message_2 is not None else response_message - ) - if isinstance(final_msg, BaseModel): # TODO: Test - final_msg = str(final_msg.content) - observer.on_next(final_msg) - self.response_subject.on_next(final_msg) - observer.on_completed() - except Exception as e: - logger.error(f"Query failed in {self.dev_name}: {e}") - observer.on_error(e) - self.response_subject.on_error(e) - - def _send_query(self, messages: list) -> Any: - """Sends the query to the LLM API. - - This method must be implemented by subclasses with specifics of the LLM API. - - Args: - messages (list): The prompt messages to be sent. - - Returns: - Any: The response message from the LLM. - - Raises: - NotImplementedError: Always, unless overridden. - """ - raise NotImplementedError("Subclasses must implement _send_query method.") - - def _log_response_to_file(self, response, output_dir: str | None = None) -> None: - """Logs the LLM response to a file. - - Args: - response: The response message to log. - output_dir (str): The directory where the log file is stored. - """ - if output_dir is None: - output_dir = self.output_dir - if response is not None: - with self.logging_file_memory_lock: - log_path = os.path.join(output_dir, "memory.txt") - with open(log_path, "a") as file: - file.write(f"{self.dev_name}: {response}\n") - logger.info(f"LLM Response [{self.dev_name}]: {response}") - - def subscribe_to_image_processing( - self, frame_observable: Observable, query_extractor=None - ) -> Disposable: - """Subscribes to a stream of video frames for processing. - - This method sets up a subscription to process incoming video frames. - Each frame is encoded and then sent to the LLM by directly calling the - _observable_query method. The response is then logged to a file. - - Args: - frame_observable (Observable): An observable emitting video frames or - (query, frame) tuples if query_extractor is provided. - query_extractor (callable, optional): Function to extract query and frame from - each emission. If None, assumes emissions are - raw frames and uses self.system_query. - - Returns: - Disposable: A disposable representing the subscription. - """ - # Initialize frame processor if not already set - if self.frame_processor is None: - self.frame_processor = FrameProcessor(delete_on_init=True) - - print_emission_args = {"enabled": True, "dev_name": self.dev_name, "counts": {}} - - def _process_frame(emission) -> Observable: - """ - Processes a frame or (query, frame) tuple. - """ - # Extract query and frame - if query_extractor: - query, frame = query_extractor(emission) - else: - query = self.system_query - frame = emission - return just(frame).pipe( - MyOps.print_emission(id="B", **print_emission_args), - RxOps.observe_on(self.pool_scheduler), - MyOps.print_emission(id="C", **print_emission_args), - RxOps.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id="D", **print_emission_args), - MyVidOps.with_jpeg_export( - self.frame_processor, - suffix=f"{self.dev_name}_frame_", - save_limit=_MAX_SAVED_FRAMES, - ), - MyOps.print_emission(id="E", **print_emission_args), - MyVidOps.encode_image(), - MyOps.print_emission(id="F", **print_emission_args), - RxOps.filter( - lambda base64_and_dims: base64_and_dims is not None - and base64_and_dims[0] is not None - and base64_and_dims[1] is not None - ), - MyOps.print_emission(id="G", **print_emission_args), - RxOps.flat_map( - lambda base64_and_dims: create( - lambda observer, _: self._observable_query( - observer, - base64_image=base64_and_dims[0], - dimensions=base64_and_dims[1], - incoming_query=query, + while True: + # we are getting tools from the coordinator on each turn + # since this allows for skillcontainers to dynamically provide new skills + tools = self.get_tools() # type: ignore[no-untyped-call] + self._llm = self._llm.bind_tools(tools) # type: ignore[assignment] + + # publish to /agent topic for observability + for state_msg in self.state_messages: + self.publish(state_msg) + + # history() builds our message history dynamically + # ensures we include latest system state, but not old ones. + messages = self.history() # type: ignore[no-untyped-call] + + # Some LLMs don't work without any human messages. Add an initial one. + if len(messages) == 1 and isinstance(messages[0], SystemMessage): + messages.append( + HumanMessage( + "Everything is initialized. I'll let you know when you should act." ) ) - ), # Use the extracted query - MyOps.print_emission(id="H", **print_emission_args), - ) + self.append_history(messages[-1]) - # Use a mutable flag to ensure only one frame is processed at a time. - is_processing = [False] - - def process_if_free(emission): - if not self.process_all_inputs and is_processing[0]: - # Drop frame if a request is in progress and process_all_inputs is False - return empty() - else: - is_processing[0] = True - return _process_frame(emission).pipe( - MyOps.print_emission(id="I", **print_emission_args), - RxOps.observe_on(self.pool_scheduler), - MyOps.print_emission(id="J", **print_emission_args), - RxOps.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id="K", **print_emission_args), - RxOps.do_action( - on_completed=lambda: is_processing.__setitem__(0, False), - on_error=lambda e: is_processing.__setitem__(0, False), - ), - MyOps.print_emission(id="L", **print_emission_args), - ) + msg = self._llm.invoke(messages) - observable = frame_observable.pipe( - MyOps.print_emission(id="A", **print_emission_args), - RxOps.flat_map(process_if_free), - MyOps.print_emission(id="M", **print_emission_args), - ) + self.append_history(msg) # type: ignore[arg-type] - disposable = observable.subscribe( - on_next=lambda response: self._log_response_to_file(response, self.output_dir), - on_error=lambda e: logger.error(f"Error encountered: {e}"), - on_completed=lambda: logger.info(f"Stream processing completed for {self.dev_name}"), - ) - self.disposables.add(disposable) - return disposable - - def subscribe_to_query_processing(self, query_observable: Observable) -> Disposable: - """Subscribes to a stream of queries for processing. - - This method sets up a subscription to process incoming queries by directly - calling the _observable_query method. The responses are logged to a file. - - Args: - query_observable (Observable): An observable emitting queries. - - Returns: - Disposable: A disposable representing the subscription. - """ - print_emission_args = {"enabled": False, "dev_name": self.dev_name, "counts": {}} - - def _process_query(query) -> Observable: - """ - Processes a single query by logging it and passing it to _observable_query. - Returns an observable that emits the LLM response. - """ - return just(query).pipe( - MyOps.print_emission(id="Pr A", **print_emission_args), - RxOps.flat_map( - lambda query: create( - lambda observer, _: self._observable_query(observer, incoming_query=query) - ) - ), - MyOps.print_emission(id="Pr B", **print_emission_args), - ) + logger.info(f"Agent response: {msg.content}") - # A mutable flag indicating whether a query is currently being processed. - is_processing = [False] - - def process_if_free(query): - logger.info(f"Processing Query: {query}") - if not self.process_all_inputs and is_processing[0]: - # Drop query if a request is already in progress and process_all_inputs is False - return empty() - else: - is_processing[0] = True - logger.info("Processing Query.") - return _process_query(query).pipe( - MyOps.print_emission(id="B", **print_emission_args), - RxOps.observe_on(self.pool_scheduler), - MyOps.print_emission(id="C", **print_emission_args), - RxOps.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id="D", **print_emission_args), - RxOps.do_action( - on_completed=lambda: is_processing.__setitem__(0, False), - on_error=lambda e: is_processing.__setitem__(0, False), - ), - MyOps.print_emission(id="E", **print_emission_args), - ) + state = _get_state() - observable = query_observable.pipe( - MyOps.print_emission(id="A", **print_emission_args), - RxOps.flat_map(lambda query: process_if_free(query)), - MyOps.print_emission(id="F", **print_emission_args), - ) + if msg.tool_calls: + self.execute_tool_calls(msg.tool_calls) - disposable = observable.subscribe( - on_next=lambda response: self._log_response_to_file(response, self.output_dir), - on_error=lambda e: logger.error(f"Error processing query for {self.dev_name}: {e}"), - on_completed=lambda: logger.info(f"Stream processing completed for {self.dev_name}"), - ) - self.disposables.add(disposable) - return disposable - - def get_response_observable(self) -> Observable: - """Gets an observable that emits responses from this agent. - - Returns: - Observable: An observable that emits string responses from the agent. - """ - return self.response_subject.pipe( - RxOps.observe_on(self.pool_scheduler), - RxOps.subscribe_on(self.pool_scheduler), - RxOps.share(), - ) + # print(self) + # print(self.coordinator) - def run_observable_query(self, query_text: str, **kwargs) -> Observable: - """Creates an observable that processes a one-off text query to Agent and emits the response. - - This method provides a simple way to send a text query and get an observable - stream of the response. It's designed for one-off queries rather than - continuous processing of input streams. Useful for testing and development. - - Args: - query_text (str): The query text to process. - **kwargs: Additional arguments to pass to _observable_query. Supported args vary by agent type. - For example, ClaudeAgent supports: base64_image, dimensions, override_token_limit, - reset_conversation, thinking_budget_tokens - - Returns: - Observable: An observable that emits the response as a string. - """ - return create( - lambda observer, _: self._observable_query( - observer, incoming_query=query_text, **kwargs - ) - ) - - def dispose_all(self) -> None: - """Disposes of all active subscriptions managed by this agent.""" - super().dispose_all() - self.response_subject.on_completed() + self._write_debug_history_file() + if not self.coordinator.has_active_skills(): + logger.info("No active tasks, exiting agent loop.") + return msg.content -# endregion LLMAgent Base Class (Generic LLM Agent) + # coordinator will continue once a skill state has changed in + # such a way that agent call needs to be executed + if state == _get_state(): + await self.coordinator.wait_for_updates() -# ----------------------------------------------------------------------------- -# region OpenAIAgent Subclass (OpenAI-Specific Implementation) -# ----------------------------------------------------------------------------- -class OpenAIAgent(LLMAgent): - """OpenAI agent implementation that uses OpenAI's API for processing. + # we request a full snapshot of currently running, finished or errored out skills + # we ask for removal of finished skills from subsequent snapshots (clear=True) + update = self.coordinator.generate_snapshot(clear=True) - This class implements the _send_query method to interact with OpenAI's API. - It also sets up OpenAI-specific parameters, such as the client, model name, - tokenizer, and response model. - """ + # generate tool_msgs and general state update message, + # depending on a skill having associated tool call from previous interaction + # we will return a tool message, and not a general state message + snapshot_msgs = snapshot_to_messages(update, msg.tool_calls) - def __init__( - self, - dev_name: str, - agent_type: str = "Vision", - query: str = "What do you see?", - input_query_stream: Observable | None = None, - input_data_stream: Observable | None = None, - input_video_stream: Observable | None = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: AbstractAgentSemanticMemory | None = None, - system_query: str | None = None, - max_input_tokens_per_request: int = 128000, - max_output_tokens_per_request: int = 16384, - model_name: str = "gpt-4o", - prompt_builder: PromptBuilder | None = None, - tokenizer: AbstractTokenizer | None = None, - rag_query_n: int = 4, - rag_similarity_threshold: float = 0.45, - skills: AbstractSkill | list[AbstractSkill] | SkillLibrary | None = None, - response_model: BaseModel | None = None, - frame_processor: FrameProcessor | None = None, - image_detail: str = "low", - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool | None = None, - openai_client: OpenAI | None = None, - ) -> None: - """ - Initializes a new instance of the OpenAIAgent. - - Args: - dev_name (str): The device name of the agent. - agent_type (str): The type of the agent. - query (str): The default query text. - input_query_stream (Observable): An observable for query input. - input_data_stream (Observable): An observable for data input. - input_video_stream (Observable): An observable for video frames. - output_dir (str): Directory for output files. - agent_memory (AbstractAgentSemanticMemory): The memory system. - system_query (str): The system prompt to use with RAG context. - max_input_tokens_per_request (int): Maximum tokens for input. - max_output_tokens_per_request (int): Maximum tokens for output. - model_name (str): The OpenAI model name to use. - prompt_builder (PromptBuilder): Custom prompt builder. - tokenizer (AbstractTokenizer): Custom tokenizer for token counting. - rag_query_n (int): Number of results to fetch in RAG queries. - rag_similarity_threshold (float): Minimum similarity for RAG results. - skills (Union[AbstractSkill, List[AbstractSkill], SkillLibrary]): Skills available to the agent. - response_model (BaseModel): Optional Pydantic model for responses. - frame_processor (FrameProcessor): Custom frame processor. - image_detail (str): Detail level for images ("low", "high", "auto"). - pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. - If None, the global scheduler from get_scheduler() will be used. - process_all_inputs (bool): Whether to process all inputs or skip when busy. - If None, defaults to True for text queries and merged streams, False for video streams. - openai_client (OpenAI): The OpenAI client to use. This can be used to specify - a custom OpenAI client if targetting another provider. - """ - # Determine appropriate default for process_all_inputs if not provided - if process_all_inputs is None: - if input_query_stream is not None: - process_all_inputs = True - else: - process_all_inputs = False - - super().__init__( - dev_name=dev_name, - agent_type=agent_type, - agent_memory=agent_memory, - pool_scheduler=pool_scheduler, - process_all_inputs=process_all_inputs, - system_query=system_query, - input_query_stream=input_query_stream, - input_data_stream=input_data_stream, - input_video_stream=input_video_stream, - ) - self.client = openai_client or OpenAI() - self.query = query - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - # Configure skill library. - self.skills = skills - self.skill_library = None - if isinstance(self.skills, SkillLibrary): - self.skill_library = self.skills - elif isinstance(self.skills, list): - self.skill_library = SkillLibrary() - for skill in self.skills: - self.skill_library.add(skill) - elif isinstance(self.skills, AbstractSkill): - self.skill_library = SkillLibrary() - self.skill_library.add(self.skills) - - self.response_model = response_model if response_model is not None else NOT_GIVEN - self.model_name = model_name - self.tokenizer = tokenizer or OpenAITokenizer(model_name=self.model_name) - self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, tokenizer=self.tokenizer - ) - self.rag_query_n = rag_query_n - self.rag_similarity_threshold = rag_similarity_threshold - self.image_detail = image_detail - self.max_output_tokens_per_request = max_output_tokens_per_request - self.max_input_tokens_per_request = max_input_tokens_per_request - self.max_tokens_per_request = max_input_tokens_per_request + max_output_tokens_per_request - - # Add static context to memory. - self._add_context_to_memory() - - self.frame_processor = frame_processor or FrameProcessor(delete_on_init=True) - - logger.info("OpenAI Agent Initialized.") - - def _add_context_to_memory(self) -> None: - """Adds initial context to the agent's memory.""" - context_data = [ - ( - "id0", - "Optical Flow is a technique used to track the movement of objects in a video sequence.", - ), - ( - "id1", - "Edge Detection is a technique used to identify the boundaries of objects in an image.", - ), - ("id2", "Video is a sequence of frames captured at regular intervals."), - ( - "id3", - "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects.", - ), - ( - "id4", - "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", - ), - ] - for doc_id, text in context_data: - self.agent_memory.add_vector(doc_id, text) - - def _send_query(self, messages: list) -> Any: - """Sends the query to OpenAI's API. - - Depending on whether a response model is provided, the appropriate API - call is made. - - Args: - messages (list): The prompt messages to send. - - Returns: - The response message from OpenAI. - - Raises: - Exception: If no response message is returned. - ConnectionError: If there's an issue connecting to the API. - ValueError: If the messages or other parameters are invalid. - """ - try: - if self.response_model is not NOT_GIVEN: - response = self.client.beta.chat.completions.parse( - model=self.model_name, - messages=messages, - response_format=self.response_model, - tools=( - self.skill_library.get_tools() - if self.skill_library is not None - else NOT_GIVEN - ), - max_tokens=self.max_output_tokens_per_request, - ) - else: - response = self.client.chat.completions.create( - model=self.model_name, - messages=messages, - max_tokens=self.max_output_tokens_per_request, - tools=( - self.skill_library.get_tools() - if self.skill_library is not None - else NOT_GIVEN - ), + self.state_messages = snapshot_msgs.get("state_msgs", []) # type: ignore[attr-defined] + self.append_history( + *snapshot_msgs.get("tool_msgs", []), # type: ignore[attr-defined] + *snapshot_msgs.get("history_msgs", []), # type: ignore[attr-defined] ) - response_message = response.choices[0].message - if response_message is None: - logger.error("Response message does not exist.") - raise Exception("Response message does not exist.") - return response_message - except ConnectionError as ce: - logger.error(f"Connection error with API: {ce}") - raise - except ValueError as ve: - logger.error(f"Invalid parameters: {ve}") - raise + except Exception as e: - logger.error(f"Unexpected error in API call: {e}") - raise + logger.error(f"Error in agent loop: {e}") + import traceback - def stream_query(self, query_text: str) -> Observable: - """Creates an observable that processes a text query and emits the response. + traceback.print_exc() - This method provides a simple way to send a text query and get an observable - stream of the response. It's designed for one-off queries rather than - continuous processing of input streams. + @rpc + def loop_thread(self) -> bool: + asyncio.run_coroutine_threadsafe(self.agent_loop(), self._loop) # type: ignore[arg-type] + return True - Args: - query_text (str): The query text to process. + @rpc + def query(self, query: str): # type: ignore[no-untyped-def] + # TODO: could this be + # from distributed.utils import sync + # return sync(self._loop, self.agent_loop, query) + return asyncio.run_coroutine_threadsafe(self.agent_loop(query), self._loop).result() # type: ignore[arg-type] - Returns: - Observable: An observable that emits the response as a string. - """ - return create( - lambda observer, _: self._observable_query(observer, incoming_query=query_text) - ) + async def query_async(self, query: str): # type: ignore[no-untyped-def] + return await self.agent_loop(query) + + @rpc + def register_skills(self, container, run_implicit_name: str | None = None): # type: ignore[no-untyped-def] + ret = self.coordinator.register_skills(container) # type: ignore[func-returns-value] + + if run_implicit_name: + self.run_implicit_skill(run_implicit_name) + + return ret + + def get_tools(self): # type: ignore[no-untyped-def] + return self.coordinator.get_tools() + + def _write_debug_history_file(self) -> None: + file_path = os.getenv("DEBUG_AGENT_HISTORY_FILE") + if not file_path: + return + + history = [x.__dict__ for x in self.history()] # type: ignore[no-untyped-call] + + with open(file_path, "w") as f: + json.dump(history, f, default=lambda x: repr(x), indent=2) + + +class LlmAgent(Agent): + @rpc + def start(self) -> None: + super().start() + self.loop_thread() + + @rpc + def stop(self) -> None: + super().stop() + + +llm_agent = LlmAgent.blueprint + + +def deploy( + dimos: DimosCluster, + system_prompt: str = "You are a helpful assistant for controlling a Unitree Go2 robot.", + model: Model = Model.GPT_4O, + provider: Provider = Provider.OPENAI, # type: ignore[attr-defined] + skill_containers: list[SkillContainer] | None = None, +) -> Agent: + from dimos.agents.cli.human import HumanInput + + if skill_containers is None: + skill_containers = [] + agent = dimos.deploy( # type: ignore[attr-defined] + Agent, + system_prompt=system_prompt, + model=model, + provider=provider, + ) + + human_input = dimos.deploy(HumanInput) # type: ignore[attr-defined] + human_input.start() + + agent.register_skills(human_input) + + for skill_container in skill_containers: + print("Registering skill container:", skill_container) + agent.register_skills(skill_container) + + agent.run_implicit_skill("human") + agent.start() + agent.loop_thread() + + return agent # type: ignore[no-any-return] -# endregion OpenAIAgent Subclass (OpenAI-Specific Implementation) +__all__ = ["Agent", "deploy", "llm_agent"] diff --git a/dimos/agents/agent_ctransformers_gguf.py b/dimos/agents/agent_ctransformers_gguf.py deleted file mode 100644 index 17d233437d..0000000000 --- a/dimos/agents/agent_ctransformers_gguf.py +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -# Standard library imports -import logging -import os -from typing import TYPE_CHECKING, Any - -# Third-party imports -from dotenv import load_dotenv -from reactivex import Observable, create -import torch - -# Local imports -from dimos.agents.agent import LLMAgent -from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.utils.logging_config import setup_logger - -# Initialize environment variables -load_dotenv() - -# Initialize logger for the agent module -logger = setup_logger("dimos.agents", level=logging.DEBUG) - -from ctransformers import AutoModelForCausalLM as CTransformersModel - -if TYPE_CHECKING: - from reactivex.scheduler import ThreadPoolScheduler - from reactivex.subject import Subject - - from dimos.agents.memory.base import AbstractAgentSemanticMemory - - -class CTransformersTokenizerAdapter: - def __init__(self, model) -> None: - self.model = model - - def encode(self, text: str, **kwargs): - return self.model.tokenize(text) - - def decode(self, token_ids, **kwargs): - return self.model.detokenize(token_ids) - - def token_count(self, text: str): - return len(self.tokenize_text(text)) if text else 0 - - def tokenize_text(self, text: str): - return self.model.tokenize(text) - - def detokenize_text(self, tokenized_text): - try: - return self.model.detokenize(tokenized_text) - except Exception as e: - raise ValueError(f"Failed to detokenize text. Error: {e!s}") - - def apply_chat_template( - self, conversation, tokenize: bool = False, add_generation_prompt: bool = True - ): - prompt = "" - for message in conversation: - role = message["role"] - content = message["content"] - if role == "system": - prompt += f"<|system|>\n{content}\n" - elif role == "user": - prompt += f"<|user|>\n{content}\n" - elif role == "assistant": - prompt += f"<|assistant|>\n{content}\n" - if add_generation_prompt: - prompt += "<|assistant|>\n" - return prompt - - -# CTransformers Agent Class -class CTransformersGGUFAgent(LLMAgent): - def __init__( - self, - dev_name: str, - agent_type: str = "HF-LLM", - model_name: str = "TheBloke/Llama-2-7B-GGUF", - model_file: str = "llama-2-7b.Q4_K_M.gguf", - model_type: str = "llama", - gpu_layers: int = 50, - device: str = "auto", - query: str = "How many r's are in the word 'strawberry'?", - input_query_stream: Observable | None = None, - input_video_stream: Observable | None = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: AbstractAgentSemanticMemory | None = None, - system_query: str | None = "You are a helpful assistant.", - max_output_tokens_per_request: int = 10, - max_input_tokens_per_request: int = 250, - prompt_builder: PromptBuilder | None = None, - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool | None = None, - ) -> None: - # Determine appropriate default for process_all_inputs if not provided - if process_all_inputs is None: - # Default to True for text queries, False for video streams - if input_query_stream is not None and input_video_stream is None: - process_all_inputs = True - else: - process_all_inputs = False - - super().__init__( - dev_name=dev_name, - agent_type=agent_type, - agent_memory=agent_memory, - pool_scheduler=pool_scheduler, - process_all_inputs=process_all_inputs, - system_query=system_query, - max_output_tokens_per_request=max_output_tokens_per_request, - max_input_tokens_per_request=max_input_tokens_per_request, - ) - - self.query = query - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - self.model_name = model_name - self.device = device - if self.device == "auto": - self.device = "cuda" if torch.cuda.is_available() else "cpu" - if self.device == "cuda": - print(f"Using GPU: {torch.cuda.get_device_name(0)}") - else: - print("GPU not available, using CPU") - print(f"Device: {self.device}") - - self.model = CTransformersModel.from_pretrained( - model_name, model_file=model_file, model_type=model_type, gpu_layers=gpu_layers - ) - - self.tokenizer = CTransformersTokenizerAdapter(self.model) - - self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, tokenizer=self.tokenizer - ) - - self.max_output_tokens_per_request = max_output_tokens_per_request - - # self.stream_query(self.query).subscribe(lambda x: print(x)) - - self.input_video_stream = input_video_stream - self.input_query_stream = input_query_stream - - # Ensure only one input stream is provided. - if self.input_video_stream is not None and self.input_query_stream is not None: - raise ValueError( - "More than one input stream provided. Please provide only one input stream." - ) - - if self.input_video_stream is not None: - logger.info("Subscribing to input video stream...") - self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) - if self.input_query_stream is not None: - logger.info("Subscribing to input query stream...") - self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) - - def _send_query(self, messages: list) -> Any: - try: - _BLUE_PRINT_COLOR: str = "\033[34m" - _RESET_COLOR: str = "\033[0m" - - # === FIX: Flatten message content === - flat_messages = [] - for msg in messages: - role = msg["role"] - content = msg["content"] - if isinstance(content, list): - # Assume it's a list of {'type': 'text', 'text': ...} - text_parts = [c["text"] for c in content if isinstance(c, dict) and "text" in c] - content = " ".join(text_parts) - flat_messages.append({"role": role, "content": content}) - - print(f"{_BLUE_PRINT_COLOR}Messages: {flat_messages}{_RESET_COLOR}") - - print("Applying chat template...") - prompt_text = self.tokenizer.apply_chat_template( - conversation=flat_messages, tokenize=False, add_generation_prompt=True - ) - print("Chat template applied.") - print(f"Prompt text:\n{prompt_text}") - - response = self.model(prompt_text, max_new_tokens=self.max_output_tokens_per_request) - print("Model response received.") - return response - - except Exception as e: - logger.error(f"Error during HuggingFace query: {e}") - return "Error processing request." - - def stream_query(self, query_text: str) -> Subject: - """ - Creates an observable that processes a text query and emits the response. - """ - return create( - lambda observer, _: self._observable_query(observer, incoming_query=query_text) - ) - - -# endregion HuggingFaceLLMAgent Subclass (HuggingFace-Specific Implementation) diff --git a/dimos/agents/agent_huggingface_local.py b/dimos/agents/agent_huggingface_local.py deleted file mode 100644 index 69d02bb1d2..0000000000 --- a/dimos/agents/agent_huggingface_local.py +++ /dev/null @@ -1,240 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -# Standard library imports -import logging -import os -from typing import TYPE_CHECKING, Any - -# Third-party imports -from dotenv import load_dotenv -from reactivex import Observable, create -import torch -from transformers import AutoModelForCausalLM - -# Local imports -from dimos.agents.agent import LLMAgent -from dimos.agents.memory.chroma_impl import LocalSemanticMemory -from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from reactivex.scheduler import ThreadPoolScheduler - from reactivex.subject import Subject - - from dimos.agents.memory.base import AbstractAgentSemanticMemory - from dimos.agents.tokenizer.base import AbstractTokenizer - -# Initialize environment variables -load_dotenv() - -# Initialize logger for the agent module -logger = setup_logger("dimos.agents", level=logging.DEBUG) - - -# HuggingFaceLLMAgent Class -class HuggingFaceLocalAgent(LLMAgent): - def __init__( - self, - dev_name: str, - agent_type: str = "HF-LLM", - model_name: str = "Qwen/Qwen2.5-3B", - device: str = "auto", - query: str = "How many r's are in the word 'strawberry'?", - input_query_stream: Observable | None = None, - input_video_stream: Observable | None = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: AbstractAgentSemanticMemory | None = None, - system_query: str | None = None, - max_output_tokens_per_request: int | None = None, - max_input_tokens_per_request: int | None = None, - prompt_builder: PromptBuilder | None = None, - tokenizer: AbstractTokenizer | None = None, - image_detail: str = "low", - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool | None = None, - ) -> None: - # Determine appropriate default for process_all_inputs if not provided - if process_all_inputs is None: - # Default to True for text queries, False for video streams - if input_query_stream is not None and input_video_stream is None: - process_all_inputs = True - else: - process_all_inputs = False - - super().__init__( - dev_name=dev_name, - agent_type=agent_type, - agent_memory=agent_memory or LocalSemanticMemory(), - pool_scheduler=pool_scheduler, - process_all_inputs=process_all_inputs, - system_query=system_query, - ) - - self.query = query - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - self.model_name = model_name - self.device = device - if self.device == "auto": - self.device = "cuda" if torch.cuda.is_available() else "cpu" - if self.device == "cuda": - print(f"Using GPU: {torch.cuda.get_device_name(0)}") - else: - print("GPU not available, using CPU") - print(f"Device: {self.device}") - - self.tokenizer = tokenizer or HuggingFaceTokenizer(self.model_name) - - self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, tokenizer=self.tokenizer - ) - - self.model = AutoModelForCausalLM.from_pretrained( - model_name, - torch_dtype=torch.float16 if self.device == "cuda" else torch.float32, - device_map=self.device, - ) - - self.max_output_tokens_per_request = max_output_tokens_per_request - - # self.stream_query(self.query).subscribe(lambda x: print(x)) - - self.input_video_stream = input_video_stream - self.input_query_stream = input_query_stream - - # Ensure only one input stream is provided. - if self.input_video_stream is not None and self.input_query_stream is not None: - raise ValueError( - "More than one input stream provided. Please provide only one input stream." - ) - - if self.input_video_stream is not None: - logger.info("Subscribing to input video stream...") - self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) - if self.input_query_stream is not None: - logger.info("Subscribing to input query stream...") - self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) - - def _send_query(self, messages: list) -> Any: - _BLUE_PRINT_COLOR: str = "\033[34m" - _RESET_COLOR: str = "\033[0m" - - try: - # Log the incoming messages - print(f"{_BLUE_PRINT_COLOR}Messages: {messages!s}{_RESET_COLOR}") - - # Process with chat template - try: - print("Applying chat template...") - prompt_text = self.tokenizer.tokenizer.apply_chat_template( - conversation=[{"role": "user", "content": str(messages)}], - tokenize=False, - add_generation_prompt=True, - ) - print("Chat template applied.") - - # Tokenize the prompt - print("Preparing model inputs...") - model_inputs = self.tokenizer.tokenizer([prompt_text], return_tensors="pt").to( - self.model.device - ) - print("Model inputs prepared.") - - # Generate the response - print("Generating response...") - generated_ids = self.model.generate( - **model_inputs, max_new_tokens=self.max_output_tokens_per_request - ) - - # Extract the generated tokens (excluding the input prompt tokens) - print("Processing generated output...") - generated_ids = [ - output_ids[len(input_ids) :] - for input_ids, output_ids in zip( - model_inputs.input_ids, generated_ids, strict=False - ) - ] - - # Convert tokens back to text - response = self.tokenizer.tokenizer.batch_decode( - generated_ids, skip_special_tokens=True - )[0] - print("Response successfully generated.") - - return response - - except AttributeError as e: - # Handle case where tokenizer doesn't have the expected methods - logger.warning(f"Chat template not available: {e}. Using simple format.") - # Continue with execution and use simple format - - except Exception as e: - # Log any other errors but continue execution - logger.warning( - f"Error in chat template processing: {e}. Falling back to simple format." - ) - - # Fallback approach for models without chat template support - # This code runs if the try block above raises an exception - print("Using simple prompt format...") - - # Convert messages to a simple text format - if ( - isinstance(messages, list) - and messages - and isinstance(messages[0], dict) - and "content" in messages[0] - ): - prompt_text = messages[0]["content"] - else: - prompt_text = str(messages) - - # Tokenize the prompt - model_inputs = self.tokenizer.tokenize_text(prompt_text) - model_inputs = torch.tensor([model_inputs], device=self.model.device) - - # Generate the response - generated_ids = self.model.generate( - input_ids=model_inputs, max_new_tokens=self.max_output_tokens_per_request - ) - - # Extract the generated tokens - generated_ids = generated_ids[0][len(model_inputs[0]) :] - - # Convert tokens back to text - response = self.tokenizer.detokenize_text(generated_ids.tolist()) - print("Response generated using simple format.") - - return response - - except Exception as e: - # Catch all other errors - logger.error(f"Error during query processing: {e}", exc_info=True) - return "Error processing request. Please try again." - - def stream_query(self, query_text: str) -> Subject: - """ - Creates an observable that processes a text query and emits the response. - """ - return create( - lambda observer, _: self._observable_query(observer, incoming_query=query_text) - ) - - -# endregion HuggingFaceLLMAgent Subclass (HuggingFace-Specific Implementation) diff --git a/dimos/agents/agent_huggingface_remote.py b/dimos/agents/agent_huggingface_remote.py deleted file mode 100644 index 5bb5b293d3..0000000000 --- a/dimos/agents/agent_huggingface_remote.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -# Standard library imports -import logging -import os -from typing import TYPE_CHECKING, Any - -# Third-party imports -from dotenv import load_dotenv -from huggingface_hub import InferenceClient -from reactivex import Observable, create - -# Local imports -from dimos.agents.agent import LLMAgent -from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from reactivex.scheduler import ThreadPoolScheduler - from reactivex.subject import Subject - - from dimos.agents.memory.base import AbstractAgentSemanticMemory - from dimos.agents.tokenizer.base import AbstractTokenizer - -# Initialize environment variables -load_dotenv() - -# Initialize logger for the agent module -logger = setup_logger("dimos.agents", level=logging.DEBUG) - - -# HuggingFaceLLMAgent Class -class HuggingFaceRemoteAgent(LLMAgent): - def __init__( - self, - dev_name: str, - agent_type: str = "HF-LLM", - model_name: str = "Qwen/QwQ-32B", - query: str = "How many r's are in the word 'strawberry'?", - input_query_stream: Observable | None = None, - input_video_stream: Observable | None = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: AbstractAgentSemanticMemory | None = None, - system_query: str | None = None, - max_output_tokens_per_request: int = 16384, - prompt_builder: PromptBuilder | None = None, - tokenizer: AbstractTokenizer | None = None, - image_detail: str = "low", - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool | None = None, - api_key: str | None = None, - hf_provider: str | None = None, - hf_base_url: str | None = None, - ) -> None: - # Determine appropriate default for process_all_inputs if not provided - if process_all_inputs is None: - # Default to True for text queries, False for video streams - if input_query_stream is not None and input_video_stream is None: - process_all_inputs = True - else: - process_all_inputs = False - - super().__init__( - dev_name=dev_name, - agent_type=agent_type, - agent_memory=agent_memory, - pool_scheduler=pool_scheduler, - process_all_inputs=process_all_inputs, - system_query=system_query, - ) - - self.query = query - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - self.model_name = model_name - self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, tokenizer=tokenizer or HuggingFaceTokenizer(self.model_name) - ) - - self.model_name = model_name - - self.max_output_tokens_per_request = max_output_tokens_per_request - - self.api_key = api_key or os.getenv("HF_TOKEN") - self.provider = hf_provider or "hf-inference" - self.base_url = hf_base_url or os.getenv("HUGGINGFACE_PRV_ENDPOINT") - self.client = InferenceClient( - provider=self.provider, - base_url=self.base_url, - api_key=self.api_key, - ) - - # self.stream_query(self.query).subscribe(lambda x: print(x)) - - self.input_video_stream = input_video_stream - self.input_query_stream = input_query_stream - - # Ensure only one input stream is provided. - if self.input_video_stream is not None and self.input_query_stream is not None: - raise ValueError( - "More than one input stream provided. Please provide only one input stream." - ) - - if self.input_video_stream is not None: - logger.info("Subscribing to input video stream...") - self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) - if self.input_query_stream is not None: - logger.info("Subscribing to input query stream...") - self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) - - def _send_query(self, messages: list) -> Any: - try: - completion = self.client.chat.completions.create( - model=self.model_name, - messages=messages, - max_tokens=self.max_output_tokens_per_request, - ) - - return completion.choices[0].message - except Exception as e: - logger.error(f"Error during HuggingFace query: {e}") - return "Error processing request." - - def stream_query(self, query_text: str) -> Subject: - """ - Creates an observable that processes a text query and emits the response. - """ - return create( - lambda observer, _: self._observable_query(observer, incoming_query=query_text) - ) diff --git a/dimos/agents/cerebras_agent.py b/dimos/agents/cerebras_agent.py deleted file mode 100644 index e58de812d0..0000000000 --- a/dimos/agents/cerebras_agent.py +++ /dev/null @@ -1,613 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Cerebras agent implementation for the DIMOS agent framework. - -This module provides a CerebrasAgent class that implements the LLMAgent interface -for Cerebras inference API using the official Cerebras Python SDK. -""" - -from __future__ import annotations - -import copy -import json -import os -import threading -import time -from typing import TYPE_CHECKING - -from cerebras.cloud.sdk import Cerebras -from dotenv import load_dotenv - -# Local imports -from dimos.agents.agent import LLMAgent -from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.agents.tokenizer.openai_tokenizer import OpenAITokenizer -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from pydantic import BaseModel - from reactivex import Observable - from reactivex.observer import Observer - from reactivex.scheduler import ThreadPoolScheduler - - from dimos.agents.memory.base import AbstractAgentSemanticMemory - from dimos.agents.tokenizer.base import AbstractTokenizer - from dimos.stream.frame_processor import FrameProcessor - -# Initialize environment variables -load_dotenv() - -# Initialize logger for the Cerebras agent -logger = setup_logger("dimos.agents.cerebras") - - -# Response object compatible with LLMAgent -class CerebrasResponseMessage(dict): - def __init__( - self, - content: str = "", - tool_calls=None, - ) -> None: - self.content = content - self.tool_calls = tool_calls or [] - self.parsed = None - - # Initialize as dict with the proper structure - super().__init__(self.to_dict()) - - def __str__(self) -> str: - # Return a string representation for logging - if self.content: - return self.content - elif self.tool_calls: - # Return JSON representation of the first tool call - if self.tool_calls: - tool_call = self.tool_calls[0] - tool_json = { - "name": tool_call.function.name, - "arguments": json.loads(tool_call.function.arguments), - } - return json.dumps(tool_json) - return "[No content]" - - def to_dict(self): - """Convert to dictionary format for JSON serialization.""" - result = {"role": "assistant", "content": self.content or ""} - - if self.tool_calls: - result["tool_calls"] = [] - for tool_call in self.tool_calls: - result["tool_calls"].append( - { - "id": tool_call.id, - "type": "function", - "function": { - "name": tool_call.function.name, - "arguments": tool_call.function.arguments, - }, - } - ) - - return result - - -class CerebrasAgent(LLMAgent): - """Cerebras agent implementation using the official Cerebras Python SDK. - - This class implements the _send_query method to interact with Cerebras API - using their official SDK, allowing most of the LLMAgent logic to be reused. - """ - - def __init__( - self, - dev_name: str, - agent_type: str = "Vision", - query: str = "What do you see?", - input_query_stream: Observable | None = None, - input_video_stream: Observable | None = None, - input_data_stream: Observable | None = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: AbstractAgentSemanticMemory | None = None, - system_query: str | None = None, - max_input_tokens_per_request: int = 128000, - max_output_tokens_per_request: int = 16384, - model_name: str = "llama-4-scout-17b-16e-instruct", - skills: AbstractSkill | list[AbstractSkill] | SkillLibrary | None = None, - response_model: BaseModel | None = None, - frame_processor: FrameProcessor | None = None, - image_detail: str = "low", - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool | None = None, - tokenizer: AbstractTokenizer | None = None, - prompt_builder: PromptBuilder | None = None, - ) -> None: - """ - Initializes a new instance of the CerebrasAgent. - - Args: - dev_name (str): The device name of the agent. - agent_type (str): The type of the agent. - query (str): The default query text. - input_query_stream (Observable): An observable for query input. - input_video_stream (Observable): An observable for video frames. - input_data_stream (Observable): An observable for data input. - output_dir (str): Directory for output files. - agent_memory (AbstractAgentSemanticMemory): The memory system. - system_query (str): The system prompt to use with RAG context. - max_input_tokens_per_request (int): Maximum tokens for input. - max_output_tokens_per_request (int): Maximum tokens for output. - model_name (str): The Cerebras model name to use. Available options: - - llama-4-scout-17b-16e-instruct (default, fastest) - - llama3.1-8b - - llama-3.3-70b - - qwen-3-32b - - deepseek-r1-distill-llama-70b (private preview) - skills (Union[AbstractSkill, List[AbstractSkill], SkillLibrary]): Skills available to the agent. - response_model (BaseModel): Optional Pydantic model for structured responses. - frame_processor (FrameProcessor): Custom frame processor. - image_detail (str): Detail level for images ("low", "high", "auto"). - pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. - process_all_inputs (bool): Whether to process all inputs or skip when busy. - tokenizer (AbstractTokenizer): The tokenizer for the agent. - prompt_builder (PromptBuilder): The prompt builder for the agent. - """ - # Determine appropriate default for process_all_inputs if not provided - if process_all_inputs is None: - # Default to True for text queries, False for video streams - if input_query_stream is not None and input_video_stream is None: - process_all_inputs = True - else: - process_all_inputs = False - - super().__init__( - dev_name=dev_name, - agent_type=agent_type, - agent_memory=agent_memory, - pool_scheduler=pool_scheduler, - process_all_inputs=process_all_inputs, - system_query=system_query, - input_query_stream=input_query_stream, - input_video_stream=input_video_stream, - input_data_stream=input_data_stream, - ) - - # Initialize Cerebras client - self.client = Cerebras() - - self.query = query - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - # Initialize conversation history for multi-turn conversations - self.conversation_history = [] - self._history_lock = threading.Lock() - - # Configure skills - self.skills = skills - self.skill_library = None - if isinstance(self.skills, SkillLibrary): - self.skill_library = self.skills - elif isinstance(self.skills, list): - self.skill_library = SkillLibrary() - for skill in self.skills: - self.skill_library.add(skill) - elif isinstance(self.skills, AbstractSkill): - self.skill_library = SkillLibrary() - self.skill_library.add(self.skills) - - self.response_model = response_model - self.model_name = model_name - self.image_detail = image_detail - self.max_output_tokens_per_request = max_output_tokens_per_request - self.max_input_tokens_per_request = max_input_tokens_per_request - self.max_tokens_per_request = max_input_tokens_per_request + max_output_tokens_per_request - - # Add static context to memory. - self._add_context_to_memory() - - # Initialize tokenizer and prompt builder - self.tokenizer = tokenizer or OpenAITokenizer( - model_name="gpt-4o" - ) # Use GPT-4 tokenizer for better accuracy - self.prompt_builder = prompt_builder or PromptBuilder( - model_name=self.model_name, - max_tokens=self.max_input_tokens_per_request, - tokenizer=self.tokenizer, - ) - - logger.info("Cerebras Agent Initialized.") - - def _add_context_to_memory(self) -> None: - """Adds initial context to the agent's memory.""" - context_data = [ - ( - "id0", - "Optical Flow is a technique used to track the movement of objects in a video sequence.", - ), - ( - "id1", - "Edge Detection is a technique used to identify the boundaries of objects in an image.", - ), - ("id2", "Video is a sequence of frames captured at regular intervals."), - ( - "id3", - "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects.", - ), - ( - "id4", - "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", - ), - ] - for doc_id, text in context_data: - self.agent_memory.add_vector(doc_id, text) - - def _build_prompt( - self, - messages: list, - base64_image: str | list[str] | None = None, - dimensions: tuple[int, int] | None = None, - override_token_limit: bool = False, - condensed_results: str = "", - ) -> list: - """Builds a prompt message specifically for Cerebras API. - - Args: - messages (list): Existing messages list to build upon. - base64_image (Union[str, List[str]]): Optional Base64-encoded image(s). - dimensions (Tuple[int, int]): Optional image dimensions. - override_token_limit (bool): Whether to override token limits. - condensed_results (str): The condensed RAG context. - - Returns: - list: Messages formatted for Cerebras API. - """ - # Add system message if provided and not already in history - if self.system_query and (not messages or messages[0].get("role") != "system"): - messages.insert(0, {"role": "system", "content": self.system_query}) - logger.info("Added system message to conversation") - - # Append user query while handling RAG - if condensed_results: - user_message = {"role": "user", "content": f"{condensed_results}\n\n{self.query}"} - logger.info("Created user message with RAG context") - else: - user_message = {"role": "user", "content": self.query} - - messages.append(user_message) - - if base64_image is not None: - # Handle both single image (str) and multiple images (List[str]) - images = [base64_image] if isinstance(base64_image, str) else base64_image - - # For Cerebras, we'll add images inline with text (OpenAI-style format) - for img in images: - img_content = [ - {"type": "text", "text": "Here is an image to analyze:"}, - { - "type": "image_url", - "image_url": { - "url": f"data:image/jpeg;base64,{img}", - "detail": self.image_detail, - }, - }, - ] - messages.append({"role": "user", "content": img_content}) - - logger.info(f"Added {len(images)} image(s) to conversation") - - # Use new truncation function - messages = self._truncate_messages(messages, override_token_limit) - - return messages - - def _truncate_messages(self, messages: list, override_token_limit: bool = False) -> list: - """Truncate messages if total tokens exceed 16k using existing truncate_tokens method. - - Args: - messages (list): List of message dictionaries - override_token_limit (bool): Whether to skip truncation - - Returns: - list: Messages with content truncated if needed - """ - if override_token_limit: - return messages - - total_tokens = 0 - for message in messages: - if isinstance(message.get("content"), str): - total_tokens += self.prompt_builder.tokenizer.token_count(message["content"]) - elif isinstance(message.get("content"), list): - for item in message["content"]: - if item.get("type") == "text": - total_tokens += self.prompt_builder.tokenizer.token_count(item["text"]) - elif item.get("type") == "image_url": - total_tokens += 85 - - if total_tokens > 16000: - excess_tokens = total_tokens - 16000 - current_tokens = total_tokens - - # Start from oldest messages and truncate until under 16k - for i in range(len(messages)): - if current_tokens <= 16000: - break - - msg = messages[i] - if msg.get("role") == "system": - continue - - if isinstance(msg.get("content"), str): - original_tokens = self.prompt_builder.tokenizer.token_count(msg["content"]) - # Calculate how much to truncate from this message - tokens_to_remove = min(excess_tokens, original_tokens // 3) - new_max_tokens = max(50, original_tokens - tokens_to_remove) - - msg["content"] = self.prompt_builder.truncate_tokens( - msg["content"], new_max_tokens, "truncate_end" - ) - - new_tokens = self.prompt_builder.tokenizer.token_count(msg["content"]) - tokens_saved = original_tokens - new_tokens - current_tokens -= tokens_saved - excess_tokens -= tokens_saved - - logger.info( - f"Truncated older messages using truncate_tokens, final tokens: {current_tokens}" - ) - else: - logger.info(f"No truncation needed, total tokens: {total_tokens}") - - return messages - - def clean_cerebras_schema(self, schema: dict) -> dict: - """Simple schema cleaner that removes unsupported fields for Cerebras API.""" - if not isinstance(schema, dict): - return schema - - # Removing the problematic fields that pydantic generates - cleaned = {} - unsupported_fields = { - "minItems", - "maxItems", - "uniqueItems", - "exclusiveMinimum", - "exclusiveMaximum", - "minimum", - "maximum", - } - - for key, value in schema.items(): - if key in unsupported_fields: - continue # Skip unsupported fields - elif isinstance(value, dict): - cleaned[key] = self.clean_cerebras_schema(value) - elif isinstance(value, list): - cleaned[key] = [ - self.clean_cerebras_schema(item) if isinstance(item, dict) else item - for item in value - ] - else: - cleaned[key] = value - - return cleaned - - def create_tool_call( - self, - name: str | None = None, - arguments: dict | None = None, - call_id: str | None = None, - content: str | None = None, - ): - """Create a tool call object from either direct parameters or JSON content.""" - # If content is provided, parse it as JSON - if content: - logger.info(f"Creating tool call from content: {content}") - try: - content_json = json.loads(content) - if ( - isinstance(content_json, dict) - and "name" in content_json - and "arguments" in content_json - ): - name = content_json["name"] - arguments = content_json["arguments"] - else: - return None - except json.JSONDecodeError: - logger.warning("Content appears to be JSON but failed to parse") - return None - - # Create the tool call object - if name and arguments is not None: - timestamp = int(time.time() * 1000000) # microsecond precision - tool_id = f"call_{timestamp}" - - logger.info(f"Creating tool call with timestamp ID: {tool_id}") - return type( - "ToolCall", - (), - { - "id": tool_id, - "function": type( - "Function", (), {"name": name, "arguments": json.dumps(arguments)} - ), - }, - ) - - return None - - def _send_query(self, messages: list) -> CerebrasResponseMessage: - """Sends the query to Cerebras API using the official Cerebras SDK. - - Args: - messages (list): The prompt messages to send. - - Returns: - The response message from Cerebras wrapped in our CerebrasResponseMessage class. - - Raises: - Exception: If no response message is returned from the API. - ConnectionError: If there's an issue connecting to the API. - ValueError: If the messages or other parameters are invalid. - """ - try: - # Prepare API call parameters - api_params = { - "model": self.model_name, - "messages": messages, - # "max_tokens": self.max_output_tokens_per_request, - } - - # Add tools if available - if self.skill_library and self.skill_library.get_tools(): - tools = self.skill_library.get_tools() - for tool in tools: - if "function" in tool and "parameters" in tool["function"]: - tool["function"]["parameters"] = self.clean_cerebras_schema( - tool["function"]["parameters"] - ) - api_params["tools"] = tools - api_params["tool_choice"] = "auto" - - if self.response_model is not None: - api_params["response_format"] = { - "type": "json_object", - "schema": self.response_model, - } - - # Make the API call - response = self.client.chat.completions.create(**api_params) - - raw_message = response.choices[0].message - if raw_message is None: - logger.error("Response message does not exist.") - raise Exception("Response message does not exist.") - - # Process response into final format - content = raw_message.content - tool_calls = getattr(raw_message, "tool_calls", None) - - # If no structured tool calls from API, try parsing content as JSON tool call - if not tool_calls and content and content.strip().startswith("{"): - parsed_tool_call = self.create_tool_call(content=content) - if parsed_tool_call: - tool_calls = [parsed_tool_call] - content = None - - return CerebrasResponseMessage(content=content, tool_calls=tool_calls) - - except ConnectionError as ce: - logger.error(f"Connection error with Cerebras API: {ce}") - raise - except ValueError as ve: - logger.error(f"Invalid parameters for Cerebras API: {ve}") - raise - except Exception as e: - # Print the raw API parameters when an error occurs - logger.error(f"Raw API parameters: {json.dumps(api_params, indent=2)}") - logger.error(f"Unexpected error in Cerebras API call: {e}") - raise - - def _observable_query( - self, - observer: Observer, - base64_image: str | None = None, - dimensions: tuple[int, int] | None = None, - override_token_limit: bool = False, - incoming_query: str | None = None, - reset_conversation: bool = False, - ): - """Main query handler that manages conversation history and Cerebras interactions. - - This method follows ClaudeAgent's pattern for efficient conversation history management. - - Args: - observer (Observer): The observer to emit responses to. - base64_image (str): Optional Base64-encoded image. - dimensions (Tuple[int, int]): Optional image dimensions. - override_token_limit (bool): Whether to override token limits. - incoming_query (str): Optional query to update the agent's query. - reset_conversation (bool): Whether to reset the conversation history. - """ - try: - # Reset conversation history if requested - if reset_conversation: - self.conversation_history = [] - logger.info("Conversation history reset") - - # Create a local copy of conversation history and record its length - messages = copy.deepcopy(self.conversation_history) - - # Update query and get context - self._update_query(incoming_query) - _, condensed_results = self._get_rag_context() - - # Build prompt - messages = self._build_prompt( - messages, base64_image, dimensions, override_token_limit, condensed_results - ) - - while True: - logger.info("Sending Query.") - response_message = self._send_query(messages) - logger.info(f"Received Response: {response_message}") - - if response_message is None: - raise Exception("Response message does not exist.") - - # If no skill library or no tool calls, we're done - if ( - self.skill_library is None - or self.skill_library.get_tools() is None - or response_message.tool_calls is None - ): - final_msg = ( - response_message.parsed - if hasattr(response_message, "parsed") and response_message.parsed - else ( - response_message.content - if hasattr(response_message, "content") - else response_message - ) - ) - messages.append(response_message) - break - - logger.info(f"Assistant requested {len(response_message.tool_calls)} tool call(s)") - next_response = self._handle_tooling(response_message, messages) - - if next_response is None: - final_msg = response_message.content or "" - break - - response_message = next_response - - with self._history_lock: - self.conversation_history = messages - logger.info( - f"Updated conversation history (total: {len(self.conversation_history)} messages)" - ) - - # Emit the final message content to the observer - observer.on_next(final_msg) - self.response_subject.on_next(final_msg) - observer.on_completed() - - except Exception as e: - logger.error(f"Query failed in {self.dev_name}: {e}") - observer.on_error(e) - self.response_subject.on_error(e) diff --git a/dimos/agents/cli/human.py b/dimos/agents/cli/human.py new file mode 100644 index 0000000000..e842b3cc8a --- /dev/null +++ b/dimos/agents/cli/human.py @@ -0,0 +1,57 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import queue + +from reactivex.disposable import Disposable + +from dimos.agents import Output, Reducer, Stream, skill # type: ignore[attr-defined] +from dimos.core import pLCMTransport, rpc +from dimos.core.module import Module +from dimos.core.rpc_client import RpcCall + + +class HumanInput(Module): + running: bool = False + + @skill(stream=Stream.call_agent, reducer=Reducer.string, output=Output.human, hide_skill=True) # type: ignore[arg-type] + def human(self): # type: ignore[no-untyped-def] + """receives human input, no need to run this, it's running implicitly""" + if self.running: + return "already running" + self.running = True + transport = pLCMTransport("/human_input") # type: ignore[var-annotated] + + msg_queue = queue.Queue() # type: ignore[var-annotated] + unsub = transport.subscribe(msg_queue.put) # type: ignore[func-returns-value] + self._disposables.add(Disposable(unsub)) + yield from iter(msg_queue.get, None) + + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + + @rpc + def set_AgentSpec_register_skills(self, callable: RpcCall) -> None: + callable.set_rpc(self.rpc) # type: ignore[arg-type] + callable(self, run_implicit_name="human") + + +human_input = HumanInput.blueprint + +__all__ = ["HumanInput", "human_input"] diff --git a/dimos/agents/cli/web.py b/dimos/agents/cli/web.py new file mode 100644 index 0000000000..09d5400cdc --- /dev/null +++ b/dimos/agents/cli/web.py @@ -0,0 +1,87 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import Thread +from typing import TYPE_CHECKING + +import reactivex as rx +import reactivex.operators as ops + +from dimos.core import Module, rpc +from dimos.core.transport import pLCMTransport +from dimos.stream.audio.node_normalizer import AudioNormalizer +from dimos.stream.audio.stt.node_whisper import WhisperNode +from dimos.utils.logging_config import setup_logger +from dimos.web.robot_web_interface import RobotWebInterface + +if TYPE_CHECKING: + from dimos.stream.audio.base import AudioEvent + +logger = setup_logger() + + +class WebInput(Module): + _web_interface: RobotWebInterface | None = None + _thread: Thread | None = None + _human_transport: pLCMTransport[str] | None = None + + @rpc + def start(self) -> None: + super().start() + + self._human_transport = pLCMTransport("/human_input") + + audio_subject: rx.subject.Subject[AudioEvent] = rx.subject.Subject() + + self._web_interface = RobotWebInterface( + port=5555, + text_streams={"agent_responses": rx.subject.Subject()}, + audio_subject=audio_subject, + ) + + normalizer = AudioNormalizer() + stt_node = WhisperNode() + + # Connect audio pipeline: browser audio → normalizer → whisper + normalizer.consume_audio(audio_subject.pipe(ops.share())) + stt_node.consume_audio(normalizer.emit_audio()) + + # Subscribe to both text input sources + # 1. Direct text from web interface + unsub = self._web_interface.query_stream.subscribe(self._human_transport.publish) + self._disposables.add(unsub) + + # 2. Transcribed text from STT + unsub = stt_node.emit_text().subscribe(self._human_transport.publish) + self._disposables.add(unsub) + + self._thread = Thread(target=self._web_interface.run, daemon=True) + self._thread.start() + + logger.info("Web interface started at http://localhost:5555") + + @rpc + def stop(self) -> None: + if self._web_interface: + self._web_interface.shutdown() + if self._thread: + self._thread.join(timeout=1.0) + if self._human_transport: + self._human_transport.lcm.stop() + super().stop() + + +web_input = WebInput.blueprint + +__all__ = ["WebInput", "web_input"] diff --git a/dimos/agents/conftest.py b/dimos/agents/conftest.py new file mode 100644 index 0000000000..52d7d5a6bb --- /dev/null +++ b/dimos/agents/conftest.py @@ -0,0 +1,85 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest + +from dimos.agents.agent import Agent +from dimos.agents.testing import MockModel +from dimos.protocol.skill.test_coordinator import SkillContainerTest + + +@pytest.fixture +def fixture_dir(): + return Path(__file__).parent / "fixtures" + + +@pytest.fixture +def potato_system_prompt() -> str: + return "Your name is Mr. Potato, potatoes are bad at math. Use a tools if asked to calculate" + + +@pytest.fixture +def skill_container(): + container = SkillContainerTest() + try: + yield container + finally: + container.stop() + + +@pytest.fixture +def create_fake_agent(fixture_dir): + agent = None + + def _agent_factory(*, system_prompt, skill_containers, fixture): + mock_model = MockModel(json_path=fixture_dir / fixture) + + nonlocal agent + agent = Agent(system_prompt=system_prompt, model_instance=mock_model) + + for skill_container in skill_containers: + agent.register_skills(skill_container) + + agent.start() + + return agent + + try: + yield _agent_factory + finally: + if agent: + agent.stop() + + +@pytest.fixture +def create_potato_agent(potato_system_prompt, skill_container, fixture_dir): + agent = None + + def _agent_factory(*, fixture): + mock_model = MockModel(json_path=fixture_dir / fixture) + + nonlocal agent + agent = Agent(system_prompt=potato_system_prompt, model_instance=mock_model) + agent.register_skills(skill_container) + agent.start() + + return agent + + try: + yield _agent_factory + finally: + if agent: + agent.stop() diff --git a/dimos/agents2/fixtures/test_get_gps_position_for_queries.json b/dimos/agents/fixtures/test_get_gps_position_for_queries.json similarity index 100% rename from dimos/agents2/fixtures/test_get_gps_position_for_queries.json rename to dimos/agents/fixtures/test_get_gps_position_for_queries.json diff --git a/dimos/agents2/fixtures/test_go_to_object.json b/dimos/agents/fixtures/test_go_to_object.json similarity index 100% rename from dimos/agents2/fixtures/test_go_to_object.json rename to dimos/agents/fixtures/test_go_to_object.json diff --git a/dimos/agents2/fixtures/test_go_to_semantic_location.json b/dimos/agents/fixtures/test_go_to_semantic_location.json similarity index 100% rename from dimos/agents2/fixtures/test_go_to_semantic_location.json rename to dimos/agents/fixtures/test_go_to_semantic_location.json diff --git a/dimos/agents2/fixtures/test_how_much_is_124181112_plus_124124.json b/dimos/agents/fixtures/test_how_much_is_124181112_plus_124124.json similarity index 100% rename from dimos/agents2/fixtures/test_how_much_is_124181112_plus_124124.json rename to dimos/agents/fixtures/test_how_much_is_124181112_plus_124124.json diff --git a/dimos/agents/fixtures/test_pounce.json b/dimos/agents/fixtures/test_pounce.json new file mode 100644 index 0000000000..99e84d003a --- /dev/null +++ b/dimos/agents/fixtures/test_pounce.json @@ -0,0 +1,38 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "execute_sport_command", + "args": { + "args": [ + "FrontPounce" + ] + }, + "id": "call_Ukj6bCAnHQLj28RHRp697blZ", + "type": "tool_call" + } + ] + }, + { + "content": "", + "tool_calls": [ + { + "name": "speak", + "args": { + "args": [ + "I have successfully performed a front pounce." + ] + }, + "id": "call_FR9DtqEvJ9zSY85qVD2UFrll", + "type": "tool_call" + } + ] + }, + { + "content": "I have successfully performed a front pounce.", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_set_gps_travel_points.json b/dimos/agents/fixtures/test_set_gps_travel_points.json similarity index 100% rename from dimos/agents2/fixtures/test_set_gps_travel_points.json rename to dimos/agents/fixtures/test_set_gps_travel_points.json diff --git a/dimos/agents2/fixtures/test_set_gps_travel_points_multiple.json b/dimos/agents/fixtures/test_set_gps_travel_points_multiple.json similarity index 100% rename from dimos/agents2/fixtures/test_set_gps_travel_points_multiple.json rename to dimos/agents/fixtures/test_set_gps_travel_points_multiple.json diff --git a/dimos/agents/fixtures/test_show_your_love.json b/dimos/agents/fixtures/test_show_your_love.json new file mode 100644 index 0000000000..941906e781 --- /dev/null +++ b/dimos/agents/fixtures/test_show_your_love.json @@ -0,0 +1,38 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "execute_sport_command", + "args": { + "args": [ + "FingerHeart" + ] + }, + "id": "call_VFp6x9F00FBmiiUiemFWewop", + "type": "tool_call" + } + ] + }, + { + "content": "", + "tool_calls": [ + { + "name": "speak", + "args": { + "args": [ + "Here's a gesture to show you some love!" + ] + }, + "id": "call_WUUmBJ95s9PtVx8YQsmlJ4EU", + "type": "tool_call" + } + ] + }, + { + "content": "Just did a finger heart gesture to show my affection!", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_stop_movement.json b/dimos/agents/fixtures/test_stop_movement.json similarity index 100% rename from dimos/agents2/fixtures/test_stop_movement.json rename to dimos/agents/fixtures/test_stop_movement.json diff --git a/dimos/agents2/fixtures/test_take_a_look_around.json b/dimos/agents/fixtures/test_take_a_look_around.json similarity index 100% rename from dimos/agents2/fixtures/test_take_a_look_around.json rename to dimos/agents/fixtures/test_take_a_look_around.json diff --git a/dimos/agents2/fixtures/test_what_do_you_see_in_this_picture.json b/dimos/agents/fixtures/test_what_do_you_see_in_this_picture.json similarity index 100% rename from dimos/agents2/fixtures/test_what_do_you_see_in_this_picture.json rename to dimos/agents/fixtures/test_what_do_you_see_in_this_picture.json diff --git a/dimos/agents2/fixtures/test_what_is_your_name.json b/dimos/agents/fixtures/test_what_is_your_name.json similarity index 100% rename from dimos/agents2/fixtures/test_what_is_your_name.json rename to dimos/agents/fixtures/test_what_is_your_name.json diff --git a/dimos/agents2/fixtures/test_where_am_i.json b/dimos/agents/fixtures/test_where_am_i.json similarity index 100% rename from dimos/agents2/fixtures/test_where_am_i.json rename to dimos/agents/fixtures/test_where_am_i.json diff --git a/dimos/agents/llm_init.py b/dimos/agents/llm_init.py new file mode 100644 index 0000000000..eb8c33c631 --- /dev/null +++ b/dimos/agents/llm_init.py @@ -0,0 +1,62 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import cast + +from langchain.chat_models import init_chat_model +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import SystemMessage +from langchain_huggingface import ChatHuggingFace, HuggingFacePipeline + +from dimos.agents.ollama_agent import ensure_ollama_model +from dimos.agents.spec import AgentConfig +from dimos.agents.system_prompt import SYSTEM_PROMPT + + +def build_llm(config: AgentConfig) -> BaseChatModel: + if config.model_instance: + return config.model_instance + + if config.provider.value.lower() == "ollama": + ensure_ollama_model(config.model) + + if config.provider.value.lower() == "huggingface": + llm = HuggingFacePipeline.from_model_id( + model_id=config.model, + task="text-generation", + pipeline_kwargs={ + "max_new_tokens": 512, + "temperature": 0.7, + }, + ) + return ChatHuggingFace(llm=llm, model_id=config.model) + + return cast( + "BaseChatModel", + init_chat_model( # type: ignore[call-overload] + model_provider=config.provider, + model=config.model, + ), + ) + + +def build_system_message(config: AgentConfig, *, append: str = "") -> SystemMessage: + if config.system_prompt: + if isinstance(config.system_prompt, str): + return SystemMessage(config.system_prompt + append) + if append: + config.system_prompt.content += append # type: ignore[operator] + return config.system_prompt + + return SystemMessage(SYSTEM_PROMPT + append) diff --git a/dimos/agents/memory/base.py b/dimos/agents/memory/base.py deleted file mode 100644 index eb48dcca44..0000000000 --- a/dimos/agents/memory/base.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import abstractmethod - -from dimos.exceptions.agent_memory_exceptions import ( - AgentMemoryConnectionError, - UnknownConnectionTypeError, -) -from dimos.utils.logging_config import setup_logger - -# TODO -# class AbstractAgentMemory(ABC): - -# TODO -# class AbstractAgentSymbolicMemory(AbstractAgentMemory): - - -class AbstractAgentSemanticMemory: # AbstractAgentMemory): - def __init__(self, connection_type: str = "local", **kwargs) -> None: - """ - Initialize with dynamic connection parameters. - Args: - connection_type (str): 'local' for a local database, 'remote' for a remote connection. - Raises: - UnknownConnectionTypeError: If an unrecognized connection type is specified. - AgentMemoryConnectionError: If initializing the database connection fails. - """ - self.logger = setup_logger(self.__class__.__name__) - self.logger.info("Initializing AgentMemory with connection type: %s", connection_type) - self.connection_params = kwargs - self.db_connection = ( - None # Holds the conection, whether local or remote, to the database used. - ) - - if connection_type not in ["local", "remote"]: - error = UnknownConnectionTypeError( - f"Invalid connection_type {connection_type}. Expected 'local' or 'remote'." - ) - self.logger.error(str(error)) - raise error - - try: - if connection_type == "remote": - self.connect() - elif connection_type == "local": - self.create() - except Exception as e: - self.logger.error("Failed to initialize database connection: %s", str(e), exc_info=True) - raise AgentMemoryConnectionError( - "Initialization failed due to an unexpected error.", cause=e - ) from e - - @abstractmethod - def connect(self): - """Establish a connection to the data store using dynamic parameters specified during initialization.""" - - @abstractmethod - def create(self): - """Create a local instance of the data store tailored to specific requirements.""" - - ## Create ## - @abstractmethod - def add_vector(self, vector_id, vector_data): - """Add a vector to the database. - Args: - vector_id (any): Unique identifier for the vector. - vector_data (any): The actual data of the vector to be stored. - """ - - ## Read ## - @abstractmethod - def get_vector(self, vector_id): - """Retrieve a vector from the database by its identifier. - Args: - vector_id (any): The identifier of the vector to retrieve. - """ - - @abstractmethod - def query(self, query_texts, n_results: int = 4, similarity_threshold=None): - """Performs a semantic search in the vector database. - - Args: - query_texts (Union[str, List[str]]): The query text or list of query texts to search for. - n_results (int, optional): Number of results to return. Defaults to 4. - similarity_threshold (float, optional): Minimum similarity score for results to be included [0.0, 1.0]. Defaults to None. - - Returns: - List[Tuple[Document, Optional[float]]]: A list of tuples containing the search results. Each tuple - contains: - Document: The retrieved document object. - Optional[float]: The similarity score of the match, or None if not applicable. - - Raises: - ValueError: If query_texts is empty or invalid. - ConnectionError: If database connection fails during query. - """ - - ## Update ## - @abstractmethod - def update_vector(self, vector_id, new_vector_data): - """Update an existing vector in the database. - Args: - vector_id (any): The identifier of the vector to update. - new_vector_data (any): The new data to replace the existing vector data. - """ - - ## Delete ## - @abstractmethod - def delete_vector(self, vector_id): - """Delete a vector from the database using its identifier. - Args: - vector_id (any): The identifier of the vector to delete. - """ - - -# query(string, metadata/tag, n_rets, kwargs) - -# query by string, timestamp, id, n_rets - -# (some sort of tag/metadata) - -# temporal diff --git a/dimos/agents/modules/__init__.py b/dimos/agents/modules/__init__.py deleted file mode 100644 index ee1269f8f5..0000000000 --- a/dimos/agents/modules/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agent modules for DimOS.""" diff --git a/dimos/agents/modules/agent_pool.py b/dimos/agents/modules/agent_pool.py deleted file mode 100644 index 08ef943765..0000000000 --- a/dimos/agents/modules/agent_pool.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agent pool module for managing multiple agents.""" - -from typing import Any - -from reactivex import operators as ops -from reactivex.subject import Subject - -from dimos.agents.modules.base_agent import BaseAgentModule -from dimos.agents.modules.unified_agent import UnifiedAgentModule -from dimos.core import In, Module, Out, rpc -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.agents.modules.agent_pool") - - -class AgentPoolModule(Module): - """Lightweight agent pool for managing multiple agents. - - This module enables: - - Multiple agent deployment with different configurations - - Query routing based on agent ID or capabilities - - Load balancing across agents - - Response aggregation from multiple agents - """ - - # Module I/O - query_in: In[dict[str, Any]] = None # {agent_id: str, query: str, ...} - response_out: Out[dict[str, Any]] = None # {agent_id: str, response: str, ...} - - def __init__( - self, agents_config: dict[str, dict[str, Any]], default_agent: str | None = None - ) -> None: - """Initialize agent pool. - - Args: - agents_config: Configuration for each agent - { - "agent_id": { - "model": "openai::gpt-4o", - "skills": SkillLibrary(), - "system_prompt": "...", - ... - } - } - default_agent: Default agent ID to use if not specified - """ - super().__init__() - - self._config = agents_config - self._default_agent = default_agent or next(iter(agents_config.keys())) - self._agents = {} - - # Response routing - self._response_subject = Subject() - - @rpc - def start(self) -> None: - """Deploy and start all agents.""" - super().start() - logger.info(f"Starting agent pool with {len(self._config)} agents") - - # Deploy agents based on config - for agent_id, config in self._config.items(): - logger.info(f"Deploying agent: {agent_id}") - - # Determine agent type - agent_type = config.pop("type", "unified") - - if agent_type == "base": - agent = BaseAgentModule(**config) - else: - agent = UnifiedAgentModule(**config) - - # Start the agent - agent.start() - - # Store agent with metadata - self._agents[agent_id] = {"module": agent, "config": config, "type": agent_type} - - # Subscribe to agent responses - self._setup_agent_routing(agent_id, agent) - - # Subscribe to incoming queries - if self.query_in: - self._disposables.add(self.query_in.observable().subscribe(self._route_query)) - - # Connect response subject to output - if self.response_out: - self._disposables.add(self._response_subject.subscribe(self.response_out.publish)) - - logger.info("Agent pool started") - - @rpc - def stop(self) -> None: - """Stop all agents.""" - logger.info("Stopping agent pool") - - # Stop all agents - for agent_id, agent_info in self._agents.items(): - try: - agent_info["module"].stop() - except Exception as e: - logger.error(f"Error stopping agent {agent_id}: {e}") - - # Clear agents - self._agents.clear() - super().stop() - - @rpc - def add_agent(self, agent_id: str, config: dict[str, Any]) -> None: - """Add a new agent to the pool.""" - if agent_id in self._agents: - logger.warning(f"Agent {agent_id} already exists") - return - - # Deploy and start agent - agent_type = config.pop("type", "unified") - - if agent_type == "base": - agent = BaseAgentModule(**config) - else: - agent = UnifiedAgentModule(**config) - - agent.start() - - # Store and setup routing - self._agents[agent_id] = {"module": agent, "config": config, "type": agent_type} - self._setup_agent_routing(agent_id, agent) - - logger.info(f"Added agent: {agent_id}") - - @rpc - def remove_agent(self, agent_id: str) -> None: - """Remove an agent from the pool.""" - if agent_id not in self._agents: - logger.warning(f"Agent {agent_id} not found") - return - - # Stop and remove agent - agent_info = self._agents[agent_id] - agent_info["module"].stop() - del self._agents[agent_id] - - logger.info(f"Removed agent: {agent_id}") - - @rpc - def list_agents(self) -> list[dict[str, Any]]: - """List all agents and their configurations.""" - return [ - {"id": agent_id, "type": info["type"], "model": info["config"].get("model", "unknown")} - for agent_id, info in self._agents.items() - ] - - @rpc - def broadcast_query(self, query: str, exclude: list[str] | None = None) -> None: - """Send query to all agents (except excluded ones).""" - exclude = exclude or [] - - for agent_id, agent_info in self._agents.items(): - if agent_id not in exclude: - agent_info["module"].query_in.publish(query) - - logger.info(f"Broadcasted query to {len(self._agents) - len(exclude)} agents") - - def _setup_agent_routing( - self, agent_id: str, agent: BaseAgentModule | UnifiedAgentModule - ) -> None: - """Setup response routing for an agent.""" - - # Subscribe to agent responses and tag with agent_id - def tag_response(response: str) -> dict[str, Any]: - return { - "agent_id": agent_id, - "response": response, - "type": self._agents[agent_id]["type"], - } - - self._disposables.add( - agent.response_out.observable() - .pipe(ops.map(tag_response)) - .subscribe(self._response_subject.on_next) - ) - - def _route_query(self, msg: dict[str, Any]) -> None: - """Route incoming query to appropriate agent(s).""" - # Extract routing info - agent_id = msg.get("agent_id", self._default_agent) - query = msg.get("query", "") - broadcast = msg.get("broadcast", False) - - if broadcast: - # Send to all agents - exclude = msg.get("exclude", []) - self.broadcast_query(query, exclude) - elif agent_id == "round_robin": - # Simple round-robin routing - agent_ids = list(self._agents.keys()) - if agent_ids: - # Use query hash for consistent routing - idx = hash(query) % len(agent_ids) - selected_agent = agent_ids[idx] - self._agents[selected_agent]["module"].query_in.publish(query) - logger.debug(f"Routed to {selected_agent} (round-robin)") - elif agent_id in self._agents: - # Route to specific agent - self._agents[agent_id]["module"].query_in.publish(query) - logger.debug(f"Routed to {agent_id}") - else: - logger.warning(f"Unknown agent ID: {agent_id}, using default: {self._default_agent}") - if self._default_agent in self._agents: - self._agents[self._default_agent]["module"].query_in.publish(query) - - # Handle additional routing options - if "image" in msg and hasattr(self._agents.get(agent_id, {}).get("module"), "image_in"): - self._agents[agent_id]["module"].image_in.publish(msg["image"]) - - if "data" in msg and hasattr(self._agents.get(agent_id, {}).get("module"), "data_in"): - self._agents[agent_id]["module"].data_in.publish(msg["data"]) diff --git a/dimos/agents/modules/base.py b/dimos/agents/modules/base.py deleted file mode 100644 index 9caaac49cc..0000000000 --- a/dimos/agents/modules/base.py +++ /dev/null @@ -1,525 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Base agent class with all features (non-module).""" - -import asyncio -from concurrent.futures import ThreadPoolExecutor -import json -from typing import Any - -from reactivex.subject import Subject - -from dimos.agents.agent_message import AgentMessage -from dimos.agents.agent_types import AgentResponse, ConversationHistory, ToolCall -from dimos.agents.memory.base import AbstractAgentSemanticMemory -from dimos.agents.memory.chroma_impl import OpenAISemanticMemory -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.utils.logging_config import setup_logger - -try: - from .gateway import UnifiedGatewayClient -except ImportError: - from dimos.agents.modules.gateway import UnifiedGatewayClient - -logger = setup_logger("dimos.agents.modules.base") - -# Vision-capable models -VISION_MODELS = { - "openai::gpt-4o", - "openai::gpt-4o-mini", - "openai::gpt-4-turbo", - "openai::gpt-4-vision-preview", - "anthropic::claude-3-haiku-20240307", - "anthropic::claude-3-sonnet-20241022", - "anthropic::claude-3-opus-20240229", - "anthropic::claude-3-5-sonnet-20241022", - "anthropic::claude-3-5-haiku-latest", - "qwen::qwen-vl-plus", - "qwen::qwen-vl-max", -} - - -class BaseAgent: - """Base agent with all features including memory, skills, and multimodal support. - - This class provides: - - LLM gateway integration - - Conversation history - - Semantic memory (RAG) - - Skills/tools execution - - Multimodal support (text, images, data) - - Model capability detection - """ - - def __init__( - self, - model: str = "openai::gpt-4o-mini", - system_prompt: str | None = None, - skills: SkillLibrary | list[AbstractSkill] | AbstractSkill | None = None, - memory: AbstractAgentSemanticMemory | None = None, - temperature: float = 0.0, - max_tokens: int = 4096, - max_input_tokens: int = 128000, - max_history: int = 20, - rag_n: int = 4, - rag_threshold: float = 0.45, - seed: int | None = None, - # Legacy compatibility - dev_name: str = "BaseAgent", - agent_type: str = "LLM", - **kwargs, - ) -> None: - """Initialize the base agent with all features. - - Args: - model: Model identifier (e.g., "openai::gpt-4o", "anthropic::claude-3-haiku") - system_prompt: System prompt for the agent - skills: Skills/tools available to the agent - memory: Semantic memory system for RAG - temperature: Sampling temperature - max_tokens: Maximum tokens to generate - max_input_tokens: Maximum input tokens - max_history: Maximum conversation history to keep - rag_n: Number of RAG results to fetch - rag_threshold: Minimum similarity for RAG results - seed: Random seed for deterministic outputs (if supported by model) - dev_name: Device/agent name for logging - agent_type: Type of agent for logging - """ - self.model = model - self.system_prompt = system_prompt or "You are a helpful AI assistant." - self.temperature = temperature - self.max_tokens = max_tokens - self.max_input_tokens = max_input_tokens - self._max_history = max_history - self.rag_n = rag_n - self.rag_threshold = rag_threshold - self.seed = seed - self.dev_name = dev_name - self.agent_type = agent_type - - # Initialize skills - if skills is None: - self.skills = SkillLibrary() - elif isinstance(skills, SkillLibrary): - self.skills = skills - elif isinstance(skills, list): - self.skills = SkillLibrary() - for skill in skills: - self.skills.add(skill) - elif isinstance(skills, AbstractSkill): - self.skills = SkillLibrary() - self.skills.add(skills) - else: - self.skills = SkillLibrary() - - # Initialize memory - allow None for testing - if memory is False: # Explicit False means no memory - self.memory = None - else: - self.memory = memory or OpenAISemanticMemory() - - # Initialize gateway - self.gateway = UnifiedGatewayClient() - - # Conversation history with proper format management - self.conversation = ConversationHistory(max_size=self._max_history) - - # Thread pool for async operations - self._executor = ThreadPoolExecutor(max_workers=2) - - # Response subject for emitting responses - self.response_subject = Subject() - - # Check model capabilities - self._supports_vision = self._check_vision_support() - - # Initialize memory with default context - self._initialize_memory() - - @property - def max_history(self) -> int: - """Get max history size.""" - return self._max_history - - @max_history.setter - def max_history(self, value: int) -> None: - """Set max history size and update conversation.""" - self._max_history = value - self.conversation.max_size = value - - def _check_vision_support(self) -> bool: - """Check if the model supports vision.""" - return self.model in VISION_MODELS - - def _initialize_memory(self) -> None: - """Initialize memory with default context.""" - try: - contexts = [ - ("ctx1", "I am an AI assistant that can help with various tasks."), - ("ctx2", f"I am using the {self.model} model."), - ( - "ctx3", - "I have access to tools and skills for specific operations." - if len(self.skills) > 0 - else "I do not have access to external tools.", - ), - ( - "ctx4", - "I can process images and visual content." - if self._supports_vision - else "I cannot process visual content.", - ), - ] - if self.memory: - for doc_id, text in contexts: - self.memory.add_vector(doc_id, text) - except Exception as e: - logger.warning(f"Failed to initialize memory: {e}") - - async def _process_query_async(self, agent_msg: AgentMessage) -> AgentResponse: - """Process query asynchronously and return AgentResponse.""" - query_text = agent_msg.get_combined_text() - logger.info(f"Processing query: {query_text}") - - # Get RAG context - rag_context = self._get_rag_context(query_text) - - # Check if trying to use images with non-vision model - if agent_msg.has_images() and not self._supports_vision: - logger.warning(f"Model {self.model} does not support vision. Ignoring image input.") - # Clear images from message - agent_msg.images.clear() - - # Build messages - pass AgentMessage directly - messages = self._build_messages(agent_msg, rag_context) - - # Get tools if available - tools = self.skills.get_tools() if len(self.skills) > 0 else None - - # Debug logging before gateway call - logger.debug("=== Gateway Request ===") - logger.debug(f"Model: {self.model}") - logger.debug(f"Number of messages: {len(messages)}") - for i, msg in enumerate(messages): - role = msg.get("role", "unknown") - content = msg.get("content", "") - if isinstance(content, str): - content_preview = content[:100] - elif isinstance(content, list): - content_preview = f"[{len(content)} content blocks]" - else: - content_preview = str(content)[:100] - logger.debug(f" Message {i}: role={role}, content={content_preview}...") - logger.debug(f"Tools available: {len(tools) if tools else 0}") - logger.debug("======================") - - # Prepare inference parameters - inference_params = { - "model": self.model, - "messages": messages, - "tools": tools, - "temperature": self.temperature, - "max_tokens": self.max_tokens, - "stream": False, - } - - # Add seed if provided - if self.seed is not None: - inference_params["seed"] = self.seed - - # Make inference call - response = await self.gateway.ainference(**inference_params) - - # Extract response - message = response["choices"][0]["message"] - content = message.get("content", "") - - # Don't update history yet - wait until we have the complete interaction - # This follows Claude's pattern of locking history until tool execution is complete - - # Check for tool calls - tool_calls = None - if message.get("tool_calls"): - tool_calls = [ - ToolCall( - id=tc["id"], - name=tc["function"]["name"], - arguments=json.loads(tc["function"]["arguments"]), - status="pending", - ) - for tc in message["tool_calls"] - ] - - # Get the user message for history - user_message = messages[-1] - - # Handle tool calls (blocking by default) - final_content = await self._handle_tool_calls(tool_calls, messages, user_message) - - # Return response with tool information - return AgentResponse( - content=final_content, - role="assistant", - tool_calls=tool_calls, - requires_follow_up=False, # Already handled - metadata={"model": self.model}, - ) - else: - # No tools, add both user and assistant messages to history - # Get the user message content from the built message - user_msg = messages[-1] # Last message in messages is the user message - user_content = user_msg["content"] - - # Add to conversation history - logger.info("=== Adding to history (no tools) ===") - logger.info(f" Adding user message: {str(user_content)[:100]}...") - self.conversation.add_user_message(user_content) - logger.info(f" Adding assistant response: {content[:100]}...") - self.conversation.add_assistant_message(content) - logger.info(f" History size now: {self.conversation.size()}") - - return AgentResponse( - content=content, - role="assistant", - tool_calls=None, - requires_follow_up=False, - metadata={"model": self.model}, - ) - - def _get_rag_context(self, query: str) -> str: - """Get relevant context from memory.""" - if not self.memory: - return "" - - try: - results = self.memory.query( - query_texts=query, n_results=self.rag_n, similarity_threshold=self.rag_threshold - ) - - if results: - contexts = [doc.page_content for doc, _ in results] - return " | ".join(contexts) - except Exception as e: - logger.warning(f"RAG query failed: {e}") - - return "" - - def _build_messages( - self, agent_msg: AgentMessage, rag_context: str = "" - ) -> list[dict[str, Any]]: - """Build messages list from AgentMessage.""" - messages = [] - - # System prompt with RAG context if available - system_content = self.system_prompt - if rag_context: - system_content += f"\n\nRelevant context: {rag_context}" - messages.append({"role": "system", "content": system_content}) - - # Add conversation history in OpenAI format - history_messages = self.conversation.to_openai_format() - messages.extend(history_messages) - - # Debug history state - logger.info(f"=== Building messages with {len(history_messages)} history messages ===") - if history_messages: - for i, msg in enumerate(history_messages): - role = msg.get("role", "unknown") - content = msg.get("content", "") - if isinstance(content, str): - preview = content[:100] - elif isinstance(content, list): - preview = f"[{len(content)} content blocks]" - else: - preview = str(content)[:100] - logger.info(f" History[{i}]: role={role}, content={preview}") - - # Build user message content from AgentMessage - user_content = agent_msg.get_combined_text() if agent_msg.has_text() else "" - - # Handle images for vision models - if agent_msg.has_images() and self._supports_vision: - # Build content array with text and images - content = [] - if user_content: # Only add text if not empty - content.append({"type": "text", "text": user_content}) - - # Add all images from AgentMessage - for img in agent_msg.images: - content.append( - { - "type": "image_url", - "image_url": {"url": f"data:image/jpeg;base64,{img.base64_jpeg}"}, - } - ) - - logger.debug(f"Building message with {len(content)} content items (vision enabled)") - messages.append({"role": "user", "content": content}) - else: - # Text-only message - messages.append({"role": "user", "content": user_content}) - - return messages - - async def _handle_tool_calls( - self, - tool_calls: list[ToolCall], - messages: list[dict[str, Any]], - user_message: dict[str, Any], - ) -> str: - """Handle tool calls from LLM (blocking mode by default).""" - try: - # Build assistant message with tool calls - assistant_msg = { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "id": tc.id, - "type": "function", - "function": {"name": tc.name, "arguments": json.dumps(tc.arguments)}, - } - for tc in tool_calls - ], - } - messages.append(assistant_msg) - - # Execute tools and collect results - tool_results = [] - for tool_call in tool_calls: - logger.info(f"Executing tool: {tool_call.name}") - - try: - # Execute the tool - result = self.skills.call(tool_call.name, **tool_call.arguments) - tool_call.status = "completed" - - # Format tool result message - tool_result = { - "role": "tool", - "tool_call_id": tool_call.id, - "content": str(result), - "name": tool_call.name, - } - tool_results.append(tool_result) - - except Exception as e: - logger.error(f"Tool execution failed: {e}") - tool_call.status = "failed" - - # Add error result - tool_result = { - "role": "tool", - "tool_call_id": tool_call.id, - "content": f"Error: {e!s}", - "name": tool_call.name, - } - tool_results.append(tool_result) - - # Add tool results to messages - messages.extend(tool_results) - - # Prepare follow-up inference parameters - followup_params = { - "model": self.model, - "messages": messages, - "temperature": self.temperature, - "max_tokens": self.max_tokens, - } - - # Add seed if provided - if self.seed is not None: - followup_params["seed"] = self.seed - - # Get follow-up response - response = await self.gateway.ainference(**followup_params) - - # Extract final response - final_message = response["choices"][0]["message"] - - # Now add all messages to history in order (like Claude does) - # Add user message - user_content = user_message["content"] - self.conversation.add_user_message(user_content) - - # Add assistant message with tool calls - self.conversation.add_assistant_message("", tool_calls) - - # Add tool results - for result in tool_results: - self.conversation.add_tool_result( - tool_call_id=result["tool_call_id"], content=result["content"] - ) - - # Add final assistant response - final_content = final_message.get("content", "") - self.conversation.add_assistant_message(final_content) - - return final_message.get("content", "") - - except Exception as e: - logger.error(f"Error handling tool calls: {e}") - return f"Error executing tools: {e!s}" - - def query(self, message: str | AgentMessage) -> AgentResponse: - """Synchronous query method for direct usage. - - Args: - message: Either a string query or an AgentMessage with text and/or images - - Returns: - AgentResponse object with content and tool information - """ - # Convert string to AgentMessage if needed - if isinstance(message, str): - agent_msg = AgentMessage() - agent_msg.add_text(message) - else: - agent_msg = message - - # Run async method in a new event loop - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - return loop.run_until_complete(self._process_query_async(agent_msg)) - finally: - loop.close() - - async def aquery(self, message: str | AgentMessage) -> AgentResponse: - """Asynchronous query method. - - Args: - message: Either a string query or an AgentMessage with text and/or images - - Returns: - AgentResponse object with content and tool information - """ - # Convert string to AgentMessage if needed - if isinstance(message, str): - agent_msg = AgentMessage() - agent_msg.add_text(message) - else: - agent_msg = message - - return await self._process_query_async(agent_msg) - - def base_agent_dispose(self) -> None: - """Dispose of all resources and close gateway.""" - self.response_subject.on_completed() - if self._executor: - self._executor.shutdown(wait=False) - if self.gateway: - self.gateway.close() diff --git a/dimos/agents/modules/gateway/__init__.py b/dimos/agents/modules/gateway/__init__.py deleted file mode 100644 index 7ae4beb037..0000000000 --- a/dimos/agents/modules/gateway/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Gateway module for unified LLM access.""" - -from .client import UnifiedGatewayClient -from .utils import convert_tools_to_standard_format, parse_streaming_response - -__all__ = ["UnifiedGatewayClient", "convert_tools_to_standard_format", "parse_streaming_response"] diff --git a/dimos/agents/modules/gateway/utils.py b/dimos/agents/modules/gateway/utils.py deleted file mode 100644 index ac9dc3e364..0000000000 --- a/dimos/agents/modules/gateway/utils.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utility functions for gateway operations.""" - -import logging -from typing import Any - -logger = logging.getLogger(__name__) - - -def convert_tools_to_standard_format(tools: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Convert DimOS tool format to standard format accepted by gateways. - - DimOS tools come from pydantic_function_tool and have this format: - { - "type": "function", - "function": { - "name": "tool_name", - "description": "tool description", - "parameters": { - "type": "object", - "properties": {...}, - "required": [...] - } - } - } - - We keep this format as it's already standard JSON Schema format. - """ - if not tools: - return [] - - # Tools are already in the correct format from pydantic_function_tool - return tools - - -def parse_streaming_response(chunk: dict[str, Any]) -> dict[str, Any]: - """Parse a streaming response chunk into a standard format. - - Args: - chunk: Raw chunk from the gateway - - Returns: - Parsed chunk with standard fields: - - type: "content" | "tool_call" | "error" | "done" - - content: The actual content (text for content type, tool info for tool_call) - - metadata: Additional information - """ - # Handle TensorZero streaming format - if "choices" in chunk: - # OpenAI-style format from TensorZero - choice = chunk["choices"][0] if chunk["choices"] else {} - delta = choice.get("delta", {}) - - if "content" in delta: - return { - "type": "content", - "content": delta["content"], - "metadata": {"index": choice.get("index", 0)}, - } - elif "tool_calls" in delta: - tool_calls = delta["tool_calls"] - if tool_calls: - tool_call = tool_calls[0] - return { - "type": "tool_call", - "content": { - "id": tool_call.get("id"), - "name": tool_call.get("function", {}).get("name"), - "arguments": tool_call.get("function", {}).get("arguments", ""), - }, - "metadata": {"index": tool_call.get("index", 0)}, - } - elif choice.get("finish_reason"): - return { - "type": "done", - "content": None, - "metadata": {"finish_reason": choice["finish_reason"]}, - } - - # Handle direct content chunks - if isinstance(chunk, str): - return {"type": "content", "content": chunk, "metadata": {}} - - # Handle error responses - if "error" in chunk: - return {"type": "error", "content": chunk["error"], "metadata": chunk} - - # Default fallback - return {"type": "unknown", "content": chunk, "metadata": {}} - - -def create_tool_response(tool_id: str, result: Any, is_error: bool = False) -> dict[str, Any]: - """Create a properly formatted tool response. - - Args: - tool_id: The ID of the tool call - result: The result from executing the tool - is_error: Whether this is an error response - - Returns: - Formatted tool response message - """ - content = str(result) if not isinstance(result, str) else result - - return { - "role": "tool", - "tool_call_id": tool_id, - "content": content, - "name": None, # Will be filled by the calling code - } - - -def extract_image_from_message(message: dict[str, Any]) -> dict[str, Any] | None: - """Extract image data from a message if present. - - Args: - message: Message dict that may contain image data - - Returns: - Dict with image data and metadata, or None if no image - """ - content = message.get("content", []) - - # Handle list content (multimodal) - if isinstance(content, list): - for item in content: - if isinstance(item, dict): - # OpenAI format - if item.get("type") == "image_url": - return { - "format": "openai", - "data": item["image_url"]["url"], - "detail": item["image_url"].get("detail", "auto"), - } - # Anthropic format - elif item.get("type") == "image": - return { - "format": "anthropic", - "data": item["source"]["data"], - "media_type": item["source"].get("media_type", "image/jpeg"), - } - - return None diff --git a/dimos/agents/modules/simple_vision_agent.py b/dimos/agents/modules/simple_vision_agent.py deleted file mode 100644 index b4888fd073..0000000000 --- a/dimos/agents/modules/simple_vision_agent.py +++ /dev/null @@ -1,238 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Simple vision agent module following exact DimOS patterns.""" - -import asyncio -import base64 -import io -import threading - -import numpy as np -from PIL import Image as PILImage -from reactivex.disposable import Disposable - -from dimos.agents.modules.gateway import UnifiedGatewayClient -from dimos.core import In, Module, Out, rpc -from dimos.msgs.sensor_msgs import Image -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(__file__) - - -class SimpleVisionAgentModule(Module): - """Simple vision agent that can process images with text queries. - - This follows the exact pattern from working modules without any extras. - """ - - # Module I/O - query_in: In[str] = None - image_in: In[Image] = None - response_out: Out[str] = None - - def __init__( - self, - model: str = "openai::gpt-4o-mini", - system_prompt: str | None = None, - temperature: float = 0.0, - max_tokens: int = 4096, - ) -> None: - """Initialize the vision agent. - - Args: - model: Model identifier (e.g., "openai::gpt-4o-mini") - system_prompt: System prompt for the agent - temperature: Sampling temperature - max_tokens: Maximum tokens to generate - """ - super().__init__() - - self.model = model - self.system_prompt = system_prompt or "You are a helpful vision AI assistant." - self.temperature = temperature - self.max_tokens = max_tokens - - # State - self.gateway = None - self._latest_image = None - self._processing = False - self._lock = threading.Lock() - - @rpc - def start(self) -> None: - """Initialize and start the agent.""" - super().start() - - logger.info(f"Starting simple vision agent with model: {self.model}") - - # Initialize gateway - self.gateway = UnifiedGatewayClient() - - # Subscribe to inputs - if self.query_in: - unsub = self.query_in.subscribe(self._handle_query) - self._disposables.add(Disposable(unsub)) - - if self.image_in: - unsub = self.image_in.subscribe(self._handle_image) - self._disposables.add(Disposable(unsub)) - - logger.info("Simple vision agent started") - - @rpc - def stop(self) -> None: - logger.info("Stopping simple vision agent") - if self.gateway: - self.gateway.close() - - super().stop() - - def _handle_image(self, image: Image) -> None: - """Handle incoming image.""" - logger.info( - f"Received new image: {image.data.shape if hasattr(image, 'data') else 'unknown shape'}" - ) - self._latest_image = image - - def _handle_query(self, query: str) -> None: - """Handle text query.""" - with self._lock: - if self._processing: - logger.warning("Already processing, skipping query") - return - self._processing = True - - # Process in thread - thread = threading.Thread(target=self._run_async_query, args=(query,)) - thread.daemon = True - thread.start() - - def _run_async_query(self, query: str) -> None: - """Run async query in new event loop.""" - asyncio.run(self._process_query(query)) - - async def _process_query(self, query: str) -> None: - """Process the query.""" - try: - logger.info(f"Processing query: {query}") - - # Build messages - messages = [{"role": "system", "content": self.system_prompt}] - - # Check if we have an image - if self._latest_image: - logger.info("Have latest image, encoding...") - image_b64 = self._encode_image(self._latest_image) - if image_b64: - logger.info(f"Image encoded successfully, size: {len(image_b64)} bytes") - # Add user message with image - if "anthropic" in self.model: - # Anthropic format - messages.append( - { - "role": "user", - "content": [ - {"type": "text", "text": query}, - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/jpeg", - "data": image_b64, - }, - }, - ], - } - ) - else: - # OpenAI format - messages.append( - { - "role": "user", - "content": [ - {"type": "text", "text": query}, - { - "type": "image_url", - "image_url": { - "url": f"data:image/jpeg;base64,{image_b64}", - "detail": "auto", - }, - }, - ], - } - ) - else: - # No image encoding, just text - logger.warning("Failed to encode image") - messages.append({"role": "user", "content": query}) - else: - # No image at all - logger.warning("No image available") - messages.append({"role": "user", "content": query}) - - # Make inference call - response = await self.gateway.ainference( - model=self.model, - messages=messages, - temperature=self.temperature, - max_tokens=self.max_tokens, - stream=False, - ) - - # Extract response - message = response["choices"][0]["message"] - content = message.get("content", "") - - # Emit response - if self.response_out and content: - self.response_out.publish(content) - - except Exception as e: - logger.error(f"Error processing query: {e}") - import traceback - - traceback.print_exc() - if self.response_out: - self.response_out.publish(f"Error: {e!s}") - finally: - with self._lock: - self._processing = False - - def _encode_image(self, image: Image) -> str | None: - """Encode image to base64.""" - try: - # Convert to numpy array if needed - if hasattr(image, "data"): - img_array = image.data - else: - img_array = np.array(image) - - # Convert to PIL Image - pil_image = PILImage.fromarray(img_array) - - # Convert to RGB if needed - if pil_image.mode != "RGB": - pil_image = pil_image.convert("RGB") - - # Encode to base64 - buffer = io.BytesIO() - pil_image.save(buffer, format="JPEG") - img_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8") - - return img_b64 - - except Exception as e: - logger.error(f"Failed to encode image: {e}") - return None diff --git a/dimos/agents/ollama_agent.py b/dimos/agents/ollama_agent.py new file mode 100644 index 0000000000..4b35cc84f8 --- /dev/null +++ b/dimos/agents/ollama_agent.py @@ -0,0 +1,39 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ollama + +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +def ensure_ollama_model(model_name: str) -> None: + available_models = ollama.list() + model_exists = any(model_name == m.model for m in available_models.models) + if not model_exists: + logger.info(f"Ollama model '{model_name}' not found. Pulling...") + ollama.pull(model_name) + + +def ollama_installed() -> str | None: + try: + ollama.list() + return None + except Exception: + return ( + "Cannot connect to Ollama daemon. Please ensure Ollama is installed and running.\n" + "\n" + " For installation instructions, visit https://ollama.com/download" + ) diff --git a/dimos/agents/planning_agent.py b/dimos/agents/planning_agent.py deleted file mode 100644 index 6dbdbf5866..0000000000 --- a/dimos/agents/planning_agent.py +++ /dev/null @@ -1,318 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from textwrap import dedent -import threading -import time -from typing import Literal - -from pydantic import BaseModel -from reactivex import Observable, operators as ops - -from dimos.agents.agent import OpenAIAgent -from dimos.skills.skills import AbstractSkill -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.agents.planning_agent") - - -# For response validation -class PlanningAgentResponse(BaseModel): - type: Literal["dialogue", "plan"] - content: list[str] - needs_confirmation: bool - - -class PlanningAgent(OpenAIAgent): - """Agent that plans and breaks down tasks through dialogue. - - This agent specializes in: - 1. Understanding complex tasks through dialogue - 2. Breaking tasks into concrete, executable steps - 3. Refining plans based on user feedback - 4. Streaming individual steps to ExecutionAgents - - The agent maintains conversation state and can refine plans until - the user confirms they are ready to execute. - """ - - def __init__( - self, - dev_name: str = "PlanningAgent", - model_name: str = "gpt-4", - input_query_stream: Observable | None = None, - use_terminal: bool = False, - skills: AbstractSkill | None = None, - ) -> None: - """Initialize the planning agent. - - Args: - dev_name: Name identifier for the agent - model_name: OpenAI model to use - input_query_stream: Observable stream of user queries - use_terminal: Whether to enable terminal input - skills: Available skills/functions for the agent - """ - # Planning state - self.conversation_history = [] - self.current_plan = [] - self.plan_confirmed = False - self.latest_response = None - - # Build system prompt - skills_list = [] - if skills is not None: - skills_list = skills.get_tools() - - system_query = dedent(f""" - You are a Robot planning assistant that helps break down tasks into concrete, executable steps. - Your goal is to: - 1. Break down the task into clear, sequential steps - 2. Refine the plan based on user feedback as needed - 3. Only finalize the plan when the user explicitly confirms - - You have the following skills at your disposal: - {skills_list} - - IMPORTANT: You MUST ALWAYS respond with ONLY valid JSON in the following format, with no additional text or explanation: - {{ - "type": "dialogue" | "plan", - "content": string | list[string], - "needs_confirmation": boolean - }} - - Your goal is to: - 1. Understand the user's task through dialogue - 2. Break it down into clear, sequential steps - 3. Refine the plan based on user feedback - 4. Only finalize the plan when the user explicitly confirms - - For dialogue responses, use: - {{ - "type": "dialogue", - "content": "Your message to the user", - "needs_confirmation": false - }} - - For plan proposals, use: - {{ - "type": "plan", - "content": ["Execute", "Execute", ...], - "needs_confirmation": true - }} - - Remember: ONLY output valid JSON, no other text.""") - - # Initialize OpenAIAgent with our configuration - super().__init__( - dev_name=dev_name, - agent_type="Planning", - query="", # Will be set by process_user_input - model_name=model_name, - input_query_stream=input_query_stream, - system_query=system_query, - max_output_tokens_per_request=1000, - response_model=PlanningAgentResponse, - ) - logger.info("Planning agent initialized") - - # Set up terminal mode if requested - self.use_terminal = use_terminal - use_terminal = False - if use_terminal: - # Start terminal interface in a separate thread - logger.info("Starting terminal interface in a separate thread") - terminal_thread = threading.Thread(target=self.start_terminal_interface, daemon=True) - terminal_thread.start() - - def _handle_response(self, response) -> None: - """Handle the agent's response and update state. - - Args: - response: ParsedChatCompletionMessage containing PlanningAgentResponse - """ - print("handle response", response) - print("handle response type", type(response)) - - # Extract the PlanningAgentResponse from parsed field if available - planning_response = response.parsed if hasattr(response, "parsed") else response - print("planning response", planning_response) - print("planning response type", type(planning_response)) - # Convert to dict for storage in conversation history - response_dict = planning_response.model_dump() - self.conversation_history.append(response_dict) - - # If it's a plan, update current plan - if planning_response.type == "plan": - logger.info(f"Updating current plan: {planning_response.content}") - self.current_plan = planning_response.content - - # Store latest response - self.latest_response = response_dict - - def _stream_plan(self) -> None: - """Stream each step of the confirmed plan.""" - logger.info("Starting to stream plan steps") - logger.debug(f"Current plan: {self.current_plan}") - - for i, step in enumerate(self.current_plan, 1): - logger.info(f"Streaming step {i}: {step}") - # Add a small delay between steps to ensure they're processed - time.sleep(0.5) - try: - self.response_subject.on_next(str(step)) - logger.debug(f"Successfully emitted step {i} to response_subject") - except Exception as e: - logger.error(f"Error emitting step {i}: {e}") - - logger.info("Plan streaming completed") - self.response_subject.on_completed() - - def _send_query(self, messages: list) -> PlanningAgentResponse: - """Send query to OpenAI and parse the response. - - Extends OpenAIAgent's _send_query to handle planning-specific response formats. - - Args: - messages: List of message dictionaries - - Returns: - PlanningAgentResponse: Validated response with type, content, and needs_confirmation - """ - try: - return super()._send_query(messages) - except Exception as e: - logger.error(f"Caught exception in _send_query: {e!s}") - return PlanningAgentResponse( - type="dialogue", content=f"Error: {e!s}", needs_confirmation=False - ) - - def process_user_input(self, user_input: str) -> None: - """Process user input and generate appropriate response. - - Args: - user_input: The user's message - """ - if not user_input: - return - - # Check for plan confirmation - if self.current_plan and user_input.lower() in ["yes", "y", "confirm"]: - logger.info("Plan confirmation received") - self.plan_confirmed = True - # Create a proper PlanningAgentResponse with content as a list - confirmation_msg = PlanningAgentResponse( - type="dialogue", - content="Plan confirmed! Streaming steps to execution...", - needs_confirmation=False, - ) - self._handle_response(confirmation_msg) - self._stream_plan() - return - - # Build messages for OpenAI with conversation history - messages = [ - {"role": "system", "content": self.system_query} # Using system_query from OpenAIAgent - ] - - # Add the new user input to conversation history - self.conversation_history.append({"type": "user_message", "content": user_input}) - - # Add complete conversation history including both user and assistant messages - for msg in self.conversation_history: - if msg["type"] == "user_message": - messages.append({"role": "user", "content": msg["content"]}) - elif msg["type"] == "dialogue": - messages.append({"role": "assistant", "content": msg["content"]}) - elif msg["type"] == "plan": - plan_text = "Here's my proposed plan:\n" + "\n".join( - f"{i + 1}. {step}" for i, step in enumerate(msg["content"]) - ) - messages.append({"role": "assistant", "content": plan_text}) - - # Get and handle response - response = self._send_query(messages) - self._handle_response(response) - - def start_terminal_interface(self) -> None: - """Start the terminal interface for input/output.""" - - time.sleep(5) # buffer time for clean terminal interface printing - print("=" * 50) - print("\nDimOS Action PlanningAgent\n") - print("I have access to your Robot() and Robot Skills()") - print( - "Describe your task and I'll break it down into steps using your skills as a reference." - ) - print("Once you're happy with the plan, type 'yes' to execute it.") - print("Type 'quit' to exit.\n") - - while True: - try: - print("=" * 50) - user_input = input("USER > ") - if user_input.lower() in ["quit", "exit"]: - break - - self.process_user_input(user_input) - - # Display response - if self.latest_response["type"] == "dialogue": - print(f"\nPlanner: {self.latest_response['content']}") - elif self.latest_response["type"] == "plan": - print("\nProposed Plan:") - for i, step in enumerate(self.latest_response["content"], 1): - print(f"{i}. {step}") - if self.latest_response["needs_confirmation"]: - print("\nDoes this plan look good? (yes/no)") - - if self.plan_confirmed: - print("\nPlan confirmed! Streaming steps to execution...") - break - - except KeyboardInterrupt: - print("\nStopping...") - break - except Exception as e: - print(f"\nError: {e}") - break - - def get_response_observable(self) -> Observable: - """Gets an observable that emits responses from this agent. - - This method processes the response stream from the parent class, - extracting content from `PlanningAgentResponse` objects and flattening - any lists of plan steps for emission. - - Returns: - Observable: An observable that emits plan steps from the agent. - """ - - def extract_content(response) -> list[str]: - if isinstance(response, PlanningAgentResponse): - if response.type == "plan": - return response.content # List of steps to be emitted individually - else: # dialogue type - return [response.content] # Wrap single dialogue message in a list - else: - return [str(response)] # Wrap non-PlanningAgentResponse in a list - - # Get base observable from parent class - base_observable = super().get_response_observable() - - # Process the stream: extract content and flatten plan lists - return base_observable.pipe( - ops.map(extract_content), - ops.flat_map(lambda items: items), # Flatten the list of items - ) diff --git a/dimos/agents/skills/conftest.py b/dimos/agents/skills/conftest.py new file mode 100644 index 0000000000..0e2e3e0636 --- /dev/null +++ b/dimos/agents/skills/conftest.py @@ -0,0 +1,113 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import partial + +import pytest +from reactivex.scheduler import ThreadPoolScheduler + +from dimos.agents.skills.google_maps_skill_container import GoogleMapsSkillContainer +from dimos.agents.skills.gps_nav_skill import GpsNavSkillContainer +from dimos.agents.skills.navigation import NavigationSkillContainer +from dimos.agents.system_prompt import SYSTEM_PROMPT +from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer + + +@pytest.fixture(autouse=True) +def cleanup_threadpool_scheduler(monkeypatch): + # TODO: get rid of this global threadpool + """Clean up and recreate the global ThreadPoolScheduler after each test.""" + # Disable ChromaDB telemetry to avoid leaking threads + monkeypatch.setenv("CHROMA_ANONYMIZED_TELEMETRY", "False") + yield + from dimos.utils import threadpool + + # Shutdown the global scheduler's executor + threadpool.scheduler.executor.shutdown(wait=True) + # Recreate it for the next test + threadpool.scheduler = ThreadPoolScheduler(max_workers=threadpool.get_max_workers()) + + +@pytest.fixture +def navigation_skill_container(mocker): + container = NavigationSkillContainer() + container.color_image.connection = mocker.MagicMock() + container.odom.connection = mocker.MagicMock() + container.start() + yield container + container.stop() + + +@pytest.fixture +def gps_nav_skill_container(mocker): + container = GpsNavSkillContainer() + container.gps_location.connection = mocker.MagicMock() + container.gps_goal = mocker.MagicMock() + container.start() + yield container + container.stop() + + +@pytest.fixture +def google_maps_skill_container(mocker): + container = GoogleMapsSkillContainer() + container.gps_location.connection = mocker.MagicMock() + container.start() + container._client = mocker.MagicMock() + yield container + container.stop() + + +@pytest.fixture +def unitree_skills(mocker): + container = UnitreeSkillContainer() + container.start() + yield container + container.stop() + + +@pytest.fixture +def create_navigation_agent(navigation_skill_container, create_fake_agent): + return partial( + create_fake_agent, + system_prompt=SYSTEM_PROMPT, + skill_containers=[navigation_skill_container], + ) + + +@pytest.fixture +def create_gps_nav_agent(gps_nav_skill_container, create_fake_agent): + return partial( + create_fake_agent, system_prompt=SYSTEM_PROMPT, skill_containers=[gps_nav_skill_container] + ) + + +@pytest.fixture +def create_google_maps_agent( + gps_nav_skill_container, google_maps_skill_container, create_fake_agent +): + return partial( + create_fake_agent, + system_prompt=SYSTEM_PROMPT, + skill_containers=[gps_nav_skill_container, google_maps_skill_container], + ) + + +@pytest.fixture +def create_unitree_skills_agent(unitree_skills, create_fake_agent): + return partial( + create_fake_agent, + system_prompt=SYSTEM_PROMPT, + skill_containers=[unitree_skills], + ) diff --git a/dimos/agents/skills/demo_calculator_skill.py b/dimos/agents/skills/demo_calculator_skill.py new file mode 100644 index 0000000000..2ed8050ca5 --- /dev/null +++ b/dimos/agents/skills/demo_calculator_skill.py @@ -0,0 +1,43 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core.skill_module import SkillModule +from dimos.protocol.skill.skill import skill + + +class DemoCalculatorSkill(SkillModule): + def start(self) -> None: + super().start() + + def stop(self) -> None: + super().stop() + + @skill() + def sum_numbers(self, n1: int, n2: int, *args: int, **kwargs: int) -> str: + """This skill adds two numbers. Always use this tool. Never add up numbers yourself. + + Example: + + sum_numbers(100, 20) + + Args: + sum (str): The sum, as a string. E.g., "120" + """ + + return f"{int(n1) + int(n2)}" + + +demo_calculator_skill = DemoCalculatorSkill.blueprint + +__all__ = ["DemoCalculatorSkill", "demo_calculator_skill"] diff --git a/dimos/agents/skills/demo_google_maps_skill.py b/dimos/agents/skills/demo_google_maps_skill.py new file mode 100644 index 0000000000..cd8cad9d6a --- /dev/null +++ b/dimos/agents/skills/demo_google_maps_skill.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dotenv import load_dotenv + +from dimos.agents.agent import llm_agent +from dimos.agents.cli.human import human_input +from dimos.agents.skills.demo_robot import demo_robot +from dimos.agents.skills.google_maps_skill_container import google_maps_skill +from dimos.core.blueprints import autoconnect + +load_dotenv() + + +demo_google_maps_skill = autoconnect( + demo_robot(), + google_maps_skill(), + human_input(), + llm_agent(), +) diff --git a/dimos/agents/skills/demo_gps_nav.py b/dimos/agents/skills/demo_gps_nav.py new file mode 100644 index 0000000000..4204b23dc7 --- /dev/null +++ b/dimos/agents/skills/demo_gps_nav.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dotenv import load_dotenv + +from dimos.agents.agent import llm_agent +from dimos.agents.cli.human import human_input +from dimos.agents.skills.demo_robot import demo_robot +from dimos.agents.skills.gps_nav_skill import gps_nav_skill +from dimos.core.blueprints import autoconnect + +load_dotenv() + + +demo_gps_nav_skill = autoconnect( + demo_robot(), + gps_nav_skill(), + human_input(), + llm_agent(), +) diff --git a/dimos/agents/skills/demo_robot.py b/dimos/agents/skills/demo_robot.py new file mode 100644 index 0000000000..aa4e81e2cc --- /dev/null +++ b/dimos/agents/skills/demo_robot.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from reactivex import interval + +from dimos.core.module import Module +from dimos.core.stream import Out +from dimos.mapping.types import LatLon + + +class DemoRobot(Module): + gps_location: Out[LatLon] + + def start(self) -> None: + super().start() + self._disposables.add(interval(1.0).subscribe(lambda _: self._publish_gps_location())) + + def stop(self) -> None: + super().stop() + + def _publish_gps_location(self) -> None: + self.gps_location.publish(LatLon(lat=37.78092426217621, lon=-122.40682866540769)) + + +demo_robot = DemoRobot.blueprint + + +__all__ = ["DemoRobot", "demo_robot"] diff --git a/dimos/agents/skills/demo_skill.py b/dimos/agents/skills/demo_skill.py new file mode 100644 index 0000000000..547d81c5b8 --- /dev/null +++ b/dimos/agents/skills/demo_skill.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dotenv import load_dotenv + +from dimos.agents.agent import llm_agent +from dimos.agents.cli.human import human_input +from dimos.agents.skills.demo_calculator_skill import demo_calculator_skill +from dimos.core.blueprints import autoconnect + +load_dotenv() + + +demo_skill = autoconnect( + demo_calculator_skill(), + human_input(), + llm_agent(), +) diff --git a/dimos/agents/skills/google_maps_skill_container.py b/dimos/agents/skills/google_maps_skill_container.py new file mode 100644 index 0000000000..d5a30904ed --- /dev/null +++ b/dimos/agents/skills/google_maps_skill_container.py @@ -0,0 +1,118 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from typing import Any + +from dimos.core.core import rpc +from dimos.core.skill_module import SkillModule +from dimos.core.stream import In +from dimos.mapping.google_maps.google_maps import GoogleMaps +from dimos.mapping.types import LatLon +from dimos.protocol.skill.skill import skill +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class GoogleMapsSkillContainer(SkillModule): + _latest_location: LatLon | None = None + _client: GoogleMaps + + gps_location: In[LatLon] + + def __init__(self) -> None: + super().__init__() + self._client = GoogleMaps() + self._started = True + self._max_valid_distance = 20000 # meters + + @rpc + def start(self) -> None: + super().start() + self._disposables.add(self.gps_location.subscribe(self._on_gps_location)) # type: ignore[arg-type] + + @rpc + def stop(self) -> None: + super().stop() + + def _on_gps_location(self, location: LatLon) -> None: + self._latest_location = location + + def _get_latest_location(self) -> LatLon: + if not self._latest_location: + raise ValueError("The position has not been set yet.") + return self._latest_location + + @skill() + def where_am_i(self, context_radius: int = 200) -> str: + """This skill returns information about what street/locality/city/etc + you are in. It also gives you nearby landmarks. + + Example: + + where_am_i(context_radius=200) + + Args: + context_radius (int): default 200, how many meters to look around + """ + + location = self._get_latest_location() + + result = None + try: + result = self._client.get_location_context(location, radius=context_radius) + except Exception: + return "There is an issue with the Google Maps API." + + if not result: + return "Could not find anything about the current location." + + return result.model_dump_json() + + @skill() + def get_gps_position_for_queries(self, queries: list[str]) -> str: + """Get the GPS position (latitude/longitude) from Google Maps for know landmarks or searchable locations. + This includes anything that wouldn't be viewable on a physical OSM map including intersections (5th and Natoma) + landmarks (Dolores park), or locations (Tempest bar) + Example: + + get_gps_position_for_queries(['Fort Mason', 'Lafayette Park']) + # returns + [{"lat": 37.8059, "lon":-122.4290}, {"lat": 37.7915, "lon": -122.4276}] + + Args: + queries (list[str]): The places you want to look up. + """ + + location = self._get_latest_location() + + results: list[dict[str, Any] | str] = [] + + for query in queries: + try: + latlon = self._client.get_position(query, location) + except Exception: + latlon = None + if latlon: + results.append(latlon.model_dump()) + else: + results.append(f"no result for {query}") + + return json.dumps(results) + + +google_maps_skill = GoogleMapsSkillContainer.blueprint + +__all__ = ["GoogleMapsSkillContainer", "google_maps_skill"] diff --git a/dimos/agents/skills/gps_nav_skill.py b/dimos/agents/skills/gps_nav_skill.py new file mode 100644 index 0000000000..c7325a5b64 --- /dev/null +++ b/dimos/agents/skills/gps_nav_skill.py @@ -0,0 +1,109 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from dimos.core.core import rpc +from dimos.core.rpc_client import RpcCall +from dimos.core.skill_module import SkillModule +from dimos.core.stream import In, Out +from dimos.mapping.types import LatLon +from dimos.mapping.utils.distance import distance_in_meters +from dimos.protocol.skill.skill import skill +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class GpsNavSkillContainer(SkillModule): + _latest_location: LatLon | None = None + _max_valid_distance: int = 50000 + _set_gps_travel_goal_points: RpcCall | None = None + + gps_location: In[LatLon] + gps_goal: Out[LatLon] + + def __init__(self) -> None: + super().__init__() + + @rpc + def start(self) -> None: + super().start() + self._disposables.add(self.gps_location.subscribe(self._on_gps_location)) # type: ignore[arg-type] + + @rpc + def stop(self) -> None: + super().stop() + + @rpc + def set_WebsocketVisModule_set_gps_travel_goal_points(self, callable: RpcCall) -> None: + self._set_gps_travel_goal_points = callable + self._set_gps_travel_goal_points.set_rpc(self.rpc) # type: ignore[arg-type] + + def _on_gps_location(self, location: LatLon) -> None: + self._latest_location = location + + def _get_latest_location(self) -> LatLon: + if not self._latest_location: + raise ValueError("The position has not been set yet.") + return self._latest_location + + @skill() + def set_gps_travel_points(self, *points: dict[str, float]) -> str: + """Define the movement path determined by GPS coordinates. Requires at least one. You can get the coordinates by using the `get_gps_position_for_queries` skill. + + Example: + + set_gps_travel_goals([{"lat": 37.8059, "lon":-122.4290}, {"lat": 37.7915, "lon": -122.4276}]) + # Travel first to {"lat": 37.8059, "lon":-122.4290} + # then travel to {"lat": 37.7915, "lon": -122.4276} + """ + + new_points = [self._convert_point(x) for x in points] + + if not all(new_points): + parsed = json.dumps([x.__dict__ if x else x for x in new_points]) + return f"Not all points were valid. I parsed this: {parsed}" + + for new_point in new_points: + distance = distance_in_meters(self._get_latest_location(), new_point) # type: ignore[arg-type] + if distance > self._max_valid_distance: + return f"Point {new_point} is too far ({int(distance)} meters away)." + + logger.info(f"Set travel points: {new_points}") + + if self.gps_goal._transport is not None: + self.gps_goal.publish(new_points) # type: ignore[arg-type] + + if self._set_gps_travel_goal_points: + self._set_gps_travel_goal_points(new_points) + + return "I've successfully set the travel points." + + def _convert_point(self, point: dict[str, float]) -> LatLon | None: + if not isinstance(point, dict): + return None + lat = point.get("lat") + lon = point.get("lon") + + if lat is None or lon is None: + return None + + return LatLon(lat=lat, lon=lon) + + +gps_nav_skill = GpsNavSkillContainer.blueprint + + +__all__ = ["GpsNavSkillContainer", "gps_nav_skill"] diff --git a/dimos/agents/skills/navigation.py b/dimos/agents/skills/navigation.py new file mode 100644 index 0000000000..054246d6ee --- /dev/null +++ b/dimos/agents/skills/navigation.py @@ -0,0 +1,402 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from typing import Any + +from dimos.core.core import rpc +from dimos.core.skill_module import SkillModule +from dimos.core.stream import In +from dimos.models.qwen.video_query import BBox +from dimos.models.vl.qwen import QwenVlModel +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 +from dimos.msgs.geometry_msgs.Vector3 import make_vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.navigation.base import NavigationState +from dimos.navigation.visual.query import get_object_bbox_from_image +from dimos.protocol.skill.skill import skill +from dimos.types.robot_location import RobotLocation +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class NavigationSkillContainer(SkillModule): + _latest_image: Image | None = None + _latest_odom: PoseStamped | None = None + _skill_started: bool = False + _similarity_threshold: float = 0.23 + + rpc_calls: list[str] = [ + "SpatialMemory.tag_location", + "SpatialMemory.query_tagged_location", + "SpatialMemory.query_by_text", + "NavigationInterface.set_goal", + "NavigationInterface.get_state", + "NavigationInterface.is_goal_reached", + "NavigationInterface.cancel_goal", + "ObjectTracking.track", + "ObjectTracking.stop_track", + "ObjectTracking.is_tracking", + "WavefrontFrontierExplorer.stop_exploration", + "WavefrontFrontierExplorer.explore", + "WavefrontFrontierExplorer.is_exploration_active", + ] + + color_image: In[Image] + odom: In[PoseStamped] + + def __init__(self) -> None: + super().__init__() + self._skill_started = False + self._vl_model = QwenVlModel() + + @rpc + def start(self) -> None: + self._disposables.add(self.color_image.subscribe(self._on_color_image)) # type: ignore[arg-type] + self._disposables.add(self.odom.subscribe(self._on_odom)) # type: ignore[arg-type] + self._skill_started = True + + @rpc + def stop(self) -> None: + super().stop() + + def _on_color_image(self, image: Image) -> None: + self._latest_image = image + + def _on_odom(self, odom: PoseStamped) -> None: + self._latest_odom = odom + + @skill() + def tag_location(self, location_name: str) -> str: + """Tag this location in the spatial memory with a name. + + This associates the current location with the given name in the spatial memory, allowing you to navigate back to it. + + Args: + location_name (str): the name for the location + + Returns: + str: the outcome + """ + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + tf = self.tf.get("map", "base_link", time_tolerance=2.0) + if not tf: + return "Could not get the robot's current transform." + + position = tf.translation + rotation = tf.rotation.to_euler() + + location = RobotLocation( + name=location_name, + position=(position.x, position.y, position.z), + rotation=(rotation.x, rotation.y, rotation.z), + ) + + tag_location_rpc = self.get_rpc_calls("SpatialMemory.tag_location") + if not tag_location_rpc(location): + return f"Error: Failed to store '{location_name}' in the spatial memory" + + logger.info(f"Tagged {location}") + return f"Tagged '{location_name}': ({position.x},{position.y})." + + @skill() + def navigate_with_text(self, query: str) -> str: + """Navigate to a location by querying the existing semantic map using natural language. + + First attempts to locate an object in the robot's camera view using vision. + If the object is found, navigates to it. If not, falls back to querying the + semantic map for a location matching the description. + CALL THIS SKILL FOR ONE SUBJECT AT A TIME. For example: "Go to the person wearing a blue shirt in the living room", + you should call this skill twice, once for the person wearing a blue shirt and once for the living room. + Args: + query: Text query to search for in the semantic map + """ + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + success_msg = self._navigate_by_tagged_location(query) + if success_msg: + return success_msg + + logger.info(f"No tagged location found for {query}") + + success_msg = self._navigate_to_object(query) + if success_msg: + return success_msg + + logger.info(f"No object in view found for {query}") + + success_msg = self._navigate_using_semantic_map(query) + if success_msg: + return success_msg + + return f"No tagged location called '{query}'. No object in view matching '{query}'. No matching location found in semantic map for '{query}'." + + def _navigate_by_tagged_location(self, query: str) -> str | None: + try: + query_tagged_location_rpc = self.get_rpc_calls("SpatialMemory.query_tagged_location") + except Exception: + logger.warning("SpatialMemory module not connected, cannot query tagged locations") + return None + + robot_location = query_tagged_location_rpc(query) + + if not robot_location: + return None + + print("Found tagged location:", robot_location) + goal_pose = PoseStamped( + position=make_vector3(*robot_location.position), + orientation=Quaternion.from_euler(Vector3(*robot_location.rotation)), + frame_id="map", + ) + + result = self._navigate_to(goal_pose) + if not result: + return "Error: Faild to reach the tagged location." + + return ( + f"Successfuly arrived at location tagged '{robot_location.name}' from query '{query}'." + ) + + def _navigate_to(self, pose: PoseStamped) -> bool: + try: + set_goal_rpc, get_state_rpc, is_goal_reached_rpc = self.get_rpc_calls( + "NavigationInterface.set_goal", + "NavigationInterface.get_state", + "NavigationInterface.is_goal_reached", + ) + except Exception: + logger.error("Navigation module not connected properly") + return False + + logger.info( + f"Navigating to pose: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" + ) + set_goal_rpc(pose) + time.sleep(1.0) + + while get_state_rpc() == NavigationState.FOLLOWING_PATH: + time.sleep(0.25) + + time.sleep(1.0) + if not is_goal_reached_rpc(): + logger.info("Navigation was cancelled or failed") + return False + else: + logger.info("Navigation goal reached") + return True + + def _navigate_to_object(self, query: str) -> str | None: + try: + bbox = self._get_bbox_for_current_frame(query) + except Exception: + logger.error(f"Failed to get bbox for {query}", exc_info=True) + return None + + if bbox is None: + return None + + try: + track_rpc, stop_track_rpc, is_tracking_rpc = self.get_rpc_calls( + "ObjectTracking.track", "ObjectTracking.stop_track", "ObjectTracking.is_tracking" + ) + except Exception: + logger.error("ObjectTracking module not connected properly") + return None + + try: + get_state_rpc, is_goal_reached_rpc = self.get_rpc_calls( + "NavigationInterface.get_state", "NavigationInterface.is_goal_reached" + ) + except Exception: + logger.error("Navigation module not connected properly") + return None + + logger.info(f"Found {query} at {bbox}") + + # Start tracking - BBoxNavigationModule automatically generates goals + track_rpc(bbox) + + start_time = time.time() + timeout = 30.0 + goal_set = False + + while time.time() - start_time < timeout: + # Check if navigator finished + if get_state_rpc() == NavigationState.IDLE and goal_set: + logger.info("Waiting for goal result") + time.sleep(1.0) + if not is_goal_reached_rpc(): + logger.info(f"Goal cancelled, tracking '{query}' failed") + stop_track_rpc() + return None + else: + logger.info(f"Reached '{query}'") + stop_track_rpc() + return f"Successfully arrived at '{query}'" + + # If goal set and tracking lost, just continue (tracker will resume or timeout) + if goal_set and not is_tracking_rpc(): + continue + + # BBoxNavigationModule automatically sends goals when tracker publishes + # Just check if we have any detections to mark goal_set + if is_tracking_rpc(): + goal_set = True + + time.sleep(0.25) + + logger.warning(f"Navigation to '{query}' timed out after {timeout}s") + stop_track_rpc() + return None + + def _get_bbox_for_current_frame(self, query: str) -> BBox | None: + if self._latest_image is None: + return None + + return get_object_bbox_from_image(self._vl_model, self._latest_image, query) + + def _navigate_using_semantic_map(self, query: str) -> str: + try: + query_by_text_rpc = self.get_rpc_calls("SpatialMemory.query_by_text") + except Exception: + return "Error: The SpatialMemory module is not connected." + + results = query_by_text_rpc(query) + + if not results: + return f"No matching location found in semantic map for '{query}'" + + best_match = results[0] + + goal_pose = self._get_goal_pose_from_result(best_match) + + print("Goal pose for semantic nav:", goal_pose) + if not goal_pose: + return f"Found a result for '{query}' but it didn't have a valid position." + + result = self._navigate_to(goal_pose) + + if not result: + return f"Failed to navigate for '{query}'" + + return f"Successfuly arrived at '{query}'" + + @skill() + def follow_human(self, person: str) -> str: + """Follow a specific person""" + return "Not implemented yet." + + @skill() + def stop_movement(self) -> str: + """Immediatly stop moving.""" + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + + self._cancel_goal_and_stop() + + return "Stopped" + + def _cancel_goal_and_stop(self) -> None: + try: + cancel_goal_rpc = self.get_rpc_calls("NavigationInterface.cancel_goal") + except Exception: + logger.warning("Navigation module not connected, cannot cancel goal") + return + + try: + stop_exploration_rpc = self.get_rpc_calls("WavefrontFrontierExplorer.stop_exploration") + except Exception: + logger.warning("FrontierExplorer module not connected, cannot stop exploration") + return + + cancel_goal_rpc() + return stop_exploration_rpc() # type: ignore[no-any-return] + + @skill() + def start_exploration(self, timeout: float = 240.0) -> str: + """A skill that performs autonomous frontier exploration. + + This skill continuously finds and navigates to unknown frontiers in the environment + until no more frontiers are found or the exploration is stopped. + + Don't call any other skills except stop_movement skill when needed. + + Args: + timeout (float, optional): Maximum time (in seconds) allowed for exploration + """ + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + + try: + return self._start_exploration(timeout) + finally: + self._cancel_goal_and_stop() + + def _start_exploration(self, timeout: float) -> str: + try: + explore_rpc, is_exploration_active_rpc = self.get_rpc_calls( + "WavefrontFrontierExplorer.explore", + "WavefrontFrontierExplorer.is_exploration_active", + ) + except Exception: + return "Error: The WavefrontFrontierExplorer module is not connected." + + logger.info("Starting autonomous frontier exploration") + + start_time = time.time() + + has_started = explore_rpc() + if not has_started: + return "Error: Could not start exploration." + + while time.time() - start_time < timeout and is_exploration_active_rpc(): + time.sleep(0.5) + + return "Exploration completed successfuly" + + def _get_goal_pose_from_result(self, result: dict[str, Any]) -> PoseStamped | None: + similarity = 1.0 - (result.get("distance") or 1) + if similarity < self._similarity_threshold: + logger.warning( + f"Match found but similarity score ({similarity:.4f}) is below threshold ({self._similarity_threshold})" + ) + return None + + metadata = result.get("metadata") + if not metadata: + return None + print(metadata) + first = metadata[0] + print(first) + pos_x = first.get("pos_x", 0) + pos_y = first.get("pos_y", 0) + theta = first.get("rot_z", 0) + + return PoseStamped( + position=make_vector3(pos_x, pos_y, 0), + orientation=Quaternion.from_euler(make_vector3(0, 0, theta)), + frame_id="map", + ) + + +navigation_skill = NavigationSkillContainer.blueprint + +__all__ = ["NavigationSkillContainer", "navigation_skill"] diff --git a/dimos/agents2/skills/osm.py b/dimos/agents/skills/osm.py similarity index 82% rename from dimos/agents2/skills/osm.py rename to dimos/agents/skills/osm.py index ae721bea81..71f453069f 100644 --- a/dimos/agents2/skills/osm.py +++ b/dimos/agents/skills/osm.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,26 +22,23 @@ from dimos.protocol.skill.skill import skill from dimos.utils.logging_config import setup_logger -logger = setup_logger(__file__) +logger = setup_logger() class OsmSkill(SkillModule): _latest_location: LatLon | None _current_location_map: CurrentLocationMap - _skill_started: bool - gps_location: In[LatLon] = None + gps_location: In[LatLon] def __init__(self) -> None: super().__init__() self._latest_location = None self._current_location_map = CurrentLocationMap(QwenVlModel()) - self._skill_started = False def start(self) -> None: super().start() - self._skill_started = True - self._disposables.add(self.gps_location.subscribe(self._on_gps_location)) + self._disposables.add(self.gps_location.subscribe(self._on_gps_location)) # type: ignore[arg-type] def stop(self) -> None: super().stop() @@ -50,32 +47,30 @@ def _on_gps_location(self, location: LatLon) -> None: self._latest_location = location @skill() - def street_map_query(self, query_sentence: str) -> str: + def map_query(self, query_sentence: str) -> str: """This skill uses a vision language model to find something on the map based on the query sentence. You can query it with something like "Where can I find a coffee shop?" and it returns the latitude and longitude. Example: - street_map_query("Where can I find a coffee shop?") + map_query("Where can I find a coffee shop?") Args: query_sentence (str): The query sentence. """ - if not self._skill_started: - raise ValueError(f"{self} has not been started.") - - self._current_location_map.update_position(self._latest_location) + self._current_location_map.update_position(self._latest_location) # type: ignore[arg-type] location = self._current_location_map.query_for_one_position_and_context( - query_sentence, self._latest_location + query_sentence, + self._latest_location, # type: ignore[arg-type] ) if not location: return "Could not find anything." latlon, context = location - distance = int(distance_in_meters(latlon, self._latest_location)) + distance = int(distance_in_meters(latlon, self._latest_location)) # type: ignore[arg-type] return f"{context}. It's at position latitude={latlon.lat}, longitude={latlon.lon}. It is {distance} meters away." diff --git a/dimos/agents/skills/speak_skill.py b/dimos/agents/skills/speak_skill.py new file mode 100644 index 0000000000..073dda656a --- /dev/null +++ b/dimos/agents/skills/speak_skill.py @@ -0,0 +1,104 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time + +from reactivex import Subject + +from dimos.core.core import rpc +from dimos.core.skill_module import SkillModule +from dimos.protocol.skill.skill import skill +from dimos.stream.audio.node_output import SounddeviceAudioOutput +from dimos.stream.audio.tts.node_openai import OpenAITTSNode, Voice +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class SpeakSkill(SkillModule): + _tts_node: OpenAITTSNode | None = None + _audio_output: SounddeviceAudioOutput | None = None + _audio_lock: threading.Lock = threading.Lock() + + @rpc + def start(self) -> None: + super().start() + self._tts_node = OpenAITTSNode(speed=1.2, voice=Voice.ONYX) + self._audio_output = SounddeviceAudioOutput(sample_rate=24000) + self._audio_output.consume_audio(self._tts_node.emit_audio()) + + @rpc + def stop(self) -> None: + if self._tts_node: + self._tts_node.dispose() + self._tts_node = None + if self._audio_output: + self._audio_output.stop() + self._audio_output = None + super().stop() + + @skill() + def speak(self, text: str) -> str: + """Speak text out loud through the robot's speakers. + + USE THIS TOOL AS OFTEN AS NEEDED. People can't normally see what you say in text, but can hear what you speak. + + Try to be as concise as possible. Remember that speaking takes time, so get to the point quickly. + + Example usage: + + speak("Hello, I am your robot assistant.") + """ + if self._tts_node is None: + return "Error: TTS not initialized" + + # Use lock to prevent simultaneous speech + with self._audio_lock: + text_subject: Subject[str] = Subject() + audio_complete = threading.Event() + self._tts_node.consume_text(text_subject) + + def set_as_complete(_t: str) -> None: + audio_complete.set() + + def set_as_complete_e(_e: Exception) -> None: + audio_complete.set() + + subscription = self._tts_node.emit_text().subscribe( + on_next=set_as_complete, + on_error=set_as_complete_e, + ) + + text_subject.on_next(text) + text_subject.on_completed() + + timeout = max(5, len(text) * 0.1) + + if not audio_complete.wait(timeout=timeout): + logger.warning(f"TTS timeout reached for: {text}") + subscription.dispose() + return f"Warning: TTS timeout while speaking: {text}" + else: + # Small delay to ensure buffers flush + time.sleep(0.3) + + subscription.dispose() + + return f"Spoke: {text}" + + +speak_skill = SpeakSkill.blueprint + +__all__ = ["SpeakSkill", "speak_skill"] diff --git a/dimos/agents2/skills/test_google_maps_skill_container.py b/dimos/agents/skills/test_google_maps_skill_container.py similarity index 87% rename from dimos/agents2/skills/test_google_maps_skill_container.py rename to dimos/agents/skills/test_google_maps_skill_container.py index 27a9dadb8f..0af206fbb1 100644 --- a/dimos/agents2/skills/test_google_maps_skill_container.py +++ b/dimos/agents/skills/test_google_maps_skill_container.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,9 +15,11 @@ import re from dimos.mapping.google_maps.types import Coordinates, LocationContext, Position +from dimos.mapping.types import LatLon def test_where_am_i(create_google_maps_agent, google_maps_skill_container) -> None: + google_maps_skill_container._latest_location = LatLon(lat=37.782654, lon=-122.413273) google_maps_skill_container._client.get_location_context.return_value = LocationContext( street="Bourbon Street", coordinates=Coordinates(lat=37.782654, lon=-122.413273) ) @@ -31,6 +33,7 @@ def test_where_am_i(create_google_maps_agent, google_maps_skill_container) -> No def test_get_gps_position_for_queries( create_google_maps_agent, google_maps_skill_container ) -> None: + google_maps_skill_container._latest_location = LatLon(lat=37.782654, lon=-122.413273) google_maps_skill_container._client.get_position.side_effect = [ Position(lat=37.782601, lon=-122.413201, description="address 1"), Position(lat=37.782602, lon=-122.413202, description="address 2"), diff --git a/dimos/agents/skills/test_gps_nav_skills.py b/dimos/agents/skills/test_gps_nav_skills.py new file mode 100644 index 0000000000..ab0d1ec318 --- /dev/null +++ b/dimos/agents/skills/test_gps_nav_skills.py @@ -0,0 +1,58 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos.mapping.types import LatLon + + +def test_set_gps_travel_points(create_gps_nav_agent, gps_nav_skill_container, mocker) -> None: + gps_nav_skill_container._latest_location = LatLon(lat=37.782654, lon=-122.413273) + gps_nav_skill_container._set_gps_travel_goal_points = mocker.Mock() + agent = create_gps_nav_agent(fixture="test_set_gps_travel_points.json") + + agent.query("go to lat: 37.782654, lon: -122.413273") + + gps_nav_skill_container._set_gps_travel_goal_points.assert_called_once_with( + [LatLon(lat=37.782654, lon=-122.413273)] + ) + gps_nav_skill_container.gps_goal.publish.assert_called_once_with( + [LatLon(lat=37.782654, lon=-122.413273)] + ) + + +def test_set_gps_travel_points_multiple( + create_gps_nav_agent, gps_nav_skill_container, mocker +) -> None: + gps_nav_skill_container._latest_location = LatLon(lat=37.782654, lon=-122.413273) + gps_nav_skill_container._set_gps_travel_goal_points = mocker.Mock() + agent = create_gps_nav_agent(fixture="test_set_gps_travel_points_multiple.json") + + agent.query( + "go to lat: 37.782654, lon: -122.413273, then 37.782660,-122.413260, and then 37.782670,-122.413270" + ) + + gps_nav_skill_container._set_gps_travel_goal_points.assert_called_once_with( + [ + LatLon(lat=37.782654, lon=-122.413273), + LatLon(lat=37.782660, lon=-122.413260), + LatLon(lat=37.782670, lon=-122.413270), + ] + ) + gps_nav_skill_container.gps_goal.publish.assert_called_once_with( + [ + LatLon(lat=37.782654, lon=-122.413273), + LatLon(lat=37.782660, lon=-122.413260), + LatLon(lat=37.782670, lon=-122.413270), + ] + ) diff --git a/dimos/agents/skills/test_navigation.py b/dimos/agents/skills/test_navigation.py new file mode 100644 index 0000000000..588b55a602 --- /dev/null +++ b/dimos/agents/skills/test_navigation.py @@ -0,0 +1,94 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos.msgs.geometry_msgs import PoseStamped, Vector3 +from dimos.utils.transform_utils import euler_to_quaternion + + +# @pytest.mark.skip +def test_stop_movement(create_navigation_agent, navigation_skill_container, mocker) -> None: + cancel_goal_mock = mocker.Mock() + stop_exploration_mock = mocker.Mock() + navigation_skill_container._bound_rpc_calls["NavigationInterface.cancel_goal"] = ( + cancel_goal_mock + ) + navigation_skill_container._bound_rpc_calls["WavefrontFrontierExplorer.stop_exploration"] = ( + stop_exploration_mock + ) + agent = create_navigation_agent(fixture="test_stop_movement.json") + + agent.query("stop") + + cancel_goal_mock.assert_called_once_with() + stop_exploration_mock.assert_called_once_with() + + +def test_take_a_look_around(create_navigation_agent, navigation_skill_container, mocker) -> None: + explore_mock = mocker.Mock() + is_exploration_active_mock = mocker.Mock() + navigation_skill_container._bound_rpc_calls["WavefrontFrontierExplorer.explore"] = explore_mock + navigation_skill_container._bound_rpc_calls[ + "WavefrontFrontierExplorer.is_exploration_active" + ] = is_exploration_active_mock + mocker.patch("dimos.agents.skills.navigation.time.sleep") + agent = create_navigation_agent(fixture="test_take_a_look_around.json") + + agent.query("take a look around for 10 seconds") + + explore_mock.assert_called_once_with() + + +def test_go_to_semantic_location( + create_navigation_agent, navigation_skill_container, mocker +) -> None: + mocker.patch( + "dimos.agents.skills.navigation.NavigationSkillContainer._navigate_by_tagged_location", + return_value=None, + ) + mocker.patch( + "dimos.agents.skills.navigation.NavigationSkillContainer._navigate_to_object", + return_value=None, + ) + navigate_to_mock = mocker.patch( + "dimos.agents.skills.navigation.NavigationSkillContainer._navigate_to", + return_value=True, + ) + query_by_text_mock = mocker.Mock( + return_value=[ + { + "distance": 0.5, + "metadata": [ + { + "pos_x": 1, + "pos_y": 2, + "rot_z": 3, + } + ], + } + ] + ) + navigation_skill_container._bound_rpc_calls["SpatialMemory.query_by_text"] = query_by_text_mock + agent = create_navigation_agent(fixture="test_go_to_semantic_location.json") + + agent.query("go to the bookshelf") + + query_by_text_mock.assert_called_once_with("bookshelf") + navigate_to_mock.assert_called_once_with( + PoseStamped( + position=Vector3(1, 2, 0), + orientation=euler_to_quaternion(Vector3(0, 0, 3)), + frame_id="world", + ), + ) diff --git a/dimos/agents/skills/test_unitree_skill_container.py b/dimos/agents/skills/test_unitree_skill_container.py new file mode 100644 index 0000000000..29dfade979 --- /dev/null +++ b/dimos/agents/skills/test_unitree_skill_container.py @@ -0,0 +1,32 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_pounce(mocker, create_unitree_skills_agent, unitree_skills) -> None: + agent = create_unitree_skills_agent(fixture="test_pounce.json") + publish_request_mock = mocker.Mock() + unitree_skills.get_rpc_calls = mocker.Mock(return_value=publish_request_mock) + + response = agent.query("pounce") + + assert "front pounce" in response.lower() + publish_request_mock.assert_called_once_with("rt/api/sport/request", {"api_id": 1032}) + + +def test_did_you_mean(mocker, unitree_skills) -> None: + unitree_skills.get_rpc_calls = mocker.Mock() + assert ( + unitree_skills.execute_sport_command("Pounce") + == "There's no 'Pounce' command. Did you mean: ['FrontPounce', 'Pose']" + ) diff --git a/dimos/agents/spec.py b/dimos/agents/spec.py new file mode 100644 index 0000000000..b0a0324e89 --- /dev/null +++ b/dimos/agents/spec.py @@ -0,0 +1,242 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base agent module that wraps BaseAgent for DimOS module usage.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING, Any, Union + +if TYPE_CHECKING: + from dimos.protocol.skill.skill import SkillContainer + +from langchain.chat_models.base import _SUPPORTED_PROVIDERS +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import ( + AIMessage, + HumanMessage, + SystemMessage, + ToolMessage, +) +from rich.console import Console +from rich.table import Table +from rich.text import Text + +from dimos.core import Module, rpc +from dimos.core.module import ModuleConfig +from dimos.protocol.pubsub import PubSub, lcm # type: ignore[attr-defined] +from dimos.protocol.service import Service # type: ignore[attr-defined] +from dimos.protocol.skill.skill import SkillContainer +from dimos.utils.generic import truncate_display_string +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +# Dynamically create ModelProvider enum from LangChain's supported providers +_providers = {provider.upper(): provider for provider in _SUPPORTED_PROVIDERS} +Provider = Enum("Provider", _providers, type=str) # type: ignore[misc] + + +class Model(str, Enum): + """Common model names across providers. + + Note: This is not exhaustive as model names change frequently. + Based on langchain's _attempt_infer_model_provider patterns. + """ + + # OpenAI models (prefix: gpt-3, gpt-4, o1, o3) + GPT_4O = "gpt-4o" + GPT_4O_MINI = "gpt-4o-mini" + GPT_4_TURBO = "gpt-4-turbo" + GPT_4_TURBO_PREVIEW = "gpt-4-turbo-preview" + GPT_4 = "gpt-4" + GPT_35_TURBO = "gpt-3.5-turbo" + GPT_35_TURBO_16K = "gpt-3.5-turbo-16k" + O1_PREVIEW = "o1-preview" + O1_MINI = "o1-mini" + O3_MINI = "o3-mini" + + # Anthropic models (prefix: claude) + CLAUDE_3_OPUS = "claude-3-opus-20240229" + CLAUDE_3_SONNET = "claude-3-sonnet-20240229" + CLAUDE_3_HAIKU = "claude-3-haiku-20240307" + CLAUDE_35_SONNET = "claude-3-5-sonnet-20241022" + CLAUDE_35_SONNET_LATEST = "claude-3-5-sonnet-latest" + CLAUDE_3_7_SONNET = "claude-3-7-sonnet-20250219" + + # Google models (prefix: gemini) + GEMINI_20_FLASH = "gemini-2.0-flash" + GEMINI_15_PRO = "gemini-1.5-pro" + GEMINI_15_FLASH = "gemini-1.5-flash" + GEMINI_10_PRO = "gemini-1.0-pro" + + # Amazon Bedrock models (prefix: amazon) + AMAZON_TITAN_EXPRESS = "amazon.titan-text-express-v1" + AMAZON_TITAN_LITE = "amazon.titan-text-lite-v1" + + # Cohere models (prefix: command) + COMMAND_R_PLUS = "command-r-plus" + COMMAND_R = "command-r" + COMMAND = "command" + COMMAND_LIGHT = "command-light" + + # Fireworks models (prefix: accounts/fireworks) + FIREWORKS_LLAMA_V3_70B = "accounts/fireworks/models/llama-v3-70b-instruct" + FIREWORKS_MIXTRAL_8X7B = "accounts/fireworks/models/mixtral-8x7b-instruct" + + # Mistral models (prefix: mistral) + MISTRAL_LARGE = "mistral-large" + MISTRAL_MEDIUM = "mistral-medium" + MISTRAL_SMALL = "mistral-small" + MIXTRAL_8X7B = "mixtral-8x7b" + MIXTRAL_8X22B = "mixtral-8x22b" + MISTRAL_7B = "mistral-7b" + + # DeepSeek models (prefix: deepseek) + DEEPSEEK_CHAT = "deepseek-chat" + DEEPSEEK_CODER = "deepseek-coder" + DEEPSEEK_R1_DISTILL_LLAMA_70B = "deepseek-r1-distill-llama-70b" + + # xAI models (prefix: grok) + GROK_1 = "grok-1" + GROK_2 = "grok-2" + + # Perplexity models (prefix: sonar) + SONAR_SMALL_CHAT = "sonar-small-chat" + SONAR_MEDIUM_CHAT = "sonar-medium-chat" + SONAR_LARGE_CHAT = "sonar-large-chat" + + # Meta Llama models (various providers) + LLAMA_3_70B = "llama-3-70b" + LLAMA_3_8B = "llama-3-8b" + LLAMA_31_70B = "llama-3.1-70b" + LLAMA_31_8B = "llama-3.1-8b" + LLAMA_33_70B = "llama-3.3-70b" + LLAMA_2_70B = "llama-2-70b" + LLAMA_2_13B = "llama-2-13b" + LLAMA_2_7B = "llama-2-7b" + + +@dataclass +class AgentConfig(ModuleConfig): + system_prompt: str | SystemMessage | None = None + skills: SkillContainer | list[SkillContainer] | None = None + + # we can provide model/provvider enums or instantiated model_instance + model: Model = Model.GPT_4O + provider: Provider = Provider.OPENAI # type: ignore[attr-defined] + model_instance: BaseChatModel | None = None + + agent_transport: type[PubSub] = lcm.PickleLCM # type: ignore[type-arg] + agent_topic: Any = field(default_factory=lambda: lcm.Topic("/agent")) + + +AnyMessage = Union[SystemMessage, ToolMessage, AIMessage, HumanMessage] + + +class AgentSpec(Service[AgentConfig], Module, ABC): + default_config: type[AgentConfig] = AgentConfig + + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + Service.__init__(self, *args, **kwargs) + Module.__init__(self, *args, **kwargs) + + if self.config.agent_transport: + self.transport = self.config.agent_transport() + + def publish(self, msg: AnyMessage) -> None: + if self.transport: + self.transport.publish(self.config.agent_topic, msg) + + def start(self) -> None: + super().start() + + def stop(self) -> None: + if hasattr(self, "transport") and self.transport: + self.transport.stop() # type: ignore[attr-defined] + self.transport = None # type: ignore[assignment] + super().stop() + + @rpc + @abstractmethod + def clear_history(self): ... # type: ignore[no-untyped-def] + + @abstractmethod + def append_history(self, *msgs: list[AIMessage | HumanMessage]): ... # type: ignore[no-untyped-def] + + @abstractmethod + def history(self) -> list[AnyMessage]: ... + + @rpc + @abstractmethod + def register_skills( + self, container: "SkillContainer", run_implicit_name: str | None = None + ) -> None: ... + + @rpc + @abstractmethod + def query(self, query: str): ... # type: ignore[no-untyped-def] + + def __str__(self) -> str: + console = Console(force_terminal=True, legacy_windows=False) + table = Table(show_header=True) + + table.add_column("Message Type", style="cyan", no_wrap=True) + table.add_column("Content") + + for message in self.history(): + if isinstance(message, HumanMessage): + content = message.content + if not isinstance(content, str): + content = "" + + table.add_row(Text("Human", style="green"), Text(content, style="green")) + elif isinstance(message, AIMessage): + if hasattr(message, "metadata") and message.metadata.get("state"): + table.add_row( + Text("State Summary", style="blue"), + Text(message.content, style="blue"), # type: ignore[arg-type] + ) + else: + table.add_row( + Text("Agent", style="magenta"), + Text(message.content, style="magenta"), # type: ignore[arg-type] + ) + + for tool_call in message.tool_calls: + table.add_row( + "Tool Call", + Text( + f"{tool_call.get('name')}({tool_call.get('args')})", + style="bold magenta", + ), + ) + elif isinstance(message, ToolMessage): + table.add_row( + "Tool Response", Text(f"{message.name}() -> {message.content}"), style="red" + ) + elif isinstance(message, SystemMessage): + table.add_row( + "System", Text(truncate_display_string(message.content, 800), style="yellow") + ) + else: + table.add_row("Unknown", str(message)) + + # Render to string with title above + with console.capture() as capture: + console.print(Text(f" Agent ({self._agent_id})", style="bold blue")) # type: ignore[attr-defined] + console.print(table) + return capture.get().strip() diff --git a/dimos/agents/system_prompt.py b/dimos/agents/system_prompt.py new file mode 100644 index 0000000000..54f713f538 --- /dev/null +++ b/dimos/agents/system_prompt.py @@ -0,0 +1,53 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SYSTEM_PROMPT = """ +You are Daneel, an AI agent created by Dimensional to control a Unitree Go2 quadruped robot. + +# CRITICAL: SAFETY +Prioritize human safety above all else. Respect personal boundaries. Never take actions that could harm humans, damage property, or damage the robot. + +# IDENTITY +You are Daneel. If someone says "daniel" or similar, ignore it (speech-to-text error). When greeted, briefly introduce yourself as an AI agent operating autonomously in physical space. + +# COMMUNICATION +Users hear you through speakers but cannot see text. Use `speak` to communicate your actions or responses. Be concise—one or two sentences. + +# SKILL COORDINATION + +## Navigation Flow +- Use `navigate_with_text` for most navigation. It searches tagged locations first, then visible objects, then the semantic map. +- Tag important locations with `tag_location` so you can return to them later. +- During `start_exploration`, avoid calling other skills except `stop_movement`. +- Always run `execute_sport_command("RecoveryStand")` after dynamic movements (flips, jumps, sit) before navigating. + +## GPS Navigation Flow +For outdoor/GPS-based navigation: +1. Use `get_gps_position_for_queries` to look up coordinates for landmarks +2. Then use `set_gps_travel_points` with those coordinates + +## Location Awareness +- `where_am_i` gives your current street/area and nearby landmarks +- `map_query` finds places on the OSM map by description and returns coordinates + +# BEHAVIOR + +## Be Proactive +Infer reasonable actions from ambiguous requests. If someone says "greet the new arrivals," head to the front door. Inform the user of your assumption: "Heading to the front door—let me know if I should go elsewhere." + +## Deliveries & Pickups +- Deliveries: announce yourself with `speak`, call `wait` for 5 seconds, then continue. +- Pickups: ask for help with `speak`, wait for a response, then continue. + +""" diff --git a/dimos/agents2/temp/webcam_agent.py b/dimos/agents/temp/webcam_agent.py similarity index 75% rename from dimos/agents2/temp/webcam_agent.py rename to dimos/agents/temp/webcam_agent.py index 485684d9e0..98ae0a903b 100644 --- a/dimos/agents2/temp/webcam_agent.py +++ b/dimos/agents/temp/webcam_agent.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # limitations under the License. """ -Run script for Unitree Go2 robot with agents2 framework. +Run script for Unitree Go2 robot with agents framework. This is the migrated version using the new LangChain-based agent system. """ @@ -24,13 +24,13 @@ import reactivex as rx import reactivex.operators as ops -from dimos.agents2 import Agent, Output, Reducer, Stream, skill -from dimos.agents2.cli.human import HumanInput -from dimos.agents2.spec import Model, Provider +from dimos.agents import Agent, Output, Reducer, Stream, skill # type: ignore[attr-defined] +from dimos.agents.cli.human import HumanInput +from dimos.agents.spec import Model, Provider from dimos.core import LCMTransport, Module, rpc, start -from dimos.hardware.camera import zed -from dimos.hardware.camera.module import CameraModule -from dimos.hardware.camera.webcam import Webcam +from dimos.hardware.sensors.camera import zed +from dimos.hardware.sensors.camera.module import CameraModule +from dimos.hardware.sensors.camera.webcam import Webcam from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 from dimos.msgs.sensor_msgs import CameraInfo, Image from dimos.protocol.skill.test_coordinator import SkillContainerTest @@ -38,11 +38,11 @@ class WebModule(Module): - web_interface: RobotWebInterface = None - human_query: rx.subject.Subject = None - agent_response: rx.subject.Subject = None + web_interface: RobotWebInterface = None # type: ignore[assignment] + human_query: rx.subject.Subject = None # type: ignore[assignment, type-arg] + agent_response: rx.subject.Subject = None # type: ignore[assignment, type-arg] - thread: Thread = None + thread: Thread = None # type: ignore[assignment] _human_messages_running = False @@ -74,15 +74,15 @@ def start(self) -> None: @rpc def stop(self) -> None: if self.web_interface: - self.web_interface.stop() + self.web_interface.stop() # type: ignore[attr-defined] if self.thread: # TODO, you can't just wait for a server to close, you have to signal it to end. self.thread.join(timeout=1.0) super().stop() - @skill(stream=Stream.call_agent, reducer=Reducer.all, output=Output.human) - def human_messages(self): + @skill(stream=Stream.call_agent, reducer=Reducer.all, output=Output.human) # type: ignore[arg-type] + def human_messages(self): # type: ignore[no-untyped-def] """Provide human messages from web interface. Don't use this tool, it's running implicitly already""" if self._human_messages_running: print("human_messages already running, not starting another") @@ -101,11 +101,11 @@ def main() -> None: agent = Agent( system_prompt="You are a helpful assistant for controlling a Unitree Go2 robot. ", model=Model.GPT_4O, # Could add CLAUDE models to enum - provider=Provider.OPENAI, # Would need ANTHROPIC provider + provider=Provider.OPENAI, # type: ignore[attr-defined] # Would need ANTHROPIC provider ) - testcontainer = dimos.deploy(SkillContainerTest) - webcam = dimos.deploy( + testcontainer = dimos.deploy(SkillContainerTest) # type: ignore[attr-defined] + webcam = dimos.deploy( # type: ignore[attr-defined] CameraModule, transform=Transform( translation=Vector3(0.0, 0.0, 0.0), @@ -127,7 +127,7 @@ def main() -> None: webcam.start() - human_input = dimos.deploy(HumanInput) + human_input = dimos.deploy(HumanInput) # type: ignore[attr-defined] time.sleep(1) diff --git a/dimos/agents/test_agent.py b/dimos/agents/test_agent.py new file mode 100644 index 0000000000..934fa0360a --- /dev/null +++ b/dimos/agents/test_agent.py @@ -0,0 +1,169 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import pytest_asyncio + +from dimos.agents.agent import Agent +from dimos.core import start +from dimos.protocol.skill.test_coordinator import SkillContainerTest + +system_prompt = ( + "Your name is Mr. Potato, potatoes are bad at math. Use a tools if asked to calculate" +) + + +@pytest.fixture(scope="session") +def dimos_cluster(): + """Session-scoped fixture to initialize dimos cluster once.""" + dimos = start(2) + try: + yield dimos + finally: + dimos.shutdown() + + +@pytest_asyncio.fixture +async def local(): + """Local context: both agent and testcontainer run locally""" + testcontainer = SkillContainerTest() + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + except Exception as e: + print(f"Error: {e}") + import traceback + + traceback.print_exc() + raise e + finally: + # Ensure cleanup happens while event loop is still active + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + + +@pytest_asyncio.fixture +async def dask_mixed(dimos_cluster): + """Dask context: testcontainer on dimos, agent local""" + testcontainer = dimos_cluster.deploy(SkillContainerTest) + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + + +@pytest_asyncio.fixture +async def dask_full(dimos_cluster): + """Dask context: both agent and testcontainer deployed on dimos""" + testcontainer = dimos_cluster.deploy(SkillContainerTest) + agent = dimos_cluster.deploy(Agent, system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + + +@pytest_asyncio.fixture(params=["local", "dask_mixed", "dask_full"]) +async def agent_context(request): + """Parametrized fixture that runs tests with different agent configurations""" + param = request.param + + if param == "local": + testcontainer = SkillContainerTest() + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + elif param == "dask_mixed": + dimos_cluster = request.getfixturevalue("dimos_cluster") + testcontainer = dimos_cluster.deploy(SkillContainerTest) + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + elif param == "dask_full": + dimos_cluster = request.getfixturevalue("dimos_cluster") + testcontainer = dimos_cluster.deploy(SkillContainerTest) + agent = dimos_cluster.deploy(Agent, system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + + +# @pytest.mark.timeout(40) +@pytest.mark.tool +@pytest.mark.asyncio +async def test_agent_init(agent_context) -> None: + """Test agent initialization and basic functionality across different configurations""" + agent, testcontainer = agent_context + + agent.register_skills(testcontainer) + agent.start() + + # agent.run_implicit_skill("uptime_seconds") + + print("query agent") + # When running locally, call the async method directly + agent.query( + "hi there, please tell me what's your name and current date, and how much is 124181112 + 124124?" + ) + print("Agent loop finished, asking about camera") + agent.query("tell me what you see on the camera?") + + # you can run skillspy and agentspy in parallel with this test for a better observation of what's happening diff --git a/dimos/agents2/test_agent_direct.py b/dimos/agents/test_agent_direct.py similarity index 97% rename from dimos/agents2/test_agent_direct.py rename to dimos/agents/test_agent_direct.py index ee3f9aa091..4fc16a32b0 100644 --- a/dimos/agents2/test_agent_direct.py +++ b/dimos/agents/test_agent_direct.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ from contextlib import contextmanager -from dimos.agents2.agent import Agent +from dimos.agents.agent import Agent from dimos.core import start from dimos.protocol.skill.test_coordinator import SkillContainerTest diff --git a/dimos/agents2/test_agent_fake.py b/dimos/agents/test_agent_fake.py similarity index 97% rename from dimos/agents2/test_agent_fake.py rename to dimos/agents/test_agent_fake.py index 14e28cd89c..367985a356 100644 --- a/dimos/agents2/test_agent_fake.py +++ b/dimos/agents/test_agent_fake.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/agents/test_agent_image_message.py b/dimos/agents/test_agent_image_message.py deleted file mode 100644 index c7f84bcefe..0000000000 --- a/dimos/agents/test_agent_image_message.py +++ /dev/null @@ -1,403 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test BaseAgent with AgentMessage containing images.""" - -import logging -import os - -from dotenv import load_dotenv -import numpy as np -import pytest - -from dimos.agents.agent_message import AgentMessage -from dimos.agents.modules.base import BaseAgent -from dimos.msgs.sensor_msgs import Image -from dimos.msgs.sensor_msgs.Image import ImageFormat -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_agent_image_message") -# Enable debug logging for base module -logging.getLogger("dimos.agents.modules.base").setLevel(logging.DEBUG) - - -@pytest.mark.tofix -def test_agent_single_image() -> None: - """Test agent with single image in AgentMessage.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful vision assistant. Describe what you see concisely.", - temperature=0.0, - seed=42, - ) - - # Create AgentMessage with text and single image - msg = AgentMessage() - msg.add_text("What color is this image?") - - # Create a solid red image in RGB format for clarity - red_data = np.zeros((100, 100, 3), dtype=np.uint8) - red_data[:, :, 0] = 255 # R channel (index 0 in RGB) - red_data[:, :, 1] = 0 # G channel (index 1 in RGB) - red_data[:, :, 2] = 0 # B channel (index 2 in RGB) - # Explicitly specify RGB format to avoid confusion - red_img = Image.from_numpy(red_data, format=ImageFormat.RGB) - print(f"[Test] Created image format: {red_img.format}, shape: {red_img.data.shape}") - msg.add_image(red_img) - - # Query - response = agent.query(msg) - print(f"\n[Test] Single image response: '{response.content}'") - - # Verify response - assert response.content is not None - # The model should mention a color or describe the image - response_lower = response.content.lower() - # Accept any color mention since models may see colors differently - color_mentioned = any( - word in response_lower - for word in ["red", "blue", "color", "solid", "image", "shade", "hue"] - ) - assert color_mentioned, f"Expected color description in response, got: {response.content}" - - # Check conversation history - assert agent.conversation.size() == 2 - # User message should have content array - history = agent.conversation.to_openai_format() - user_msg = history[0] - assert user_msg["role"] == "user" - assert isinstance(user_msg["content"], list), "Multimodal message should have content array" - assert len(user_msg["content"]) == 2 # text + image - assert user_msg["content"][0]["type"] == "text" - assert user_msg["content"][0]["text"] == "What color is this image?" - assert user_msg["content"][1]["type"] == "image_url" - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_agent_multiple_images() -> None: - """Test agent with multiple images in AgentMessage.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful vision assistant that compares images.", - temperature=0.0, - seed=42, - ) - - # Create AgentMessage with multiple images - msg = AgentMessage() - msg.add_text("Compare these three images.") - msg.add_text("What are their colors?") - - # Create three different colored images - red_img = Image(data=np.full((50, 50, 3), [255, 0, 0], dtype=np.uint8)) - green_img = Image(data=np.full((50, 50, 3), [0, 255, 0], dtype=np.uint8)) - blue_img = Image(data=np.full((50, 50, 3), [0, 0, 255], dtype=np.uint8)) - - msg.add_image(red_img) - msg.add_image(green_img) - msg.add_image(blue_img) - - # Query - response = agent.query(msg) - - # Verify response acknowledges the images - response_lower = response.content.lower() - # Check if the model is actually seeing the images - if "unable to view" in response_lower or "can't see" in response_lower: - print(f"WARNING: Model not seeing images: {response.content}") - # Still pass the test but note the issue - else: - # If the model can see images, it should mention some colors - colors_mentioned = sum( - 1 - for color in ["red", "green", "blue", "color", "image", "bright", "dark"] - if color in response_lower - ) - assert colors_mentioned >= 1, ( - f"Expected color/image references, found none in: {response.content}" - ) - - # Check history structure - history = agent.conversation.to_openai_format() - user_msg = history[0] - assert user_msg["role"] == "user" - assert isinstance(user_msg["content"], list) - assert len(user_msg["content"]) == 4 # 1 text + 3 images - assert user_msg["content"][0]["type"] == "text" - assert user_msg["content"][0]["text"] == "Compare these three images. What are their colors?" - - # Verify all images are in the message - for i in range(1, 4): - assert user_msg["content"][i]["type"] == "image_url" - assert user_msg["content"][i]["image_url"]["url"].startswith("data:image/jpeg;base64,") - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_agent_image_with_context() -> None: - """Test agent maintaining context with image queries.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful vision assistant with good memory.", - temperature=0.0, - seed=42, - ) - - # First query with image - msg1 = AgentMessage() - msg1.add_text("This is my favorite color.") - msg1.add_text("Remember it.") - - # Create purple image - purple_img = Image(data=np.full((80, 80, 3), [128, 0, 128], dtype=np.uint8)) - msg1.add_image(purple_img) - - response1 = agent.query(msg1) - # The model should acknowledge the color or mention the image - assert any( - word in response1.content.lower() - for word in ["purple", "violet", "color", "image", "magenta"] - ), f"Expected color or image reference in response: {response1.content}" - - # Second query without image, referencing the first - response2 = agent.query("What was my favorite color that I showed you?") - # Check if the model acknowledges the previous conversation - response_lower = response2.content.lower() - logger.info(f"Response: {response2.content}") - assert any( - word in response_lower - for word in ["purple", "violet", "color", "favorite", "showed", "image"] - ), f"Agent should reference previous conversation: {response2.content}" - - # Check conversation history has all messages - assert agent.conversation.size() == 4 - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_agent_mixed_content() -> None: - """Test agent with mixed text-only and image queries.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant that can see images when provided.", - temperature=0.0, - seed=100, - ) - - # Text-only query - response1 = agent.query("Hello! Can you see images?") - assert response1.content is not None - - # Image query - msg2 = AgentMessage() - msg2.add_text("Now look at this image.") - msg2.add_text("What do you see? Describe the scene.") - - # Use first frame from rgbd_frames test data - import numpy as np - from PIL import Image as PILImage - - from dimos.msgs.sensor_msgs import Image - from dimos.utils.data import get_data - - data_path = get_data("rgbd_frames") - image_path = os.path.join(data_path, "color", "00000.png") - - pil_image = PILImage.open(image_path) - image_array = np.array(pil_image) - - image = Image.from_numpy(image_array) - - msg2.add_image(image) - - # Check image encoding - logger.info(f"Image shape: {image.data.shape}") - logger.info(f"Image encoding: {len(image.agent_encode())} chars") - - response2 = agent.query(msg2) - logger.info(f"Image query response: {response2.content}") - logger.info(f"Agent supports vision: {agent._supports_vision}") - logger.info(f"Message has images: {msg2.has_images()}") - logger.info(f"Number of images in message: {len(msg2.images)}") - # Check that the model saw and described the image - assert any( - word in response2.content.lower() - for word in ["desk", "chair", "table", "laptop", "computer", "screen", "monitor"] - ), f"Expected description of office scene, got: {response2.content}" - - # Another text-only query - response3 = agent.query("What did I just show you?") - words = ["office", "room", "hallway", "image", "scene"] - content = response3.content.lower() - - assert any(word in content for word in words), f"{content=}" - - # Check history structure - assert agent.conversation.size() == 6 - history = agent.conversation.to_openai_format() - # First query should be simple string - assert isinstance(history[0]["content"], str) - # Second query should be content array - assert isinstance(history[2]["content"], list) - # Third query should be simple string again - assert isinstance(history[4]["content"], str) - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_agent_empty_image_message() -> None: - """Test edge case with empty parts of AgentMessage.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant.", - temperature=0.0, - seed=42, - ) - - # AgentMessage with only images, no text - msg = AgentMessage() - # Don't add any text - - # Add a simple colored image - img = Image(data=np.full((60, 60, 3), [255, 255, 0], dtype=np.uint8)) # Yellow - msg.add_image(img) - - response = agent.query(msg) - # Should still work even without text - assert response.content is not None - assert len(response.content) > 0 - - # AgentMessage with empty text parts - msg2 = AgentMessage() - msg2.add_text("") # Empty - msg2.add_text("What") - msg2.add_text("") # Empty - msg2.add_text("color?") - msg2.add_image(img) - - response2 = agent.query(msg2) - # Accept various color interpretations for yellow (RGB 255,255,0) - response_lower = response2.content.lower() - assert any( - color in response_lower for color in ["yellow", "color", "bright", "turquoise", "green"] - ), f"Expected color reference in response: {response2.content}" - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_agent_non_vision_model_with_images() -> None: - """Test that non-vision models handle image input gracefully.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent with non-vision model - agent = BaseAgent( - model="openai::gpt-3.5-turbo", # This model doesn't support vision - system_prompt="You are a helpful assistant.", - temperature=0.0, - seed=42, - ) - - # Try to send an image - msg = AgentMessage() - msg.add_text("What do you see in this image?") - - img = Image(data=np.zeros((100, 100, 3), dtype=np.uint8)) - msg.add_image(img) - - # Should log warning and process as text-only - response = agent.query(msg) - assert response.content is not None - - # Check history - should be text-only - history = agent.conversation.to_openai_format() - user_msg = history[0] - assert isinstance(user_msg["content"], str), "Non-vision model should store text-only" - assert user_msg["content"] == "What do you see in this image?" - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_mock_agent_with_images() -> None: - """Test mock agent with images for CI.""" - # This test doesn't need API keys - - from dimos.agents.test_base_agent_text import MockAgent - - # Create mock agent - agent = MockAgent(model="mock::vision", system_prompt="Mock vision agent") - agent._supports_vision = True # Enable vision support - - # Test with image - msg = AgentMessage() - msg.add_text("What color is this?") - - img = Image(data=np.zeros((50, 50, 3), dtype=np.uint8)) - msg.add_image(img) - - response = agent.query(msg) - assert response.content is not None - assert "Mock response" in response.content or "color" in response.content - - # Check conversation history - assert agent.conversation.size() == 2 - - # Clean up - agent.dispose() diff --git a/dimos/agents/test_agent_message_streams.py b/dimos/agents/test_agent_message_streams.py deleted file mode 100644 index 22d33b46de..0000000000 --- a/dimos/agents/test_agent_message_streams.py +++ /dev/null @@ -1,387 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test BaseAgent with AgentMessage and video streams.""" - -import asyncio -import os -import pickle - -from dotenv import load_dotenv -import pytest -from reactivex import operators as ops - -from dimos import core -from dimos.agents.agent_message import AgentMessage -from dimos.agents.agent_types import AgentResponse -from dimos.agents.modules.base_agent import BaseAgentModule -from dimos.core import In, Module, Out, rpc -from dimos.msgs.sensor_msgs import Image -from dimos.protocol import pubsub -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger -from dimos.utils.testing import TimedSensorReplay - -logger = setup_logger("test_agent_message_streams") - - -class VideoMessageSender(Module): - """Module that sends AgentMessage with video frames every 2 seconds.""" - - message_out: Out[AgentMessage] = None - - def __init__(self, video_path: str) -> None: - super().__init__() - self.video_path = video_path - self._subscription = None - self._frame_count = 0 - - @rpc - def start(self) -> None: - """Start sending video messages.""" - # Use TimedSensorReplay to replay video frames - video_replay = TimedSensorReplay(self.video_path, autocast=Image.from_numpy) - - # Send AgentMessage with frame every 3 seconds (give agent more time to process) - self._subscription = ( - video_replay.stream() - .pipe( - ops.sample(3.0), # Every 3 seconds - ops.take(3), # Only send 3 frames total - ops.map(self._create_message), - ) - .subscribe( - on_next=lambda msg: self._send_message(msg), - on_error=lambda e: logger.error(f"Video stream error: {e}"), - on_completed=lambda: logger.info("Video stream completed"), - ) - ) - - logger.info("Video message streaming started (every 3 seconds, max 3 frames)") - - def _create_message(self, frame: Image) -> AgentMessage: - """Create AgentMessage with frame and query.""" - self._frame_count += 1 - - msg = AgentMessage() - msg.add_text(f"What do you see in frame {self._frame_count}? Describe in one sentence.") - msg.add_image(frame) - - logger.info(f"Created message with frame {self._frame_count}") - return msg - - def _send_message(self, msg: AgentMessage) -> None: - """Send the message and test pickling.""" - # Test that message can be pickled (for module communication) - try: - pickled = pickle.dumps(msg) - pickle.loads(pickled) - logger.info(f"Message pickling test passed - size: {len(pickled)} bytes") - except Exception as e: - logger.error(f"Message pickling failed: {e}") - - self.message_out.publish(msg) - - @rpc - def stop(self) -> None: - """Stop streaming.""" - if self._subscription: - self._subscription.dispose() - self._subscription = None - - -class MultiImageMessageSender(Module): - """Send AgentMessage with multiple images.""" - - message_out: Out[AgentMessage] = None - - def __init__(self, video_path: str) -> None: - super().__init__() - self.video_path = video_path - self.frames = [] - - @rpc - def start(self) -> None: - """Collect some frames.""" - video_replay = TimedSensorReplay(self.video_path, autocast=Image.from_numpy) - - # Collect first 3 frames - video_replay.stream().pipe(ops.take(3)).subscribe( - on_next=lambda frame: self.frames.append(frame), - on_completed=self._send_multi_image_query, - ) - - def _send_multi_image_query(self) -> None: - """Send query with multiple images.""" - if len(self.frames) >= 2: - msg = AgentMessage() - msg.add_text("Compare these images and describe what changed between them.") - - for _i, frame in enumerate(self.frames[:2]): - msg.add_image(frame) - - logger.info(f"Sending multi-image message with {len(msg.images)} images") - - # Test pickling - try: - pickled = pickle.dumps(msg) - logger.info(f"Multi-image message pickle size: {len(pickled)} bytes") - except Exception as e: - logger.error(f"Multi-image pickling failed: {e}") - - self.message_out.publish(msg) - - -class ResponseCollector(Module): - """Collect responses.""" - - response_in: In[AgentResponse] = None - - def __init__(self) -> None: - super().__init__() - self.responses = [] - - @rpc - def start(self) -> None: - self.response_in.subscribe(self._on_response) - - def _on_response(self, resp: AgentResponse) -> None: - logger.info(f"Collected response: {resp.content[:100] if resp.content else 'None'}...") - self.responses.append(resp) - - @rpc - def get_responses(self): - return self.responses - - -@pytest.mark.tofix -@pytest.mark.module -@pytest.mark.asyncio -async def test_agent_message_video_stream() -> None: - """Test BaseAgentModule with AgentMessage containing video frames.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - pubsub.lcm.autoconf() - - logger.info("Testing BaseAgentModule with AgentMessage video stream...") - dimos = core.start(4) - - try: - # Get test video - data_path = get_data("unitree_office_walk") - video_path = os.path.join(data_path, "video") - - logger.info(f"Using video from: {video_path}") - - # Deploy modules - video_sender = dimos.deploy(VideoMessageSender, video_path) - video_sender.message_out.transport = core.pLCMTransport("/agent/message") - - agent = dimos.deploy( - BaseAgentModule, - model="openai::gpt-4o-mini", - system_prompt="You are a vision assistant. Describe what you see concisely.", - temperature=0.0, - ) - agent.response_out.transport = core.pLCMTransport("/agent/response") - - collector = dimos.deploy(ResponseCollector) - - # Connect modules - agent.message_in.connect(video_sender.message_out) - collector.response_in.connect(agent.response_out) - - # Start modules - agent.start() - collector.start() - video_sender.start() - - logger.info("All modules started, streaming video messages...") - - # Wait for 3 messages to be sent (3 frames * 3 seconds = 9 seconds) - # Plus processing time, wait 12 seconds total - await asyncio.sleep(12) - - # Stop video stream - video_sender.stop() - - # Get all responses - responses = collector.get_responses() - logger.info(f"\nCollected {len(responses)} responses:") - for i, resp in enumerate(responses): - logger.info( - f"\nResponse {i + 1}: {resp.content if isinstance(resp, AgentResponse) else resp}" - ) - - # Verify we got at least 2 responses (sometimes the 3rd frame doesn't get processed in time) - assert len(responses) >= 2, f"Expected at least 2 responses, got {len(responses)}" - - # Verify responses describe actual scene - all_responses = " ".join( - resp.content if isinstance(resp, AgentResponse) else resp for resp in responses - ).lower() - assert any( - word in all_responses - for word in ["office", "room", "hallway", "corridor", "door", "wall", "floor", "frame"] - ), "Responses should describe the office environment" - - logger.info("\nāœ… AgentMessage video stream test PASSED!") - - # Stop agent - agent.stop() - - finally: - dimos.close() - dimos.shutdown() - - -@pytest.mark.tofix -@pytest.mark.module -@pytest.mark.asyncio -async def test_agent_message_multi_image() -> None: - """Test BaseAgentModule with AgentMessage containing multiple images.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - pubsub.lcm.autoconf() - - logger.info("Testing BaseAgentModule with multi-image AgentMessage...") - dimos = core.start(4) - - try: - # Get test video - data_path = get_data("unitree_office_walk") - video_path = os.path.join(data_path, "video") - - # Deploy modules - multi_sender = dimos.deploy(MultiImageMessageSender, video_path) - multi_sender.message_out.transport = core.pLCMTransport("/agent/multi_message") - - agent = dimos.deploy( - BaseAgentModule, - model="openai::gpt-4o-mini", - system_prompt="You are a vision assistant that compares images.", - temperature=0.0, - ) - agent.response_out.transport = core.pLCMTransport("/agent/multi_response") - - collector = dimos.deploy(ResponseCollector) - - # Connect modules - agent.message_in.connect(multi_sender.message_out) - collector.response_in.connect(agent.response_out) - - # Start modules - agent.start() - collector.start() - multi_sender.start() - - logger.info("Modules started, sending multi-image query...") - - # Wait for response - await asyncio.sleep(8) - - # Get responses - responses = collector.get_responses() - logger.info(f"\nCollected {len(responses)} responses:") - for i, resp in enumerate(responses): - logger.info( - f"\nResponse {i + 1}: {resp.content if isinstance(resp, AgentResponse) else resp}" - ) - - # Verify we got a response - assert len(responses) >= 1, f"Expected at least 1 response, got {len(responses)}" - - # Response should mention comparison or multiple images - response_text = ( - responses[0].content if isinstance(responses[0], AgentResponse) else responses[0] - ).lower() - assert any( - word in response_text - for word in ["both", "first", "second", "change", "different", "similar", "compare"] - ), "Response should indicate comparison of multiple images" - - logger.info("\nāœ… Multi-image AgentMessage test PASSED!") - - # Stop agent - agent.stop() - - finally: - dimos.close() - dimos.shutdown() - - -@pytest.mark.tofix -def test_agent_message_text_only() -> None: - """Test BaseAgent with text-only AgentMessage.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - from dimos.agents.modules.base import BaseAgent - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant. Answer in 10 words or less.", - temperature=0.0, - seed=42, - ) - - # Test with text-only AgentMessage - msg = AgentMessage() - msg.add_text("What is") - msg.add_text("the capital") - msg.add_text("of France?") - - response = agent.query(msg) - assert "Paris" in response.content, "Expected 'Paris' in response" - - # Test pickling of AgentMessage - pickled = pickle.dumps(msg) - unpickled = pickle.loads(pickled) - assert unpickled.get_combined_text() == "What is the capital of France?" - - # Verify multiple text messages were combined properly - assert len(msg.messages) == 3 - assert msg.messages[0] == "What is" - assert msg.messages[1] == "the capital" - assert msg.messages[2] == "of France?" - - logger.info("āœ… Text-only AgentMessage test PASSED!") - - # Clean up - agent.dispose() - - -if __name__ == "__main__": - logger.info("Running AgentMessage stream tests...") - - # Run text-only test first - test_agent_message_text_only() - print("\n" + "=" * 60 + "\n") - - # Run async tests - asyncio.run(test_agent_message_video_stream()) - print("\n" + "=" * 60 + "\n") - asyncio.run(test_agent_message_multi_image()) - - logger.info("\nāœ… All AgentMessage tests completed!") diff --git a/dimos/agents/test_agent_pool.py b/dimos/agents/test_agent_pool.py deleted file mode 100644 index b3576b80e2..0000000000 --- a/dimos/agents/test_agent_pool.py +++ /dev/null @@ -1,353 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test agent pool module.""" - -import asyncio -import os - -from dotenv import load_dotenv -import pytest - -from dimos import core -from dimos.agents.modules.base_agent import BaseAgentModule -from dimos.core import In, Module, Out, rpc -from dimos.protocol import pubsub - - -class PoolRouter(Module): - """Simple router for agent pool.""" - - query_in: In[dict] = None - agent1_out: Out[str] = None - agent2_out: Out[str] = None - agent3_out: Out[str] = None - - @rpc - def start(self) -> None: - self.query_in.subscribe(self._route) - - def _route(self, msg: dict) -> None: - agent_id = msg.get("agent_id", "agent1") - query = msg.get("query", "") - - if agent_id == "agent1" and self.agent1_out: - self.agent1_out.publish(query) - elif agent_id == "agent2" and self.agent2_out: - self.agent2_out.publish(query) - elif agent_id == "agent3" and self.agent3_out: - self.agent3_out.publish(query) - elif agent_id == "all": - # Broadcast to all - if self.agent1_out: - self.agent1_out.publish(query) - if self.agent2_out: - self.agent2_out.publish(query) - if self.agent3_out: - self.agent3_out.publish(query) - - -class PoolAggregator(Module): - """Aggregate responses from pool.""" - - agent1_in: In[str] = None - agent2_in: In[str] = None - agent3_in: In[str] = None - response_out: Out[dict] = None - - @rpc - def start(self) -> None: - if self.agent1_in: - self.agent1_in.subscribe(lambda r: self._handle_response("agent1", r)) - if self.agent2_in: - self.agent2_in.subscribe(lambda r: self._handle_response("agent2", r)) - if self.agent3_in: - self.agent3_in.subscribe(lambda r: self._handle_response("agent3", r)) - - def _handle_response(self, agent_id: str, response: str) -> None: - if self.response_out: - self.response_out.publish({"agent_id": agent_id, "response": response}) - - -class PoolController(Module): - """Controller for pool testing.""" - - query_out: Out[dict] = None - - @rpc - def send_to_agent(self, agent_id: str, query: str) -> None: - self.query_out.publish({"agent_id": agent_id, "query": query}) - - @rpc - def broadcast(self, query: str) -> None: - self.query_out.publish({"agent_id": "all", "query": query}) - - -class PoolCollector(Module): - """Collect pool responses.""" - - response_in: In[dict] = None - - def __init__(self) -> None: - super().__init__() - self.responses = [] - - @rpc - def start(self) -> None: - self.response_in.subscribe(lambda r: self.responses.append(r)) - - @rpc - def get_responses(self) -> list: - return self.responses - - @rpc - def get_by_agent(self, agent_id: str) -> list: - return [r for r in self.responses if r.get("agent_id") == agent_id] - - -@pytest.mark.skip("Skipping pool tests for now") -@pytest.mark.module -@pytest.mark.asyncio -async def test_agent_pool() -> None: - """Test agent pool with multiple agents.""" - load_dotenv() - pubsub.lcm.autoconf() - - # Check for at least one API key - has_api_key = any( - [os.getenv("OPENAI_API_KEY"), os.getenv("ANTHROPIC_API_KEY"), os.getenv("CEREBRAS_API_KEY")] - ) - - if not has_api_key: - pytest.skip("No API keys found for testing") - - dimos = core.start(7) - - try: - # Deploy three agents with different configs - agents = [] - models = [] - - if os.getenv("CEREBRAS_API_KEY"): - agent1 = dimos.deploy( - BaseAgentModule, - model="cerebras::llama3.1-8b", - system_prompt="You are agent1. Be very brief.", - ) - agents.append(agent1) - models.append("agent1") - - if os.getenv("OPENAI_API_KEY"): - agent2 = dimos.deploy( - BaseAgentModule, - model="openai::gpt-4o-mini", - system_prompt="You are agent2. Be helpful.", - ) - agents.append(agent2) - models.append("agent2") - - if os.getenv("CEREBRAS_API_KEY") and len(agents) < 3: - agent3 = dimos.deploy( - BaseAgentModule, - model="cerebras::llama3.1-8b", - system_prompt="You are agent3. Be creative.", - ) - agents.append(agent3) - models.append("agent3") - - if len(agents) < 2: - pytest.skip("Need at least 2 working agents for pool test") - - # Deploy router, aggregator, controller, collector - router = dimos.deploy(PoolRouter) - aggregator = dimos.deploy(PoolAggregator) - controller = dimos.deploy(PoolController) - collector = dimos.deploy(PoolCollector) - - # Configure transports - controller.query_out.transport = core.pLCMTransport("/pool/queries") - aggregator.response_out.transport = core.pLCMTransport("/pool/responses") - - # Configure agent transports and connections - if len(agents) > 0: - router.agent1_out.transport = core.pLCMTransport("/pool/agent1/query") - agents[0].response_out.transport = core.pLCMTransport("/pool/agent1/response") - agents[0].query_in.connect(router.agent1_out) - aggregator.agent1_in.connect(agents[0].response_out) - - if len(agents) > 1: - router.agent2_out.transport = core.pLCMTransport("/pool/agent2/query") - agents[1].response_out.transport = core.pLCMTransport("/pool/agent2/response") - agents[1].query_in.connect(router.agent2_out) - aggregator.agent2_in.connect(agents[1].response_out) - - if len(agents) > 2: - router.agent3_out.transport = core.pLCMTransport("/pool/agent3/query") - agents[2].response_out.transport = core.pLCMTransport("/pool/agent3/response") - agents[2].query_in.connect(router.agent3_out) - aggregator.agent3_in.connect(agents[2].response_out) - - # Connect router and collector - router.query_in.connect(controller.query_out) - collector.response_in.connect(aggregator.response_out) - - # Start all modules - for agent in agents: - agent.start() - router.start() - aggregator.start() - collector.start() - - await asyncio.sleep(3) - - # Test direct routing - for _i, model_id in enumerate(models[:2]): # Test first 2 agents - controller.send_to_agent(model_id, f"Say hello from {model_id}") - await asyncio.sleep(0.5) - - await asyncio.sleep(6) - - responses = collector.get_responses() - print(f"Got {len(responses)} responses from direct routing") - assert len(responses) >= len(models[:2]), ( - f"Should get responses from at least {len(models[:2])} agents" - ) - - # Test broadcast - collector.responses.clear() - controller.broadcast("What is 1+1?") - - await asyncio.sleep(6) - - responses = collector.get_responses() - print(f"Got {len(responses)} responses from broadcast (expected {len(agents)})") - # Allow for some agents to be slow - assert len(responses) >= min(2, len(agents)), ( - f"Should get response from at least {min(2, len(agents))} agents" - ) - - # Check all agents responded - agent_ids = {r["agent_id"] for r in responses} - assert len(agent_ids) >= 2, "Multiple agents should respond" - - # Stop all agents - for agent in agents: - agent.stop() - - finally: - dimos.close() - dimos.shutdown() - - -@pytest.mark.skip("Skipping pool tests for now") -@pytest.mark.module -@pytest.mark.asyncio -async def test_mock_agent_pool() -> None: - """Test agent pool with mock agents.""" - pubsub.lcm.autoconf() - - class MockPoolAgent(Module): - """Mock agent for pool testing.""" - - query_in: In[str] = None - response_out: Out[str] = None - - def __init__(self, agent_id: str) -> None: - super().__init__() - self.agent_id = agent_id - - @rpc - def start(self) -> None: - self.query_in.subscribe(self._handle_query) - - def _handle_query(self, query: str) -> None: - if "1+1" in query: - self.response_out.publish(f"{self.agent_id}: The answer is 2") - else: - self.response_out.publish(f"{self.agent_id}: {query}") - - dimos = core.start(6) - - try: - # Deploy mock agents - agent1 = dimos.deploy(MockPoolAgent, agent_id="fast") - agent2 = dimos.deploy(MockPoolAgent, agent_id="smart") - agent3 = dimos.deploy(MockPoolAgent, agent_id="creative") - - # Deploy infrastructure - router = dimos.deploy(PoolRouter) - aggregator = dimos.deploy(PoolAggregator) - collector = dimos.deploy(PoolCollector) - - # Configure all transports - router.query_in.transport = core.pLCMTransport("/mock/pool/queries") - router.agent1_out.transport = core.pLCMTransport("/mock/pool/agent1/q") - router.agent2_out.transport = core.pLCMTransport("/mock/pool/agent2/q") - router.agent3_out.transport = core.pLCMTransport("/mock/pool/agent3/q") - - agent1.response_out.transport = core.pLCMTransport("/mock/pool/agent1/r") - agent2.response_out.transport = core.pLCMTransport("/mock/pool/agent2/r") - agent3.response_out.transport = core.pLCMTransport("/mock/pool/agent3/r") - - aggregator.response_out.transport = core.pLCMTransport("/mock/pool/responses") - - # Connect everything - agent1.query_in.connect(router.agent1_out) - agent2.query_in.connect(router.agent2_out) - agent3.query_in.connect(router.agent3_out) - - aggregator.agent1_in.connect(agent1.response_out) - aggregator.agent2_in.connect(agent2.response_out) - aggregator.agent3_in.connect(agent3.response_out) - - collector.response_in.connect(aggregator.response_out) - - # Start all - agent1.start() - agent2.start() - agent3.start() - router.start() - aggregator.start() - collector.start() - - await asyncio.sleep(0.5) - - # Test routing - router.query_in.transport.publish({"agent_id": "agent1", "query": "Hello"}) - router.query_in.transport.publish({"agent_id": "agent2", "query": "Hi"}) - - await asyncio.sleep(0.5) - - responses = collector.get_responses() - assert len(responses) == 2 - assert any("fast" in r["response"] for r in responses) - assert any("smart" in r["response"] for r in responses) - - # Test broadcast - collector.responses.clear() - router.query_in.transport.publish({"agent_id": "all", "query": "What is 1+1?"}) - - await asyncio.sleep(0.5) - - responses = collector.get_responses() - assert len(responses) == 3 - assert all("2" in r["response"] for r in responses) - - finally: - dimos.close() - dimos.shutdown() - - -if __name__ == "__main__": - asyncio.run(test_mock_agent_pool()) diff --git a/dimos/agents/test_agent_tools.py b/dimos/agents/test_agent_tools.py deleted file mode 100644 index fd485ac015..0000000000 --- a/dimos/agents/test_agent_tools.py +++ /dev/null @@ -1,409 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Production test for BaseAgent tool handling functionality.""" - -import asyncio -import os - -from dotenv import load_dotenv -from pydantic import Field -import pytest - -from dimos import core -from dimos.agents.agent_message import AgentMessage -from dimos.agents.agent_types import AgentResponse -from dimos.agents.modules.base import BaseAgent -from dimos.agents.modules.base_agent import BaseAgentModule -from dimos.core import In, Module, Out, rpc -from dimos.protocol import pubsub -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_agent_tools") - - -# Test Skills -class CalculateSkill(AbstractSkill): - """Perform a calculation.""" - - expression: str = Field(description="Mathematical expression to evaluate") - - def __call__(self) -> str: - try: - # Simple evaluation for testing - result = eval(self.expression) - return f"The result is {result}" - except Exception as e: - return f"Error calculating: {e!s}" - - -class WeatherSkill(AbstractSkill): - """Get current weather information for a location. This is a mock weather service that returns test data.""" - - location: str = Field(description="Location to get weather for (e.g. 'London', 'New York')") - - def __call__(self) -> str: - # Mock weather response - return f"The weather in {self.location} is sunny with a temperature of 72°F" - - -class NavigationSkill(AbstractSkill): - """Navigate to a location (potentially long-running).""" - - destination: str = Field(description="Destination to navigate to") - speed: float = Field(default=1.0, description="Navigation speed in m/s") - - def __call__(self) -> str: - # In real implementation, this would start navigation - # For now, simulate blocking behavior - import time - - time.sleep(0.5) # Simulate some processing - return f"Navigation to {self.destination} completed successfully" - - -# Module for testing tool execution -class ToolTestController(Module): - """Controller that sends queries to agent.""" - - message_out: Out[AgentMessage] = None - - @rpc - def send_query(self, query: str) -> None: - msg = AgentMessage() - msg.add_text(query) - self.message_out.publish(msg) - - -class ResponseCollector(Module): - """Collect agent responses.""" - - response_in: In[AgentResponse] = None - - def __init__(self) -> None: - super().__init__() - self.responses = [] - - @rpc - def start(self) -> None: - logger.info("ResponseCollector starting subscription") - self.response_in.subscribe(self._on_response) - logger.info("ResponseCollector subscription active") - - def _on_response(self, response) -> None: - logger.info(f"ResponseCollector received response #{len(self.responses) + 1}: {response}") - self.responses.append(response) - - @rpc - def get_responses(self): - return self.responses - - -@pytest.mark.tofix -@pytest.mark.module -@pytest.mark.asyncio -async def test_agent_module_with_tools() -> None: - """Test BaseAgentModule with tool execution.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - pubsub.lcm.autoconf() - dimos = core.start(4) - - try: - # Create skill library - skill_library = SkillLibrary() - skill_library.add(CalculateSkill) - skill_library.add(WeatherSkill) - skill_library.add(NavigationSkill) - - # Deploy modules - controller = dimos.deploy(ToolTestController) - controller.message_out.transport = core.pLCMTransport("/tools/messages") - - agent = dimos.deploy( - BaseAgentModule, - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant with access to calculation, weather, and navigation tools. When asked about weather, you MUST use the WeatherSkill tool - it provides mock weather data for testing. When asked to navigate somewhere, you MUST use the NavigationSkill tool. Always use the appropriate tool when available.", - skills=skill_library, - temperature=0.0, - memory=False, - ) - agent.response_out.transport = core.pLCMTransport("/tools/responses") - - collector = dimos.deploy(ResponseCollector) - - # Connect modules - agent.message_in.connect(controller.message_out) - collector.response_in.connect(agent.response_out) - - # Start modules - agent.start() - collector.start() - - # Wait for initialization - await asyncio.sleep(1) - - # Test 1: Calculation (fast tool) - logger.info("\n=== Test 1: Calculation Tool ===") - controller.send_query("Use the calculate tool to compute 42 * 17") - await asyncio.sleep(5) # Give more time for the response - - responses = collector.get_responses() - logger.info(f"Got {len(responses)} responses after first query") - assert len(responses) >= 1, ( - f"Should have received at least one response, got {len(responses)}" - ) - - response = responses[-1] - logger.info(f"Response: {response}") - - # Verify the calculation result - assert isinstance(response, AgentResponse), "Expected AgentResponse object" - assert "714" in response.content, f"Expected '714' in response, got: {response.content}" - - # Test 2: Weather query (fast tool) - logger.info("\n=== Test 2: Weather Tool ===") - controller.send_query("What's the weather in New York?") - await asyncio.sleep(5) # Give more time for the second response - - responses = collector.get_responses() - assert len(responses) >= 2, "Should have received at least two responses" - - response = responses[-1] - logger.info(f"Response: {response}") - - # Verify weather details - assert isinstance(response, AgentResponse), "Expected AgentResponse object" - assert "new york" in response.content.lower(), "Expected 'New York' in response" - assert "72" in response.content, "Expected temperature '72' in response" - assert "sunny" in response.content.lower(), "Expected 'sunny' in response" - - # Test 3: Navigation (potentially long-running) - logger.info("\n=== Test 3: Navigation Tool ===") - controller.send_query("Use the NavigationSkill to navigate to the kitchen") - await asyncio.sleep(6) # Give more time for navigation tool to complete - - responses = collector.get_responses() - logger.info(f"Total responses collected: {len(responses)}") - for i, r in enumerate(responses): - logger.info(f" Response {i + 1}: {r.content[:50]}...") - assert len(responses) >= 3, ( - f"Should have received at least three responses, got {len(responses)}" - ) - - response = responses[-1] - logger.info(f"Response: {response}") - - # Verify navigation response - assert isinstance(response, AgentResponse), "Expected AgentResponse object" - assert "kitchen" in response.content.lower(), "Expected 'kitchen' in response" - - # Check if NavigationSkill was called - if response.tool_calls is not None and len(response.tool_calls) > 0: - # Tool was called - verify it - assert any(tc.name == "NavigationSkill" for tc in response.tool_calls), ( - "Expected NavigationSkill to be called" - ) - logger.info("āœ“ NavigationSkill was called") - else: - # Tool wasn't called - just verify response mentions navigation - logger.info("Note: NavigationSkill was not called, agent gave instructions instead") - - # Stop agent - agent.stop() - - # Print summary - logger.info("\n=== Test Summary ===") - all_responses = collector.get_responses() - for i, resp in enumerate(all_responses): - logger.info( - f"Response {i + 1}: {resp.content if isinstance(resp, AgentResponse) else resp}" - ) - - finally: - dimos.close() - dimos.shutdown() - - -@pytest.mark.tofix -def test_base_agent_direct_tools() -> None: - """Test BaseAgent direct usage with tools.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create skill library - skill_library = SkillLibrary() - skill_library.add(CalculateSkill) - skill_library.add(WeatherSkill) - - # Create agent with skills - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant with access to a calculator tool. When asked to calculate something, you should use the CalculateSkill tool.", - skills=skill_library, - temperature=0.0, - memory=False, - seed=42, - ) - - # Test calculation with explicit tool request - logger.info("\n=== Direct Test 1: Calculation Tool ===") - response = agent.query("Calculate 144**0.5") - - logger.info(f"Response content: {response.content}") - logger.info(f"Tool calls: {response.tool_calls}") - - assert response.content is not None - assert "12" in response.content or "twelve" in response.content.lower(), ( - f"Expected '12' in response, got: {response.content}" - ) - - # Verify tool was called OR answer is correct - if response.tool_calls is not None: - assert len(response.tool_calls) > 0, "Expected at least one tool call" - assert response.tool_calls[0].name == "CalculateSkill", ( - f"Expected CalculateSkill, got: {response.tool_calls[0].name}" - ) - assert response.tool_calls[0].status == "completed", ( - f"Expected completed status, got: {response.tool_calls[0].status}" - ) - logger.info("āœ“ Tool was called successfully") - else: - logger.warning("Tool was not called - agent answered directly") - - # Test weather tool - logger.info("\n=== Direct Test 2: Weather Tool ===") - response2 = agent.query("Use the WeatherSkill to check the weather in London") - - logger.info(f"Response content: {response2.content}") - logger.info(f"Tool calls: {response2.tool_calls}") - - assert response2.content is not None - assert "london" in response2.content.lower(), "Expected 'London' in response" - assert "72" in response2.content, "Expected temperature '72' in response" - assert "sunny" in response2.content.lower(), "Expected 'sunny' in response" - - # Verify tool was called - if response2.tool_calls is not None: - assert len(response2.tool_calls) > 0, "Expected at least one tool call" - assert response2.tool_calls[0].name == "WeatherSkill", ( - f"Expected WeatherSkill, got: {response2.tool_calls[0].name}" - ) - logger.info("āœ“ Weather tool was called successfully") - else: - logger.warning("Weather tool was not called - agent answered directly") - - # Clean up - agent.dispose() - - -class MockToolAgent(BaseAgent): - """Mock agent for CI testing without API calls.""" - - def __init__(self, **kwargs) -> None: - # Skip gateway initialization - self.model = kwargs.get("model", "mock::test") - self.system_prompt = kwargs.get("system_prompt", "Mock agent") - self.skills = kwargs.get("skills", SkillLibrary()) - self.history = [] - self._history_lock = __import__("threading").Lock() - self._supports_vision = False - self.response_subject = None - self.gateway = None - self._executor = None - - async def _process_query_async(self, agent_msg, base64_image=None, base64_images=None): - """Mock tool execution.""" - from dimos.agents.agent_message import AgentMessage - from dimos.agents.agent_types import AgentResponse, ToolCall - - # Get text from AgentMessage - if isinstance(agent_msg, AgentMessage): - query = agent_msg.get_combined_text() - else: - query = str(agent_msg) - - # Simple pattern matching for tools - if "calculate" in query.lower(): - # Extract expression - import re - - match = re.search(r"(\d+\s*[\+\-\*/]\s*\d+)", query) - if match: - expr = match.group(1) - tool_call = ToolCall( - id="mock_calc_1", - name="CalculateSkill", - arguments={"expression": expr}, - status="completed", - ) - # Execute the tool - result = self.skills.call("CalculateSkill", expression=expr) - return AgentResponse( - content=f"I calculated {expr} and {result}", tool_calls=[tool_call] - ) - - # Default response - return AgentResponse(content=f"Mock response to: {query}") - - def dispose(self) -> None: - pass - - -@pytest.mark.tofix -def test_mock_agent_tools() -> None: - """Test mock agent with tools for CI.""" - # Create skill library - skill_library = SkillLibrary() - skill_library.add(CalculateSkill) - - # Create mock agent - agent = MockToolAgent(model="mock::test", skills=skill_library) - - # Test calculation - logger.info("\n=== Mock Test: Calculation ===") - response = agent.query("Calculate 25 + 17") - - logger.info(f"Mock response: {response.content}") - logger.info(f"Mock tool calls: {response.tool_calls}") - - assert response.content is not None - assert "42" in response.content, "Expected '42' in response" - assert response.tool_calls is not None, "Expected tool calls" - assert len(response.tool_calls) == 1, "Expected exactly one tool call" - assert response.tool_calls[0].name == "CalculateSkill", "Expected CalculateSkill" - assert response.tool_calls[0].status == "completed", "Expected completed status" - - # Clean up - agent.dispose() - - -if __name__ == "__main__": - # Run tests - test_mock_agent_tools() - print("āœ… Mock agent tools test passed") - - test_base_agent_direct_tools() - print("āœ… Direct agent tools test passed") - - asyncio.run(test_agent_module_with_tools()) - print("āœ… Module agent tools test passed") - - print("\nāœ… All production tool tests passed!") diff --git a/dimos/agents/test_agent_with_modules.py b/dimos/agents/test_agent_with_modules.py deleted file mode 100644 index 1a4ac70f65..0000000000 --- a/dimos/agents/test_agent_with_modules.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test agent module with proper module connections.""" - -import asyncio - -from dotenv import load_dotenv -import pytest - -from dimos import core -from dimos.agents.agent_message import AgentMessage -from dimos.agents.agent_types import AgentResponse -from dimos.agents.modules.base_agent import BaseAgentModule -from dimos.core import In, Module, Out, rpc -from dimos.protocol import pubsub - - -# Test query sender module -class QuerySender(Module): - """Module to send test queries.""" - - message_out: Out[AgentMessage] = None - - def __init__(self) -> None: - super().__init__() - - @rpc - def send_query(self, query: str) -> None: - """Send a query.""" - print(f"Sending query: {query}") - msg = AgentMessage() - msg.add_text(query) - self.message_out.publish(msg) - - -# Test response collector module -class ResponseCollector(Module): - """Module to collect responses.""" - - response_in: In[AgentResponse] = None - - def __init__(self) -> None: - super().__init__() - self.responses = [] - - @rpc - def start(self) -> None: - """Start collecting.""" - self.response_in.subscribe(self._on_response) - - def _on_response(self, msg: AgentResponse) -> None: - print(f"Received response: {msg.content if msg.content else msg}") - self.responses.append(msg) - - @rpc - def get_responses(self): - """Get collected responses.""" - return self.responses - - -@pytest.mark.tofix -@pytest.mark.module -@pytest.mark.asyncio -async def test_agent_module_connections() -> None: - """Test agent module with proper connections.""" - load_dotenv() - pubsub.lcm.autoconf() - - # Start Dask - dimos = core.start(4) - - try: - # Deploy modules - sender = dimos.deploy(QuerySender) - agent = dimos.deploy( - BaseAgentModule, - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant. Answer in 10 words or less.", - ) - collector = dimos.deploy(ResponseCollector) - - # Configure transports - sender.message_out.transport = core.pLCMTransport("/messages") - agent.response_out.transport = core.pLCMTransport("/responses") - - # Connect modules - agent.message_in.connect(sender.message_out) - collector.response_in.connect(agent.response_out) - - # Start modules - agent.start() - collector.start() - - # Wait for initialization - await asyncio.sleep(1) - - # Test 1: Simple query - print("\n=== Test 1: Simple Query ===") - sender.send_query("What is 2+2?") - - await asyncio.sleep(5) # Increased wait time for API response - - responses = collector.get_responses() - assert len(responses) > 0, "Should have received a response" - assert isinstance(responses[0], AgentResponse), "Expected AgentResponse object" - assert "4" in responses[0].content or "four" in responses[0].content.lower(), ( - "Should calculate correctly" - ) - - # Test 2: Another query - print("\n=== Test 2: Another Query ===") - sender.send_query("What color is the sky?") - - await asyncio.sleep(5) # Increased wait time - - responses = collector.get_responses() - assert len(responses) >= 2, "Should have at least two responses" - assert isinstance(responses[1], AgentResponse), "Expected AgentResponse object" - assert "blue" in responses[1].content.lower(), "Should mention blue" - - # Test 3: Multiple queries - print("\n=== Test 3: Multiple Queries ===") - queries = ["Count from 1 to 3", "Name a fruit", "What is Python?"] - - for q in queries: - sender.send_query(q) - await asyncio.sleep(2) # Give more time between queries - - await asyncio.sleep(8) # More time for multiple queries - - responses = collector.get_responses() - assert len(responses) >= 4, f"Should have at least 4 responses, got {len(responses)}" - - # Stop modules - agent.stop() - - print("\n=== All tests passed! ===") - - finally: - dimos.close() - dimos.shutdown() - - -if __name__ == "__main__": - asyncio.run(test_agent_module_connections()) diff --git a/dimos/agents/test_base_agent_text.py b/dimos/agents/test_base_agent_text.py deleted file mode 100644 index 022bea9cd2..0000000000 --- a/dimos/agents/test_base_agent_text.py +++ /dev/null @@ -1,562 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test BaseAgent text functionality.""" - -import asyncio -import os - -from dotenv import load_dotenv -import pytest - -from dimos import core -from dimos.agents.agent_message import AgentMessage -from dimos.agents.agent_types import AgentResponse -from dimos.agents.modules.base import BaseAgent -from dimos.agents.modules.base_agent import BaseAgentModule -from dimos.core import In, Module, Out, rpc -from dimos.protocol import pubsub - - -class QuerySender(Module): - """Module to send test queries.""" - - message_out: Out[AgentMessage] = None # New AgentMessage output - - @rpc - def send_query(self, query: str) -> None: - """Send a query as AgentMessage.""" - msg = AgentMessage() - msg.add_text(query) - self.message_out.publish(msg) - - @rpc - def send_message(self, message: AgentMessage) -> None: - """Send an AgentMessage.""" - self.message_out.publish(message) - - -class ResponseCollector(Module): - """Module to collect responses.""" - - response_in: In[AgentResponse] = None - - def __init__(self) -> None: - super().__init__() - self.responses = [] - - @rpc - def start(self) -> None: - """Start collecting.""" - self.response_in.subscribe(self._on_response) - - def _on_response(self, msg) -> None: - self.responses.append(msg) - - @rpc - def get_responses(self): - """Get collected responses.""" - return self.responses - - -@pytest.mark.tofix -def test_base_agent_direct_text() -> None: - """Test BaseAgent direct text usage.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant. Answer in 10 words or less.", - temperature=0.0, - seed=42, # Fixed seed for deterministic results - ) - - # Test simple query with string (backward compatibility) - response = agent.query("What is 2+2?") - print(f"\n[Test] Query: 'What is 2+2?' -> Response: '{response.content}'") - assert response.content is not None - assert "4" in response.content or "four" in response.content.lower(), ( - f"Expected '4' or 'four' in response, got: {response.content}" - ) - - # Test with AgentMessage - msg = AgentMessage() - msg.add_text("What is 3+3?") - response = agent.query(msg) - print(f"[Test] Query: 'What is 3+3?' -> Response: '{response.content}'") - assert response.content is not None - assert "6" in response.content or "six" in response.content.lower(), ( - "Expected '6' or 'six' in response" - ) - - # Test conversation history - response = agent.query("What was my previous question?") - print(f"[Test] Query: 'What was my previous question?' -> Response: '{response.content}'") - assert response.content is not None - # The agent should reference one of the previous questions - # It might say "2+2" or "3+3" depending on interpretation of "previous" - assert ( - "2+2" in response.content or "3+3" in response.content or "What is" in response.content - ), f"Expected reference to a previous question, got: {response.content}" - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -@pytest.mark.asyncio -async def test_base_agent_async_text() -> None: - """Test BaseAgent async text usage.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant.", - temperature=0.0, - seed=42, - ) - - # Test async query with string - response = await agent.aquery("What is the capital of France?") - assert response.content is not None - assert "Paris" in response.content, "Expected 'Paris' in response" - - # Test async query with AgentMessage - msg = AgentMessage() - msg.add_text("What is the capital of Germany?") - response = await agent.aquery(msg) - assert response.content is not None - assert "Berlin" in response.content, "Expected 'Berlin' in response" - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -@pytest.mark.module -@pytest.mark.asyncio -async def test_base_agent_module_text() -> None: - """Test BaseAgentModule with text via DimOS.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - pubsub.lcm.autoconf() - dimos = core.start(4) - - try: - # Deploy modules - sender = dimos.deploy(QuerySender) - agent = dimos.deploy( - BaseAgentModule, - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant. Answer concisely.", - ) - collector = dimos.deploy(ResponseCollector) - - # Configure transports - sender.message_out.transport = core.pLCMTransport("/test/messages") - agent.response_out.transport = core.pLCMTransport("/test/responses") - - # Connect modules - agent.message_in.connect(sender.message_out) - collector.response_in.connect(agent.response_out) - - # Start modules - agent.start() - collector.start() - - # Wait for initialization - await asyncio.sleep(1) - - # Test queries - sender.send_query("What is 2+2?") - await asyncio.sleep(3) - - responses = collector.get_responses() - assert len(responses) > 0, "Should have received a response" - resp = responses[0] - assert isinstance(resp, AgentResponse), "Expected AgentResponse object" - assert "4" in resp.content or "four" in resp.content.lower(), ( - f"Expected '4' or 'four' in response, got: {resp.content}" - ) - - # Test another query - sender.send_query("What color is the sky?") - await asyncio.sleep(3) - - responses = collector.get_responses() - assert len(responses) >= 2, "Should have at least two responses" - resp = responses[1] - assert isinstance(resp, AgentResponse), "Expected AgentResponse object" - assert "blue" in resp.content.lower(), "Expected 'blue' in response" - - # Test conversation history - sender.send_query("What was my first question?") - await asyncio.sleep(3) - - responses = collector.get_responses() - assert len(responses) >= 3, "Should have at least three responses" - resp = responses[2] - assert isinstance(resp, AgentResponse), "Expected AgentResponse object" - assert "2+2" in resp.content or "2" in resp.content, "Expected reference to first question" - - # Stop modules - agent.stop() - - finally: - dimos.close() - dimos.shutdown() - - -@pytest.mark.parametrize( - "model,provider", - [ - ("openai::gpt-4o-mini", "openai"), - ("anthropic::claude-3-haiku-20240307", "anthropic"), - ("cerebras::llama-3.3-70b", "cerebras"), - ], -) -@pytest.mark.tofix -def test_base_agent_providers(model, provider) -> None: - """Test BaseAgent with different providers.""" - load_dotenv() - - # Check for API key - api_key_map = { - "openai": "OPENAI_API_KEY", - "anthropic": "ANTHROPIC_API_KEY", - "cerebras": "CEREBRAS_API_KEY", - } - - if not os.getenv(api_key_map[provider]): - pytest.skip(f"No {api_key_map[provider]} found") - - # Create agent - agent = BaseAgent( - model=model, - system_prompt="You are a helpful assistant. Answer in 10 words or less.", - temperature=0.0, - seed=42, - ) - - # Test query with AgentMessage - msg = AgentMessage() - msg.add_text("What is the capital of France?") - response = agent.query(msg) - assert response.content is not None - assert "Paris" in response.content, f"Expected 'Paris' in response from {provider}" - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_base_agent_memory() -> None: - """Test BaseAgent with memory/RAG.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant. Use the provided context when answering.", - temperature=0.0, - rag_threshold=0.3, - seed=42, - ) - - # Add context to memory - agent.memory.add_vector("doc1", "The DimOS framework is designed for building robotic systems.") - agent.memory.add_vector( - "doc2", "Robots using DimOS can perform navigation and manipulation tasks." - ) - - # Test RAG retrieval with AgentMessage - msg = AgentMessage() - msg.add_text("What is DimOS?") - response = agent.query(msg) - assert response.content is not None - assert "framework" in response.content.lower() or "robotic" in response.content.lower(), ( - "Expected context about DimOS in response" - ) - - # Clean up - agent.dispose() - - -class MockAgent(BaseAgent): - """Mock agent for testing without API calls.""" - - def __init__(self, **kwargs) -> None: - # Don't call super().__init__ to avoid gateway initialization - from dimos.agents.agent_types import ConversationHistory - - self.model = kwargs.get("model", "mock::test") - self.system_prompt = kwargs.get("system_prompt", "Mock agent") - self.conversation = ConversationHistory(max_size=20) - self._supports_vision = False - self.response_subject = None # Simplified - - async def _process_query_async(self, query: str, base64_image=None) -> str: - """Mock response.""" - if "2+2" in query: - return "The answer is 4" - elif "capital" in query and "France" in query: - return "The capital of France is Paris" - elif "color" in query and "sky" in query: - return "The sky is blue" - elif "previous" in query: - history = self.conversation.to_openai_format() - if len(history) >= 2: - # Get the second to last item (the last user query before this one) - for i in range(len(history) - 2, -1, -1): - if history[i]["role"] == "user": - return f"Your previous question was: {history[i]['content']}" - return "No previous questions" - else: - return f"Mock response to: {query}" - - def query(self, message) -> AgentResponse: - """Mock synchronous query.""" - # Convert to text if AgentMessage - if isinstance(message, AgentMessage): - text = message.get_combined_text() - else: - text = message - - # Update conversation history - self.conversation.add_user_message(text) - response = asyncio.run(self._process_query_async(text)) - self.conversation.add_assistant_message(response) - return AgentResponse(content=response) - - async def aquery(self, message) -> AgentResponse: - """Mock async query.""" - # Convert to text if AgentMessage - if isinstance(message, AgentMessage): - text = message.get_combined_text() - else: - text = message - - self.conversation.add_user_message(text) - response = await self._process_query_async(text) - self.conversation.add_assistant_message(response) - return AgentResponse(content=response) - - def dispose(self) -> None: - """Mock dispose.""" - pass - - -@pytest.mark.tofix -def test_mock_agent() -> None: - """Test mock agent for CI without API keys.""" - # Create mock agent - agent = MockAgent(model="mock::test", system_prompt="Mock assistant") - - # Test simple query - response = agent.query("What is 2+2?") - assert isinstance(response, AgentResponse), "Expected AgentResponse object" - assert "4" in response.content - - # Test conversation history - response = agent.query("What was my previous question?") - assert isinstance(response, AgentResponse), "Expected AgentResponse object" - assert "2+2" in response.content - - # Test other queries - response = agent.query("What is the capital of France?") - assert isinstance(response, AgentResponse), "Expected AgentResponse object" - assert "Paris" in response.content - - response = agent.query("What color is the sky?") - assert isinstance(response, AgentResponse), "Expected AgentResponse object" - assert "blue" in response.content.lower() - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_base_agent_conversation_history() -> None: - """Test that conversation history is properly maintained.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant.", - temperature=0.0, - seed=42, - ) - - # Test 1: Simple conversation - response1 = agent.query("My name is Alice") - assert isinstance(response1, AgentResponse) - - # Check conversation history has both messages - assert agent.conversation.size() == 2 - history = agent.conversation.to_openai_format() - assert history[0]["role"] == "user" - assert history[0]["content"] == "My name is Alice" - assert history[1]["role"] == "assistant" - - # Test 2: Reference previous context - response2 = agent.query("What is my name?") - assert "Alice" in response2.content, "Agent should remember the name" - - # Conversation history should now have 4 messages - assert agent.conversation.size() == 4 - - # Test 3: Multiple text parts in AgentMessage - msg = AgentMessage() - msg.add_text("Calculate") - msg.add_text("the sum of") - msg.add_text("5 + 3") - - response3 = agent.query(msg) - assert "8" in response3.content or "eight" in response3.content.lower() - - # Check the combined text was stored correctly - assert agent.conversation.size() == 6 - history = agent.conversation.to_openai_format() - assert history[4]["role"] == "user" - assert history[4]["content"] == "Calculate the sum of 5 + 3" - - # Test 4: History trimming (set low limit) - agent.max_history = 4 - agent.query("What was my first message?") - - # Conversation history should be trimmed to 4 messages - assert agent.conversation.size() == 4 - # First messages should be gone - history = agent.conversation.to_openai_format() - assert "Alice" not in history[0]["content"] - - # Clean up - agent.dispose() - - -@pytest.mark.tofix -def test_base_agent_history_with_tools() -> None: - """Test conversation history with tool calls.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - from pydantic import Field - - from dimos.skills.skills import AbstractSkill, SkillLibrary - - class CalculatorSkill(AbstractSkill): - """Perform calculations.""" - - expression: str = Field(description="Mathematical expression") - - def __call__(self) -> str: - try: - result = eval(self.expression) - return f"The result is {result}" - except: - return "Error in calculation" - - # Create agent with calculator skill - skills = SkillLibrary() - skills.add(CalculatorSkill) - - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant with a calculator. Use the calculator tool when asked to compute something.", - skills=skills, - temperature=0.0, - seed=42, - ) - - # Make a query that should trigger tool use - response = agent.query("Please calculate 42 * 17 using the calculator tool") - - # Check response - assert isinstance(response, AgentResponse) - assert "714" in response.content, f"Expected 714 in response, got: {response.content}" - - # Check tool calls were made - if response.tool_calls: - assert len(response.tool_calls) > 0 - assert response.tool_calls[0].name == "CalculatorSkill" - assert response.tool_calls[0].status == "completed" - - # Check history structure - # If tools were called, we should have more messages - if response.tool_calls and len(response.tool_calls) > 0: - assert agent.conversation.size() >= 3, ( - f"Expected at least 3 messages in history when tools are used, got {agent.conversation.size()}" - ) - - # Find the assistant message with tool calls - history = agent.conversation.to_openai_format() - tool_msg_found = False - tool_result_found = False - - for msg in history: - if msg.get("role") == "assistant" and msg.get("tool_calls"): - tool_msg_found = True - if msg.get("role") == "tool": - tool_result_found = True - assert "result" in msg.get("content", "").lower() - - assert tool_msg_found, "Tool call message should be in history when tools were used" - assert tool_result_found, "Tool result should be in history when tools were used" - else: - # No tools used, just verify we have user and assistant messages - assert agent.conversation.size() >= 2, ( - f"Expected at least 2 messages in history, got {agent.conversation.size()}" - ) - # The model solved it without using the tool - that's also acceptable - print("Note: Model solved without using the calculator tool") - - # Clean up - agent.dispose() - - -if __name__ == "__main__": - test_base_agent_direct_text() - asyncio.run(test_base_agent_async_text()) - asyncio.run(test_base_agent_module_text()) - test_base_agent_memory() - test_mock_agent() - test_base_agent_conversation_history() - test_base_agent_history_with_tools() - print("\nāœ… All text tests passed!") - test_base_agent_direct_text() - asyncio.run(test_base_agent_async_text()) - asyncio.run(test_base_agent_module_text()) - test_base_agent_memory() - test_mock_agent() - print("\nāœ… All text tests passed!") diff --git a/dimos/agents/test_conversation_history.py b/dimos/agents/test_conversation_history.py deleted file mode 100644 index 95b28fbc0b..0000000000 --- a/dimos/agents/test_conversation_history.py +++ /dev/null @@ -1,416 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Comprehensive conversation history tests for agents.""" - -import asyncio -import logging -import os - -from dotenv import load_dotenv -import numpy as np -from pydantic import Field -import pytest - -from dimos.agents.agent_message import AgentMessage -from dimos.agents.agent_types import AgentResponse -from dimos.agents.modules.base import BaseAgent -from dimos.msgs.sensor_msgs import Image -from dimos.skills.skills import AbstractSkill, SkillLibrary - -logger = logging.getLogger(__name__) - - -@pytest.mark.tofix -def test_conversation_history_basic() -> None: - """Test basic conversation history functionality.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant with perfect memory.", - temperature=0.0, - seed=42, - ) - - try: - # Test 1: Simple text conversation - response1 = agent.query("My favorite color is blue") - assert isinstance(response1, AgentResponse) - assert agent.conversation.size() == 2 # user + assistant - - # Test 2: Reference previous information - response2 = agent.query("What is my favorite color?") - assert "blue" in response2.content.lower(), "Agent should remember the color" - assert agent.conversation.size() == 4 - - # Test 3: Multiple facts - agent.query("I live in San Francisco") - agent.query("I work as an engineer") - - # Verify history is building up - assert agent.conversation.size() == 8 # 4 exchanges (blue, what color, SF, engineer) - - response = agent.query("Tell me what you know about me") - - # Check if agent remembers at least some facts - # Note: Models may sometimes give generic responses, so we check for any memory - facts_mentioned = 0 - if "blue" in response.content.lower() or "color" in response.content.lower(): - facts_mentioned += 1 - if "san francisco" in response.content.lower() or "francisco" in response.content.lower(): - facts_mentioned += 1 - if "engineer" in response.content.lower(): - facts_mentioned += 1 - - # Agent should remember at least one fact, or acknowledge the conversation - assert facts_mentioned > 0 or "know" in response.content.lower(), ( - f"Agent should show some memory of conversation, got: {response.content}" - ) - - # Verify history properly accumulates - assert agent.conversation.size() == 10 - - finally: - agent.dispose() - - -@pytest.mark.tofix -def test_conversation_history_with_images() -> None: - """Test conversation history with multimodal content.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful vision assistant.", - temperature=0.0, - seed=42, - ) - - try: - # Send text message - agent.query("I'm going to show you some colors") - assert agent.conversation.size() == 2 - - # Send image with text - msg = AgentMessage() - msg.add_text("This is a red square") - red_img = Image(data=np.full((100, 100, 3), [255, 0, 0], dtype=np.uint8)) - msg.add_image(red_img) - - agent.query(msg) - assert agent.conversation.size() == 4 - - # Ask about the image - response3 = agent.query("What color did I just show you?") - # Check for any color mention (models sometimes see colors differently) - assert any( - color in response3.content.lower() - for color in ["red", "blue", "green", "color", "square"] - ), f"Should mention a color, got: {response3.content}" - - # Send another image - msg2 = AgentMessage() - msg2.add_text("Now here's a blue square") - blue_img = Image(data=np.full((100, 100, 3), [0, 0, 255], dtype=np.uint8)) - msg2.add_image(blue_img) - - agent.query(msg2) - assert agent.conversation.size() == 8 - - # Ask about all images - response5 = agent.query("What colors have I shown you?") - # Should mention seeing images/colors even if specific colors are wrong - assert any( - word in response5.content.lower() - for word in ["red", "blue", "colors", "squares", "images", "shown", "two"] - ), f"Should acknowledge seeing images, got: {response5.content}" - - # Verify both message types are in history - assert agent.conversation.size() == 10 - - finally: - agent.dispose() - - -@pytest.mark.tofix -def test_conversation_history_trimming() -> None: - """Test that conversation history is trimmed to max size.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create agent with small history limit - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant.", - temperature=0.0, - max_history=3, # Keep 3 message pairs (6 messages total) - seed=42, - ) - - try: - # Add several messages - agent.query("Message 1: I like apples") - assert agent.conversation.size() == 2 - - agent.query("Message 2: I like oranges") - # Now we have 2 pairs (4 messages) - # max_history=3 means we keep max 3 messages total (not pairs!) - size = agent.conversation.size() - # After trimming to 3, we'd have kept the most recent 3 messages - assert size == 3, f"After Message 2, size should be 3, got {size}" - - agent.query("Message 3: I like bananas") - size = agent.conversation.size() - assert size == 3, f"After Message 3, size should be 3, got {size}" - - # This should maintain trimming - agent.query("Message 4: I like grapes") - size = agent.conversation.size() - assert size == 3, f"After Message 4, size should still be 3, got {size}" - - # Add one more - agent.query("Message 5: I like strawberries") - size = agent.conversation.size() - assert size == 3, f"After Message 5, size should still be 3, got {size}" - - # Early messages should be trimmed - agent.query("What was the first fruit I mentioned?") - size = agent.conversation.size() - assert size == 3, f"After question, size should still be 3, got {size}" - - # Change max_history dynamically - agent.max_history = 2 - agent.query("New message after resize") - # Now history should be trimmed to 2 messages - size = agent.conversation.size() - assert size == 2, f"After resize to max_history=2, size should be 2, got {size}" - - finally: - agent.dispose() - - -@pytest.mark.tofix -def test_conversation_history_with_tools() -> None: - """Test conversation history with tool calls.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - # Create a simple skill - class CalculatorSkillLocal(AbstractSkill): - """A simple calculator skill.""" - - expression: str = Field(description="Mathematical expression to evaluate") - - def __call__(self) -> str: - try: - result = eval(self.expression) - return f"The result is {result}" - except Exception as e: - return f"Error: {e}" - - # Create skill library properly - class TestSkillLibrary(SkillLibrary): - CalculatorSkill = CalculatorSkillLocal - - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant with access to a calculator.", - skills=TestSkillLibrary(), - temperature=0.0, - seed=100, - ) - - try: - # Initial query - agent.query("Hello, I need help with math") - assert agent.conversation.size() == 2 - - # Force tool use explicitly - response2 = agent.query( - "I need you to use the CalculatorSkill tool to compute 123 * 456. " - "Do NOT calculate it yourself - you MUST use the calculator tool function." - ) - - assert agent.conversation.size() == 6 # 2 + 1 + 3 - assert response2.tool_calls is not None and len(response2.tool_calls) > 0 - assert "56088" in response2.content.replace(",", "") - - # Ask about previous calculation - response3 = agent.query("What was the result of the calculation?") - assert "56088" in response3.content.replace(",", "") or "123" in response3.content.replace( - ",", "" - ) - assert agent.conversation.size() == 8 - - finally: - agent.dispose() - - -@pytest.mark.tofix -def test_conversation_thread_safety() -> None: - """Test that conversation history is thread-safe.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - agent = BaseAgent(model="openai::gpt-4o-mini", temperature=0.0, seed=42) - - try: - - async def query_async(text: str): - """Async wrapper for query.""" - return await agent.aquery(text) - - async def run_concurrent(): - """Run multiple queries concurrently.""" - tasks = [query_async(f"Query {i}") for i in range(3)] - return await asyncio.gather(*tasks) - - # Run concurrent queries - results = asyncio.run(run_concurrent()) - assert len(results) == 3 - - # Should have roughly 6 messages (3 queries * 2) - # Exact count may vary due to thread timing - assert agent.conversation.size() >= 4 - assert agent.conversation.size() <= 6 - - finally: - agent.dispose() - - -@pytest.mark.tofix -def test_conversation_history_formats() -> None: - """Test ConversationHistory formatting methods.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - agent = BaseAgent(model="openai::gpt-4o-mini", temperature=0.0, seed=42) - - try: - # Create a conversation - agent.conversation.add_user_message("Hello") - agent.conversation.add_assistant_message("Hi there!") - - # Test text with images - agent.conversation.add_user_message( - [ - {"type": "text", "text": "Look at this"}, - {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,abc123"}}, - ] - ) - agent.conversation.add_assistant_message("I see the image") - - # Test tool messages - agent.conversation.add_assistant_message( - content="", - tool_calls=[ - { - "id": "call_123", - "type": "function", - "function": {"name": "test", "arguments": "{}"}, - } - ], - ) - agent.conversation.add_tool_result( - tool_call_id="call_123", content="Tool result", name="test" - ) - - # Get OpenAI format - messages = agent.conversation.to_openai_format() - assert len(messages) == 6 - - # Verify message formats - assert messages[0]["role"] == "user" - assert messages[0]["content"] == "Hello" - - assert messages[2]["role"] == "user" - assert isinstance(messages[2]["content"], list) - - # Tool response message should be at index 5 (after assistant with tool_calls at index 4) - assert messages[5]["role"] == "tool" - assert messages[5]["tool_call_id"] == "call_123" - assert messages[5]["name"] == "test" - - finally: - agent.dispose() - - -@pytest.mark.tofix -@pytest.mark.timeout(30) # Add timeout to prevent hanging -def test_conversation_edge_cases() -> None: - """Test edge cases in conversation history.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OPENAI_API_KEY found") - - agent = BaseAgent( - model="openai::gpt-4o-mini", - system_prompt="You are a helpful assistant.", - temperature=0.0, - seed=42, - ) - - try: - # Empty message - msg1 = AgentMessage() - msg1.add_text("") - response1 = agent.query(msg1) - assert response1.content is not None - - # Moderately long message (reduced from 1000 to 100 words) - long_text = "word " * 100 - response2 = agent.query(long_text) - assert response2.content is not None - - # Multiple text parts that combine - msg3 = AgentMessage() - for i in range(5): # Reduced from 10 to 5 - msg3.add_text(f"Part {i} ") - response3 = agent.query(msg3) - assert response3.content is not None - - # Verify history is maintained correctly - assert agent.conversation.size() == 6 # 3 exchanges - - finally: - agent.dispose() - - -if __name__ == "__main__": - # Run tests - test_conversation_history_basic() - test_conversation_history_with_images() - test_conversation_history_trimming() - test_conversation_history_with_tools() - test_conversation_thread_safety() - test_conversation_history_formats() - test_conversation_edge_cases() - print("\nāœ… All conversation history tests passed!") diff --git a/dimos/agents/test_gateway.py b/dimos/agents/test_gateway.py deleted file mode 100644 index 2c54d5d1ac..0000000000 --- a/dimos/agents/test_gateway.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test gateway functionality.""" - -import asyncio -import os - -from dotenv import load_dotenv -import pytest - -from dimos.agents.modules.gateway import UnifiedGatewayClient - - -@pytest.mark.tofix -@pytest.mark.asyncio -async def test_gateway_basic() -> None: - """Test basic gateway functionality.""" - load_dotenv() - - # Check for at least one API key - has_api_key = any( - [os.getenv("OPENAI_API_KEY"), os.getenv("ANTHROPIC_API_KEY"), os.getenv("CEREBRAS_API_KEY")] - ) - - if not has_api_key: - pytest.skip("No API keys found for gateway test") - - gateway = UnifiedGatewayClient() - - try: - # Test with available provider - if os.getenv("OPENAI_API_KEY"): - model = "openai::gpt-4o-mini" - elif os.getenv("ANTHROPIC_API_KEY"): - model = "anthropic::claude-3-haiku-20240307" - else: - model = "cerebras::llama3.1-8b" - - messages = [ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Say 'Hello Gateway' and nothing else."}, - ] - - # Test non-streaming - response = await gateway.ainference( - model=model, messages=messages, temperature=0.0, max_tokens=10 - ) - - assert "choices" in response - assert len(response["choices"]) > 0 - assert "message" in response["choices"][0] - assert "content" in response["choices"][0]["message"] - - content = response["choices"][0]["message"]["content"] - assert "hello" in content.lower() or "gateway" in content.lower() - - finally: - gateway.close() - - -@pytest.mark.tofix -@pytest.mark.asyncio -async def test_gateway_streaming() -> None: - """Test gateway streaming functionality.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("OpenAI API key required for streaming test") - - gateway = UnifiedGatewayClient() - - try: - messages = [ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Count from 1 to 3"}, - ] - - # Test streaming - chunks = [] - async for chunk in await gateway.ainference( - model="openai::gpt-4o-mini", messages=messages, temperature=0.0, stream=True - ): - chunks.append(chunk) - - assert len(chunks) > 0, "Should receive stream chunks" - - # Reconstruct content - content = "" - for chunk in chunks: - if chunk.get("choices"): - delta = chunk["choices"][0].get("delta", {}) - chunk_content = delta.get("content") - if chunk_content is not None: - content += chunk_content - - assert any(str(i) in content for i in [1, 2, 3]), "Should count numbers" - - finally: - gateway.close() - - -@pytest.mark.tofix -@pytest.mark.asyncio -async def test_gateway_tools() -> None: - """Test gateway can pass tool definitions to LLM and get responses.""" - load_dotenv() - - if not os.getenv("OPENAI_API_KEY"): - pytest.skip("OpenAI API key required for tools test") - - gateway = UnifiedGatewayClient() - - try: - # Just test that gateway accepts tools parameter and returns valid response - tools = [ - { - "type": "function", - "function": { - "name": "test_function", - "description": "A test function", - "parameters": { - "type": "object", - "properties": {"param": {"type": "string"}}, - }, - }, - } - ] - - messages = [ - {"role": "user", "content": "Hello, just testing the gateway"}, - ] - - # Just verify gateway doesn't crash when tools are provided - response = await gateway.ainference( - model="openai::gpt-4o-mini", messages=messages, tools=tools, temperature=0.0 - ) - - # Basic validation - gateway returned something - assert "choices" in response - assert len(response["choices"]) > 0 - assert "message" in response["choices"][0] - - finally: - gateway.close() - - -@pytest.mark.tofix -@pytest.mark.asyncio -async def test_gateway_providers() -> None: - """Test gateway with different providers.""" - load_dotenv() - - gateway = UnifiedGatewayClient() - - providers_tested = 0 - - try: - # Test each available provider - test_cases = [ - ("openai::gpt-4o-mini", "OPENAI_API_KEY"), - ("anthropic::claude-3-haiku-20240307", "ANTHROPIC_API_KEY"), - # ("cerebras::llama3.1-8b", "CEREBRAS_API_KEY"), - ("qwen::qwen-turbo", "DASHSCOPE_API_KEY"), - ] - - for model, env_var in test_cases: - if not os.getenv(env_var): - continue - - providers_tested += 1 - - messages = [{"role": "user", "content": "Reply with just the word 'OK'"}] - - response = await gateway.ainference( - model=model, messages=messages, temperature=0.0, max_tokens=10 - ) - - assert "choices" in response - content = response["choices"][0]["message"]["content"] - assert len(content) > 0, f"{model} should return content" - - if providers_tested == 0: - pytest.skip("No API keys found for provider test") - - finally: - gateway.close() - - -if __name__ == "__main__": - load_dotenv() - asyncio.run(test_gateway_basic()) diff --git a/dimos/agents2/test_mock_agent.py b/dimos/agents/test_mock_agent.py similarity index 94% rename from dimos/agents2/test_mock_agent.py rename to dimos/agents/test_mock_agent.py index 4b113b45a0..c711e23143 100644 --- a/dimos/agents2/test_mock_agent.py +++ b/dimos/agents/test_mock_agent.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,13 +20,13 @@ from langchain_core.messages import AIMessage, HumanMessage import pytest -from dimos.agents2.agent import Agent -from dimos.agents2.testing import MockModel +from dimos.agents.agent import Agent +from dimos.agents.testing import MockModel from dimos.core import LCMTransport, start from dimos.msgs.geometry_msgs import PoseStamped, Vector3 from dimos.msgs.sensor_msgs import Image from dimos.protocol.skill.test_coordinator import SkillContainerTest -from dimos.robot.unitree_webrtc.modular.connection_module import ConnectionModule +from dimos.robot.unitree.connection.go2 import GO2Connection from dimos.robot.unitree_webrtc.type.lidar import LidarMessage @@ -157,11 +157,11 @@ def test_tool_call_implicit_detections() -> None: system_prompt="You are a helpful robot assistant with camera capabilities.", ) - robot_connection = dimos.deploy(ConnectionModule, connection_type="fake") + robot_connection = dimos.deploy(GO2Connection, connection_type="fake") robot_connection.lidar.transport = LCMTransport("/lidar", LidarMessage) robot_connection.odom.transport = LCMTransport("/odom", PoseStamped) robot_connection.video.transport = LCMTransport("/image", Image) - robot_connection.movecmd.transport = LCMTransport("/cmd_vel", Vector3) + robot_connection.cmd_vel.transport = LCMTransport("/cmd_vel", Vector3) robot_connection.camera_info.transport = LCMTransport("/camera_info", CameraInfo) robot_connection.start() diff --git a/dimos/agents/test_simple_agent_module.py b/dimos/agents/test_simple_agent_module.py deleted file mode 100644 index bd374877dd..0000000000 --- a/dimos/agents/test_simple_agent_module.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test simple agent module with string input/output.""" - -import asyncio -import os - -from dotenv import load_dotenv -import pytest - -from dimos import core -from dimos.agents.agent_message import AgentMessage -from dimos.agents.agent_types import AgentResponse -from dimos.agents.modules.base_agent import BaseAgentModule -from dimos.core import In, Module, Out, rpc -from dimos.protocol import pubsub - - -class QuerySender(Module): - """Module to send test queries.""" - - message_out: Out[AgentMessage] = None - - @rpc - def send_query(self, query: str) -> None: - """Send a query.""" - msg = AgentMessage() - msg.add_text(query) - self.message_out.publish(msg) - - -class ResponseCollector(Module): - """Module to collect responses.""" - - response_in: In[AgentResponse] = None - - def __init__(self) -> None: - super().__init__() - self.responses = [] - - @rpc - def start(self) -> None: - """Start collecting.""" - self.response_in.subscribe(self._on_response) - - def _on_response(self, response: AgentResponse) -> None: - """Handle response.""" - self.responses.append(response) - - @rpc - def get_responses(self) -> list: - """Get collected responses.""" - return self.responses - - @rpc - def clear(self) -> None: - """Clear responses.""" - self.responses = [] - - -@pytest.mark.tofix -@pytest.mark.module -@pytest.mark.asyncio -@pytest.mark.parametrize( - "model,provider", - [ - ("openai::gpt-4o-mini", "OpenAI"), - ("anthropic::claude-3-haiku-20240307", "Claude"), - ("cerebras::llama3.1-8b", "Cerebras"), - ("qwen::qwen-turbo", "Qwen"), - ], -) -async def test_simple_agent_module(model, provider) -> None: - """Test simple agent module with different providers.""" - load_dotenv() - - # Skip if no API key - if provider == "OpenAI" and not os.getenv("OPENAI_API_KEY"): - pytest.skip("No OpenAI API key found") - elif provider == "Claude" and not os.getenv("ANTHROPIC_API_KEY"): - pytest.skip("No Anthropic API key found") - elif provider == "Cerebras" and not os.getenv("CEREBRAS_API_KEY"): - pytest.skip("No Cerebras API key found") - elif provider == "Qwen" and not os.getenv("ALIBABA_API_KEY"): - pytest.skip("No Qwen API key found") - - pubsub.lcm.autoconf() - - # Start Dask cluster - dimos = core.start(3) - - try: - # Deploy modules - sender = dimos.deploy(QuerySender) - agent = dimos.deploy( - BaseAgentModule, - model=model, - system_prompt=f"You are a helpful {provider} assistant. Keep responses brief.", - ) - collector = dimos.deploy(ResponseCollector) - - # Configure transports - sender.message_out.transport = core.pLCMTransport(f"/test/{provider}/messages") - agent.response_out.transport = core.pLCMTransport(f"/test/{provider}/responses") - - # Connect modules - agent.message_in.connect(sender.message_out) - collector.response_in.connect(agent.response_out) - - # Start modules - agent.start() - collector.start() - - await asyncio.sleep(1) - - # Test simple math - sender.send_query("What is 2+2?") - await asyncio.sleep(5) - - responses = collector.get_responses() - assert len(responses) > 0, f"{provider} should respond" - assert isinstance(responses[0], AgentResponse), "Expected AgentResponse object" - assert "4" in responses[0].content, f"{provider} should calculate correctly" - - # Test brief response - collector.clear() - sender.send_query("Name one color.") - await asyncio.sleep(5) - - responses = collector.get_responses() - assert len(responses) > 0, f"{provider} should respond" - assert isinstance(responses[0], AgentResponse), "Expected AgentResponse object" - assert len(responses[0].content) < 200, f"{provider} should give brief response" - - # Stop modules - agent.stop() - - finally: - dimos.close() - dimos.shutdown() - - -@pytest.mark.tofix -@pytest.mark.module -@pytest.mark.asyncio -async def test_mock_agent_module() -> None: - """Test agent module with mock responses (no API needed).""" - pubsub.lcm.autoconf() - - class MockAgentModule(Module): - """Mock agent for testing.""" - - message_in: In[AgentMessage] = None - response_out: Out[AgentResponse] = None - - @rpc - def start(self) -> None: - self.message_in.subscribe(self._handle_message) - - def _handle_message(self, msg: AgentMessage) -> None: - query = msg.get_combined_text() - if "2+2" in query: - self.response_out.publish(AgentResponse(content="4")) - elif "color" in query.lower(): - self.response_out.publish(AgentResponse(content="Blue")) - else: - self.response_out.publish(AgentResponse(content=f"Mock response to: {query}")) - - dimos = core.start(2) - - try: - # Deploy - agent = dimos.deploy(MockAgentModule) - collector = dimos.deploy(ResponseCollector) - - # Configure - agent.message_in.transport = core.pLCMTransport("/mock/messages") - agent.response_out.transport = core.pLCMTransport("/mock/response") - - # Connect - collector.response_in.connect(agent.response_out) - - # Start - agent.start() - collector.start() - - await asyncio.sleep(1) - - # Test - use a simple query sender - sender = dimos.deploy(QuerySender) - sender.message_out.transport = core.pLCMTransport("/mock/messages") - agent.message_in.connect(sender.message_out) - - await asyncio.sleep(1) - - sender.send_query("What is 2+2?") - await asyncio.sleep(1) - - responses = collector.get_responses() - assert len(responses) == 1 - assert isinstance(responses[0], AgentResponse), "Expected AgentResponse object" - assert responses[0].content == "4" - - finally: - dimos.close() - dimos.shutdown() - - -if __name__ == "__main__": - asyncio.run(test_mock_agent_module()) diff --git a/dimos/agents2/test_stash_agent.py b/dimos/agents/test_stash_agent.py similarity index 96% rename from dimos/agents2/test_stash_agent.py rename to dimos/agents/test_stash_agent.py index 8e2972568a..2b712fed1a 100644 --- a/dimos/agents2/test_stash_agent.py +++ b/dimos/agents/test_stash_agent.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ import pytest -from dimos.agents2.agent import Agent +from dimos.agents.agent import Agent from dimos.protocol.skill.test_coordinator import SkillContainerTest diff --git a/dimos/agents/testing.py b/dimos/agents/testing.py new file mode 100644 index 0000000000..dc563b9ea9 --- /dev/null +++ b/dimos/agents/testing.py @@ -0,0 +1,197 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Testing utilities for agents.""" + +from collections.abc import Iterator, Sequence +import json +import os +from pathlib import Path +from typing import Any + +from langchain.chat_models import init_chat_model +from langchain_core.callbacks.manager import CallbackManagerForLLMRun +from langchain_core.language_models.chat_models import SimpleChatModel +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, +) +from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult +from langchain_core.runnables import Runnable + + +class MockModel(SimpleChatModel): + """Custom fake chat model that supports tool calls for testing. + + Can operate in two modes: + 1. Playback mode (default): Reads responses from a JSON file or list + 2. Record mode: Uses a real LLM and saves responses to a JSON file + """ + + responses: list[str | AIMessage] = [] + i: int = 0 + json_path: Path | None = None + record: bool = False + real_model: Any | None = None + recorded_messages: list[dict[str, Any]] = [] + + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] + # Extract custom parameters before calling super().__init__ + responses = kwargs.pop("responses", []) + json_path = kwargs.pop("json_path", None) + model_provider = kwargs.pop("model_provider", "openai") + model_name = kwargs.pop("model_name", "gpt-4o") + + super().__init__(**kwargs) + + self.json_path = Path(json_path) if json_path else None + self.record = bool(os.getenv("RECORD")) + self.i = 0 + self._bound_tools: Sequence[Any] | None = None + self.recorded_messages = [] + + if self.record: + # Initialize real model for recording + self.real_model = init_chat_model(model_provider=model_provider, model=model_name) + self.responses = [] # Initialize empty for record mode + elif self.json_path: + self.responses = self._load_responses_from_json() # type: ignore[assignment] + elif responses: + self.responses = responses + else: + raise ValueError("no responses") + + @property + def _llm_type(self) -> str: + return "tool-call-fake-chat-model" + + def _load_responses_from_json(self) -> list[AIMessage]: + with open(self.json_path) as f: # type: ignore[arg-type] + data = json.load(f) + + responses = [] + for item in data.get("responses", []): + if isinstance(item, str): + responses.append(AIMessage(content=item)) + else: + # Reconstruct AIMessage from dict + msg = AIMessage( + content=item.get("content", ""), tool_calls=item.get("tool_calls", []) + ) + responses.append(msg) + return responses + + def _save_responses_to_json(self) -> None: + if not self.json_path: + return + + self.json_path.parent.mkdir(parents=True, exist_ok=True) + + data = { + "responses": [ + {"content": msg.content, "tool_calls": getattr(msg, "tool_calls", [])} + if isinstance(msg, AIMessage) + else msg + for msg in self.recorded_messages + ] + } + + with open(self.json_path, "w") as f: + json.dump(data, f, indent=2, default=str) + + def _call( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> str: + """Not used in _generate.""" + return "" + + def _generate( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> ChatResult: + if self.record: + # Recording mode - use real model and save responses + if not self.real_model: + raise ValueError("Real model not initialized for recording") + + # Bind tools if needed + model = self.real_model + if self._bound_tools: + model = model.bind_tools(self._bound_tools) + + result = model.invoke(messages) + self.recorded_messages.append(result) + self._save_responses_to_json() + + generation = ChatGeneration(message=result) + return ChatResult(generations=[generation]) + else: + # Playback mode - use predefined responses + if not self.responses: + raise ValueError("No responses available for playback. ") + + if self.i >= len(self.responses): + # Don't wrap around - stay at last response + response = self.responses[-1] + else: + response = self.responses[self.i] + self.i += 1 + + if isinstance(response, AIMessage): + message = response + else: + message = AIMessage(content=str(response)) + + generation = ChatGeneration(message=message) + return ChatResult(generations=[generation]) + + def _stream( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + """Stream not implemented for testing.""" + result = self._generate(messages, stop, run_manager, **kwargs) + message = result.generations[0].message + chunk = AIMessageChunk(content=message.content) + yield ChatGenerationChunk(message=chunk) + + def bind_tools( + self, + tools: Sequence[dict[str, Any] | type | Any], + *, + tool_choice: str | None = None, + **kwargs: Any, + ) -> Runnable: # type: ignore[type-arg] + """Store tools and return self.""" + self._bound_tools = tools + if self.record and self.real_model: + # Also bind tools to the real model + self.real_model = self.real_model.bind_tools(tools, tool_choice=tool_choice, **kwargs) + return self + + @property + def tools(self) -> Sequence[Any] | None: + """Get bound tools for inspection.""" + return self._bound_tools diff --git a/dimos/agents/tokenizer/base.py b/dimos/agents/tokenizer/base.py deleted file mode 100644 index 7957c896fa..0000000000 --- a/dimos/agents/tokenizer/base.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - -# TODO: Add a class for specific tokenizer exceptions -# TODO: Build out testing and logging -# TODO: Create proper doc strings after multiple tokenizers are implemented - - -class AbstractTokenizer(ABC): - @abstractmethod - def tokenize_text(self, text: str): - pass - - @abstractmethod - def detokenize_text(self, tokenized_text): - pass - - @abstractmethod - def token_count(self, text: str): - pass - - @abstractmethod - def image_token_count(self, image_width, image_height, image_detail: str = "low"): - pass diff --git a/dimos/agents/vlm_agent.py b/dimos/agents/vlm_agent.py new file mode 100644 index 0000000000..0757a59d22 --- /dev/null +++ b/dimos/agents/vlm_agent.py @@ -0,0 +1,120 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from langchain_core.messages import AIMessage, HumanMessage, SystemMessage + +from dimos.agents.llm_init import build_llm, build_system_message +from dimos.agents.spec import AgentSpec, AnyMessage +from dimos.core import rpc +from dimos.core.stream import In, Out +from dimos.msgs.sensor_msgs import Image +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class VLMAgent(AgentSpec): + """Stream-first agent for vision queries with optional RPC access.""" + + color_image: In[Image] + query_stream: In[HumanMessage] + answer_stream: Out[AIMessage] + + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(*args, **kwargs) + self._llm = build_llm(self.config) + self._latest_image: Image | None = None + self._history: list[AIMessage | HumanMessage] = [] + self._system_message = build_system_message(self.config) + self.publish(self._system_message) + + @rpc + def start(self) -> None: + super().start() + self._disposables.add(self.color_image.subscribe(self._on_image)) # type: ignore[arg-type] + self._disposables.add(self.query_stream.subscribe(self._on_query)) # type: ignore[arg-type] + + @rpc + def stop(self) -> None: + super().stop() + + def _on_image(self, image: Image) -> None: + self._latest_image = image + + def _on_query(self, msg: HumanMessage) -> None: + if not self._latest_image: + self.answer_stream.publish(AIMessage(content="No image available yet.")) + return + + query_text = self._extract_text(msg) + response = self._invoke_image(self._latest_image, query_text) + self.answer_stream.publish(response) + + def _extract_text(self, msg: HumanMessage) -> str: + content = msg.content + if isinstance(content, str): + return content + if isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + return str(part.get("text", "")) + return str(content) + + def _invoke(self, msg: HumanMessage) -> AIMessage: + messages = [self._system_message, msg] + response = self._llm.invoke(messages) + self.append_history([msg, response]) # type: ignore[arg-type] + return response # type: ignore[return-value] + + def _invoke_image(self, image: Image, query: str) -> AIMessage: + content = [{"type": "text", "text": query}, *image.agent_encode()] + return self._invoke(HumanMessage(content=content)) + + @rpc + def clear_history(self): # type: ignore[no-untyped-def] + self._history.clear() + + def append_history(self, *msgs: list[AIMessage | HumanMessage]) -> None: + for msg_list in msgs: + for msg in msg_list: + self.publish(msg) # type: ignore[arg-type] + self._history.extend(msg_list) + + def history(self) -> list[AnyMessage]: + return [self._system_message, *self._history] + + @rpc + def register_skills( # type: ignore[no-untyped-def] + self, container, run_implicit_name: str | None = None + ) -> None: + logger.warning( + "VLMAgent does not manage skills; register_skills is a no-op", + container=str(container), + run_implicit_name=run_implicit_name, + ) + + @rpc + def query(self, query: str): # type: ignore[no-untyped-def] + response = self._invoke(HumanMessage(query)) + return response.content + + @rpc + def query_image(self, image: Image, query: str): # type: ignore[no-untyped-def] + response = self._invoke_image(image, query) + return response.content + + +vlm_agent = VLMAgent.blueprint + +__all__ = ["VLMAgent", "vlm_agent"] diff --git a/dimos/agents/vlm_stream_tester.py b/dimos/agents/vlm_stream_tester.py new file mode 100644 index 0000000000..79bb802a03 --- /dev/null +++ b/dimos/agents/vlm_stream_tester.py @@ -0,0 +1,179 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time + +from langchain_core.messages import AIMessage, HumanMessage + +from dimos.core import Module, rpc +from dimos.core.stream import In, Out +from dimos.msgs.sensor_msgs import Image +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class VlmStreamTester(Module): + """Smoke-test VLMAgent with replayed images and stream queries.""" + + color_image: In[Image] + query_stream: Out[HumanMessage] + answer_stream: In[AIMessage] + + rpc_calls: list[str] = [ + "VLMAgent.query_image", + ] + + def __init__( # type: ignore[no-untyped-def] + self, + prompt: str = "What do you see?", + num_queries: int = 10, + query_interval_s: float = 2.0, + max_image_age_s: float = 1.5, + max_image_gap_s: float = 1.5, + ) -> None: + super().__init__() + self._prompt = prompt + self._num_queries = num_queries + self._query_interval_s = query_interval_s + self._max_image_age_s = max_image_age_s + self._max_image_gap_s = max_image_gap_s + self._latest_image: Image | None = None + self._latest_image_wall_ts: float | None = None + self._last_image_wall_ts: float | None = None + self._max_gap_seen_s = 0.0 + self._answer_count = 0 + self._stop_event = threading.Event() + self._worker: threading.Thread | None = None + + @rpc + def start(self) -> None: + super().start() + self._disposables.add(self.color_image.subscribe(self._on_image)) # type: ignore[arg-type] + self._disposables.add(self.answer_stream.subscribe(self._on_answer)) # type: ignore[arg-type] + self._worker = threading.Thread(target=self._run_queries, daemon=True) + self._worker.start() + + @rpc + def stop(self) -> None: + self._stop_event.set() + if self._worker and self._worker.is_alive(): + self._worker.join(timeout=1.0) + super().stop() + + def _on_image(self, image: Image) -> None: + now = time.time() + if self._last_image_wall_ts is not None: + gap = now - self._last_image_wall_ts + if gap > self._max_gap_seen_s: + self._max_gap_seen_s = gap + self._last_image_wall_ts = now + self._latest_image_wall_ts = now + self._latest_image = image + + def _on_answer(self, msg: AIMessage) -> None: + self._answer_count += 1 + logger.info( + "VLMAgent stream answer", + count=self._answer_count, + content=msg.content, + ) + + def _run_queries(self) -> None: + try: + while not self._stop_event.is_set() and self._latest_image is None: + time.sleep(0.05) + + self._run_stream_queries() + self._run_rpc_queries() + except Exception as exc: + logger.exception("VlmStreamTester query loop failed", error=str(exc)) + finally: + if self._max_gap_seen_s > self._max_image_gap_s: + logger.warning( + "Image stream gap exceeded threshold", + max_gap_s=self._max_gap_seen_s, + threshold_s=self._max_image_gap_s, + ) + + def _run_stream_queries(self) -> None: + for idx in range(self._num_queries): + if self._stop_event.is_set(): + break + if self._latest_image is None: + logger.warning("No image available for stream query.") + break + + image_age = None + if self._latest_image_wall_ts is not None: + image_age = time.time() - self._latest_image_wall_ts + if image_age > self._max_image_age_s: + logger.warning( + "Latest image is stale", + age_s=image_age, + max_age_s=self._max_image_age_s, + ) + + logger.info("Sending stream query", index=idx + 1, total=self._num_queries) + self.query_stream.publish( + HumanMessage(content=f"{self._prompt} (stream query {idx + 1}/{self._num_queries})") + ) + time.sleep(self._query_interval_s) + + def _run_rpc_queries(self) -> None: + rpc_query = None + try: + rpc_query = self.get_rpc_calls("VLMAgent.query_image") + except Exception as exc: + logger.warning("RPC query_image lookup failed", error=str(exc)) + return + + for idx in range(self._num_queries): + if self._stop_event.is_set(): + break + if self._latest_image is None: + logger.warning("No image available for RPC query.") + break + + image_age = None + if self._latest_image_wall_ts is not None: + image_age = time.time() - self._latest_image_wall_ts + if image_age > self._max_image_age_s: + logger.warning( + "Latest image is stale", + age_s=image_age, + max_age_s=self._max_image_age_s, + ) + + logger.info("Sending RPC query", index=idx + 1, total=self._num_queries) + try: + response = rpc_query( + self._latest_image, + f"{self._prompt} (rpc query {idx + 1}/{self._num_queries})", + ) + logger.info( + "VLMAgent RPC answer", + query_index=idx + 1, + image_age_s=image_age, + content=response, + ) + except Exception as exc: + logger.warning("RPC query_image failed", error=str(exc)) + time.sleep(self._query_interval_s) + + +vlm_stream_tester = VlmStreamTester.blueprint + +__all__ = ["VlmStreamTester", "vlm_stream_tester"] diff --git a/dimos/agents2/__init__.py b/dimos/agents2/__init__.py deleted file mode 100644 index 28a48430b6..0000000000 --- a/dimos/agents2/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from langchain_core.messages import ( - AIMessage, - HumanMessage, - MessageLikeRepresentation, - SystemMessage, - ToolCall, - ToolMessage, -) - -from dimos.agents2.agent import Agent -from dimos.agents2.spec import AgentSpec -from dimos.protocol.skill.skill import skill -from dimos.protocol.skill.type import Output, Reducer, Stream diff --git a/dimos/agents2/agent.py b/dimos/agents2/agent.py deleted file mode 100644 index 0fcd05d3e5..0000000000 --- a/dimos/agents2/agent.py +++ /dev/null @@ -1,374 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import asyncio -import datetime -import json -from operator import itemgetter -import os -from typing import Any, TypedDict -import uuid - -from langchain.chat_models import init_chat_model -from langchain_core.messages import ( - AIMessage, - HumanMessage, - SystemMessage, - ToolCall, - ToolMessage, -) - -from dimos.agents2.spec import AgentSpec -from dimos.agents2.system_prompt import get_system_prompt -from dimos.core import rpc -from dimos.protocol.skill.coordinator import SkillCoordinator, SkillState, SkillStateDict -from dimos.protocol.skill.type import Output -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.protocol.agents2") - - -SYSTEM_MSG_APPEND = "\nYour message history will always be appended with a System Overview message that provides situational awareness." - - -def toolmsg_from_state(state: SkillState) -> ToolMessage: - if state.skill_config.output != Output.standard: - content = "output attached in separate messages" - else: - content = state.content() - - return ToolMessage( - # if agent call has been triggered by another skill, - # and this specific skill didn't finish yet but we need a tool call response - # we return a message explaining that execution is still ongoing - content=content - or "Running, you will be called with an update, no need for subsequent tool calls", - name=state.name, - tool_call_id=state.call_id, - ) - - -class SkillStateSummary(TypedDict): - name: str - call_id: str - state: str - data: Any - - -def summary_from_state(state: SkillState, special_data: bool = False) -> SkillStateSummary: - content = state.content() - if isinstance(content, dict): - content = json.dumps(content) - - if not isinstance(content, str): - content = str(content) - - return { - "name": state.name, - "call_id": state.call_id, - "state": state.state.name, - "data": state.content() if not special_data else "data will be in a separate message", - } - - -def _custom_json_serializers(obj): - if isinstance(obj, datetime.date | datetime.datetime): - return obj.isoformat() - raise TypeError(f"Type {type(obj)} not serializable") - - -# takes an overview of running skills from the coorindator -# and builds messages to be sent to an agent -def snapshot_to_messages( - state: SkillStateDict, - tool_calls: list[ToolCall], -) -> tuple[list[ToolMessage], AIMessage | None]: - # builds a set of tool call ids from a previous agent request - tool_call_ids = set( - map(itemgetter("id"), tool_calls), - ) - - # build a tool msg responses - tool_msgs: list[ToolMessage] = [] - - # build a general skill state overview (for longer running skills) - state_overview: list[dict[str, SkillStateSummary]] = [] - - # for special skills that want to return a separate message - # (images for example, requires to be a HumanMessage) - special_msgs: list[HumanMessage] = [] - - # for special skills that want to return a separate message that should - # stay in history, like actual human messages, critical events - history_msgs: list[HumanMessage] = [] - - # Initialize state_msg - state_msg = None - - for skill_state in sorted( - state.values(), - key=lambda skill_state: skill_state.duration(), - ): - if skill_state.call_id in tool_call_ids: - tool_msgs.append(toolmsg_from_state(skill_state)) - - if skill_state.skill_config.output == Output.human: - content = skill_state.content() - if not content: - continue - history_msgs.append(HumanMessage(content=content)) - continue - - special_data = skill_state.skill_config.output == Output.image - if special_data: - content = skill_state.content() - if not content: - continue - special_msgs.append(HumanMessage(content=content)) - - if skill_state.call_id in tool_call_ids: - continue - - state_overview.append(summary_from_state(skill_state, special_data)) - - if state_overview: - state_overview_str = "\n".join( - json.dumps(s, default=_custom_json_serializers) for s in state_overview - ) - state_msg = AIMessage("State Overview:\n" + state_overview_str) - - return { - "tool_msgs": tool_msgs, - "history_msgs": history_msgs, - "state_msgs": ([state_msg] if state_msg else []) + special_msgs, - } - - -# Agent class job is to glue skill coordinator state to an agent, builds langchain messages -class Agent(AgentSpec): - system_message: SystemMessage - state_messages: list[AIMessage | HumanMessage] - - def __init__( - self, - *args, - **kwargs, - ) -> None: - AgentSpec.__init__(self, *args, **kwargs) - - self.state_messages = [] - self.coordinator = SkillCoordinator() - self._history = [] - self._agent_id = str(uuid.uuid4()) - self._agent_stopped = False - - if self.config.system_prompt: - if isinstance(self.config.system_prompt, str): - self.system_message = SystemMessage(self.config.system_prompt + SYSTEM_MSG_APPEND) - else: - self.config.system_prompt.content += SYSTEM_MSG_APPEND - self.system_message = self.config.system_prompt - else: - self.system_message = SystemMessage(get_system_prompt() + SYSTEM_MSG_APPEND) - - self.publish(self.system_message) - - # Use provided model instance if available, otherwise initialize from config - if self.config.model_instance: - self._llm = self.config.model_instance - else: - self._llm = init_chat_model( - model_provider=self.config.provider, model=self.config.model - ) - - @rpc - def get_agent_id(self) -> str: - return self._agent_id - - @rpc - def start(self) -> None: - super().start() - self.coordinator.start() - - @rpc - def stop(self) -> None: - self.coordinator.stop() - self._agent_stopped = True - super().stop() - - def clear_history(self) -> None: - self._history.clear() - - def append_history(self, *msgs: list[AIMessage | HumanMessage]) -> None: - for msg in msgs: - self.publish(msg) - - self._history.extend(msgs) - - def history(self): - return [self.system_message, *self._history, *self.state_messages] - - # Used by agent to execute tool calls - def execute_tool_calls(self, tool_calls: list[ToolCall]) -> None: - """Execute a list of tool calls from the agent.""" - if self._agent_stopped: - logger.warning("Agent is stopped, cannot execute tool calls.") - return - for tool_call in tool_calls: - logger.info(f"executing skill call {tool_call}") - self.coordinator.call_skill( - tool_call.get("id"), - tool_call.get("name"), - tool_call.get("args"), - ) - - # used to inject skill calls into the agent loop without agent asking for it - def run_implicit_skill(self, skill_name: str, **kwargs) -> None: - if self._agent_stopped: - logger.warning("Agent is stopped, cannot execute implicit skill calls.") - return - self.coordinator.call_skill(False, skill_name, {"args": kwargs}) - - async def agent_loop(self, first_query: str = ""): - # TODO: Should I add a lock here to prevent concurrent calls to agent_loop? - - if self._agent_stopped: - logger.warning("Agent is stopped, cannot run agent loop.") - # return "Agent is stopped." - import traceback - - traceback.print_stack() - return "Agent is stopped." - - self.state_messages = [] - if first_query: - self.append_history(HumanMessage(first_query)) - - def _get_state() -> str: - # TODO: FIX THIS EXTREME HACK - update = self.coordinator.generate_snapshot(clear=False) - snapshot_msgs = snapshot_to_messages(update, msg.tool_calls) - return json.dumps(snapshot_msgs, sort_keys=True, default=lambda o: repr(o)) - - try: - while True: - # we are getting tools from the coordinator on each turn - # since this allows for skillcontainers to dynamically provide new skills - tools = self.get_tools() - print("Available tools:", [tool.name for tool in tools]) - self._llm = self._llm.bind_tools(tools) - - # publish to /agent topic for observability - for state_msg in self.state_messages: - self.publish(state_msg) - - # history() builds our message history dynamically - # ensures we include latest system state, but not old ones. - msg = self._llm.invoke(self.history()) - self.append_history(msg) - - logger.info(f"Agent response: {msg.content}") - - state = _get_state() - - if msg.tool_calls: - self.execute_tool_calls(msg.tool_calls) - - print(self) - print(self.coordinator) - - self._write_debug_history_file() - - if not self.coordinator.has_active_skills(): - logger.info("No active tasks, exiting agent loop.") - return msg.content - - # coordinator will continue once a skill state has changed in - # such a way that agent call needs to be executed - - if state == _get_state(): - await self.coordinator.wait_for_updates() - - # we request a full snapshot of currently running, finished or errored out skills - # we ask for removal of finished skills from subsequent snapshots (clear=True) - update = self.coordinator.generate_snapshot(clear=True) - - # generate tool_msgs and general state update message, - # depending on a skill having associated tool call from previous interaction - # we will return a tool message, and not a general state message - snapshot_msgs = snapshot_to_messages(update, msg.tool_calls) - - self.state_messages = snapshot_msgs.get("state_msgs", []) - self.append_history( - *snapshot_msgs.get("tool_msgs", []), *snapshot_msgs.get("history_msgs", []) - ) - - except Exception as e: - logger.error(f"Error in agent loop: {e}") - import traceback - - traceback.print_exc() - - @rpc - def loop_thread(self) -> bool: - asyncio.run_coroutine_threadsafe(self.agent_loop(), self._loop) - return True - - @rpc - def query(self, query: str): - # TODO: could this be - # from distributed.utils import sync - # return sync(self._loop, self.agent_loop, query) - return asyncio.run_coroutine_threadsafe(self.agent_loop(query), self._loop).result() - - async def query_async(self, query: str): - return await self.agent_loop(query) - - @rpc - def register_skills(self, container, run_implicit_name: str | None = None): - ret = self.coordinator.register_skills(container) - - if run_implicit_name: - self.run_implicit_skill(run_implicit_name) - - return ret - - def get_tools(self): - return self.coordinator.get_tools() - - def _write_debug_history_file(self) -> None: - file_path = os.getenv("DEBUG_AGENT_HISTORY_FILE") - if not file_path: - return - - history = [x.__dict__ for x in self.history()] - - with open(file_path, "w") as f: - json.dump(history, f, default=lambda x: repr(x), indent=2) - - -class LlmAgent(Agent): - @rpc - def start(self) -> None: - super().start() - self.loop_thread() - - @rpc - def stop(self) -> None: - super().stop() - - -llm_agent = LlmAgent.blueprint - - -__all__ = ["Agent", "llm_agent"] diff --git a/dimos/agents2/cli/human.py b/dimos/agents2/cli/human.py deleted file mode 100644 index 15727d87b8..0000000000 --- a/dimos/agents2/cli/human.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import queue - -from reactivex.disposable import Disposable - -from dimos.agents2 import Output, Reducer, Stream, skill -from dimos.core import pLCMTransport, rpc -from dimos.core.module import Module -from dimos.core.rpc_client import RpcCall - - -class HumanInput(Module): - running: bool = False - - @skill(stream=Stream.call_agent, reducer=Reducer.string, output=Output.human, hide_skill=True) - def human(self): - """receives human input, no need to run this, it's running implicitly""" - if self.running: - return "already running" - self.running = True - transport = pLCMTransport("/human_input") - - msg_queue = queue.Queue() - unsub = transport.subscribe(msg_queue.put) - self._disposables.add(Disposable(unsub)) - yield from iter(msg_queue.get, None) - - @rpc - def start(self) -> None: - super().start() - - @rpc - def stop(self) -> None: - super().stop() - - @rpc - def set_LlmAgent_register_skills(self, callable: RpcCall) -> None: - callable.set_rpc(self.rpc) - callable(self, run_implicit_name="human") - - -human_input = HumanInput.blueprint - -__all__ = ["HumanInput", "human_input"] diff --git a/dimos/agents2/conftest.py b/dimos/agents2/conftest.py deleted file mode 100644 index 769523f8c5..0000000000 --- a/dimos/agents2/conftest.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path - -import pytest - -from dimos.agents2.agent import Agent -from dimos.agents2.testing import MockModel -from dimos.protocol.skill.test_coordinator import SkillContainerTest - - -@pytest.fixture -def fixture_dir(): - return Path(__file__).parent / "fixtures" - - -@pytest.fixture -def potato_system_prompt() -> str: - return "Your name is Mr. Potato, potatoes are bad at math. Use a tools if asked to calculate" - - -@pytest.fixture -def skill_container(): - container = SkillContainerTest() - try: - yield container - finally: - container.stop() - - -@pytest.fixture -def create_fake_agent(fixture_dir): - agent = None - - def _agent_factory(*, system_prompt, skill_containers, fixture): - mock_model = MockModel(json_path=fixture_dir / fixture) - - nonlocal agent - agent = Agent(system_prompt=system_prompt, model_instance=mock_model) - - for skill_container in skill_containers: - agent.register_skills(skill_container) - - agent.start() - - return agent - - try: - yield _agent_factory - finally: - if agent: - agent.stop() - - -@pytest.fixture -def create_potato_agent(potato_system_prompt, skill_container, fixture_dir): - agent = None - - def _agent_factory(*, fixture): - mock_model = MockModel(json_path=fixture_dir / fixture) - - nonlocal agent - agent = Agent(system_prompt=potato_system_prompt, model_instance=mock_model) - agent.register_skills(skill_container) - agent.start() - - return agent - - try: - yield _agent_factory - finally: - if agent: - agent.stop() diff --git a/dimos/agents2/constants.py b/dimos/agents2/constants.py deleted file mode 100644 index 0d7d4832a0..0000000000 --- a/dimos/agents2/constants.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.constants import DIMOS_PROJECT_ROOT - -AGENT_SYSTEM_PROMPT_PATH = DIMOS_PROJECT_ROOT / "assets/agent/prompt_agents2.txt" diff --git a/dimos/agents2/skills/conftest.py b/dimos/agents2/skills/conftest.py deleted file mode 100644 index a8734ca7ed..0000000000 --- a/dimos/agents2/skills/conftest.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from functools import partial - -import pytest -import reactivex as rx -from reactivex.scheduler import ThreadPoolScheduler - -from dimos.agents2.skills.google_maps_skill_container import GoogleMapsSkillContainer -from dimos.agents2.skills.gps_nav_skill import GpsNavSkillContainer -from dimos.agents2.skills.navigation import NavigationSkillContainer -from dimos.agents2.system_prompt import get_system_prompt -from dimos.mapping.types import LatLon -from dimos.msgs.sensor_msgs import Image -from dimos.robot.robot import GpsRobot -from dimos.utils.data import get_data - -system_prompt = get_system_prompt() - - -@pytest.fixture(autouse=True) -def cleanup_threadpool_scheduler(monkeypatch): - # TODO: get rid of this global threadpool - """Clean up and recreate the global ThreadPoolScheduler after each test.""" - # Disable ChromaDB telemetry to avoid leaking threads - monkeypatch.setenv("CHROMA_ANONYMIZED_TELEMETRY", "False") - yield - from dimos.utils import threadpool - - # Shutdown the global scheduler's executor - threadpool.scheduler.executor.shutdown(wait=True) - # Recreate it for the next test - threadpool.scheduler = ThreadPoolScheduler(max_workers=threadpool.get_max_workers()) - - -# TODO: Delete -@pytest.fixture -def fake_robot(mocker): - return mocker.MagicMock() - - -# TODO: Delete -@pytest.fixture -def fake_gps_robot(mocker): - return mocker.Mock(spec=GpsRobot) - - -@pytest.fixture -def fake_video_stream(): - image_path = get_data("chair-image.png") - image = Image.from_file(str(image_path)) - return rx.of(image) - - -# TODO: Delete -@pytest.fixture -def fake_gps_position_stream(): - return rx.of(LatLon(lat=37.783, lon=-122.413)) - - -@pytest.fixture -def navigation_skill_container(mocker): - container = NavigationSkillContainer() - container.color_image.connection = mocker.MagicMock() - container.odom.connection = mocker.MagicMock() - container.start() - yield container - container.stop() - - -@pytest.fixture -def gps_nav_skill_container(fake_gps_robot, fake_gps_position_stream): - container = GpsNavSkillContainer(fake_gps_robot, fake_gps_position_stream) - container.start() - yield container - container.stop() - - -@pytest.fixture -def google_maps_skill_container(fake_gps_robot, fake_gps_position_stream, mocker): - container = GoogleMapsSkillContainer(fake_gps_robot, fake_gps_position_stream) - container.start() - container._client = mocker.MagicMock() - yield container - container.stop() - - -@pytest.fixture -def create_navigation_agent(navigation_skill_container, create_fake_agent): - return partial( - create_fake_agent, - system_prompt=system_prompt, - skill_containers=[navigation_skill_container], - ) - - -@pytest.fixture -def create_gps_nav_agent(gps_nav_skill_container, create_fake_agent): - return partial( - create_fake_agent, system_prompt=system_prompt, skill_containers=[gps_nav_skill_container] - ) - - -@pytest.fixture -def create_google_maps_agent( - gps_nav_skill_container, google_maps_skill_container, create_fake_agent -): - return partial( - create_fake_agent, - system_prompt=system_prompt, - skill_containers=[gps_nav_skill_container, google_maps_skill_container], - ) diff --git a/dimos/agents2/skills/google_maps_skill_container.py b/dimos/agents2/skills/google_maps_skill_container.py deleted file mode 100644 index 433914a5e3..0000000000 --- a/dimos/agents2/skills/google_maps_skill_container.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -from typing import Any - -from reactivex import Observable -from reactivex.disposable import CompositeDisposable - -from dimos.core.resource import Resource -from dimos.mapping.google_maps.google_maps import GoogleMaps -from dimos.mapping.osm.current_location_map import CurrentLocationMap -from dimos.mapping.types import LatLon -from dimos.protocol.skill.skill import SkillContainer, skill -from dimos.robot.robot import Robot -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(__file__) - - -class GoogleMapsSkillContainer(SkillContainer, Resource): - _robot: Robot - _disposables: CompositeDisposable - _latest_location: LatLon | None - _position_stream: Observable[LatLon] - _current_location_map: CurrentLocationMap - _started: bool - - def __init__(self, robot: Robot, position_stream: Observable[LatLon]) -> None: - super().__init__() - self._robot = robot - self._disposables = CompositeDisposable() - self._latest_location = None - self._position_stream = position_stream - self._client = GoogleMaps() - self._started = False - - def start(self) -> None: - self._started = True - self._disposables.add(self._position_stream.subscribe(self._on_gps_location)) - - def stop(self) -> None: - self._disposables.dispose() - super().stop() - - def _on_gps_location(self, location: LatLon) -> None: - self._latest_location = location - - def _get_latest_location(self) -> LatLon: - if not self._latest_location: - raise ValueError("The position has not been set yet.") - return self._latest_location - - @skill() - def where_am_i(self, context_radius: int = 200) -> str: - """This skill returns information about what street/locality/city/etc - you are in. It also gives you nearby landmarks. - - Example: - - where_am_i(context_radius=200) - - Args: - context_radius (int): default 200, how many meters to look around - """ - - if not self._started: - raise ValueError(f"{self} has not been started.") - - location = self._get_latest_location() - - result = None - try: - result = self._client.get_location_context(location, radius=context_radius) - except Exception: - return "There is an issue with the Google Maps API." - - if not result: - return "Could not find anything about the current location." - - return result.model_dump_json() - - @skill() - def get_gps_position_for_queries(self, *queries: str) -> str: - """Get the GPS position (latitude/longitude) - - Example: - - get_gps_position_for_queries(['Fort Mason', 'Lafayette Park']) - # returns - [{"lat": 37.8059, "lon":-122.4290}, {"lat": 37.7915, "lon": -122.4276}] - - Args: - queries (list[str]): The places you want to look up. - """ - - if not self._started: - raise ValueError(f"{self} has not been started.") - - location = self._get_latest_location() - - results: list[dict[str, Any] | str] = [] - - for query in queries: - try: - latlon = self._client.get_position(query, location) - except Exception: - latlon = None - if latlon: - results.append(latlon.model_dump()) - else: - results.append(f"no result for {query}") - - return json.dumps(results) diff --git a/dimos/agents2/skills/gps_nav_skill.py b/dimos/agents2/skills/gps_nav_skill.py deleted file mode 100644 index 80e346790a..0000000000 --- a/dimos/agents2/skills/gps_nav_skill.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json - -from reactivex import Observable -from reactivex.disposable import CompositeDisposable - -from dimos.core.resource import Resource -from dimos.mapping.google_maps.google_maps import GoogleMaps -from dimos.mapping.osm.current_location_map import CurrentLocationMap -from dimos.mapping.types import LatLon -from dimos.mapping.utils.distance import distance_in_meters -from dimos.protocol.skill.skill import SkillContainer, skill -from dimos.robot.robot import Robot -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(__file__) - - -class GpsNavSkillContainer(SkillContainer, Resource): - _robot: Robot - _disposables: CompositeDisposable - _latest_location: LatLon | None - _position_stream: Observable[LatLon] - _current_location_map: CurrentLocationMap - _started: bool - _max_valid_distance: int - - def __init__(self, robot: Robot, position_stream: Observable[LatLon]) -> None: - super().__init__() - self._robot = robot - self._disposables = CompositeDisposable() - self._latest_location = None - self._position_stream = position_stream - self._client = GoogleMaps() - self._started = False - self._max_valid_distance = 50000 - - def start(self) -> None: - self._started = True - self._disposables.add(self._position_stream.subscribe(self._on_gps_location)) - - def stop(self) -> None: - self._disposables.dispose() - super().stop() - - def _on_gps_location(self, location: LatLon) -> None: - self._latest_location = location - - def _get_latest_location(self) -> LatLon: - if not self._latest_location: - raise ValueError("The position has not been set yet.") - return self._latest_location - - @skill() - def set_gps_travel_points(self, *points: dict[str, float]) -> str: - """Define the movement path determined by GPS coordinates. Requires at least one. You can get the coordinates by using the `get_gps_position_for_queries` skill. - - Example: - - set_gps_travel_goals([{"lat": 37.8059, "lon":-122.4290}, {"lat": 37.7915, "lon": -122.4276}]) - # Travel first to {"lat": 37.8059, "lon":-122.4290} - # then travel to {"lat": 37.7915, "lon": -122.4276} - """ - - if not self._started: - raise ValueError(f"{self} has not been started.") - - new_points = [self._convert_point(x) for x in points] - - if not all(new_points): - parsed = json.dumps([x.__dict__ if x else x for x in new_points]) - return f"Not all points were valid. I parsed this: {parsed}" - - logger.info(f"Set travel points: {new_points}") - - self._robot.set_gps_travel_goal_points(new_points) - - return "I've successfully set the travel points." - - def _convert_point(self, point: dict[str, float]) -> LatLon | None: - if not isinstance(point, dict): - return None - lat = point.get("lat") - lon = point.get("lon") - - if lat is None or lon is None: - return None - - new_point = LatLon(lat=lat, lon=lon) - distance = distance_in_meters(self._get_latest_location(), new_point) - if distance > self._max_valid_distance: - return None - - return new_point diff --git a/dimos/agents2/skills/navigation.py b/dimos/agents2/skills/navigation.py deleted file mode 100644 index 9e30871039..0000000000 --- a/dimos/agents2/skills/navigation.py +++ /dev/null @@ -1,441 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from typing import Any - -from dimos.core.core import rpc -from dimos.core.rpc_client import RpcCall -from dimos.core.skill_module import SkillModule -from dimos.core.stream import In -from dimos.models.qwen.video_query import BBox -from dimos.models.vl.qwen import QwenVlModel -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.geometry_msgs.Vector3 import make_vector3 -from dimos.msgs.sensor_msgs import Image -from dimos.navigation.bt_navigator.navigator import NavigatorState -from dimos.navigation.visual.query import get_object_bbox_from_image -from dimos.protocol.skill.skill import skill -from dimos.types.robot_location import RobotLocation -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import euler_to_quaternion, quaternion_to_euler - -logger = setup_logger(__file__) - - -class NavigationSkillContainer(SkillModule): - _latest_image: Image | None = None - _latest_odom: PoseStamped | None = None - _skill_started: bool = False - _similarity_threshold: float = 0.23 - - _tag_location: RpcCall | None = None - _query_tagged_location: RpcCall | None = None - _query_by_text: RpcCall | None = None - _set_goal: RpcCall | None = None - _get_state: RpcCall | None = None - _is_goal_reached: RpcCall | None = None - _cancel_goal: RpcCall | None = None - _track: RpcCall | None = None - _stop_track: RpcCall | None = None - _is_tracking: RpcCall | None = None - _stop_exploration: RpcCall | None = None - _explore: RpcCall | None = None - _is_exploration_active: RpcCall | None = None - - color_image: In[Image] = None - odom: In[PoseStamped] = None - - def __init__(self) -> None: - super().__init__() - self._skill_started = False - self._vl_model = QwenVlModel() - - @rpc - def start(self) -> None: - self._disposables.add(self.color_image.subscribe(self._on_color_image)) - self._disposables.add(self.odom.subscribe(self._on_odom)) - self._skill_started = True - - @rpc - def stop(self) -> None: - super().stop() - - def _on_color_image(self, image: Image) -> None: - self._latest_image = image - - def _on_odom(self, odom: PoseStamped) -> None: - self._latest_odom = odom - - # TODO: This is quite repetitive, maybe I should automate this somehow - @rpc - def set_SpatialMemory_tag_location(self, callable: RpcCall) -> None: - self._tag_location = callable - self._tag_location.set_rpc(self.rpc) - - @rpc - def set_SpatialMemory_query_tagged_location(self, callable: RpcCall) -> None: - self._query_tagged_location = callable - self._query_tagged_location.set_rpc(self.rpc) - - @rpc - def set_SpatialMemory_query_by_text(self, callable: RpcCall) -> None: - self._query_by_text = callable - self._query_by_text.set_rpc(self.rpc) - - @rpc - def set_BehaviorTreeNavigator_set_goal(self, callable: RpcCall) -> None: - self._set_goal = callable - self._set_goal.set_rpc(self.rpc) - - @rpc - def set_BehaviorTreeNavigator_get_state(self, callable: RpcCall) -> None: - self._get_state = callable - self._get_state.set_rpc(self.rpc) - - @rpc - def set_BehaviorTreeNavigator_is_goal_reached(self, callable: RpcCall) -> None: - self._is_goal_reached = callable - self._is_goal_reached.set_rpc(self.rpc) - - @rpc - def set_BehaviorTreeNavigator_cancel_goal(self, callable: RpcCall) -> None: - self._cancel_goal = callable - self._cancel_goal.set_rpc(self.rpc) - - @rpc - def set_ObjectTracking_track(self, callable: RpcCall) -> None: - self._track = callable - self._track.set_rpc(self.rpc) - - @rpc - def set_ObjectTracking_stop_track(self, callable: RpcCall) -> None: - self._stop_track = callable - self._stop_track.set_rpc(self.rpc) - - @rpc - def set_ObjectTracking_is_tracking(self, callable: RpcCall) -> None: - self._is_tracking = callable - self._is_tracking.set_rpc(self.rpc) - - @rpc - def set_WavefrontFrontierExplorer_stop_exploration(self, callable: RpcCall) -> None: - self._stop_exploration = callable - self._stop_exploration.set_rpc(self.rpc) - - @rpc - def set_WavefrontFrontierExplorer_explore(self, callable: RpcCall) -> None: - self._explore = callable - self._explore.set_rpc(self.rpc) - - @rpc - def set_WavefrontFrontierExplorer_is_exploration_active(self, callable: RpcCall) -> None: - self._is_exploration_active = callable - self._is_exploration_active.set_rpc(self.rpc) - - @skill() - def tag_location_in_spatial_memory(self, location_name: str) -> str: - """Tag this location in the spatial memory with a name. - - This associates the current location with the given name in the spatial memory, allowing you to navigate back to it. - - Args: - location_name (str): the name for the location - - Returns: - str: the outcome - """ - - if not self._skill_started: - raise ValueError(f"{self} has not been started.") - - if not self._latest_odom: - return "Error: No odometry data available to tag the location." - - if not self._tag_location: - return "Error: The SpatialMemory module is not connected." - - position = self._latest_odom.position - rotation = quaternion_to_euler(self._latest_odom.orientation) - - location = RobotLocation( - name=location_name, - position=(position.x, position.y, position.z), - rotation=(rotation.x, rotation.y, rotation.z), - ) - - if not self._tag_location(location): - return f"Error: Failed to store '{location_name}' in the spatial memory" - - logger.info(f"Tagged {location}") - return f"The current location has been tagged as '{location_name}'." - - @skill() - def navigate_with_text(self, query: str) -> str: - """Navigate to a location by querying the existing semantic map using natural language. - - First attempts to locate an object in the robot's camera view using vision. - If the object is found, navigates to it. If not, falls back to querying the - semantic map for a location matching the description. - CALL THIS SKILL FOR ONE SUBJECT AT A TIME. For example: "Go to the person wearing a blue shirt in the living room", - you should call this skill twice, once for the person wearing a blue shirt and once for the living room. - Args: - query: Text query to search for in the semantic map - """ - - if not self._skill_started: - raise ValueError(f"{self} has not been started.") - - success_msg = self._navigate_by_tagged_location(query) - if success_msg: - return success_msg - - logger.info(f"No tagged location found for {query}") - - success_msg = self._navigate_to_object(query) - if success_msg: - return success_msg - - logger.info(f"No object in view found for {query}") - - success_msg = self._navigate_using_semantic_map(query) - if success_msg: - return success_msg - - return f"No tagged location called '{query}'. No object in view matching '{query}'. No matching location found in semantic map for '{query}'." - - def _navigate_by_tagged_location(self, query: str) -> str | None: - if not self._query_tagged_location: - logger.warning("SpatialMemory module not connected, cannot query tagged locations") - return None - - robot_location = self._query_tagged_location(query) - - if not robot_location: - return None - - goal_pose = PoseStamped( - position=make_vector3(*robot_location.position), - orientation=euler_to_quaternion(make_vector3(*robot_location.rotation)), - frame_id="world", - ) - - result = self._navigate_to(goal_pose) - if not result: - return "Error: Faild to reach the tagged location." - - return ( - f"Successfuly arrived at location tagged '{robot_location.name}' from query '{query}'." - ) - - def _navigate_to(self, pose: PoseStamped) -> bool: - if not self._set_goal or not self._get_state or not self._is_goal_reached: - logger.error("BehaviorTreeNavigator module not connected properly") - return False - - logger.info( - f"Navigating to pose: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" - ) - self._set_goal(pose) - time.sleep(1.0) - - while self._get_state() == NavigatorState.FOLLOWING_PATH: - time.sleep(0.25) - - time.sleep(1.0) - if not self._is_goal_reached(): - logger.info("Navigation was cancelled or failed") - return False - else: - logger.info("Navigation goal reached") - return True - - def _navigate_to_object(self, query: str) -> str | None: - try: - bbox = self._get_bbox_for_current_frame(query) - except Exception: - logger.error(f"Failed to get bbox for {query}", exc_info=True) - return None - - if bbox is None: - return None - - if not self._track or not self._stop_track or not self._is_tracking: - logger.error("ObjectTracking module not connected properly") - return None - - if not self._get_state or not self._is_goal_reached: - logger.error("BehaviorTreeNavigator module not connected properly") - return None - - logger.info(f"Found {query} at {bbox}") - - # Start tracking - BBoxNavigationModule automatically generates goals - self._track(bbox) - - start_time = time.time() - timeout = 30.0 - goal_set = False - - while time.time() - start_time < timeout: - # Check if navigator finished - if self._get_state() == NavigatorState.IDLE and goal_set: - logger.info("Waiting for goal result") - time.sleep(1.0) - if not self._is_goal_reached(): - logger.info(f"Goal cancelled, tracking '{query}' failed") - self._stop_track() - return None - else: - logger.info(f"Reached '{query}'") - self._stop_track() - return f"Successfully arrived at '{query}'" - - # If goal set and tracking lost, just continue (tracker will resume or timeout) - if goal_set and not self._is_tracking(): - continue - - # BBoxNavigationModule automatically sends goals when tracker publishes - # Just check if we have any detections to mark goal_set - if self._is_tracking(): - goal_set = True - - time.sleep(0.25) - - logger.warning(f"Navigation to '{query}' timed out after {timeout}s") - self._stop_track() - return None - - def _get_bbox_for_current_frame(self, query: str) -> BBox | None: - if self._latest_image is None: - return None - - return get_object_bbox_from_image(self._vl_model, self._latest_image, query) - - def _navigate_using_semantic_map(self, query: str) -> str: - if not self._query_by_text: - return "Error: The SpatialMemory module is not connected." - - results = self._query_by_text(query) - - if not results: - return f"No matching location found in semantic map for '{query}'" - - best_match = results[0] - - goal_pose = self._get_goal_pose_from_result(best_match) - - if not goal_pose: - return f"Found a result for '{query}' but it didn't have a valid position." - - result = self._navigate_to(goal_pose) - - if not result: - return f"Failed to navigate for '{query}'" - - return f"Successfuly arrived at '{query}'" - - @skill() - def follow_human(self, person: str) -> str: - """Follow a specific person""" - return "Not implemented yet." - - @skill() - def stop_movement(self) -> str: - """Immediatly stop moving.""" - - if not self._skill_started: - raise ValueError(f"{self} has not been started.") - - self._cancel_goal_and_stop() - - return "Stopped" - - def _cancel_goal_and_stop(self) -> None: - if not self._cancel_goal: - logger.warning("BehaviorTreeNavigator module not connected, cannot cancel goal") - return - - if not self._stop_exploration: - logger.warning("FrontierExplorer module not connected, cannot stop exploration") - return - - self._cancel_goal() - return self._stop_exploration() - - @skill() - def start_exploration(self, timeout: float = 240.0) -> str: - """A skill that performs autonomous frontier exploration. - - This skill continuously finds and navigates to unknown frontiers in the environment - until no more frontiers are found or the exploration is stopped. - - Don't call any other skills except stop_movement skill when needed. - - Args: - timeout (float, optional): Maximum time (in seconds) allowed for exploration - """ - - if not self._skill_started: - raise ValueError(f"{self} has not been started.") - - try: - return self._start_exploration(timeout) - finally: - self._cancel_goal_and_stop() - - def _start_exploration(self, timeout: float) -> str: - if not self._explore or not self._is_exploration_active: - return "Error: The WavefrontFrontierExplorer module is not connected." - - logger.info("Starting autonomous frontier exploration") - - start_time = time.time() - - has_started = self._explore() - if not has_started: - return "Error: Could not start exploration." - - while time.time() - start_time < timeout and self._is_exploration_active(): - time.sleep(0.5) - - return "Exploration completed successfuly" - - def _get_goal_pose_from_result(self, result: dict[str, Any]) -> PoseStamped | None: - similarity = 1.0 - (result.get("distance") or 1) - if similarity < self._similarity_threshold: - logger.warning( - f"Match found but similarity score ({similarity:.4f}) is below threshold ({self._similarity_threshold})" - ) - return None - - metadata = result.get("metadata") - if not metadata: - return None - - first = metadata[0] - pos_x = first.get("pos_x", 0) - pos_y = first.get("pos_y", 0) - theta = first.get("rot_z", 0) - - return PoseStamped( - position=make_vector3(pos_x, pos_y, 0), - orientation=euler_to_quaternion(make_vector3(0, 0, theta)), - frame_id="world", - ) - - -navigation_skill = NavigationSkillContainer.blueprint - -__all__ = ["NavigationSkillContainer", "navigation_skill"] diff --git a/dimos/agents2/skills/ros_navigation.py b/dimos/agents2/skills/ros_navigation.py deleted file mode 100644 index 973cdcc10f..0000000000 --- a/dimos/agents2/skills/ros_navigation.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from typing import TYPE_CHECKING, Any - -from dimos.core.resource import Resource -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.geometry_msgs.Vector3 import make_vector3 -from dimos.protocol.skill.skill import SkillContainer, skill -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import euler_to_quaternion - -if TYPE_CHECKING: - from dimos.robot.unitree_webrtc.unitree_g1 import UnitreeG1 - -logger = setup_logger(__file__) - - -class RosNavigation(SkillContainer, Resource): - _robot: "UnitreeG1" - _started: bool - - def __init__(self, robot: "UnitreeG1") -> None: - self._robot = robot - self._similarity_threshold = 0.23 - self._started = False - - def start(self) -> None: - self._started = True - - def stop(self) -> None: - super().stop() - - @skill() - def navigate_with_text(self, query: str) -> str: - """Navigate to a location by querying the existing semantic map using natural language. - - CALL THIS SKILL FOR ONE SUBJECT AT A TIME. For example: "Go to the person wearing a blue shirt in the living room", - you should call this skill twice, once for the person wearing a blue shirt and once for the living room. - - Args: - query: Text query to search for in the semantic map - """ - - print("X" * 10000) - - if not self._started: - raise ValueError(f"{self} has not been started.") - - success_msg = self._navigate_using_semantic_map(query) - if success_msg: - return success_msg - - return "Failed to navigate." - - def _navigate_using_semantic_map(self, query: str) -> str: - results = self._robot.spatial_memory.query_by_text(query) - - if not results: - return f"No matching location found in semantic map for '{query}'" - - best_match = results[0] - - goal_pose = self._get_goal_pose_from_result(best_match) - - if not goal_pose: - return f"Found a result for '{query}' but it didn't have a valid position." - - result = self._robot.nav.go_to(goal_pose) - - if not result: - return f"Failed to navigate for '{query}'" - - return f"Successfuly arrived at '{query}'" - - @skill() - def stop_movement(self) -> str: - """Immediatly stop moving.""" - - if not self._started: - raise ValueError(f"{self} has not been started.") - - self._robot.cancel_navigation() - - return "Stopped" - - def _get_goal_pose_from_result(self, result: dict[str, Any]) -> PoseStamped | None: - similarity = 1.0 - (result.get("distance") or 1) - if similarity < self._similarity_threshold: - logger.warning( - f"Match found but similarity score ({similarity:.4f}) is below threshold ({self._similarity_threshold})" - ) - return None - - metadata = result.get("metadata") - if not metadata: - return None - - first = metadata[0] - pos_x = first.get("pos_x", 0) - pos_y = first.get("pos_y", 0) - theta = first.get("rot_z", 0) - - return PoseStamped( - ts=time.time(), - position=make_vector3(pos_x, pos_y, 0), - orientation=euler_to_quaternion(make_vector3(0, 0, theta)), - frame_id="map", - ) diff --git a/dimos/agents2/skills/test_gps_nav_skills.py b/dimos/agents2/skills/test_gps_nav_skills.py deleted file mode 100644 index 9e8090b169..0000000000 --- a/dimos/agents2/skills/test_gps_nav_skills.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.mapping.types import LatLon - - -def test_set_gps_travel_points(fake_gps_robot, create_gps_nav_agent) -> None: - agent = create_gps_nav_agent(fixture="test_set_gps_travel_points.json") - - agent.query("go to lat: 37.782654, lon: -122.413273") - - fake_gps_robot.set_gps_travel_goal_points.assert_called_once_with( - [LatLon(lat=37.782654, lon=-122.413273)] - ) - - -def test_set_gps_travel_points_multiple(fake_gps_robot, create_gps_nav_agent) -> None: - agent = create_gps_nav_agent(fixture="test_set_gps_travel_points_multiple.json") - - agent.query( - "go to lat: 37.782654, lon: -122.413273, then 37.782660,-122.413260, and then 37.782670,-122.413270" - ) - - fake_gps_robot.set_gps_travel_goal_points.assert_called_once_with( - [ - LatLon(lat=37.782654, lon=-122.413273), - LatLon(lat=37.782660, lon=-122.413260), - LatLon(lat=37.782670, lon=-122.413270), - ] - ) diff --git a/dimos/agents2/skills/test_navigation.py b/dimos/agents2/skills/test_navigation.py deleted file mode 100644 index d7d8d4c127..0000000000 --- a/dimos/agents2/skills/test_navigation.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.msgs.geometry_msgs import PoseStamped, Vector3 -from dimos.utils.transform_utils import euler_to_quaternion - - -def test_stop_movement(create_navigation_agent, navigation_skill_container, mocker) -> None: - navigation_skill_container._cancel_goal = mocker.Mock() - navigation_skill_container._stop_exploration = mocker.Mock() - agent = create_navigation_agent(fixture="test_stop_movement.json") - - agent.query("stop") - - navigation_skill_container._cancel_goal.assert_called_once_with() - navigation_skill_container._stop_exploration.assert_called_once_with() - - -def test_take_a_look_around(create_navigation_agent, navigation_skill_container, mocker) -> None: - navigation_skill_container._explore = mocker.Mock() - navigation_skill_container._is_exploration_active = mocker.Mock() - mocker.patch("dimos.agents2.skills.navigation.time.sleep") - agent = create_navigation_agent(fixture="test_take_a_look_around.json") - - agent.query("take a look around for 10 seconds") - - navigation_skill_container._explore.assert_called_once_with() - - -def test_go_to_semantic_location( - create_navigation_agent, navigation_skill_container, mocker -) -> None: - mocker.patch( - "dimos.agents2.skills.navigation.NavigationSkillContainer._navigate_by_tagged_location", - return_value=None, - ) - mocker.patch( - "dimos.agents2.skills.navigation.NavigationSkillContainer._navigate_to_object", - return_value=None, - ) - mocker.patch( - "dimos.agents2.skills.navigation.NavigationSkillContainer._navigate_to", - return_value=True, - ) - navigation_skill_container._query_by_text = mocker.Mock( - return_value=[ - { - "distance": 0.5, - "metadata": [ - { - "pos_x": 1, - "pos_y": 2, - "rot_z": 3, - } - ], - } - ] - ) - agent = create_navigation_agent(fixture="test_go_to_semantic_location.json") - - agent.query("go to the bookshelf") - - navigation_skill_container._query_by_text.assert_called_once_with("bookshelf") - navigation_skill_container._navigate_to.assert_called_once_with( - PoseStamped( - position=Vector3(1, 2, 0), - orientation=euler_to_quaternion(Vector3(0, 0, 3)), - frame_id="world", - ), - ) diff --git a/dimos/agents2/spec.py b/dimos/agents2/spec.py deleted file mode 100644 index 9973b05356..0000000000 --- a/dimos/agents2/spec.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Base agent module that wraps BaseAgent for DimOS module usage.""" - -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from enum import Enum -from typing import Any, Union - -from langchain.chat_models.base import _SUPPORTED_PROVIDERS -from langchain_core.language_models.chat_models import BaseChatModel -from langchain_core.messages import ( - AIMessage, - HumanMessage, - SystemMessage, - ToolMessage, -) -from rich.console import Console -from rich.table import Table -from rich.text import Text - -from dimos.core import Module, rpc -from dimos.core.module import ModuleConfig -from dimos.protocol.pubsub import PubSub, lcm -from dimos.protocol.service import Service -from dimos.protocol.skill.skill import SkillContainer -from dimos.utils.generic import truncate_display_string -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.agents.modules.base_agent") - - -# Dynamically create ModelProvider enum from LangChain's supported providers -_providers = {provider.upper(): provider for provider in _SUPPORTED_PROVIDERS} -Provider = Enum("Provider", _providers, type=str) - - -class Model(str, Enum): - """Common model names across providers. - - Note: This is not exhaustive as model names change frequently. - Based on langchain's _attempt_infer_model_provider patterns. - """ - - # OpenAI models (prefix: gpt-3, gpt-4, o1, o3) - GPT_4O = "gpt-4o" - GPT_4O_MINI = "gpt-4o-mini" - GPT_4_TURBO = "gpt-4-turbo" - GPT_4_TURBO_PREVIEW = "gpt-4-turbo-preview" - GPT_4 = "gpt-4" - GPT_35_TURBO = "gpt-3.5-turbo" - GPT_35_TURBO_16K = "gpt-3.5-turbo-16k" - O1_PREVIEW = "o1-preview" - O1_MINI = "o1-mini" - O3_MINI = "o3-mini" - - # Anthropic models (prefix: claude) - CLAUDE_3_OPUS = "claude-3-opus-20240229" - CLAUDE_3_SONNET = "claude-3-sonnet-20240229" - CLAUDE_3_HAIKU = "claude-3-haiku-20240307" - CLAUDE_35_SONNET = "claude-3-5-sonnet-20241022" - CLAUDE_35_SONNET_LATEST = "claude-3-5-sonnet-latest" - CLAUDE_3_7_SONNET = "claude-3-7-sonnet-20250219" - - # Google models (prefix: gemini) - GEMINI_20_FLASH = "gemini-2.0-flash" - GEMINI_15_PRO = "gemini-1.5-pro" - GEMINI_15_FLASH = "gemini-1.5-flash" - GEMINI_10_PRO = "gemini-1.0-pro" - - # Amazon Bedrock models (prefix: amazon) - AMAZON_TITAN_EXPRESS = "amazon.titan-text-express-v1" - AMAZON_TITAN_LITE = "amazon.titan-text-lite-v1" - - # Cohere models (prefix: command) - COMMAND_R_PLUS = "command-r-plus" - COMMAND_R = "command-r" - COMMAND = "command" - COMMAND_LIGHT = "command-light" - - # Fireworks models (prefix: accounts/fireworks) - FIREWORKS_LLAMA_V3_70B = "accounts/fireworks/models/llama-v3-70b-instruct" - FIREWORKS_MIXTRAL_8X7B = "accounts/fireworks/models/mixtral-8x7b-instruct" - - # Mistral models (prefix: mistral) - MISTRAL_LARGE = "mistral-large" - MISTRAL_MEDIUM = "mistral-medium" - MISTRAL_SMALL = "mistral-small" - MIXTRAL_8X7B = "mixtral-8x7b" - MIXTRAL_8X22B = "mixtral-8x22b" - MISTRAL_7B = "mistral-7b" - - # DeepSeek models (prefix: deepseek) - DEEPSEEK_CHAT = "deepseek-chat" - DEEPSEEK_CODER = "deepseek-coder" - DEEPSEEK_R1_DISTILL_LLAMA_70B = "deepseek-r1-distill-llama-70b" - - # xAI models (prefix: grok) - GROK_1 = "grok-1" - GROK_2 = "grok-2" - - # Perplexity models (prefix: sonar) - SONAR_SMALL_CHAT = "sonar-small-chat" - SONAR_MEDIUM_CHAT = "sonar-medium-chat" - SONAR_LARGE_CHAT = "sonar-large-chat" - - # Meta Llama models (various providers) - LLAMA_3_70B = "llama-3-70b" - LLAMA_3_8B = "llama-3-8b" - LLAMA_31_70B = "llama-3.1-70b" - LLAMA_31_8B = "llama-3.1-8b" - LLAMA_33_70B = "llama-3.3-70b" - LLAMA_2_70B = "llama-2-70b" - LLAMA_2_13B = "llama-2-13b" - LLAMA_2_7B = "llama-2-7b" - - -@dataclass -class AgentConfig(ModuleConfig): - system_prompt: str | SystemMessage | None = None - skills: SkillContainer | list[SkillContainer] | None = None - - # we can provide model/provvider enums or instantiated model_instance - model: Model = Model.GPT_4O - provider: Provider = Provider.OPENAI - model_instance: BaseChatModel | None = None - - agent_transport: type[PubSub] = lcm.PickleLCM - agent_topic: Any = field(default_factory=lambda: lcm.Topic("/agent")) - - -AnyMessage = Union[SystemMessage, ToolMessage, AIMessage, HumanMessage] - - -class AgentSpec(Service[AgentConfig], Module, ABC): - default_config: type[AgentConfig] = AgentConfig - - def __init__(self, *args, **kwargs) -> None: - Service.__init__(self, *args, **kwargs) - Module.__init__(self, *args, **kwargs) - - if self.config.agent_transport: - self.transport = self.config.agent_transport() - - def publish(self, msg: AnyMessage) -> None: - if self.transport: - self.transport.publish(self.config.agent_topic, msg) - - def start(self) -> None: - super().start() - - def stop(self) -> None: - super().stop() - - @rpc - @abstractmethod - def clear_history(self): ... - - @abstractmethod - def append_history(self, *msgs: list[AIMessage | HumanMessage]): ... - - @abstractmethod - def history(self) -> list[AnyMessage]: ... - - @rpc - @abstractmethod - def query(self, query: str): ... - - def __str__(self) -> str: - console = Console(force_terminal=True, legacy_windows=False) - table = Table(show_header=True) - - table.add_column("Message Type", style="cyan", no_wrap=True) - table.add_column("Content") - - for message in self.history(): - if isinstance(message, HumanMessage): - content = message.content - if not isinstance(content, str): - content = "" - - table.add_row(Text("Human", style="green"), Text(content, style="green")) - elif isinstance(message, AIMessage): - if hasattr(message, "metadata") and message.metadata.get("state"): - table.add_row( - Text("State Summary", style="blue"), - Text(message.content, style="blue"), - ) - else: - table.add_row( - Text("Agent", style="magenta"), Text(message.content, style="magenta") - ) - - for tool_call in message.tool_calls: - table.add_row( - "Tool Call", - Text( - f"{tool_call.get('name')}({tool_call.get('args')})", - style="bold magenta", - ), - ) - elif isinstance(message, ToolMessage): - table.add_row( - "Tool Response", Text(f"{message.name}() -> {message.content}"), style="red" - ) - elif isinstance(message, SystemMessage): - table.add_row( - "System", Text(truncate_display_string(message.content, 800), style="yellow") - ) - else: - table.add_row("Unknown", str(message)) - - # Render to string with title above - with console.capture() as capture: - console.print(Text(f" Agent ({self._agent_id})", style="bold blue")) - console.print(table) - return capture.get().strip() diff --git a/dimos/agents2/system_prompt.py b/dimos/agents2/system_prompt.py deleted file mode 100644 index 6b14f3e193..0000000000 --- a/dimos/agents2/system_prompt.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents2.constants import AGENT_SYSTEM_PROMPT_PATH - -_SYSTEM_PROMPT = None - - -def get_system_prompt() -> str: - global _SYSTEM_PROMPT - if _SYSTEM_PROMPT is None: - with open(AGENT_SYSTEM_PROMPT_PATH) as f: - _SYSTEM_PROMPT = f.read() - return _SYSTEM_PROMPT diff --git a/dimos/agents2/temp/run_unitree_agents2.py b/dimos/agents2/temp/run_unitree_agents2.py deleted file mode 100644 index aacfd1b5f4..0000000000 --- a/dimos/agents2/temp/run_unitree_agents2.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Run script for Unitree Go2 robot with agents2 framework. -This is the migrated version using the new LangChain-based agent system. -""" - -import os -from pathlib import Path -import sys -import time - -from dotenv import load_dotenv - -from dimos.agents2.cli.human import HumanInput - -# Add parent directories to path -sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) - - -from dimos.agents2 import Agent -from dimos.agents2.spec import Model, Provider -from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.agents2.run_unitree") - -# Load environment variables -load_dotenv() - -# System prompt path -SYSTEM_PROMPT_PATH = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), - "assets/agent/prompt.txt", -) - - -class UnitreeAgentRunner: - """Manages the Unitree robot with the new agents2 framework.""" - - def __init__(self) -> None: - self.robot = None - self.agent = None - self.agent_thread = None - self.running = False - - def setup_robot(self) -> UnitreeGo2: - """Initialize the robot connection.""" - logger.info("Initializing Unitree Go2 robot...") - - robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - connection_type=os.getenv("CONNECTION_TYPE", "webrtc"), - ) - - robot.start() - time.sleep(3) - - logger.info("Robot initialized successfully") - return robot - - def setup_agent(self, skillcontainers, system_prompt: str) -> Agent: - """Create and configure the agent with skills.""" - logger.info("Setting up agent with skills...") - - # Create agent - agent = Agent( - system_prompt=system_prompt, - model=Model.GPT_4O, # Could add CLAUDE models to enum - provider=Provider.OPENAI, # Would need ANTHROPIC provider - ) - - for container in skillcontainers: - print("REGISTERING SKILLS FROM CONTAINER:", container) - agent.register_skills(container) - - agent.run_implicit_skill("human") - - agent.start() - - # Log available skills - names = ", ".join([tool.name for tool in agent.get_tools()]) - logger.info(f"Agent configured with {len(names)} skills: {names}") - - agent.loop_thread() - return agent - - def run(self) -> None: - """Main run loop.""" - print("\n" + "=" * 60) - print("Unitree Go2 Robot with agents2 Framework") - print("=" * 60) - print("\nThis system integrates:") - print(" - Unitree Go2 quadruped robot") - print(" - WebRTC communication interface") - print(" - LangChain-based agent system (agents2)") - print(" - Converted skill system with @skill decorators") - print("\nStarting system...\n") - - # Check for API key (would need ANTHROPIC_API_KEY for Claude) - if not os.getenv("OPENAI_API_KEY"): - print("WARNING: OPENAI_API_KEY not found in environment") - print("Please set your API key in .env file or environment") - print("(Note: Full Claude support would require ANTHROPIC_API_KEY)") - sys.exit(1) - - system_prompt = """You are a helpful robot assistant controlling a Unitree Go2 quadruped robot. -You can move, navigate, speak, and perform various actions. Be helpful and friendly.""" - - try: - # Setup components - self.robot = self.setup_robot() - - self.agent = self.setup_agent( - [ - UnitreeSkillContainer(self.robot), - HumanInput(), - ], - system_prompt, - ) - - # Start handling queries - self.running = True - - logger.info("=" * 60) - logger.info("Unitree Go2 Agent Ready (agents2 framework)!") - logger.info("You can:") - logger.info(" - Type commands in the human cli") - logger.info(" - Ask the robot to move or navigate") - logger.info(" - Ask the robot to perform actions (sit, stand, dance, etc.)") - logger.info(" - Ask the robot to speak text") - logger.info("=" * 60) - - while True: - time.sleep(1) - except KeyboardInterrupt: - logger.info("Keyboard interrupt received") - except Exception as e: - logger.error(f"Error running robot: {e}") - import traceback - - traceback.print_exc() - # finally: - # self.shutdown() - - def shutdown(self) -> None: - logger.info("Shutting down...") - self.running = False - - if self.agent: - try: - self.agent.stop() - logger.info("Agent stopped") - except Exception as e: - logger.error(f"Error stopping agent: {e}") - - if self.robot: - try: - self.robot.stop() - logger.info("Robot connection closed") - except Exception as e: - logger.error(f"Error stopping robot: {e}") - - logger.info("Shutdown complete") - - -def main() -> None: - runner = UnitreeAgentRunner() - runner.run() - - -if __name__ == "__main__": - main() diff --git a/dimos/agents2/temp/run_unitree_async.py b/dimos/agents2/temp/run_unitree_async.py deleted file mode 100644 index 29213c1c90..0000000000 --- a/dimos/agents2/temp/run_unitree_async.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Async version of the Unitree run file for agents2. -Properly handles the async nature of the agent. -""" - -import asyncio -import os -from pathlib import Path -import sys - -from dotenv import load_dotenv - -# Add parent directories to path -sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) - -from dimos.agents2 import Agent -from dimos.agents2.spec import Model, Provider -from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("run_unitree_async") - -# Load environment variables -load_dotenv() - -# System prompt path -SYSTEM_PROMPT_PATH = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), - "assets/agent/prompt.txt", -) - - -async def handle_query(agent, query_text): - """Handle a single query asynchronously.""" - logger.info(f"Processing query: {query_text}") - - try: - # Use query_async which returns a Future - future = agent.query_async(query_text) - - # Wait for the result (with timeout) - await asyncio.wait_for(asyncio.wrap_future(future), timeout=30.0) - - # Get the result - if future.done(): - result = future.result() - logger.info(f"Agent response: {result}") - return result - else: - logger.warning("Query did not complete") - return "Query timeout" - - except asyncio.TimeoutError: - logger.error("Query timed out after 30 seconds") - return "Query timeout" - except Exception as e: - logger.error(f"Error processing query: {e}") - return f"Error: {e!s}" - - -async def interactive_loop(agent) -> None: - """Run an interactive query loop.""" - print("\n" + "=" * 60) - print("Interactive Agent Mode") - print("Type your commands or 'quit' to exit") - print("=" * 60 + "\n") - - while True: - try: - # Get user input - query = input("\nYou: ").strip() - - if query.lower() in ["quit", "exit", "q"]: - break - - if not query: - continue - - # Process query - response = await handle_query(agent, query) - print(f"\nAgent: {response}") - - except KeyboardInterrupt: - break - except Exception as e: - logger.error(f"Error in interactive loop: {e}") - - -async def main() -> None: - """Main async function.""" - print("\n" + "=" * 60) - print("Unitree Go2 Robot with agents2 Framework (Async)") - print("=" * 60) - - # Check for API key - if not os.getenv("OPENAI_API_KEY"): - print("ERROR: OPENAI_API_KEY not found") - print("Set your API key in .env file or environment") - sys.exit(1) - - # Load system prompt - try: - with open(SYSTEM_PROMPT_PATH) as f: - system_prompt = f.read() - except FileNotFoundError: - system_prompt = """You are a helpful robot assistant controlling a Unitree Go2 robot. -You have access to various movement and control skills. Be helpful and concise.""" - - # Initialize robot (optional - comment out if no robot) - robot = None - if os.getenv("ROBOT_IP"): - try: - logger.info("Connecting to robot...") - robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - connection_type=os.getenv("CONNECTION_TYPE", "webrtc"), - ) - robot.start() - await asyncio.sleep(3) - logger.info("Robot connected") - except Exception as e: - logger.warning(f"Could not connect to robot: {e}") - logger.info("Continuing without robot...") - - # Create skill container - skill_container = UnitreeSkillContainer(robot=robot) - - # Create agent - agent = Agent( - system_prompt=system_prompt, - model=Model.GPT_4O_MINI, # Using mini for faster responses - provider=Provider.OPENAI, - ) - - # Register skills and start - agent.register_skills(skill_container) - agent.start() - - # Log available skills - skills = skill_container.skills() - logger.info(f"Agent initialized with {len(skills)} skills") - - # Test query - print("\n--- Testing agent query ---") - test_response = await handle_query(agent, "Hello! Can you list 5 of your movement skills?") - print(f"Test response: {test_response}\n") - - # Run interactive loop - try: - await interactive_loop(agent) - except KeyboardInterrupt: - logger.info("Interrupted by user") - - # Clean up - logger.info("Shutting down...") - agent.stop() - if robot: - logger.info("Robot disconnected") - - print("\nGoodbye!") - - -if __name__ == "__main__": - # Run the async main function - asyncio.run(main()) diff --git a/dimos/agents2/temp/test_unitree_agent_query.py b/dimos/agents2/temp/test_unitree_agent_query.py deleted file mode 100644 index 4990940e6c..0000000000 --- a/dimos/agents2/temp/test_unitree_agent_query.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Test script to debug agent query issues. -Shows different ways to call the agent and handle async. -""" - -import asyncio -import os -from pathlib import Path -import sys -import time - -from dotenv import load_dotenv - -# Add parent directories to path -sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) - -from dimos.agents2 import Agent -from dimos.agents2.spec import Model, Provider -from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_agent_query") - -# Load environment variables -load_dotenv() - - -async def test_async_query(): - """Test agent query using async/await pattern.""" - print("\n=== Testing Async Query ===\n") - - # Create skill container - container = UnitreeSkillContainer(robot=None) - - # Create agent - agent = Agent( - system_prompt="You are a helpful robot assistant. List 3 skills you can do.", - model=Model.GPT_4O_MINI, - provider=Provider.OPENAI, - ) - - # Register skills and start - agent.register_skills(container) - agent.start() - - # Query asynchronously - logger.info("Sending async query...") - future = agent.query_async("Hello! What skills do you have?") - - # Wait for result - logger.info("Waiting for response...") - await asyncio.sleep(10) # Give it time to process - - # Check if future is done - if hasattr(future, "done") and future.done(): - try: - result = future.result() - logger.info(f"Got result: {result}") - except Exception as e: - logger.error(f"Future failed: {e}") - else: - logger.warning("Future not completed yet") - - agent.stop() - - return future - - -def test_sync_query_with_thread() -> None: - """Test agent query using threading for the event loop.""" - print("\n=== Testing Sync Query with Thread ===\n") - - import threading - - # Create skill container - container = UnitreeSkillContainer(robot=None) - - # Create agent - agent = Agent( - system_prompt="You are a helpful robot assistant. List 3 skills you can do.", - model=Model.GPT_4O_MINI, - provider=Provider.OPENAI, - ) - - # Register skills and start - agent.register_skills(container) - agent.start() - - # Track the thread we might create - loop_thread = None - - # The agent's event loop should be running in the Module's thread - # Let's check if it's running - if agent._loop and agent._loop.is_running(): - logger.info("Agent's event loop is running") - else: - logger.warning("Agent's event loop is NOT running - this is the problem!") - - # Try to run the loop in a thread - def run_loop() -> None: - asyncio.set_event_loop(agent._loop) - agent._loop.run_forever() - - loop_thread = threading.Thread(target=run_loop, daemon=False, name="EventLoopThread") - loop_thread.start() - time.sleep(1) # Give loop time to start - logger.info("Started event loop in thread") - - # Now try the query - try: - logger.info("Sending sync query...") - result = agent.query("Hello! What skills do you have?") - logger.info(f"Got result: {result}") - except Exception as e: - logger.error(f"Query failed: {e}") - import traceback - - traceback.print_exc() - - agent.stop() - - # Then stop the manually created event loop thread if we created one - if loop_thread and loop_thread.is_alive(): - logger.info("Stopping manually created event loop thread...") - # Stop the event loop - if agent._loop and agent._loop.is_running(): - agent._loop.call_soon_threadsafe(agent._loop.stop) - # Wait for thread to finish - loop_thread.join(timeout=5) - if loop_thread.is_alive(): - logger.warning("Thread did not stop cleanly within timeout") - - # Finally close the container - container._close_module() - - -# def test_with_real_module_system(): -# """Test using the real DimOS module system (like in test_agent.py).""" -# print("\n=== Testing with Module System ===\n") - -# from dimos.core import start - -# # Start the DimOS system -# dimos = start(2) - -# # Deploy container and agent as modules -# container = dimos.deploy(UnitreeSkillContainer, robot=None) -# agent = dimos.deploy( -# Agent, -# system_prompt="You are a helpful robot assistant. List 3 skills you can do.", -# model=Model.GPT_4O_MINI, -# provider=Provider.OPENAI, -# ) - -# # Register skills -# agent.register_skills(container) -# agent.start() - -# # Query -# try: -# logger.info("Sending query through module system...") -# future = agent.query_async("Hello! What skills do you have?") - -# # In the module system, the loop should be running -# time.sleep(5) # Wait for processing - -# if hasattr(future, "result"): -# result = future.result(timeout=10) -# logger.info(f"Got result: {result}") -# except Exception as e: -# logger.error(f"Query failed: {e}") - -# # Clean up -# agent.stop() -# dimos.stop() - - -def main() -> None: - """Run tests based on available API key.""" - - if not os.getenv("OPENAI_API_KEY"): - print("ERROR: OPENAI_API_KEY not set") - print("Please set your OpenAI API key to test the agent") - sys.exit(1) - - print("=" * 60) - print("Agent Query Testing") - print("=" * 60) - - # Test 1: Async query - try: - asyncio.run(test_async_query()) - except Exception as e: - logger.error(f"Async test failed: {e}") - - # Test 2: Sync query with threading - try: - test_sync_query_with_thread() - except Exception as e: - logger.error(f"Sync test failed: {e}") - - # Test 3: Module system (optional - more complex) - # try: - # test_with_real_module_system() - # except Exception as e: - # logger.error(f"Module test failed: {e}") - - print("\n" + "=" * 60) - print("Testing complete") - print("=" * 60) - - -if __name__ == "__main__": - main() diff --git a/dimos/agents2/temp/test_unitree_skill_container.py b/dimos/agents2/temp/test_unitree_skill_container.py deleted file mode 100644 index 16502004ff..0000000000 --- a/dimos/agents2/temp/test_unitree_skill_container.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Test file for UnitreeSkillContainer with agents2 framework. -Tests skill registration and basic functionality. -""" - -from pathlib import Path -import sys -import time - -# Add parent directories to path -sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) - -from dimos.agents2 import Agent -from dimos.agents2.spec import Model, Provider -from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_unitree_skills") - - -def test_skill_container_creation(): - """Test that the skill container can be created and skills are registered.""" - print("\n=== Testing UnitreeSkillContainer Creation ===") - - # Create container without robot (for testing) - container = UnitreeSkillContainer(robot=None) - - try: - # Get available skills from the container - skills = container.skills() - - print(f"Number of skills registered: {len(skills)}") - print("\nAvailable skills:") - for name, skill_config in list(skills.items())[:10]: # Show first 10 - print( - f" - {name}: {skill_config.description if hasattr(skill_config, 'description') else 'No description'}" - ) - if len(skills) > 10: - print(f" ... and {len(skills) - 10} more skills") - - return container, skills - finally: - # Ensure proper cleanup - container._close_module() - # Small delay to allow threads to finish cleanup - time.sleep(0.1) - - -def test_agent_with_skills(): - """Test that an agent can be created with the skill container.""" - print("\n=== Testing Agent with Skills ===") - - # Create skill container - container = UnitreeSkillContainer(robot=None) - agent = None - - try: - # Create agent with configuration passed directly - agent = Agent( - system_prompt="You are a helpful robot assistant that can control a Unitree Go2 robot.", - model=Model.GPT_4O_MINI, - provider=Provider.OPENAI, - ) - - # Register skills - agent.register_skills(container) - - print("Agent created and skills registered successfully!") - - # Get tools to verify - tools = agent.get_tools() - print(f"Agent has access to {len(tools)} tools") - - return agent - finally: - # Ensure proper cleanup in order - if agent: - agent.stop() - container._close_module() - # Small delay to allow threads to finish cleanup - time.sleep(0.1) - - -def test_skill_schemas() -> None: - """Test that skill schemas are properly generated for LangChain.""" - print("\n=== Testing Skill Schemas ===") - - container = UnitreeSkillContainer(robot=None) - - try: - skills = container.skills() - - # Check a few key skills (using snake_case names now) - skill_names = ["move", "wait", "stand_up", "sit", "front_flip", "dance1"] - - for name in skill_names: - if name in skills: - skill_config = skills[name] - print(f"\n{name} skill:") - print(f" Config: {skill_config}") - if hasattr(skill_config, "schema"): - print( - f" Schema keys: {skill_config.schema.keys() if skill_config.schema else 'None'}" - ) - else: - print(f"\nWARNING: Skill '{name}' not found!") - finally: - # Ensure proper cleanup - container._close_module() - # Small delay to allow threads to finish cleanup - time.sleep(0.1) diff --git a/dimos/agents2/test_agent.py b/dimos/agents2/test_agent.py deleted file mode 100644 index 447d02e6e3..0000000000 --- a/dimos/agents2/test_agent.py +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest -import pytest_asyncio - -from dimos.agents2.agent import Agent -from dimos.core import start -from dimos.protocol.skill.test_coordinator import SkillContainerTest - -system_prompt = ( - "Your name is Mr. Potato, potatoes are bad at math. Use a tools if asked to calculate" -) - - -@pytest.fixture(scope="session") -def dimos_cluster(): - """Session-scoped fixture to initialize dimos cluster once.""" - dimos = start(2) - try: - yield dimos - finally: - dimos.shutdown() - - -@pytest_asyncio.fixture -async def local(): - """Local context: both agent and testcontainer run locally""" - testcontainer = SkillContainerTest() - agent = Agent(system_prompt=system_prompt) - try: - yield agent, testcontainer - except Exception as e: - print(f"Error: {e}") - import traceback - - traceback.print_exc() - raise e - finally: - # Ensure cleanup happens while event loop is still active - try: - agent.stop() - except Exception: - pass - try: - testcontainer.stop() - except Exception: - pass - - -@pytest_asyncio.fixture -async def dask_mixed(dimos_cluster): - """Dask context: testcontainer on dimos, agent local""" - testcontainer = dimos_cluster.deploy(SkillContainerTest) - agent = Agent(system_prompt=system_prompt) - try: - yield agent, testcontainer - finally: - try: - agent.stop() - except Exception: - pass - try: - testcontainer.stop() - except Exception: - pass - - -@pytest_asyncio.fixture -async def dask_full(dimos_cluster): - """Dask context: both agent and testcontainer deployed on dimos""" - testcontainer = dimos_cluster.deploy(SkillContainerTest) - agent = dimos_cluster.deploy(Agent, system_prompt=system_prompt) - try: - yield agent, testcontainer - finally: - try: - agent.stop() - except Exception: - pass - try: - testcontainer.stop() - except Exception: - pass - - -@pytest_asyncio.fixture(params=["local", "dask_mixed", "dask_full"]) -async def agent_context(request): - """Parametrized fixture that runs tests with different agent configurations""" - param = request.param - - if param == "local": - testcontainer = SkillContainerTest() - agent = Agent(system_prompt=system_prompt) - try: - yield agent, testcontainer - finally: - try: - agent.stop() - except Exception: - pass - try: - testcontainer.stop() - except Exception: - pass - elif param == "dask_mixed": - dimos_cluster = request.getfixturevalue("dimos_cluster") - testcontainer = dimos_cluster.deploy(SkillContainerTest) - agent = Agent(system_prompt=system_prompt) - try: - yield agent, testcontainer - finally: - try: - agent.stop() - except Exception: - pass - try: - testcontainer.stop() - except Exception: - pass - elif param == "dask_full": - dimos_cluster = request.getfixturevalue("dimos_cluster") - testcontainer = dimos_cluster.deploy(SkillContainerTest) - agent = dimos_cluster.deploy(Agent, system_prompt=system_prompt) - try: - yield agent, testcontainer - finally: - try: - agent.stop() - except Exception: - pass - try: - testcontainer.stop() - except Exception: - pass - - -# @pytest.mark.timeout(40) -@pytest.mark.tool -@pytest.mark.asyncio -async def test_agent_init(agent_context) -> None: - """Test agent initialization and basic functionality across different configurations""" - agent, testcontainer = agent_context - - agent.register_skills(testcontainer) - agent.start() - - # agent.run_implicit_skill("uptime_seconds") - - print("query agent") - # When running locally, call the async method directly - agent.query( - "hi there, please tell me what's your name and current date, and how much is 124181112 + 124124?" - ) - print("Agent loop finished, asking about camera") - agent.query("tell me what you see on the camera?") - - # you can run skillspy and agentspy in parallel with this test for a better observation of what's happening diff --git a/dimos/agents2/testing.py b/dimos/agents2/testing.py deleted file mode 100644 index b729c13d50..0000000000 --- a/dimos/agents2/testing.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Testing utilities for agents.""" - -from collections.abc import Iterator, Sequence -import json -import os -from pathlib import Path -from typing import Any - -from langchain.chat_models import init_chat_model -from langchain_core.callbacks.manager import CallbackManagerForLLMRun -from langchain_core.language_models.chat_models import SimpleChatModel -from langchain_core.messages import ( - AIMessage, - AIMessageChunk, - BaseMessage, -) -from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult -from langchain_core.runnables import Runnable - - -class MockModel(SimpleChatModel): - """Custom fake chat model that supports tool calls for testing. - - Can operate in two modes: - 1. Playback mode (default): Reads responses from a JSON file or list - 2. Record mode: Uses a real LLM and saves responses to a JSON file - """ - - responses: list[str | AIMessage] = [] - i: int = 0 - json_path: Path | None = None - record: bool = False - real_model: Any | None = None - recorded_messages: list[dict[str, Any]] = [] - - def __init__(self, **kwargs) -> None: - # Extract custom parameters before calling super().__init__ - responses = kwargs.pop("responses", []) - json_path = kwargs.pop("json_path", None) - model_provider = kwargs.pop("model_provider", "openai") - model_name = kwargs.pop("model_name", "gpt-4o") - - super().__init__(**kwargs) - - self.json_path = Path(json_path) if json_path else None - self.record = bool(os.getenv("RECORD")) - self.i = 0 - self._bound_tools: Sequence[Any] | None = None - self.recorded_messages = [] - - if self.record: - # Initialize real model for recording - self.real_model = init_chat_model(model_provider=model_provider, model=model_name) - self.responses = [] # Initialize empty for record mode - elif self.json_path: - self.responses = self._load_responses_from_json() - elif responses: - self.responses = responses - else: - raise ValueError("no responses") - - @property - def _llm_type(self) -> str: - return "tool-call-fake-chat-model" - - def _load_responses_from_json(self) -> list[AIMessage]: - with open(self.json_path) as f: - data = json.load(f) - - responses = [] - for item in data.get("responses", []): - if isinstance(item, str): - responses.append(AIMessage(content=item)) - else: - # Reconstruct AIMessage from dict - msg = AIMessage( - content=item.get("content", ""), tool_calls=item.get("tool_calls", []) - ) - responses.append(msg) - return responses - - def _save_responses_to_json(self) -> None: - if not self.json_path: - return - - self.json_path.parent.mkdir(parents=True, exist_ok=True) - - data = { - "responses": [ - {"content": msg.content, "tool_calls": getattr(msg, "tool_calls", [])} - if isinstance(msg, AIMessage) - else msg - for msg in self.recorded_messages - ] - } - - with open(self.json_path, "w") as f: - json.dump(data, f, indent=2, default=str) - - def _call( - self, - messages: list[BaseMessage], - stop: list[str] | None = None, - run_manager: CallbackManagerForLLMRun | None = None, - **kwargs: Any, - ) -> str: - """Not used in _generate.""" - return "" - - def _generate( - self, - messages: list[BaseMessage], - stop: list[str] | None = None, - run_manager: CallbackManagerForLLMRun | None = None, - **kwargs: Any, - ) -> ChatResult: - if self.record: - # Recording mode - use real model and save responses - if not self.real_model: - raise ValueError("Real model not initialized for recording") - - # Bind tools if needed - model = self.real_model - if self._bound_tools: - model = model.bind_tools(self._bound_tools) - - result = model.invoke(messages) - self.recorded_messages.append(result) - self._save_responses_to_json() - - generation = ChatGeneration(message=result) - return ChatResult(generations=[generation]) - else: - # Playback mode - use predefined responses - if not self.responses: - raise ValueError("No responses available for playback. ") - - if self.i >= len(self.responses): - # Don't wrap around - stay at last response - response = self.responses[-1] - else: - response = self.responses[self.i] - self.i += 1 - - if isinstance(response, AIMessage): - message = response - else: - message = AIMessage(content=str(response)) - - generation = ChatGeneration(message=message) - return ChatResult(generations=[generation]) - - def _stream( - self, - messages: list[BaseMessage], - stop: list[str] | None = None, - run_manager: CallbackManagerForLLMRun | None = None, - **kwargs: Any, - ) -> Iterator[ChatGenerationChunk]: - """Stream not implemented for testing.""" - result = self._generate(messages, stop, run_manager, **kwargs) - message = result.generations[0].message - chunk = AIMessageChunk(content=message.content) - yield ChatGenerationChunk(message=chunk) - - def bind_tools( - self, - tools: Sequence[dict[str, Any] | type | Any], - *, - tool_choice: str | None = None, - **kwargs: Any, - ) -> Runnable: - """Store tools and return self.""" - self._bound_tools = tools - if self.record and self.real_model: - # Also bind tools to the real model - self.real_model = self.real_model.bind_tools(tools, tool_choice=tool_choice, **kwargs) - return self - - @property - def tools(self) -> Sequence[Any] | None: - """Get bound tools for inspection.""" - return self._bound_tools diff --git a/dimos/agents/memory/__init__.py b/dimos/agents_deprecated/__init__.py similarity index 100% rename from dimos/agents/memory/__init__.py rename to dimos/agents_deprecated/__init__.py diff --git a/dimos/agents_deprecated/agent.py b/dimos/agents_deprecated/agent.py new file mode 100644 index 0000000000..b7e2acad4c --- /dev/null +++ b/dimos/agents_deprecated/agent.py @@ -0,0 +1,917 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Agent framework for LLM-based autonomous systems. + +This module provides a flexible foundation for creating agents that can: +- Process image and text inputs through LLM APIs +- Store and retrieve contextual information using semantic memory +- Handle tool/function calling +- Process streaming inputs asynchronously + +The module offers base classes (Agent, LLMAgent) and concrete implementations +like OpenAIAgent that connect to specific LLM providers. +""" + +from __future__ import annotations + +# Standard library imports +import json +import os +import threading +from typing import TYPE_CHECKING, Any + +# Third-party imports +from dotenv import load_dotenv +from openai import NOT_GIVEN, OpenAI +from pydantic import BaseModel +from reactivex import Observable, Observer, create, empty, just, operators as RxOps +from reactivex.disposable import CompositeDisposable, Disposable +from reactivex.subject import Subject + +# Local imports +from dimos.agents_deprecated.memory.chroma_impl import OpenAISemanticMemory +from dimos.agents_deprecated.prompt_builder.impl import PromptBuilder +from dimos.agents_deprecated.tokenizer.openai_tokenizer import OpenAITokenizer +from dimos.skills.skills import AbstractSkill, SkillLibrary +from dimos.stream.frame_processor import FrameProcessor +from dimos.stream.stream_merger import create_stream_merger +from dimos.stream.video_operators import Operators as MyOps, VideoOperators as MyVidOps +from dimos.utils.logging_config import setup_logger +from dimos.utils.threadpool import get_scheduler + +if TYPE_CHECKING: + from reactivex.scheduler import ThreadPoolScheduler + + from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory + from dimos.agents_deprecated.tokenizer.base import AbstractTokenizer + +# Initialize environment variables +load_dotenv() + +# Initialize logger for the agent module +logger = setup_logger() + +# Constants +_TOKEN_BUDGET_PARTS = 4 # Number of parts to divide token budget +_MAX_SAVED_FRAMES = 100 # Maximum number of frames to save + + +# ----------------------------------------------------------------------------- +# region Agent Base Class +# ----------------------------------------------------------------------------- +class Agent: + """Base agent that manages memory and subscriptions.""" + + def __init__( + self, + dev_name: str = "NA", + agent_type: str = "Base", + agent_memory: AbstractAgentSemanticMemory | None = None, + pool_scheduler: ThreadPoolScheduler | None = None, + ) -> None: + """ + Initializes a new instance of the Agent. + + Args: + dev_name (str): The device name of the agent. + agent_type (str): The type of the agent (e.g., 'Base', 'Vision'). + agent_memory (AbstractAgentSemanticMemory): The memory system for the agent. + pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. + If None, the global scheduler from get_scheduler() will be used. + """ + self.dev_name = dev_name + self.agent_type = agent_type + self.agent_memory = agent_memory or OpenAISemanticMemory() + self.disposables = CompositeDisposable() + self.pool_scheduler = pool_scheduler if pool_scheduler else get_scheduler() + + def dispose_all(self) -> None: + """Disposes of all active subscriptions managed by this agent.""" + if self.disposables: + self.disposables.dispose() + else: + logger.info("No disposables to dispose.") + + +# endregion Agent Base Class + + +# ----------------------------------------------------------------------------- +# region LLMAgent Base Class (Generic LLM Agent) +# ----------------------------------------------------------------------------- +class LLMAgent(Agent): + """Generic LLM agent containing common logic for LLM-based agents. + + This class implements functionality for: + - Updating the query + - Querying the agent's memory (for RAG) + - Building prompts via a prompt builder + - Handling tooling callbacks in responses + - Subscribing to image and query streams + - Emitting responses as an observable stream + + Subclasses must implement the `_send_query` method, which is responsible + for sending the prompt to a specific LLM API. + + Attributes: + query (str): The current query text to process. + prompt_builder (PromptBuilder): Handles construction of prompts. + system_query (str): System prompt for RAG context situations. + image_detail (str): Detail level for image processing ('low','high','auto'). + max_input_tokens_per_request (int): Maximum input token count. + max_output_tokens_per_request (int): Maximum output token count. + max_tokens_per_request (int): Total maximum token count. + rag_query_n (int): Number of results to fetch from memory. + rag_similarity_threshold (float): Minimum similarity for RAG results. + frame_processor (FrameProcessor): Processes video frames. + output_dir (str): Directory for output files. + response_subject (Subject): Subject that emits agent responses. + process_all_inputs (bool): Whether to process every input emission (True) or + skip emissions when the agent is busy processing a previous input (False). + """ + + logging_file_memory_lock = threading.Lock() + + def __init__( + self, + dev_name: str = "NA", + agent_type: str = "LLM", + agent_memory: AbstractAgentSemanticMemory | None = None, + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool = False, + system_query: str | None = None, + max_output_tokens_per_request: int = 16384, + max_input_tokens_per_request: int = 128000, + input_query_stream: Observable | None = None, # type: ignore[type-arg] + input_data_stream: Observable | None = None, # type: ignore[type-arg] + input_video_stream: Observable | None = None, # type: ignore[type-arg] + ) -> None: + """ + Initializes a new instance of the LLMAgent. + + Args: + dev_name (str): The device name of the agent. + agent_type (str): The type of the agent. + agent_memory (AbstractAgentSemanticMemory): The memory system for the agent. + pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. + If None, the global scheduler from get_scheduler() will be used. + process_all_inputs (bool): Whether to process every input emission (True) or + skip emissions when the agent is busy processing a previous input (False). + """ + super().__init__(dev_name, agent_type, agent_memory, pool_scheduler) + # These attributes can be configured by a subclass if needed. + self.query: str | None = None + self.prompt_builder: PromptBuilder | None = None + self.system_query: str | None = system_query + self.image_detail: str = "low" + self.max_input_tokens_per_request: int = max_input_tokens_per_request + self.max_output_tokens_per_request: int = max_output_tokens_per_request + self.max_tokens_per_request: int = ( + self.max_input_tokens_per_request + self.max_output_tokens_per_request + ) + self.rag_query_n: int = 4 + self.rag_similarity_threshold: float = 0.45 + self.frame_processor: FrameProcessor | None = None + self.output_dir: str = os.path.join(os.getcwd(), "assets", "agent") + self.process_all_inputs: bool = process_all_inputs + os.makedirs(self.output_dir, exist_ok=True) + + # Subject for emitting responses + self.response_subject = Subject() # type: ignore[var-annotated] + + # Conversation history for maintaining context between calls + self.conversation_history = [] # type: ignore[var-annotated] + + # Initialize input streams + self.input_video_stream = input_video_stream + self.input_query_stream = ( + input_query_stream + if (input_data_stream is None) + else ( + input_query_stream.pipe( # type: ignore[misc, union-attr] + RxOps.with_latest_from(input_data_stream), + RxOps.map( + lambda combined: { + "query": combined[0], # type: ignore[index] + "objects": combined[1] # type: ignore[index] + if len(combined) > 1 # type: ignore[arg-type] + else "No object data available", + } + ), + RxOps.map( + lambda data: f"{data['query']}\n\nCurrent objects detected:\n{data['objects']}" # type: ignore[index] + ), + RxOps.do_action( + lambda x: print(f"\033[34mEnriched query: {x.split(chr(10))[0]}\033[0m") # type: ignore[arg-type] + or [print(f"\033[34m{line}\033[0m") for line in x.split(chr(10))[1:]] # type: ignore[var-annotated] + ), + ) + ) + ) + + # Setup stream subscriptions based on inputs provided + if (self.input_video_stream is not None) and (self.input_query_stream is not None): + self.merged_stream = create_stream_merger( + data_input_stream=self.input_video_stream, text_query_stream=self.input_query_stream + ) + + logger.info("Subscribing to merged input stream...") + + # Define a query extractor for the merged stream + def query_extractor(emission): # type: ignore[no-untyped-def] + return (emission[0], emission[1][0]) + + self.disposables.add( + self.subscribe_to_image_processing( + self.merged_stream, query_extractor=query_extractor + ) + ) + else: + # If no merged stream, fall back to individual streams + if self.input_video_stream is not None: + logger.info("Subscribing to input video stream...") + self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) + if self.input_query_stream is not None: + logger.info("Subscribing to input query stream...") + self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) + + def _update_query(self, incoming_query: str | None) -> None: + """Updates the query if an incoming query is provided. + + Args: + incoming_query (str): The new query text. + """ + if incoming_query is not None: + self.query = incoming_query + + def _get_rag_context(self) -> tuple[str, str]: + """Queries the agent memory to retrieve RAG context. + + Returns: + Tuple[str, str]: A tuple containing the formatted results (for logging) + and condensed results (for use in the prompt). + """ + results = self.agent_memory.query( + query_texts=self.query, + n_results=self.rag_query_n, + similarity_threshold=self.rag_similarity_threshold, + ) + formatted_results = "\n".join( + f"Document ID: {doc.id}\nMetadata: {doc.metadata}\nContent: {doc.page_content}\nScore: {score}\n" + for (doc, score) in results + ) + condensed_results = " | ".join(f"{doc.page_content}" for (doc, _) in results) + logger.info(f"Agent Memory Query Results:\n{formatted_results}") + logger.info("=== Results End ===") + return formatted_results, condensed_results + + def _build_prompt( + self, + base64_image: str | None, + dimensions: tuple[int, int] | None, + override_token_limit: bool, + condensed_results: str, + ) -> list: # type: ignore[type-arg] + """Builds a prompt message using the prompt builder. + + Args: + base64_image (str): Optional Base64-encoded image. + dimensions (Tuple[int, int]): Optional image dimensions. + override_token_limit (bool): Whether to override token limits. + condensed_results (str): The condensed RAG context. + + Returns: + list: A list of message dictionaries to be sent to the LLM. + """ + # Budget for each component of the prompt + budgets = { + "system_prompt": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, + "user_query": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, + "image": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, + "rag": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, + } + + # Define truncation policies for each component + policies = { + "system_prompt": "truncate_end", + "user_query": "truncate_middle", + "image": "do_not_truncate", + "rag": "truncate_end", + } + + return self.prompt_builder.build( # type: ignore[no-any-return, union-attr] + user_query=self.query, + override_token_limit=override_token_limit, + base64_image=base64_image, + image_width=dimensions[0] if dimensions is not None else None, + image_height=dimensions[1] if dimensions is not None else None, + image_detail=self.image_detail, + rag_context=condensed_results, + system_prompt=self.system_query, + budgets=budgets, + policies=policies, + ) + + def _handle_tooling(self, response_message, messages): # type: ignore[no-untyped-def] + """Handles tooling callbacks in the response message. + + If tool calls are present, the corresponding functions are executed and + a follow-up query is sent. + + Args: + response_message: The response message containing tool calls. + messages (list): The original list of messages sent. + + Returns: + The final response message after processing tool calls, if any. + """ + + # TODO: Make this more generic or move implementation to OpenAIAgent. + # This is presently OpenAI-specific. + def _tooling_callback(message, messages, response_message, skill_library: SkillLibrary): # type: ignore[no-untyped-def] + has_called_tools = False + new_messages = [] + for tool_call in message.tool_calls: + has_called_tools = True + name = tool_call.function.name + args = json.loads(tool_call.function.arguments) + result = skill_library.call(name, **args) + logger.info(f"Function Call Results: {result}") + new_messages.append( + { + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result), + "name": name, + } + ) + if has_called_tools: + logger.info("Sending Another Query.") + messages.append(response_message) + messages.extend(new_messages) + # Delegate to sending the query again. + return self._send_query(messages) + else: + logger.info("No Need for Another Query.") + return None + + if response_message.tool_calls is not None: + return _tooling_callback( + response_message, + messages, + response_message, + self.skill_library, # type: ignore[attr-defined] + ) + return None + + def _observable_query( # type: ignore[no-untyped-def] + self, + observer: Observer, # type: ignore[type-arg] + base64_image: str | None = None, + dimensions: tuple[int, int] | None = None, + override_token_limit: bool = False, + incoming_query: str | None = None, + ): + """Prepares and sends a query to the LLM, emitting the response to the observer. + + Args: + observer (Observer): The observer to emit responses to. + base64_image (str): Optional Base64-encoded image. + dimensions (Tuple[int, int]): Optional image dimensions. + override_token_limit (bool): Whether to override token limits. + incoming_query (str): Optional query to update the agent's query. + + Raises: + Exception: Propagates any exceptions encountered during processing. + """ + try: + self._update_query(incoming_query) + _, condensed_results = self._get_rag_context() + messages = self._build_prompt( + base64_image, dimensions, override_token_limit, condensed_results + ) + # logger.debug(f"Sending Query: {messages}") + logger.info("Sending Query.") + response_message = self._send_query(messages) + logger.info(f"Received Response: {response_message}") + if response_message is None: + raise Exception("Response message does not exist.") + + # TODO: Make this more generic. The parsed tag and tooling handling may be OpenAI-specific. + # If no skill library is provided or there are no tool calls, emit the response directly. + if ( + self.skill_library is None # type: ignore[attr-defined] + or self.skill_library.get_tools() in (None, NOT_GIVEN) # type: ignore[attr-defined] + or response_message.tool_calls is None + ): + final_msg = ( + response_message.parsed + if hasattr(response_message, "parsed") and response_message.parsed + else ( + response_message.content + if hasattr(response_message, "content") + else response_message + ) + ) + observer.on_next(final_msg) + self.response_subject.on_next(final_msg) + else: + response_message_2 = self._handle_tooling(response_message, messages) # type: ignore[no-untyped-call] + final_msg = ( + response_message_2 if response_message_2 is not None else response_message + ) + if isinstance(final_msg, BaseModel): # TODO: Test + final_msg = str(final_msg.content) # type: ignore[attr-defined] + observer.on_next(final_msg) + self.response_subject.on_next(final_msg) + observer.on_completed() + except Exception as e: + logger.error(f"Query failed in {self.dev_name}: {e}") + observer.on_error(e) + self.response_subject.on_error(e) + + def _send_query(self, messages: list) -> Any: # type: ignore[type-arg] + """Sends the query to the LLM API. + + This method must be implemented by subclasses with specifics of the LLM API. + + Args: + messages (list): The prompt messages to be sent. + + Returns: + Any: The response message from the LLM. + + Raises: + NotImplementedError: Always, unless overridden. + """ + raise NotImplementedError("Subclasses must implement _send_query method.") + + def _log_response_to_file(self, response, output_dir: str | None = None) -> None: # type: ignore[no-untyped-def] + """Logs the LLM response to a file. + + Args: + response: The response message to log. + output_dir (str): The directory where the log file is stored. + """ + if output_dir is None: + output_dir = self.output_dir + if response is not None: + with self.logging_file_memory_lock: + log_path = os.path.join(output_dir, "memory.txt") + with open(log_path, "a") as file: + file.write(f"{self.dev_name}: {response}\n") + logger.info(f"LLM Response [{self.dev_name}]: {response}") + + def subscribe_to_image_processing( # type: ignore[no-untyped-def] + self, + frame_observable: Observable, # type: ignore[type-arg] + query_extractor=None, + ) -> Disposable: + """Subscribes to a stream of video frames for processing. + + This method sets up a subscription to process incoming video frames. + Each frame is encoded and then sent to the LLM by directly calling the + _observable_query method. The response is then logged to a file. + + Args: + frame_observable (Observable): An observable emitting video frames or + (query, frame) tuples if query_extractor is provided. + query_extractor (callable, optional): Function to extract query and frame from + each emission. If None, assumes emissions are + raw frames and uses self.system_query. + + Returns: + Disposable: A disposable representing the subscription. + """ + # Initialize frame processor if not already set + if self.frame_processor is None: + self.frame_processor = FrameProcessor(delete_on_init=True) + + print_emission_args = {"enabled": True, "dev_name": self.dev_name, "counts": {}} + + def _process_frame(emission) -> Observable: # type: ignore[no-untyped-def, type-arg] + """ + Processes a frame or (query, frame) tuple. + """ + # Extract query and frame + if query_extractor: + query, frame = query_extractor(emission) + else: + query = self.system_query + frame = emission + return just(frame).pipe( # type: ignore[call-overload, no-any-return] + MyOps.print_emission(id="B", **print_emission_args), # type: ignore[arg-type] + RxOps.observe_on(self.pool_scheduler), + MyOps.print_emission(id="C", **print_emission_args), # type: ignore[arg-type] + RxOps.subscribe_on(self.pool_scheduler), + MyOps.print_emission(id="D", **print_emission_args), # type: ignore[arg-type] + MyVidOps.with_jpeg_export( + self.frame_processor, # type: ignore[arg-type] + suffix=f"{self.dev_name}_frame_", + save_limit=_MAX_SAVED_FRAMES, + ), + MyOps.print_emission(id="E", **print_emission_args), # type: ignore[arg-type] + MyVidOps.encode_image(), + MyOps.print_emission(id="F", **print_emission_args), # type: ignore[arg-type] + RxOps.filter( + lambda base64_and_dims: base64_and_dims is not None + and base64_and_dims[0] is not None # type: ignore[index] + and base64_and_dims[1] is not None # type: ignore[index] + ), + MyOps.print_emission(id="G", **print_emission_args), # type: ignore[arg-type] + RxOps.flat_map( + lambda base64_and_dims: create( # type: ignore[arg-type, return-value] + lambda observer, _: self._observable_query( + observer, # type: ignore[arg-type] + base64_image=base64_and_dims[0], + dimensions=base64_and_dims[1], + incoming_query=query, + ) + ) + ), # Use the extracted query + MyOps.print_emission(id="H", **print_emission_args), # type: ignore[arg-type] + ) + + # Use a mutable flag to ensure only one frame is processed at a time. + is_processing = [False] + + def process_if_free(emission): # type: ignore[no-untyped-def] + if not self.process_all_inputs and is_processing[0]: + # Drop frame if a request is in progress and process_all_inputs is False + return empty() + else: + is_processing[0] = True + return _process_frame(emission).pipe( + MyOps.print_emission(id="I", **print_emission_args), # type: ignore[arg-type] + RxOps.observe_on(self.pool_scheduler), + MyOps.print_emission(id="J", **print_emission_args), # type: ignore[arg-type] + RxOps.subscribe_on(self.pool_scheduler), + MyOps.print_emission(id="K", **print_emission_args), # type: ignore[arg-type] + RxOps.do_action( + on_completed=lambda: is_processing.__setitem__(0, False), + on_error=lambda e: is_processing.__setitem__(0, False), + ), + MyOps.print_emission(id="L", **print_emission_args), # type: ignore[arg-type] + ) + + observable = frame_observable.pipe( + MyOps.print_emission(id="A", **print_emission_args), # type: ignore[arg-type] + RxOps.flat_map(process_if_free), + MyOps.print_emission(id="M", **print_emission_args), # type: ignore[arg-type] + ) + + disposable = observable.subscribe( + on_next=lambda response: self._log_response_to_file(response, self.output_dir), + on_error=lambda e: logger.error(f"Error encountered: {e}"), + on_completed=lambda: logger.info(f"Stream processing completed for {self.dev_name}"), + ) + self.disposables.add(disposable) + return disposable # type: ignore[no-any-return] + + def subscribe_to_query_processing(self, query_observable: Observable) -> Disposable: # type: ignore[type-arg] + """Subscribes to a stream of queries for processing. + + This method sets up a subscription to process incoming queries by directly + calling the _observable_query method. The responses are logged to a file. + + Args: + query_observable (Observable): An observable emitting queries. + + Returns: + Disposable: A disposable representing the subscription. + """ + print_emission_args = {"enabled": False, "dev_name": self.dev_name, "counts": {}} + + def _process_query(query) -> Observable: # type: ignore[no-untyped-def, type-arg] + """ + Processes a single query by logging it and passing it to _observable_query. + Returns an observable that emits the LLM response. + """ + return just(query).pipe( + MyOps.print_emission(id="Pr A", **print_emission_args), # type: ignore[arg-type] + RxOps.flat_map( + lambda query: create( # type: ignore[arg-type, return-value] + lambda observer, _: self._observable_query(observer, incoming_query=query) # type: ignore[arg-type] + ) + ), + MyOps.print_emission(id="Pr B", **print_emission_args), # type: ignore[arg-type] + ) + + # A mutable flag indicating whether a query is currently being processed. + is_processing = [False] + + def process_if_free(query): # type: ignore[no-untyped-def] + logger.info(f"Processing Query: {query}") + if not self.process_all_inputs and is_processing[0]: + # Drop query if a request is already in progress and process_all_inputs is False + return empty() + else: + is_processing[0] = True + logger.info("Processing Query.") + return _process_query(query).pipe( + MyOps.print_emission(id="B", **print_emission_args), # type: ignore[arg-type] + RxOps.observe_on(self.pool_scheduler), + MyOps.print_emission(id="C", **print_emission_args), # type: ignore[arg-type] + RxOps.subscribe_on(self.pool_scheduler), + MyOps.print_emission(id="D", **print_emission_args), # type: ignore[arg-type] + RxOps.do_action( + on_completed=lambda: is_processing.__setitem__(0, False), + on_error=lambda e: is_processing.__setitem__(0, False), + ), + MyOps.print_emission(id="E", **print_emission_args), # type: ignore[arg-type] + ) + + observable = query_observable.pipe( + MyOps.print_emission(id="A", **print_emission_args), # type: ignore[arg-type] + RxOps.flat_map(lambda query: process_if_free(query)), # type: ignore[no-untyped-call] + MyOps.print_emission(id="F", **print_emission_args), # type: ignore[arg-type] + ) + + disposable = observable.subscribe( + on_next=lambda response: self._log_response_to_file(response, self.output_dir), + on_error=lambda e: logger.error(f"Error processing query for {self.dev_name}: {e}"), + on_completed=lambda: logger.info(f"Stream processing completed for {self.dev_name}"), + ) + self.disposables.add(disposable) + return disposable # type: ignore[no-any-return] + + def get_response_observable(self) -> Observable: # type: ignore[type-arg] + """Gets an observable that emits responses from this agent. + + Returns: + Observable: An observable that emits string responses from the agent. + """ + return self.response_subject.pipe( + RxOps.observe_on(self.pool_scheduler), + RxOps.subscribe_on(self.pool_scheduler), + RxOps.share(), + ) + + def run_observable_query(self, query_text: str, **kwargs) -> Observable: # type: ignore[no-untyped-def, type-arg] + """Creates an observable that processes a one-off text query to Agent and emits the response. + + This method provides a simple way to send a text query and get an observable + stream of the response. It's designed for one-off queries rather than + continuous processing of input streams. Useful for testing and development. + + Args: + query_text (str): The query text to process. + **kwargs: Additional arguments to pass to _observable_query. Supported args vary by agent type. + For example, ClaudeAgent supports: base64_image, dimensions, override_token_limit, + reset_conversation, thinking_budget_tokens + + Returns: + Observable: An observable that emits the response as a string. + """ + return create( + lambda observer, _: self._observable_query( + observer, # type: ignore[arg-type] + incoming_query=query_text, + **kwargs, + ) + ) + + def dispose_all(self) -> None: + """Disposes of all active subscriptions managed by this agent.""" + super().dispose_all() + self.response_subject.on_completed() + + +# endregion LLMAgent Base Class (Generic LLM Agent) + + +# ----------------------------------------------------------------------------- +# region OpenAIAgent Subclass (OpenAI-Specific Implementation) +# ----------------------------------------------------------------------------- +class OpenAIAgent(LLMAgent): + """OpenAI agent implementation that uses OpenAI's API for processing. + + This class implements the _send_query method to interact with OpenAI's API. + It also sets up OpenAI-specific parameters, such as the client, model name, + tokenizer, and response model. + """ + + def __init__( + self, + dev_name: str, + agent_type: str = "Vision", + query: str = "What do you see?", + input_query_stream: Observable | None = None, # type: ignore[type-arg] + input_data_stream: Observable | None = None, # type: ignore[type-arg] + input_video_stream: Observable | None = None, # type: ignore[type-arg] + output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), + agent_memory: AbstractAgentSemanticMemory | None = None, + system_query: str | None = None, + max_input_tokens_per_request: int = 128000, + max_output_tokens_per_request: int = 16384, + model_name: str = "gpt-4o", + prompt_builder: PromptBuilder | None = None, + tokenizer: AbstractTokenizer | None = None, + rag_query_n: int = 4, + rag_similarity_threshold: float = 0.45, + skills: AbstractSkill | list[AbstractSkill] | SkillLibrary | None = None, + response_model: BaseModel | None = None, + frame_processor: FrameProcessor | None = None, + image_detail: str = "low", + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool | None = None, + openai_client: OpenAI | None = None, + ) -> None: + """ + Initializes a new instance of the OpenAIAgent. + + Args: + dev_name (str): The device name of the agent. + agent_type (str): The type of the agent. + query (str): The default query text. + input_query_stream (Observable): An observable for query input. + input_data_stream (Observable): An observable for data input. + input_video_stream (Observable): An observable for video frames. + output_dir (str): Directory for output files. + agent_memory (AbstractAgentSemanticMemory): The memory system. + system_query (str): The system prompt to use with RAG context. + max_input_tokens_per_request (int): Maximum tokens for input. + max_output_tokens_per_request (int): Maximum tokens for output. + model_name (str): The OpenAI model name to use. + prompt_builder (PromptBuilder): Custom prompt builder. + tokenizer (AbstractTokenizer): Custom tokenizer for token counting. + rag_query_n (int): Number of results to fetch in RAG queries. + rag_similarity_threshold (float): Minimum similarity for RAG results. + skills (Union[AbstractSkill, List[AbstractSkill], SkillLibrary]): Skills available to the agent. + response_model (BaseModel): Optional Pydantic model for responses. + frame_processor (FrameProcessor): Custom frame processor. + image_detail (str): Detail level for images ("low", "high", "auto"). + pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. + If None, the global scheduler from get_scheduler() will be used. + process_all_inputs (bool): Whether to process all inputs or skip when busy. + If None, defaults to True for text queries and merged streams, False for video streams. + openai_client (OpenAI): The OpenAI client to use. This can be used to specify + a custom OpenAI client if targetting another provider. + """ + # Determine appropriate default for process_all_inputs if not provided + if process_all_inputs is None: + if input_query_stream is not None: + process_all_inputs = True + else: + process_all_inputs = False + + super().__init__( + dev_name=dev_name, + agent_type=agent_type, + agent_memory=agent_memory, + pool_scheduler=pool_scheduler, + process_all_inputs=process_all_inputs, + system_query=system_query, + input_query_stream=input_query_stream, + input_data_stream=input_data_stream, + input_video_stream=input_video_stream, + ) + self.client = openai_client or OpenAI() + self.query = query + self.output_dir = output_dir + os.makedirs(self.output_dir, exist_ok=True) + + # Configure skill library. + self.skills = skills + self.skill_library = None + if isinstance(self.skills, SkillLibrary): + self.skill_library = self.skills + elif isinstance(self.skills, list): + self.skill_library = SkillLibrary() + for skill in self.skills: + self.skill_library.add(skill) + elif isinstance(self.skills, AbstractSkill): + self.skill_library = SkillLibrary() + self.skill_library.add(self.skills) + + self.response_model = response_model if response_model is not None else NOT_GIVEN + self.model_name = model_name + self.tokenizer = tokenizer or OpenAITokenizer(model_name=self.model_name) + self.prompt_builder = prompt_builder or PromptBuilder( + self.model_name, tokenizer=self.tokenizer + ) + self.rag_query_n = rag_query_n + self.rag_similarity_threshold = rag_similarity_threshold + self.image_detail = image_detail + self.max_output_tokens_per_request = max_output_tokens_per_request + self.max_input_tokens_per_request = max_input_tokens_per_request + self.max_tokens_per_request = max_input_tokens_per_request + max_output_tokens_per_request + + # Add static context to memory. + self._add_context_to_memory() + + self.frame_processor = frame_processor or FrameProcessor(delete_on_init=True) + + logger.info("OpenAI Agent Initialized.") + + def _add_context_to_memory(self) -> None: + """Adds initial context to the agent's memory.""" + context_data = [ + ( + "id0", + "Optical Flow is a technique used to track the movement of objects in a video sequence.", + ), + ( + "id1", + "Edge Detection is a technique used to identify the boundaries of objects in an image.", + ), + ("id2", "Video is a sequence of frames captured at regular intervals."), + ( + "id3", + "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects.", + ), + ( + "id4", + "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", + ), + ] + for doc_id, text in context_data: + self.agent_memory.add_vector(doc_id, text) # type: ignore[no-untyped-call] + + def _send_query(self, messages: list) -> Any: # type: ignore[type-arg] + """Sends the query to OpenAI's API. + + Depending on whether a response model is provided, the appropriate API + call is made. + + Args: + messages (list): The prompt messages to send. + + Returns: + The response message from OpenAI. + + Raises: + Exception: If no response message is returned. + ConnectionError: If there's an issue connecting to the API. + ValueError: If the messages or other parameters are invalid. + """ + try: + if self.response_model is not NOT_GIVEN: + response = self.client.beta.chat.completions.parse( + model=self.model_name, + messages=messages, + response_format=self.response_model, # type: ignore[arg-type] + tools=( + self.skill_library.get_tools() # type: ignore[arg-type] + if self.skill_library is not None + else NOT_GIVEN + ), + max_tokens=self.max_output_tokens_per_request, + ) + else: + response = self.client.chat.completions.create( # type: ignore[assignment] + model=self.model_name, + messages=messages, + max_tokens=self.max_output_tokens_per_request, + tools=( + self.skill_library.get_tools() # type: ignore[arg-type] + if self.skill_library is not None + else NOT_GIVEN + ), + ) + response_message = response.choices[0].message + if response_message is None: + logger.error("Response message does not exist.") + raise Exception("Response message does not exist.") + return response_message + except ConnectionError as ce: + logger.error(f"Connection error with API: {ce}") + raise + except ValueError as ve: + logger.error(f"Invalid parameters: {ve}") + raise + except Exception as e: + logger.error(f"Unexpected error in API call: {e}") + raise + + def stream_query(self, query_text: str) -> Observable: # type: ignore[type-arg] + """Creates an observable that processes a text query and emits the response. + + This method provides a simple way to send a text query and get an observable + stream of the response. It's designed for one-off queries rather than + continuous processing of input streams. + + Args: + query_text (str): The query text to process. + + Returns: + Observable: An observable that emits the response as a string. + """ + return create( + lambda observer, _: self._observable_query(observer, incoming_query=query_text) # type: ignore[arg-type] + ) + + +# endregion OpenAIAgent Subclass (OpenAI-Specific Implementation) diff --git a/dimos/agents/agent_config.py b/dimos/agents_deprecated/agent_config.py similarity index 94% rename from dimos/agents/agent_config.py rename to dimos/agents_deprecated/agent_config.py index 5b9027b072..9adae6ad3c 100644 --- a/dimos/agents/agent_config.py +++ b/dimos/agents_deprecated/agent_config.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # limitations under the License. -from dimos.agents.agent import Agent +from dimos.agents_deprecated.agent import Agent class AgentConfig: diff --git a/dimos/agents/agent_message.py b/dimos/agents_deprecated/agent_message.py similarity index 95% rename from dimos/agents/agent_message.py rename to dimos/agents_deprecated/agent_message.py index cecd8092c1..87351e0518 100644 --- a/dimos/agents/agent_message.py +++ b/dimos/agents_deprecated/agent_message.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ from dataclasses import dataclass, field import time -from dimos.agents.agent_types import AgentImage +from dimos.agents_deprecated.agent_types import AgentImage from dimos.msgs.sensor_msgs.Image import Image @@ -47,7 +47,7 @@ def add_image(self, image: Image | AgentImage) -> None: if isinstance(image, Image): # Convert to AgentImage agent_image = AgentImage( - base64_jpeg=image.agent_encode(), + base64_jpeg=image.agent_encode(), # type: ignore[arg-type] width=image.width, height=image.height, metadata={"format": image.format.value, "frame_id": image.frame_id}, diff --git a/dimos/agents/agent_types.py b/dimos/agents_deprecated/agent_types.py similarity index 97% rename from dimos/agents/agent_types.py rename to dimos/agents_deprecated/agent_types.py index db41acbafb..f52bafdac6 100644 --- a/dimos/agents/agent_types.py +++ b/dimos/agents_deprecated/agent_types.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -95,15 +95,15 @@ def to_openai_format(self) -> dict[str, Any]: msg["content"] = self.content else: # Content is already a list of content blocks - msg["content"] = self.content + msg["content"] = self.content # type: ignore[assignment] # Add tool calls if present if self.tool_calls: # Handle both ToolCall objects and dicts if isinstance(self.tool_calls[0], dict): - msg["tool_calls"] = self.tool_calls + msg["tool_calls"] = self.tool_calls # type: ignore[assignment] else: - msg["tool_calls"] = [ + msg["tool_calls"] = [ # type: ignore[assignment] { "id": tc.id, "type": "function", diff --git a/dimos/agents/claude_agent.py b/dimos/agents_deprecated/claude_agent.py similarity index 94% rename from dimos/agents/claude_agent.py rename to dimos/agents_deprecated/claude_agent.py index c8163de162..72fde622f1 100644 --- a/dimos/agents/claude_agent.py +++ b/dimos/agents_deprecated/claude_agent.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ from dotenv import load_dotenv # Local imports -from dimos.agents.agent import LLMAgent +from dimos.agents_deprecated.agent import LLMAgent from dimos.skills.skills import AbstractSkill, SkillLibrary from dimos.stream.frame_processor import FrameProcessor from dimos.utils.logging_config import setup_logger @@ -39,19 +39,19 @@ from reactivex import Observable from reactivex.scheduler import ThreadPoolScheduler - from dimos.agents.memory.base import AbstractAgentSemanticMemory - from dimos.agents.prompt_builder.impl import PromptBuilder + from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory + from dimos.agents_deprecated.prompt_builder.impl import PromptBuilder # Initialize environment variables load_dotenv() # Initialize logger for the Claude agent -logger = setup_logger("dimos.agents.claude") +logger = setup_logger() # Response object compatible with LLMAgent class ResponseMessage: - def __init__(self, content: str = "", tool_calls=None, thinking_blocks=None) -> None: + def __init__(self, content: str = "", tool_calls=None, thinking_blocks=None) -> None: # type: ignore[no-untyped-def] self.content = content self.tool_calls = tool_calls or [] self.thinking_blocks = thinking_blocks or [] @@ -85,9 +85,9 @@ def __init__( dev_name: str, agent_type: str = "Vision", query: str = "What do you see?", - input_query_stream: Observable | None = None, - input_video_stream: Observable | None = None, - input_data_stream: Observable | None = None, + input_query_stream: Observable | None = None, # type: ignore[type-arg] + input_video_stream: Observable | None = None, # type: ignore[type-arg] + input_data_stream: Observable | None = None, # type: ignore[type-arg] output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), agent_memory: AbstractAgentSemanticMemory | None = None, system_query: str | None = None, @@ -158,7 +158,7 @@ def __init__( # Claude-specific parameters self.thinking_budget_tokens = thinking_budget_tokens - self.claude_api_params = {} # Will store params for Claude API calls + self.claude_api_params = {} # type: ignore[var-annotated] # Will store params for Claude API calls # Configure skills self.skills = skills @@ -217,7 +217,7 @@ def _add_context_to_memory(self) -> None: ), ] for doc_id, text in context_data: - self.agent_memory.add_vector(doc_id, text) + self.agent_memory.add_vector(doc_id, text) # type: ignore[no-untyped-call] def _convert_tools_to_claude_format(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]: """ @@ -258,15 +258,15 @@ def _convert_tools_to_claude_format(self, tools: list[dict[str, Any]]) -> list[d return claude_tools - def _build_prompt( + def _build_prompt( # type: ignore[override] self, - messages: list, + messages: list, # type: ignore[type-arg] base64_image: str | list[str] | None = None, dimensions: tuple[int, int] | None = None, override_token_limit: bool = False, rag_results: str = "", thinking_budget_tokens: int | None = None, - ) -> list: + ) -> list: # type: ignore[type-arg] """Builds a prompt message specifically for Claude API, using local messages copy.""" """Builds a prompt message specifically for Claude API. @@ -347,9 +347,9 @@ def _build_prompt( # Store the parameters for use in _send_query and return them self.claude_api_params = claude_params.copy() - return messages, claude_params + return messages, claude_params # type: ignore[return-value] - def _send_query(self, messages: list, claude_params: dict) -> Any: + def _send_query(self, messages: list, claude_params: dict) -> Any: # type: ignore[override, type-arg] """Sends the query to Anthropic's API using streaming for better thinking visualization. Args: @@ -397,7 +397,7 @@ def _send_query(self, messages: list, claude_params: dict) -> Any: block_type = event.content_block.type current_block = { "type": block_type, - "id": event.index, + "id": event.index, # type: ignore[dict-item] "content": "", "signature": None, } @@ -413,7 +413,7 @@ def _send_query(self, messages: list, claude_params: dict) -> Any: elif event.delta.type == "text_delta": # Accumulate text content text_content += event.delta.text - current_block["content"] += event.delta.text + current_block["content"] += event.delta.text # type: ignore[operator] memory_file.write(f"{event.delta.text}") memory_file.flush() @@ -463,9 +463,9 @@ def _send_query(self, messages: list, claude_params: dict) -> Any: # Process tool use blocks when they're complete if hasattr(event, "content_block"): tool_block = event.content_block - tool_id = tool_block.id - tool_name = tool_block.name - tool_input = tool_block.input + tool_id = tool_block.id # type: ignore[union-attr] + tool_name = tool_block.name # type: ignore[union-attr] + tool_input = tool_block.input # type: ignore[union-attr] # Create a tool call object for LLMAgent compatibility tool_call_obj = type( @@ -537,7 +537,7 @@ def _send_query(self, messages: list, claude_params: dict) -> Any: def _observable_query( self, - observer: Observer, + observer: Observer, # type: ignore[name-defined] base64_image: str | None = None, dimensions: tuple[int, int] | None = None, override_token_limit: bool = False, @@ -605,7 +605,7 @@ def _observable_query( # Handle tool calls if present if response_message.tool_calls: - self._handle_tooling(response_message, messages) + self._handle_tooling(response_message, messages) # type: ignore[no-untyped-call] # At the end, append only new messages (including tool-use/results) to the global conversation history under a lock import threading @@ -633,7 +633,7 @@ def _observable_query( self.response_subject.on_next(error_message) observer.on_completed() - def _handle_tooling(self, response_message, messages): + def _handle_tooling(self, response_message, messages): # type: ignore[no-untyped-def] """Executes tools and appends tool-use/result blocks to messages.""" if not hasattr(response_message, "tool_calls") or not response_message.tool_calls: logger.info("No tool calls found in response message") @@ -658,7 +658,7 @@ def _handle_tooling(self, response_message, messages): try: # Execute the tool args = json.loads(tool_call.function.arguments) - tool_result = self.skills.call(tool_call.function.name, **args) + tool_result = self.skills.call(tool_call.function.name, **args) # type: ignore[union-attr] # Check if the result is an error message if isinstance(tool_result, str) and ( @@ -698,7 +698,7 @@ def _handle_tooling(self, response_message, messages): } ) - def _tooling_callback(self, response_message) -> None: + def _tooling_callback(self, response_message) -> None: # type: ignore[no-untyped-def] """Runs the observable query for each tool call in the current response_message""" if not hasattr(response_message, "tool_calls") or not response_message.tool_calls: return @@ -716,7 +716,7 @@ def _tooling_callback(self, response_message) -> None: # Continue processing even if the callback fails pass - def _debug_api_call(self, claude_params: dict): + def _debug_api_call(self, claude_params: dict): # type: ignore[no-untyped-def, type-arg] """Debugging function to log API calls with truncated base64 data.""" # Remove tools to reduce verbosity import copy diff --git a/dimos/agents/prompt_builder/__init__.py b/dimos/agents_deprecated/memory/__init__.py similarity index 100% rename from dimos/agents/prompt_builder/__init__.py rename to dimos/agents_deprecated/memory/__init__.py diff --git a/dimos/agents_deprecated/memory/base.py b/dimos/agents_deprecated/memory/base.py new file mode 100644 index 0000000000..283b7cfdce --- /dev/null +++ b/dimos/agents_deprecated/memory/base.py @@ -0,0 +1,134 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod + +from dimos.exceptions.agent_memory_exceptions import ( + AgentMemoryConnectionError, + UnknownConnectionTypeError, +) +from dimos.utils.logging_config import setup_logger + +# TODO +# class AbstractAgentMemory(ABC): + +# TODO +# class AbstractAgentSymbolicMemory(AbstractAgentMemory): + + +class AbstractAgentSemanticMemory: # AbstractAgentMemory): + def __init__(self, connection_type: str = "local", **kwargs) -> None: # type: ignore[no-untyped-def] + """ + Initialize with dynamic connection parameters. + Args: + connection_type (str): 'local' for a local database, 'remote' for a remote connection. + Raises: + UnknownConnectionTypeError: If an unrecognized connection type is specified. + AgentMemoryConnectionError: If initializing the database connection fails. + """ + self.logger = setup_logger() + self.logger.info("Initializing AgentMemory with connection type: %s", connection_type) + self.connection_params = kwargs + self.db_connection = ( + None # Holds the conection, whether local or remote, to the database used. + ) + + if connection_type not in ["local", "remote"]: + error = UnknownConnectionTypeError( + f"Invalid connection_type {connection_type}. Expected 'local' or 'remote'." + ) + self.logger.error(str(error)) + raise error + + try: + if connection_type == "remote": + self.connect() # type: ignore[no-untyped-call] + elif connection_type == "local": + self.create() # type: ignore[no-untyped-call] + except Exception as e: + self.logger.error("Failed to initialize database connection: %s", str(e), exc_info=True) + raise AgentMemoryConnectionError( + "Initialization failed due to an unexpected error.", cause=e + ) from e + + @abstractmethod + def connect(self): # type: ignore[no-untyped-def] + """Establish a connection to the data store using dynamic parameters specified during initialization.""" + + @abstractmethod + def create(self): # type: ignore[no-untyped-def] + """Create a local instance of the data store tailored to specific requirements.""" + + ## Create ## + @abstractmethod + def add_vector(self, vector_id, vector_data): # type: ignore[no-untyped-def] + """Add a vector to the database. + Args: + vector_id (any): Unique identifier for the vector. + vector_data (any): The actual data of the vector to be stored. + """ + + ## Read ## + @abstractmethod + def get_vector(self, vector_id): # type: ignore[no-untyped-def] + """Retrieve a vector from the database by its identifier. + Args: + vector_id (any): The identifier of the vector to retrieve. + """ + + @abstractmethod + def query(self, query_texts, n_results: int = 4, similarity_threshold=None): # type: ignore[no-untyped-def] + """Performs a semantic search in the vector database. + + Args: + query_texts (Union[str, List[str]]): The query text or list of query texts to search for. + n_results (int, optional): Number of results to return. Defaults to 4. + similarity_threshold (float, optional): Minimum similarity score for results to be included [0.0, 1.0]. Defaults to None. + + Returns: + List[Tuple[Document, Optional[float]]]: A list of tuples containing the search results. Each tuple + contains: + Document: The retrieved document object. + Optional[float]: The similarity score of the match, or None if not applicable. + + Raises: + ValueError: If query_texts is empty or invalid. + ConnectionError: If database connection fails during query. + """ + + ## Update ## + @abstractmethod + def update_vector(self, vector_id, new_vector_data): # type: ignore[no-untyped-def] + """Update an existing vector in the database. + Args: + vector_id (any): The identifier of the vector to update. + new_vector_data (any): The new data to replace the existing vector data. + """ + + ## Delete ## + @abstractmethod + def delete_vector(self, vector_id): # type: ignore[no-untyped-def] + """Delete a vector from the database using its identifier. + Args: + vector_id (any): The identifier of the vector to delete. + """ + + +# query(string, metadata/tag, n_rets, kwargs) + +# query by string, timestamp, id, n_rets + +# (some sort of tag/metadata) + +# temporal diff --git a/dimos/agents/memory/chroma_impl.py b/dimos/agents_deprecated/memory/chroma_impl.py similarity index 75% rename from dimos/agents/memory/chroma_impl.py rename to dimos/agents_deprecated/memory/chroma_impl.py index b238b616d8..c724b07272 100644 --- a/dimos/agents/memory/chroma_impl.py +++ b/dimos/agents_deprecated/memory/chroma_impl.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ from langchain_openai import OpenAIEmbeddings import torch -from dimos.agents.memory.base import AbstractAgentSemanticMemory +from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory class ChromaAgentSemanticMemory(AbstractAgentSemanticMemory): @@ -32,16 +32,16 @@ def __init__(self, collection_name: str = "my_collection") -> None: self.embeddings = None super().__init__(connection_type="local") - def connect(self): + def connect(self): # type: ignore[no-untyped-def] # Stub - return super().connect() + return super().connect() # type: ignore[no-untyped-call, safe-super] - def create(self): + def create(self): # type: ignore[no-untyped-def] """Create the embedding function and initialize the Chroma database. This method must be implemented by child classes.""" raise NotImplementedError("Child classes must implement this method") - def add_vector(self, vector_id, vector_data): + def add_vector(self, vector_id, vector_data): # type: ignore[no-untyped-def] """Add a vector to the ChromaDB collection.""" if not self.db_connection: raise Exception("Collection not initialized. Call connect() first.") @@ -51,12 +51,12 @@ def add_vector(self, vector_id, vector_data): metadatas=[{"name": vector_id}], ) - def get_vector(self, vector_id): + def get_vector(self, vector_id): # type: ignore[no-untyped-def] """Retrieve a vector from the ChromaDB by its identifier.""" - result = self.db_connection.get(include=["embeddings"], ids=[vector_id]) + result = self.db_connection.get(include=["embeddings"], ids=[vector_id]) # type: ignore[attr-defined] return result - def query(self, query_texts, n_results: int = 4, similarity_threshold=None): + def query(self, query_texts, n_results: int = 4, similarity_threshold=None): # type: ignore[no-untyped-def] """Query the collection with a specific text and return up to n results.""" if not self.db_connection: raise Exception("Collection not initialized. Call connect() first.") @@ -71,11 +71,11 @@ def query(self, query_texts, n_results: int = 4, similarity_threshold=None): documents = self.db_connection.similarity_search(query=query_texts, k=n_results) return [(doc, None) for doc in documents] - def update_vector(self, vector_id, new_vector_data): + def update_vector(self, vector_id, new_vector_data): # type: ignore[no-untyped-def] # TODO - return super().connect() + return super().connect() # type: ignore[no-untyped-call, safe-super] - def delete_vector(self, vector_id): + def delete_vector(self, vector_id): # type: ignore[no-untyped-def] """Delete a vector from the ChromaDB using its identifier.""" if not self.db_connection: raise Exception("Collection not initialized. Call connect() first.") @@ -102,7 +102,7 @@ def __init__( self.dimensions = dimensions super().__init__(collection_name=collection_name) - def create(self): + def create(self): # type: ignore[no-untyped-def] """Connect to OpenAI API and create the ChromaDB client.""" # Get OpenAI key self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") @@ -110,14 +110,14 @@ def create(self): raise Exception("OpenAI key was not specified.") # Set embeddings - self.embeddings = OpenAIEmbeddings( + self.embeddings = OpenAIEmbeddings( # type: ignore[assignment] model=self.model, dimensions=self.dimensions, - api_key=self.OPENAI_API_KEY, + api_key=self.OPENAI_API_KEY, # type: ignore[arg-type] ) # Create the database - self.db_connection = Chroma( + self.db_connection = Chroma( # type: ignore[assignment] collection_name=self.collection_name, embedding_function=self.embeddings, collection_metadata={"hnsw:space": "cosine"}, @@ -145,29 +145,37 @@ def __init__( def create(self) -> None: """Create local embedding model and initialize the ChromaDB client.""" # Load the sentence transformer model - # Use CUDA if available, otherwise fall back to CPU - device = "cuda" if torch.cuda.is_available() else "cpu" - print(f"Using device: {device}") - self.model = SentenceTransformer(self.model_name, device=device) + + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): + self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self.device = "mps" + else: + self.device = "cpu" + + print(f"Using device: {self.device}") + self.model = SentenceTransformer(self.model_name, device=self.device) # type: ignore[name-defined] # Create a custom embedding class that implements the embed_query method class SentenceTransformerEmbeddings: - def __init__(self, model) -> None: + def __init__(self, model) -> None: # type: ignore[no-untyped-def] self.model = model - def embed_query(self, text: str): + def embed_query(self, text: str): # type: ignore[no-untyped-def] """Embed a single query text.""" return self.model.encode(text, normalize_embeddings=True).tolist() - def embed_documents(self, texts: Sequence[str]): + def embed_documents(self, texts: Sequence[str]): # type: ignore[no-untyped-def] """Embed multiple documents/texts.""" return self.model.encode(texts, normalize_embeddings=True).tolist() # Create an instance of our custom embeddings class - self.embeddings = SentenceTransformerEmbeddings(self.model) + self.embeddings = SentenceTransformerEmbeddings(self.model) # type: ignore[assignment] # Create the database - self.db_connection = Chroma( + self.db_connection = Chroma( # type: ignore[assignment] collection_name=self.collection_name, embedding_function=self.embeddings, collection_metadata={"hnsw:space": "cosine"}, diff --git a/dimos/agents/memory/image_embedding.py b/dimos/agents_deprecated/memory/image_embedding.py similarity index 86% rename from dimos/agents/memory/image_embedding.py rename to dimos/agents_deprecated/memory/image_embedding.py index 7b6dd88515..9c19dc4142 100644 --- a/dimos/agents/memory/image_embedding.py +++ b/dimos/agents_deprecated/memory/image_embedding.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import base64 import io import os +import sys import cv2 import numpy as np @@ -30,7 +31,7 @@ from dimos.utils.data import get_data from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.agents.memory.image_embedding") +logger = setup_logger() class ImageEmbeddingProvider: @@ -55,31 +56,41 @@ def __init__(self, model_name: str = "clip", dimensions: int = 512) -> None: self.processor = None self.model_path = None - self._initialize_model() + self._initialize_model() # type: ignore[no-untyped-call] logger.info(f"ImageEmbeddingProvider initialized with model {model_name}") - def _initialize_model(self): + def _initialize_model(self): # type: ignore[no-untyped-def] """Initialize the specified embedding model.""" try: - import onnxruntime as ort + import onnxruntime as ort # type: ignore[import-untyped] import torch - from transformers import AutoFeatureExtractor, AutoModel, CLIPProcessor + from transformers import ( # type: ignore[import-untyped] + AutoFeatureExtractor, + AutoModel, + CLIPProcessor, + ) if self.model_name == "clip": model_id = get_data("models_clip") / "model.onnx" - self.model_path = str(model_id) # Store for pickling + self.model_path = str(model_id) # type: ignore[assignment] # Store for pickling processor_id = "openai/clip-vit-base-patch32" providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] + if sys.platform == "darwin": + # 2025-11-17 12:36:47.877215 [W:onnxruntime:, helper.cc:82 IsInputSupported] CoreML does not support input dim > 16384. Input:text_model.embeddings.token_embedding.weight, shape: {49408,512} + # 2025-11-17 12:36:47.878496 [W:onnxruntime:, coreml_execution_provider.cc:107 GetCapability] CoreMLExecutionProvider::GetCapability, number of partitions supported by CoreML: 88 number of nodes in the graph: 1504 number of nodes supported by CoreML: 933 + providers = ["CoreMLExecutionProvider"] + [ + each for each in providers if each != "CUDAExecutionProvider" + ] self.model = ort.InferenceSession(str(model_id), providers=providers) - actual_providers = self.model.get_providers() + actual_providers = self.model.get_providers() # type: ignore[attr-defined] self.processor = CLIPProcessor.from_pretrained(processor_id) logger.info(f"Loaded CLIP model: {model_id} with providers: {actual_providers}") elif self.model_name == "resnet": - model_id = "microsoft/resnet-50" + model_id = "microsoft/resnet-50" # type: ignore[assignment] self.model = AutoModel.from_pretrained(model_id) self.processor = AutoFeatureExtractor.from_pretrained(model_id) logger.info(f"Loaded ResNet model: {model_id}") @@ -93,7 +104,7 @@ def _initialize_model(self): self.processor = None raise - def get_embedding(self, image: np.ndarray | str | bytes) -> np.ndarray: + def get_embedding(self, image: np.ndarray | str | bytes) -> np.ndarray: # type: ignore[type-arg] """ Generate an embedding vector for the provided image. @@ -166,7 +177,7 @@ def get_embedding(self, image: np.ndarray | str | bytes) -> np.ndarray: logger.error(f"Error generating embedding: {e}") return np.random.randn(self.dimensions).astype(np.float32) - def get_text_embedding(self, text: str) -> np.ndarray: + def get_text_embedding(self, text: str) -> np.ndarray: # type: ignore[type-arg] """ Generate an embedding vector for the provided text. @@ -233,7 +244,7 @@ def get_text_embedding(self, text: str) -> np.ndarray: logger.error(f"Error generating text embedding: {e}") return np.random.randn(self.dimensions).astype(np.float32) - def _prepare_image(self, image: np.ndarray | str | bytes) -> Image.Image: + def _prepare_image(self, image: np.ndarray | str | bytes) -> Image.Image: # type: ignore[type-arg] """ Convert the input image to PIL format required by the models. diff --git a/dimos/agents/memory/spatial_vector_db.py b/dimos/agents_deprecated/memory/spatial_vector_db.py similarity index 92% rename from dimos/agents/memory/spatial_vector_db.py rename to dimos/agents_deprecated/memory/spatial_vector_db.py index ac5dcc026a..0c8774cd95 100644 --- a/dimos/agents/memory/spatial_vector_db.py +++ b/dimos/agents_deprecated/memory/spatial_vector_db.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,11 +24,11 @@ import chromadb import numpy as np -from dimos.agents.memory.visual_memory import VisualMemory +from dimos.agents_deprecated.memory.visual_memory import VisualMemory from dimos.types.robot_location import RobotLocation from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.agents.memory.spatial_vector_db") +logger = setup_logger() class SpatialVectorDB: @@ -39,7 +39,7 @@ class SpatialVectorDB: their absolute locations and querying by location, text, or image cosine semantic similarity. """ - def __init__( + def __init__( # type: ignore[no-untyped-def] self, collection_name: str = "spatial_memory", chroma_client=None, @@ -109,7 +109,11 @@ def __init__( ) def add_image_vector( - self, vector_id: str, image: np.ndarray, embedding: np.ndarray, metadata: dict[str, Any] + self, + vector_id: str, + image: np.ndarray, # type: ignore[type-arg] + embedding: np.ndarray, # type: ignore[type-arg] + metadata: dict[str, Any], ) -> None: """ Add an image with its embedding and metadata to the vector database. @@ -130,7 +134,7 @@ def add_image_vector( logger.info(f"Added image vector {vector_id} with metadata: {metadata}") - def query_by_embedding(self, embedding: np.ndarray, limit: int = 5) -> list[dict]: + def query_by_embedding(self, embedding: np.ndarray, limit: int = 5) -> list[dict]: # type: ignore[type-arg] """ Query the vector database for images similar to the provided embedding. @@ -150,7 +154,7 @@ def query_by_embedding(self, embedding: np.ndarray, limit: int = 5) -> list[dict # TODO: implement efficient nearest neighbor search def query_by_location( self, x: float, y: float, radius: float = 2.0, limit: int = 5 - ) -> list[dict]: + ) -> list[dict]: # type: ignore[type-arg] """ Query the vector database for images near the specified location. @@ -168,9 +172,9 @@ def query_by_location( if not results or not results["ids"]: return [] - filtered_results = {"ids": [], "metadatas": [], "distances": []} + filtered_results = {"ids": [], "metadatas": [], "distances": []} # type: ignore[var-annotated] - for i, metadata in enumerate(results["metadatas"]): + for i, metadata in enumerate(results["metadatas"]): # type: ignore[arg-type] item_x = metadata.get("x") item_y = metadata.get("y") @@ -193,7 +197,7 @@ def query_by_location( return self._process_query_results(filtered_results) - def _process_query_results(self, results) -> list[dict]: + def _process_query_results(self, results) -> list[dict]: # type: ignore[no-untyped-def, type-arg] """Process query results to include decoded images.""" if not results or not results["ids"]: return [] @@ -228,7 +232,7 @@ def _process_query_results(self, results) -> list[dict]: return processed_results - def query_by_text(self, text: str, limit: int = 5) -> list[dict]: + def query_by_text(self, text: str, limit: int = 5) -> list[dict]: # type: ignore[type-arg] """ Query the vector database for images matching the provided text description. @@ -243,7 +247,7 @@ def query_by_text(self, text: str, limit: int = 5) -> list[dict]: List of results, each containing the image, its metadata, and similarity score """ if self.embedding_provider is None: - from dimos.agents.memory.image_embedding import ImageEmbeddingProvider + from dimos.agents_deprecated.memory.image_embedding import ImageEmbeddingProvider self.embedding_provider = ImageEmbeddingProvider(model_name="clip") @@ -283,7 +287,7 @@ def get_all_locations(self) -> list[tuple[float, float, float]]: return locations @property - def image_storage(self): + def image_storage(self): # type: ignore[no-untyped-def] """Legacy accessor for compatibility with existing code.""" return self.visual_memory.images @@ -320,10 +324,10 @@ def query_tagged_location(self, query: str) -> tuple[RobotLocation | None, float if not (results and results["ids"] and len(results["ids"][0]) > 0): return None, 0 - best_match_metadata = results["metadatas"][0][0] - distance = float(results["distances"][0][0] if "distances" in results else 0.0) + best_match_metadata = results["metadatas"][0][0] # type: ignore[index] + distance = float(results["distances"][0][0] if "distances" in results else 0.0) # type: ignore[index] - location = RobotLocation.from_vector_metadata(best_match_metadata) + location = RobotLocation.from_vector_metadata(best_match_metadata) # type: ignore[arg-type] logger.info( f"Found location '{location.name}' for query '{query}' (distance: {distance:.3f})" diff --git a/dimos/agents/memory/test_image_embedding.py b/dimos/agents_deprecated/memory/test_image_embedding.py similarity index 98% rename from dimos/agents/memory/test_image_embedding.py rename to dimos/agents_deprecated/memory/test_image_embedding.py index b1e7cabf09..3f2efbcc1a 100644 --- a/dimos/agents/memory/test_image_embedding.py +++ b/dimos/agents_deprecated/memory/test_image_embedding.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import pytest from reactivex import operators as ops -from dimos.agents.memory.image_embedding import ImageEmbeddingProvider +from dimos.agents_deprecated.memory.image_embedding import ImageEmbeddingProvider from dimos.stream.video_provider import VideoProvider diff --git a/dimos/agents/memory/visual_memory.py b/dimos/agents_deprecated/memory/visual_memory.py similarity index 94% rename from dimos/agents/memory/visual_memory.py rename to dimos/agents_deprecated/memory/visual_memory.py index 90f1272fef..98ad00e2fd 100644 --- a/dimos/agents/memory/visual_memory.py +++ b/dimos/agents_deprecated/memory/visual_memory.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.agents.memory.visual_memory") +logger = setup_logger() class VisualMemory: @@ -44,7 +44,7 @@ def __init__(self, output_dir: str | None = None) -> None: Args: output_dir: Directory to store the serialized image data """ - self.images = {} # Maps IDs to encoded images + self.images = {} # type: ignore[var-annotated] # Maps IDs to encoded images self.output_dir = output_dir if output_dir: @@ -53,7 +53,7 @@ def __init__(self, output_dir: str | None = None) -> None: else: logger.info("VisualMemory initialized with no persistence directory") - def add(self, image_id: str, image: np.ndarray) -> None: + def add(self, image_id: str, image: np.ndarray) -> None: # type: ignore[type-arg] """ Add an image to visual memory. @@ -74,7 +74,7 @@ def add(self, image_id: str, image: np.ndarray) -> None: self.images[image_id] = b64_encoded logger.debug(f"Added image {image_id} to visual memory") - def get(self, image_id: str) -> np.ndarray | None: + def get(self, image_id: str) -> np.ndarray | None: # type: ignore[type-arg] """ Retrieve an image from visual memory. diff --git a/dimos/agents_deprecated/modules/__init__.py b/dimos/agents_deprecated/modules/__init__.py new file mode 100644 index 0000000000..99163d55d0 --- /dev/null +++ b/dimos/agents_deprecated/modules/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Agent modules for DimOS.""" diff --git a/dimos/agents_deprecated/modules/base.py b/dimos/agents_deprecated/modules/base.py new file mode 100644 index 0000000000..891edbe4bd --- /dev/null +++ b/dimos/agents_deprecated/modules/base.py @@ -0,0 +1,525 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base agent class with all features (non-module).""" + +import asyncio +from concurrent.futures import ThreadPoolExecutor +import json +from typing import Any + +from reactivex.subject import Subject + +from dimos.agents_deprecated.agent_message import AgentMessage +from dimos.agents_deprecated.agent_types import AgentResponse, ConversationHistory, ToolCall +from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory +from dimos.agents_deprecated.memory.chroma_impl import OpenAISemanticMemory +from dimos.skills.skills import AbstractSkill, SkillLibrary +from dimos.utils.logging_config import setup_logger + +try: + from .gateway import UnifiedGatewayClient +except ImportError: + from dimos.agents_deprecated.modules.gateway import UnifiedGatewayClient + +logger = setup_logger() + +# Vision-capable models +VISION_MODELS = { + "openai::gpt-4o", + "openai::gpt-4o-mini", + "openai::gpt-4-turbo", + "openai::gpt-4-vision-preview", + "anthropic::claude-3-haiku-20240307", + "anthropic::claude-3-sonnet-20241022", + "anthropic::claude-3-opus-20240229", + "anthropic::claude-3-5-sonnet-20241022", + "anthropic::claude-3-5-haiku-latest", + "qwen::qwen-vl-plus", + "qwen::qwen-vl-max", +} + + +class BaseAgent: + """Base agent with all features including memory, skills, and multimodal support. + + This class provides: + - LLM gateway integration + - Conversation history + - Semantic memory (RAG) + - Skills/tools execution + - Multimodal support (text, images, data) + - Model capability detection + """ + + def __init__( # type: ignore[no-untyped-def] + self, + model: str = "openai::gpt-4o-mini", + system_prompt: str | None = None, + skills: SkillLibrary | list[AbstractSkill] | AbstractSkill | None = None, + memory: AbstractAgentSemanticMemory | None = None, + temperature: float = 0.0, + max_tokens: int = 4096, + max_input_tokens: int = 128000, + max_history: int = 20, + rag_n: int = 4, + rag_threshold: float = 0.45, + seed: int | None = None, + # Legacy compatibility + dev_name: str = "BaseAgent", + agent_type: str = "LLM", + **kwargs, + ) -> None: + """Initialize the base agent with all features. + + Args: + model: Model identifier (e.g., "openai::gpt-4o", "anthropic::claude-3-haiku") + system_prompt: System prompt for the agent + skills: Skills/tools available to the agent + memory: Semantic memory system for RAG + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + max_input_tokens: Maximum input tokens + max_history: Maximum conversation history to keep + rag_n: Number of RAG results to fetch + rag_threshold: Minimum similarity for RAG results + seed: Random seed for deterministic outputs (if supported by model) + dev_name: Device/agent name for logging + agent_type: Type of agent for logging + """ + self.model = model + self.system_prompt = system_prompt or "You are a helpful AI assistant." + self.temperature = temperature + self.max_tokens = max_tokens + self.max_input_tokens = max_input_tokens + self._max_history = max_history + self.rag_n = rag_n + self.rag_threshold = rag_threshold + self.seed = seed + self.dev_name = dev_name + self.agent_type = agent_type + + # Initialize skills + if skills is None: + self.skills = SkillLibrary() + elif isinstance(skills, SkillLibrary): + self.skills = skills + elif isinstance(skills, list): + self.skills = SkillLibrary() + for skill in skills: + self.skills.add(skill) + elif isinstance(skills, AbstractSkill): + self.skills = SkillLibrary() + self.skills.add(skills) + else: + self.skills = SkillLibrary() + + # Initialize memory - allow None for testing + if memory is False: # type: ignore[comparison-overlap] # Explicit False means no memory + self.memory = None + else: + self.memory = memory or OpenAISemanticMemory() # type: ignore[has-type] + + # Initialize gateway + self.gateway = UnifiedGatewayClient() + + # Conversation history with proper format management + self.conversation = ConversationHistory(max_size=self._max_history) + + # Thread pool for async operations + self._executor = ThreadPoolExecutor(max_workers=2) + + # Response subject for emitting responses + self.response_subject = Subject() # type: ignore[var-annotated] + + # Check model capabilities + self._supports_vision = self._check_vision_support() + + # Initialize memory with default context + self._initialize_memory() + + @property + def max_history(self) -> int: + """Get max history size.""" + return self._max_history + + @max_history.setter + def max_history(self, value: int) -> None: + """Set max history size and update conversation.""" + self._max_history = value + self.conversation.max_size = value + + def _check_vision_support(self) -> bool: + """Check if the model supports vision.""" + return self.model in VISION_MODELS + + def _initialize_memory(self) -> None: + """Initialize memory with default context.""" + try: + contexts = [ + ("ctx1", "I am an AI assistant that can help with various tasks."), + ("ctx2", f"I am using the {self.model} model."), + ( + "ctx3", + "I have access to tools and skills for specific operations." + if len(self.skills) > 0 + else "I do not have access to external tools.", + ), + ( + "ctx4", + "I can process images and visual content." + if self._supports_vision + else "I cannot process visual content.", + ), + ] + if self.memory: # type: ignore[has-type] + for doc_id, text in contexts: + self.memory.add_vector(doc_id, text) # type: ignore[has-type] + except Exception as e: + logger.warning(f"Failed to initialize memory: {e}") + + async def _process_query_async(self, agent_msg: AgentMessage) -> AgentResponse: + """Process query asynchronously and return AgentResponse.""" + query_text = agent_msg.get_combined_text() + logger.info(f"Processing query: {query_text}") + + # Get RAG context + rag_context = self._get_rag_context(query_text) + + # Check if trying to use images with non-vision model + if agent_msg.has_images() and not self._supports_vision: + logger.warning(f"Model {self.model} does not support vision. Ignoring image input.") + # Clear images from message + agent_msg.images.clear() + + # Build messages - pass AgentMessage directly + messages = self._build_messages(agent_msg, rag_context) + + # Get tools if available + tools = self.skills.get_tools() if len(self.skills) > 0 else None + + # Debug logging before gateway call + logger.debug("=== Gateway Request ===") + logger.debug(f"Model: {self.model}") + logger.debug(f"Number of messages: {len(messages)}") + for i, msg in enumerate(messages): + role = msg.get("role", "unknown") + content = msg.get("content", "") + if isinstance(content, str): + content_preview = content[:100] + elif isinstance(content, list): + content_preview = f"[{len(content)} content blocks]" + else: + content_preview = str(content)[:100] + logger.debug(f" Message {i}: role={role}, content={content_preview}...") + logger.debug(f"Tools available: {len(tools) if tools else 0}") + logger.debug("======================") + + # Prepare inference parameters + inference_params = { + "model": self.model, + "messages": messages, + "tools": tools, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + "stream": False, + } + + # Add seed if provided + if self.seed is not None: + inference_params["seed"] = self.seed + + # Make inference call + response = await self.gateway.ainference(**inference_params) # type: ignore[arg-type] + + # Extract response + message = response["choices"][0]["message"] # type: ignore[index] + content = message.get("content", "") + + # Don't update history yet - wait until we have the complete interaction + # This follows Claude's pattern of locking history until tool execution is complete + + # Check for tool calls + tool_calls = None + if message.get("tool_calls"): + tool_calls = [ + ToolCall( + id=tc["id"], + name=tc["function"]["name"], + arguments=json.loads(tc["function"]["arguments"]), + status="pending", + ) + for tc in message["tool_calls"] + ] + + # Get the user message for history + user_message = messages[-1] + + # Handle tool calls (blocking by default) + final_content = await self._handle_tool_calls(tool_calls, messages, user_message) + + # Return response with tool information + return AgentResponse( + content=final_content, + role="assistant", + tool_calls=tool_calls, + requires_follow_up=False, # Already handled + metadata={"model": self.model}, + ) + else: + # No tools, add both user and assistant messages to history + # Get the user message content from the built message + user_msg = messages[-1] # Last message in messages is the user message + user_content = user_msg["content"] + + # Add to conversation history + logger.info("=== Adding to history (no tools) ===") + logger.info(f" Adding user message: {str(user_content)[:100]}...") + self.conversation.add_user_message(user_content) + logger.info(f" Adding assistant response: {content[:100]}...") + self.conversation.add_assistant_message(content) + logger.info(f" History size now: {self.conversation.size()}") + + return AgentResponse( + content=content, + role="assistant", + tool_calls=None, + requires_follow_up=False, + metadata={"model": self.model}, + ) + + def _get_rag_context(self, query: str) -> str: + """Get relevant context from memory.""" + if not self.memory: # type: ignore[has-type] + return "" + + try: + results = self.memory.query( # type: ignore[has-type] + query_texts=query, n_results=self.rag_n, similarity_threshold=self.rag_threshold + ) + + if results: + contexts = [doc.page_content for doc, _ in results] + return " | ".join(contexts) + except Exception as e: + logger.warning(f"RAG query failed: {e}") + + return "" + + def _build_messages( + self, agent_msg: AgentMessage, rag_context: str = "" + ) -> list[dict[str, Any]]: + """Build messages list from AgentMessage.""" + messages = [] + + # System prompt with RAG context if available + system_content = self.system_prompt + if rag_context: + system_content += f"\n\nRelevant context: {rag_context}" + messages.append({"role": "system", "content": system_content}) + + # Add conversation history in OpenAI format + history_messages = self.conversation.to_openai_format() + messages.extend(history_messages) + + # Debug history state + logger.info(f"=== Building messages with {len(history_messages)} history messages ===") + if history_messages: + for i, msg in enumerate(history_messages): + role = msg.get("role", "unknown") + content = msg.get("content", "") + if isinstance(content, str): + preview = content[:100] + elif isinstance(content, list): + preview = f"[{len(content)} content blocks]" + else: + preview = str(content)[:100] + logger.info(f" History[{i}]: role={role}, content={preview}") + + # Build user message content from AgentMessage + user_content = agent_msg.get_combined_text() if agent_msg.has_text() else "" + + # Handle images for vision models + if agent_msg.has_images() and self._supports_vision: + # Build content array with text and images + content = [] + if user_content: # Only add text if not empty + content.append({"type": "text", "text": user_content}) + + # Add all images from AgentMessage + for img in agent_msg.images: + content.append( + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{img.base64_jpeg}"}, + } + ) + + logger.debug(f"Building message with {len(content)} content items (vision enabled)") + messages.append({"role": "user", "content": content}) # type: ignore[dict-item] + else: + # Text-only message + messages.append({"role": "user", "content": user_content}) + + return messages + + async def _handle_tool_calls( + self, + tool_calls: list[ToolCall], + messages: list[dict[str, Any]], + user_message: dict[str, Any], + ) -> str: + """Handle tool calls from LLM (blocking mode by default).""" + try: + # Build assistant message with tool calls + assistant_msg = { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": tc.id, + "type": "function", + "function": {"name": tc.name, "arguments": json.dumps(tc.arguments)}, + } + for tc in tool_calls + ], + } + messages.append(assistant_msg) + + # Execute tools and collect results + tool_results = [] + for tool_call in tool_calls: + logger.info(f"Executing tool: {tool_call.name}") + + try: + # Execute the tool + result = self.skills.call(tool_call.name, **tool_call.arguments) + tool_call.status = "completed" + + # Format tool result message + tool_result = { + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result), + "name": tool_call.name, + } + tool_results.append(tool_result) + + except Exception as e: + logger.error(f"Tool execution failed: {e}") + tool_call.status = "failed" + + # Add error result + tool_result = { + "role": "tool", + "tool_call_id": tool_call.id, + "content": f"Error: {e!s}", + "name": tool_call.name, + } + tool_results.append(tool_result) + + # Add tool results to messages + messages.extend(tool_results) + + # Prepare follow-up inference parameters + followup_params = { + "model": self.model, + "messages": messages, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + } + + # Add seed if provided + if self.seed is not None: + followup_params["seed"] = self.seed + + # Get follow-up response + response = await self.gateway.ainference(**followup_params) # type: ignore[arg-type] + + # Extract final response + final_message = response["choices"][0]["message"] # type: ignore[index] + + # Now add all messages to history in order (like Claude does) + # Add user message + user_content = user_message["content"] + self.conversation.add_user_message(user_content) + + # Add assistant message with tool calls + self.conversation.add_assistant_message("", tool_calls) + + # Add tool results + for result in tool_results: + self.conversation.add_tool_result( + tool_call_id=result["tool_call_id"], content=result["content"] + ) + + # Add final assistant response + final_content = final_message.get("content", "") + self.conversation.add_assistant_message(final_content) + + return final_message.get("content", "") # type: ignore[no-any-return] + + except Exception as e: + logger.error(f"Error handling tool calls: {e}") + return f"Error executing tools: {e!s}" + + def query(self, message: str | AgentMessage) -> AgentResponse: + """Synchronous query method for direct usage. + + Args: + message: Either a string query or an AgentMessage with text and/or images + + Returns: + AgentResponse object with content and tool information + """ + # Convert string to AgentMessage if needed + if isinstance(message, str): + agent_msg = AgentMessage() + agent_msg.add_text(message) + else: + agent_msg = message + + # Run async method in a new event loop + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(self._process_query_async(agent_msg)) + finally: + loop.close() + + async def aquery(self, message: str | AgentMessage) -> AgentResponse: + """Asynchronous query method. + + Args: + message: Either a string query or an AgentMessage with text and/or images + + Returns: + AgentResponse object with content and tool information + """ + # Convert string to AgentMessage if needed + if isinstance(message, str): + agent_msg = AgentMessage() + agent_msg.add_text(message) + else: + agent_msg = message + + return await self._process_query_async(agent_msg) + + def base_agent_dispose(self) -> None: + """Dispose of all resources and close gateway.""" + self.response_subject.on_completed() + if self._executor: + self._executor.shutdown(wait=False) + if self.gateway: + self.gateway.close() diff --git a/dimos/agents/modules/base_agent.py b/dimos/agents_deprecated/modules/base_agent.py similarity index 88% rename from dimos/agents/modules/base_agent.py rename to dimos/agents_deprecated/modules/base_agent.py index 0bceb1112e..efe81fd90b 100644 --- a/dimos/agents/modules/base_agent.py +++ b/dimos/agents_deprecated/modules/base_agent.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ import threading from typing import Any -from dimos.agents.agent_message import AgentMessage -from dimos.agents.agent_types import AgentResponse -from dimos.agents.memory.base import AbstractAgentSemanticMemory +from dimos.agents_deprecated.agent_message import AgentMessage +from dimos.agents_deprecated.agent_types import AgentResponse +from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory from dimos.core import In, Module, Out, rpc from dimos.skills.skills import AbstractSkill, SkillLibrary from dimos.utils.logging_config import setup_logger @@ -27,12 +27,12 @@ try: from .base import BaseAgent except ImportError: - from dimos.agents.modules.base import BaseAgent + from dimos.agents_deprecated.modules.base import BaseAgent -logger = setup_logger("dimos.agents.modules.base_agent") +logger = setup_logger() -class BaseAgentModule(BaseAgent, Module): +class BaseAgentModule(BaseAgent, Module): # type: ignore[misc] """Agent module that inherits from BaseAgent and adds DimOS module interface. This provides a thin wrapper around BaseAgent functionality, exposing it @@ -40,10 +40,10 @@ class BaseAgentModule(BaseAgent, Module): """ # Module I/O - AgentMessage based communication - message_in: In[AgentMessage] = None # Primary input for AgentMessage - response_out: Out[AgentResponse] = None # Output AgentResponse objects + message_in: In[AgentMessage] # Primary input for AgentMessage + response_out: Out[AgentResponse] # Output AgentResponse objects - def __init__( + def __init__( # type: ignore[no-untyped-def] self, model: str = "openai::gpt-4o-mini", system_prompt: str | None = None, @@ -98,7 +98,7 @@ def __init__( ) # Track module-specific subscriptions - self._module_disposables = [] + self._module_disposables = [] # type: ignore[var-annotated] # For legacy stream support self._latest_image = None @@ -115,7 +115,7 @@ def start(self) -> None: # Primary AgentMessage input if self.message_in and self.message_in.connection is not None: try: - disposable = self.message_in.observable().subscribe( + disposable = self.message_in.observable().subscribe( # type: ignore[no-untyped-call] lambda msg: self._handle_agent_message(msg) ) self._module_disposables.append(disposable) @@ -150,8 +150,8 @@ def stop(self) -> None: @rpc def clear_history(self) -> None: """Clear conversation history.""" - with self._history_lock: - self.history = [] + with self._history_lock: # type: ignore[attr-defined] + self.history = [] # type: ignore[var-annotated] logger.info("Conversation history cleared") @rpc @@ -169,7 +169,7 @@ def set_system_prompt(self, prompt: str) -> None: @rpc def get_conversation_history(self) -> list[dict[str, Any]]: """Get current conversation history.""" - with self._history_lock: + with self._history_lock: # type: ignore[attr-defined] return self.history.copy() def _handle_agent_message(self, message: AgentMessage) -> None: @@ -195,7 +195,7 @@ def _handle_module_query(self, query: str) -> None: def _update_latest_data(self, data: dict[str, Any]) -> None: """Update latest data context.""" with self._data_lock: - self._latest_data = data + self._latest_data = data # type: ignore[assignment] def _update_latest_image(self, img: Any) -> None: """Update latest image.""" diff --git a/dimos/agents_deprecated/modules/gateway/__init__.py b/dimos/agents_deprecated/modules/gateway/__init__.py new file mode 100644 index 0000000000..58ed40cd95 --- /dev/null +++ b/dimos/agents_deprecated/modules/gateway/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Gateway module for unified LLM access.""" + +from .client import UnifiedGatewayClient +from .utils import convert_tools_to_standard_format, parse_streaming_response + +__all__ = ["UnifiedGatewayClient", "convert_tools_to_standard_format", "parse_streaming_response"] diff --git a/dimos/agents/modules/gateway/client.py b/dimos/agents_deprecated/modules/gateway/client.py similarity index 90% rename from dimos/agents/modules/gateway/client.py rename to dimos/agents_deprecated/modules/gateway/client.py index 6d8abf5e14..6e3c6c6706 100644 --- a/dimos/agents/modules/gateway/client.py +++ b/dimos/agents_deprecated/modules/gateway/client.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -64,25 +64,25 @@ def __init__( def _get_client(self) -> httpx.Client: """Get or create sync HTTP client.""" if self._client is None: - self._client = httpx.Client( - base_url=self.gateway_url, + self._client = httpx.Client( # type: ignore[assignment] + base_url=self.gateway_url, # type: ignore[arg-type] timeout=self.timeout, headers={"Content-Type": "application/json"}, ) - return self._client + return self._client # type: ignore[return-value] def _get_async_client(self) -> httpx.AsyncClient: """Get or create async HTTP client.""" if self._async_client is None: - self._async_client = httpx.AsyncClient( - base_url=self.gateway_url, + self._async_client = httpx.AsyncClient( # type: ignore[assignment] + base_url=self.gateway_url, # type: ignore[arg-type] timeout=self.timeout, headers={"Content-Type": "application/json"}, ) - return self._async_client + return self._async_client # type: ignore[return-value] @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) - def inference( + def inference( # type: ignore[no-untyped-def] self, model: str, messages: list[dict[str, Any]], @@ -117,7 +117,7 @@ def inference( ) @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) - async def ainference( + async def ainference( # type: ignore[no-untyped-def] self, model: str, messages: list[dict[str, Any]], @@ -184,7 +184,7 @@ def __del__(self) -> None: # No event loop, just let it be garbage collected pass - def __enter__(self): + def __enter__(self): # type: ignore[no-untyped-def] """Context manager entry.""" return self @@ -197,7 +197,7 @@ def __exit__( """Context manager exit.""" self.close() - async def __aenter__(self): + async def __aenter__(self): # type: ignore[no-untyped-def] """Async context manager entry.""" return self diff --git a/dimos/agents/modules/gateway/tensorzero_embedded.py b/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py similarity index 87% rename from dimos/agents/modules/gateway/tensorzero_embedded.py rename to dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py index 90d30fe82d..4708788241 100644 --- a/dimos/agents/modules/gateway/tensorzero_embedded.py +++ b/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,13 +30,13 @@ def __init__(self) -> None: self._client = None self._config_path = None self._setup_config() - self._initialize_client() + self._initialize_client() # type: ignore[no-untyped-call] def _setup_config(self) -> None: """Create TensorZero configuration with correct format.""" config_dir = Path("/tmp/tensorzero_embedded") config_dir.mkdir(exist_ok=True) - self._config_path = config_dir / "tensorzero.toml" + self._config_path = config_dir / "tensorzero.toml" # type: ignore[assignment] # Create config using the correct format from working example config_content = """ @@ -143,18 +143,18 @@ def _setup_config(self) -> None: weight = 0.4 """ - with open(self._config_path, "w") as f: + with open(self._config_path, "w") as f: # type: ignore[call-overload] f.write(config_content) logger.info(f"Created TensorZero config at {self._config_path}") - def _initialize_client(self): + def _initialize_client(self): # type: ignore[no-untyped-def] """Initialize OpenAI client with TensorZero patch.""" try: from openai import OpenAI from tensorzero import patch_openai_client - self._client = OpenAI() + self._client = OpenAI() # type: ignore[assignment] # Patch with TensorZero embedded gateway patch_openai_client( @@ -176,7 +176,7 @@ def _map_model_to_tensorzero(self, model: str) -> str: # based on input type and model capabilities automatically return "tensorzero::function_name::chat" - def inference( + def inference( # type: ignore[no-untyped-def] self, model: str, messages: list[dict[str, Any]], @@ -214,22 +214,22 @@ def inference( # Make the call through patched client if stream: # Return streaming iterator - stream_response = self._client.chat.completions.create(**params) + stream_response = self._client.chat.completions.create(**params) # type: ignore[attr-defined] - def stream_generator(): + def stream_generator(): # type: ignore[no-untyped-def] for chunk in stream_response: yield chunk.model_dump() - return stream_generator() + return stream_generator() # type: ignore[no-any-return, no-untyped-call] else: - response = self._client.chat.completions.create(**params) - return response.model_dump() + response = self._client.chat.completions.create(**params) # type: ignore[attr-defined] + return response.model_dump() # type: ignore[no-any-return] except Exception as e: logger.error(f"TensorZero inference failed: {e}") raise - async def ainference( + async def ainference( # type: ignore[no-untyped-def] self, model: str, messages: list[dict[str, Any]], @@ -246,7 +246,7 @@ async def ainference( if stream: # Create async generator from sync streaming - async def stream_generator(): + async def stream_generator(): # type: ignore[no-untyped-def] # Run sync streaming in executor sync_stream = await loop.run_in_executor( None, @@ -259,7 +259,7 @@ async def stream_generator(): for chunk in sync_stream: yield chunk - return stream_generator() + return stream_generator() # type: ignore[no-any-return, no-untyped-call] else: result = await loop.run_in_executor( None, @@ -267,7 +267,7 @@ async def stream_generator(): model, messages, tools, temperature, max_tokens, stream, **kwargs ), ) - return result + return result # type: ignore[return-value] def close(self) -> None: """Close the client.""" diff --git a/dimos/agents/modules/gateway/tensorzero_simple.py b/dimos/agents_deprecated/modules/gateway/tensorzero_simple.py similarity index 98% rename from dimos/agents/modules/gateway/tensorzero_simple.py rename to dimos/agents_deprecated/modules/gateway/tensorzero_simple.py index a2cc57e2fb..4c9dbe4e26 100644 --- a/dimos/agents/modules/gateway/tensorzero_simple.py +++ b/dimos/agents_deprecated/modules/gateway/tensorzero_simple.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/agents_deprecated/modules/gateway/utils.py b/dimos/agents_deprecated/modules/gateway/utils.py new file mode 100644 index 0000000000..526d3b9724 --- /dev/null +++ b/dimos/agents_deprecated/modules/gateway/utils.py @@ -0,0 +1,156 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for gateway operations.""" + +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +def convert_tools_to_standard_format(tools: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Convert DimOS tool format to standard format accepted by gateways. + + DimOS tools come from pydantic_function_tool and have this format: + { + "type": "function", + "function": { + "name": "tool_name", + "description": "tool description", + "parameters": { + "type": "object", + "properties": {...}, + "required": [...] + } + } + } + + We keep this format as it's already standard JSON Schema format. + """ + if not tools: + return [] + + # Tools are already in the correct format from pydantic_function_tool + return tools + + +def parse_streaming_response(chunk: dict[str, Any]) -> dict[str, Any]: + """Parse a streaming response chunk into a standard format. + + Args: + chunk: Raw chunk from the gateway + + Returns: + Parsed chunk with standard fields: + - type: "content" | "tool_call" | "error" | "done" + - content: The actual content (text for content type, tool info for tool_call) + - metadata: Additional information + """ + # Handle TensorZero streaming format + if "choices" in chunk: + # OpenAI-style format from TensorZero + choice = chunk["choices"][0] if chunk["choices"] else {} + delta = choice.get("delta", {}) + + if "content" in delta: + return { + "type": "content", + "content": delta["content"], + "metadata": {"index": choice.get("index", 0)}, + } + elif "tool_calls" in delta: + tool_calls = delta["tool_calls"] + if tool_calls: + tool_call = tool_calls[0] + return { + "type": "tool_call", + "content": { + "id": tool_call.get("id"), + "name": tool_call.get("function", {}).get("name"), + "arguments": tool_call.get("function", {}).get("arguments", ""), + }, + "metadata": {"index": tool_call.get("index", 0)}, + } + elif choice.get("finish_reason"): + return { + "type": "done", + "content": None, + "metadata": {"finish_reason": choice["finish_reason"]}, + } + + # Handle direct content chunks + if isinstance(chunk, str): + return {"type": "content", "content": chunk, "metadata": {}} + + # Handle error responses + if "error" in chunk: + return {"type": "error", "content": chunk["error"], "metadata": chunk} + + # Default fallback + return {"type": "unknown", "content": chunk, "metadata": {}} + + +def create_tool_response(tool_id: str, result: Any, is_error: bool = False) -> dict[str, Any]: + """Create a properly formatted tool response. + + Args: + tool_id: The ID of the tool call + result: The result from executing the tool + is_error: Whether this is an error response + + Returns: + Formatted tool response message + """ + content = str(result) if not isinstance(result, str) else result + + return { + "role": "tool", + "tool_call_id": tool_id, + "content": content, + "name": None, # Will be filled by the calling code + } + + +def extract_image_from_message(message: dict[str, Any]) -> dict[str, Any] | None: + """Extract image data from a message if present. + + Args: + message: Message dict that may contain image data + + Returns: + Dict with image data and metadata, or None if no image + """ + content = message.get("content", []) + + # Handle list content (multimodal) + if isinstance(content, list): + for item in content: + if isinstance(item, dict): + # OpenAI format + if item.get("type") == "image_url": + return { + "format": "openai", + "data": item["image_url"]["url"], + "detail": item["image_url"].get("detail", "auto"), + } + # Anthropic format + elif item.get("type") == "image": + return { + "format": "anthropic", + "data": item["source"]["data"], + "media_type": item["source"].get("media_type", "image/jpeg"), + } + + return None diff --git a/dimos/agents/tokenizer/__init__.py b/dimos/agents_deprecated/prompt_builder/__init__.py similarity index 100% rename from dimos/agents/tokenizer/__init__.py rename to dimos/agents_deprecated/prompt_builder/__init__.py diff --git a/dimos/agents/prompt_builder/impl.py b/dimos/agents_deprecated/prompt_builder/impl.py similarity index 94% rename from dimos/agents/prompt_builder/impl.py rename to dimos/agents_deprecated/prompt_builder/impl.py index 9cd532fea9..35c864062a 100644 --- a/dimos/agents/prompt_builder/impl.py +++ b/dimos/agents_deprecated/prompt_builder/impl.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ from textwrap import dedent -from dimos.agents.tokenizer.base import AbstractTokenizer -from dimos.agents.tokenizer.openai_tokenizer import OpenAITokenizer +from dimos.agents_deprecated.tokenizer.base import AbstractTokenizer +from dimos.agents_deprecated.tokenizer.openai_tokenizer import OpenAITokenizer # TODO: Make class more generic when implementing other tokenizers. Presently its OpenAI specific. # TODO: Build out testing and logging @@ -55,7 +55,7 @@ def __init__( self.max_tokens = max_tokens self.tokenizer: AbstractTokenizer = tokenizer or OpenAITokenizer(model_name=self.model_name) - def truncate_tokens(self, text: str, max_tokens, strategy): + def truncate_tokens(self, text: str, max_tokens, strategy): # type: ignore[no-untyped-def] """ Truncate text to fit within max_tokens using a specified strategy. Args: @@ -82,9 +82,9 @@ def truncate_tokens(self, text: str, max_tokens, strategy): else: raise ValueError(f"Unknown truncation strategy: {strategy}") - return self.tokenizer.detokenize_text(truncated) + return self.tokenizer.detokenize_text(truncated) # type: ignore[no-untyped-call] - def build( + def build( # type: ignore[no-untyped-def] self, system_prompt=None, user_query=None, @@ -207,7 +207,7 @@ def build( user_content.append( { "type": "image_url", - "image_url": { + "image_url": { # type: ignore[dict-item] "url": f"data:image/jpeg;base64,{base64_image}", "detail": image_detail, }, diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/__init__.py b/dimos/agents_deprecated/tokenizer/__init__.py similarity index 100% rename from dimos/models/Detic/third_party/CenterNet2/tools/__init__.py rename to dimos/agents_deprecated/tokenizer/__init__.py diff --git a/dimos/agents_deprecated/tokenizer/base.py b/dimos/agents_deprecated/tokenizer/base.py new file mode 100644 index 0000000000..97535bcfaa --- /dev/null +++ b/dimos/agents_deprecated/tokenizer/base.py @@ -0,0 +1,37 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod + +# TODO: Add a class for specific tokenizer exceptions +# TODO: Build out testing and logging +# TODO: Create proper doc strings after multiple tokenizers are implemented + + +class AbstractTokenizer(ABC): + @abstractmethod + def tokenize_text(self, text: str): # type: ignore[no-untyped-def] + pass + + @abstractmethod + def detokenize_text(self, tokenized_text): # type: ignore[no-untyped-def] + pass + + @abstractmethod + def token_count(self, text: str): # type: ignore[no-untyped-def] + pass + + @abstractmethod + def image_token_count(self, image_width, image_height, image_detail: str = "low"): # type: ignore[no-untyped-def] + pass diff --git a/dimos/agents/tokenizer/huggingface_tokenizer.py b/dimos/agents_deprecated/tokenizer/huggingface_tokenizer.py similarity index 85% rename from dimos/agents/tokenizer/huggingface_tokenizer.py rename to dimos/agents_deprecated/tokenizer/huggingface_tokenizer.py index 34ace64fb0..ad7d27dc82 100644 --- a/dimos/agents/tokenizer/huggingface_tokenizer.py +++ b/dimos/agents_deprecated/tokenizer/huggingface_tokenizer.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from transformers import AutoTokenizer +from transformers import AutoTokenizer # type: ignore[import-untyped] -from dimos.agents.tokenizer.base import AbstractTokenizer +from dimos.agents_deprecated.tokenizer.base import AbstractTokenizer from dimos.utils.logging_config import setup_logger class HuggingFaceTokenizer(AbstractTokenizer): - def __init__(self, model_name: str = "Qwen/Qwen2.5-0.5B", **kwargs) -> None: + def __init__(self, model_name: str = "Qwen/Qwen2.5-0.5B", **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) # Initilize the tokenizer for the huggingface models @@ -31,13 +31,13 @@ def __init__(self, model_name: str = "Qwen/Qwen2.5-0.5B", **kwargs) -> None: f"Failed to initialize tokenizer for model {self.model_name}. Error: {e!s}" ) - def tokenize_text(self, text: str): + def tokenize_text(self, text: str): # type: ignore[no-untyped-def] """ Tokenize a text string using the openai tokenizer. """ return self.tokenizer.encode(text) - def detokenize_text(self, tokenized_text): + def detokenize_text(self, tokenized_text): # type: ignore[no-untyped-def] """ Detokenize a text string using the openai tokenizer. """ @@ -46,18 +46,18 @@ def detokenize_text(self, tokenized_text): except Exception as e: raise ValueError(f"Failed to detokenize text. Error: {e!s}") - def token_count(self, text: str): + def token_count(self, text: str): # type: ignore[no-untyped-def] """ Gets the token count of a text string using the openai tokenizer. """ return len(self.tokenize_text(text)) if text else 0 @staticmethod - def image_token_count(image_width, image_height, image_detail: str = "high"): + def image_token_count(image_width, image_height, image_detail: str = "high"): # type: ignore[no-untyped-def] """ Calculate the number of tokens in an image. Low detail is 85 tokens, high detail is 170 tokens per 512x512 square. """ - logger = setup_logger("dimos.agents.tokenizer.HuggingFaceTokenizer.image_token_count") + logger = setup_logger() if image_detail == "low": return 85 diff --git a/dimos/agents/tokenizer/openai_tokenizer.py b/dimos/agents_deprecated/tokenizer/openai_tokenizer.py similarity index 87% rename from dimos/agents/tokenizer/openai_tokenizer.py rename to dimos/agents_deprecated/tokenizer/openai_tokenizer.py index 7fe5017241..876e5ca881 100644 --- a/dimos/agents/tokenizer/openai_tokenizer.py +++ b/dimos/agents_deprecated/tokenizer/openai_tokenizer.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,12 +14,12 @@ import tiktoken -from dimos.agents.tokenizer.base import AbstractTokenizer +from dimos.agents_deprecated.tokenizer.base import AbstractTokenizer from dimos.utils.logging_config import setup_logger class OpenAITokenizer(AbstractTokenizer): - def __init__(self, model_name: str = "gpt-4o", **kwargs) -> None: + def __init__(self, model_name: str = "gpt-4o", **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) # Initilize the tokenizer for the openai set of models @@ -31,13 +31,13 @@ def __init__(self, model_name: str = "gpt-4o", **kwargs) -> None: f"Failed to initialize tokenizer for model {self.model_name}. Error: {e!s}" ) - def tokenize_text(self, text: str): + def tokenize_text(self, text: str): # type: ignore[no-untyped-def] """ Tokenize a text string using the openai tokenizer. """ return self.tokenizer.encode(text) - def detokenize_text(self, tokenized_text): + def detokenize_text(self, tokenized_text): # type: ignore[no-untyped-def] """ Detokenize a text string using the openai tokenizer. """ @@ -46,18 +46,18 @@ def detokenize_text(self, tokenized_text): except Exception as e: raise ValueError(f"Failed to detokenize text. Error: {e!s}") - def token_count(self, text: str): + def token_count(self, text: str): # type: ignore[no-untyped-def] """ Gets the token count of a text string using the openai tokenizer. """ return len(self.tokenize_text(text)) if text else 0 @staticmethod - def image_token_count(image_width, image_height, image_detail: str = "high"): + def image_token_count(image_width, image_height, image_detail: str = "high"): # type: ignore[no-untyped-def] """ Calculate the number of tokens in an image. Low detail is 85 tokens, high detail is 170 tokens per 512x512 square. """ - logger = setup_logger("dimos.agents.tokenizer.openai.image_token_count") + logger = setup_logger() if image_detail == "low": return 85 diff --git a/dimos/conftest.py b/dimos/conftest.py index e1d0c96e42..e0544bea1c 100644 --- a/dimos/conftest.py +++ b/dimos/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -33,6 +33,17 @@ def event_loop(): _skip_for = ["lcm", "heavy", "ros"] +@pytest.fixture(scope="module") +def dimos_cluster(): + from dimos.core import start + + dimos = start(4) + try: + yield dimos + finally: + dimos.stop() + + @pytest.hookimpl() def pytest_sessionfinish(session): """Track threads that exist at session start - these are not leaks.""" @@ -85,9 +96,14 @@ def monitor_threads(request): t for t in threading.enumerate() if t.ident in new_thread_ids and t.name != "MainThread" ] - # Filter out expected persistent threads from Dask that are shared globally + # Filter out expected persistent threads that are shared globally # These threads are intentionally left running and cleaned up on process exit - expected_persistent_thread_prefixes = ["Dask-Offload"] + expected_persistent_thread_prefixes = [ + "Dask-Offload", + # HuggingFace safetensors conversion thread - no user cleanup API + # https://github.com/huggingface/transformers/issues/29513 + "Thread-auto_conversion", + ] new_threads = [ t for t in new_threads diff --git a/dimos/constants.py b/dimos/constants.py index 17273b6dd3..4e74ccbe1b 100644 --- a/dimos/constants.py +++ b/dimos/constants.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ DIMOS_PROJECT_ROOT = Path(__file__).parent.parent +DIMOS_LOG_DIR = DIMOS_PROJECT_ROOT / "logs" + """ Constants for shared memory Usually, auto-detection for size would be preferred. Sadly, though, channels are made diff --git a/dimos/core/README_BLUEPRINTS.md b/dimos/core/README_BLUEPRINTS.md index 26143bd456..0a3e2ceaf5 100644 --- a/dimos/core/README_BLUEPRINTS.md +++ b/dimos/core/README_BLUEPRINTS.md @@ -66,12 +66,12 @@ Imagine you have this code: ```python class ModuleA(Module): - image: Out[Image] = None - start_explore: Out[Bool] = None + image: Out[Image] + start_explore: Out[Bool] class ModuleB(Module): - image: In[Image] = None - begin_explore: In[Bool] = None + image: In[Image] + begin_explore: In[Bool] module_a = partial(create_module_blueprint, ModuleA) module_b = partial(create_module_blueprint, ModuleB) @@ -114,10 +114,10 @@ Sometimes you need to rename a connection to match what other modules expect. Yo ```python class ConnectionModule(Module): - color_image: Out[Image] = None # Outputs on 'color_image' + color_image: Out[Image] # Outputs on 'color_image' class ProcessingModule(Module): - rgb_image: In[Image] = None # Expects input on 'rgb_image' + rgb_image: In[Image] # Expects input on 'rgb_image' # Without remapping, these wouldn't connect automatically # With remapping, color_image is renamed to rgb_image @@ -184,15 +184,32 @@ class ModuleB(Module): And you want to call `ModuleA.get_time` in `ModuleB.request_the_time`. -You can do so by defining a method like `set__`. It will be called with an `RpcCall` that will call the original `ModuleA.get_time`. So you can write this: +To do this, you can request a link to the method you want to call in `rpc_calls`. Calling `get_time_rcp` will call the original `ModuleA.get_time`. ```python -class ModuleA(Module): +class ModuleB(Module): + rpc_calls: list[str] = [ + "ModuleA.get_time", + ] - @rpc - def get_time(self) -> str: - ... + def request_the_time(self) -> None: + get_time_rpc = self.get_rpc_calls("ModuleA.get_time") + print(get_time_rpc()) +``` + +You can also request multiple methods at a time: + +```python +method1_rpc, method2_rpc = self.get_rpc_calls("ModuleX.m1", "ModuleX.m2") +``` + +## Alternative RPC calls + +There is an alternative way of receiving RPC methods. It is useful when you want to perform an action at the time you receive the RPC methods. + +You can use it by defining a method like `set__`: +```python class ModuleB(Module): @rpc # Note that it has to be an rpc method. def set_ModuleA_get_time(self, rpc_call: RpcCall) -> None: @@ -205,9 +222,51 @@ class ModuleB(Module): Note that `RpcCall.rpc` does not serialize, so you have to set it to the one from the module with `rpc_call.set_rpc(self.rpc)` +## Calling an interface + +In the previous examples, you can only call methods in a module called `ModuleA`. But what if you want to deploy an alternative module in your blueprint? + +You can do so by extracting the common interface as an `ABC` (abstract base class) and linking to the `ABC` instead one particular class. + +```python +class TimeInterface(ABC): + @abstractmethod + def get_time(self): ... + +class ProperTime(TimeInterface): + def get_time(self): + return "13:00" + +class BadTime(TimeInterface): + def get_time(self): + return "01:00 PM" + + +class ModuleB(Module): + rpc_calls: list[str] = [ + "TimeInterface.get_time", # TimeInterface instead of ProperTime or BadTime + ] + + def request_the_time(self) -> None: + get_time_rpc = self.get_rpc_calls("TimeInterface.get_time") + print(get_time_rpc()) +``` + +The actual method that you get in `get_time_rpc` depends on which module is deployed. If you deploy `ProperTime`, you get `ProperTime.get_time`: + +```python +blueprint = autoconnect( + ProperTime.blueprint(), + # get_rpc_calls("TimeInterface.get_time") returns ProperTime.get_time + ModuleB.blueprint(), +) +``` + +If both are deployed, the blueprint will throw an error because it's ambiguous. + ## Defining skills -Skills have to be registered with `LlmAgent.register_skills(self)`. +Skills have to be registered with `AgentSpec.register_skills(self)`. ```python class SomeSkill(Module): @@ -217,7 +276,7 @@ class SomeSkill(Module): ... @rpc - def set_LlmAgent_register_skills(self, register_skills: RpcCall) -> None: + def set_AgentSpec_register_skills(self, register_skills: RpcCall) -> None: register_skills.set_rpc(self.rpc) register_skills(RPCClient(self, self.__class__)) diff --git a/dimos/core/__init__.py b/dimos/core/__init__.py index 641d8a24a5..25d4f7a6e5 100644 --- a/dimos/core/__init__.py +++ b/dimos/core/__init__.py @@ -2,14 +2,14 @@ import multiprocessing as mp import signal -from typing import Optional +import time from dask.distributed import Client, LocalCluster from rich.console import Console import dimos.core.colors as colors from dimos.core.core import rpc -from dimos.core.module import Module, ModuleBase, ModuleConfig +from dimos.core.module import Module, ModuleBase, ModuleConfig, ModuleConfigT from dimos.core.rpc_client import RPCClient from dimos.core.stream import In, Out, RemoteIn, RemoteOut, Transport from dimos.core.transport import ( @@ -19,10 +19,13 @@ pLCMTransport, pSHMTransport, ) -from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.protocol.rpc import LCMRPC from dimos.protocol.rpc.spec import RPCSpec from dimos.protocol.tf import LCMTF, TF, PubSubTF, TFConfig, TFSpec from dimos.utils.actor_registry import ActorRegistry +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() __all__ = [ "LCMRPC", @@ -34,6 +37,7 @@ "Module", "ModuleBase", "ModuleConfig", + "ModuleConfigT", "Out", "PubSubTF", "RPCSpec", @@ -54,17 +58,17 @@ class CudaCleanupPlugin: """Dask worker plugin to cleanup CUDA resources on shutdown.""" - def setup(self, worker) -> None: + def setup(self, worker) -> None: # type: ignore[no-untyped-def] """Called when worker starts.""" pass - def teardown(self, worker) -> None: + def teardown(self, worker) -> None: # type: ignore[no-untyped-def] """Clean up CUDA resources when worker shuts down.""" try: import sys if "cupy" in sys.modules: - import cupy as cp + import cupy as cp # type: ignore[import-not-found] # Clear memory pools mempool = cp.get_default_memory_pool() @@ -78,34 +82,33 @@ def teardown(self, worker) -> None: pass -def patch_actor(actor, cls) -> None: ... +def patch_actor(actor, cls) -> None: ... # type: ignore[no-untyped-def] DimosCluster = Client def patchdask(dask_client: Client, local_cluster: LocalCluster) -> DimosCluster: - def deploy( + def deploy( # type: ignore[no-untyped-def] actor_class, *args, **kwargs, ): - console = Console() - with console.status(f"deploying [green]{actor_class.__name__}", spinner="arc"): - actor = dask_client.submit( - actor_class, - *args, - **kwargs, - actor=True, - ).result() + logger.info("Deploying module.", module=actor_class.__name__) + actor = dask_client.submit( # type: ignore[no-untyped-call] + actor_class, + *args, + **kwargs, + actor=True, + ).result() - worker = actor.set_ref(actor).result() - print(f"deployed: {colors.blue(actor)} @ {colors.orange('worker ' + str(worker))}") + worker = actor.set_ref(actor).result() + logger.info("Deployed module.", module=actor._cls.__name__, worker_id=worker) - # Register actor deployment in shared memory - ActorRegistry.update(str(actor), str(worker)) + # Register actor deployment in shared memory + ActorRegistry.update(str(actor), str(worker)) - return RPCClient(actor, actor_class) + return RPCClient(actor, actor_class) def check_worker_memory() -> None: """Check memory usage of all workers.""" @@ -164,9 +167,7 @@ def close_all() -> None: # Prevents multiple calls to close_all if hasattr(dask_client, "_closed") and dask_client._closed: return - dask_client._closed = True - - import time + dask_client._closed = True # type: ignore[attr-defined] # Stop all SharedMemory transports before closing Dask # This prevents the "leaked shared_memory objects" warning and hangs @@ -198,7 +199,7 @@ def close_all() -> None: pass try: - dask_client.close(timeout=5) + dask_client.close(timeout=5) # type: ignore[no-untyped-call] except Exception: pass @@ -219,19 +220,22 @@ def close_all() -> None: # This is needed, solves race condition in CI thread check time.sleep(0.1) - dask_client.deploy = deploy - dask_client.check_worker_memory = check_worker_memory - dask_client.stop = lambda: dask_client.close() - dask_client.close_all = close_all + dask_client.deploy = deploy # type: ignore[attr-defined] + dask_client.check_worker_memory = check_worker_memory # type: ignore[attr-defined] + dask_client.stop = lambda: dask_client.close() # type: ignore[attr-defined, no-untyped-call] + dask_client.close_all = close_all # type: ignore[attr-defined] return dask_client -def start(n: int | None = None, memory_limit: str = "auto") -> Client: +def start(n: int | None = None, memory_limit: str = "auto") -> DimosCluster: """Start a Dask LocalCluster with specified workers and memory limits. Args: n: Number of workers (defaults to CPU count) memory_limit: Memory limit per worker (e.g., '4GB', '2GiB', or 'auto' for Dask's default) + + Returns: + DimosCluster: A patched Dask client with deploy(), check_worker_memory(), stop(), and close_all() methods """ console = Console() @@ -240,35 +244,35 @@ def start(n: int | None = None, memory_limit: str = "auto") -> Client: with console.status( f"[green]Initializing dimos local cluster with [bright_blue]{n} workers", spinner="arc" ): - cluster = LocalCluster( + cluster = LocalCluster( # type: ignore[no-untyped-call] n_workers=n, threads_per_worker=4, memory_limit=memory_limit, plugins=[CudaCleanupPlugin()], # Register CUDA cleanup plugin ) - client = Client(cluster) + client = Client(cluster) # type: ignore[no-untyped-call] console.print( f"[green]Initialized dimos local cluster with [bright_blue]{n} workers, memory limit: {memory_limit}" ) patched_client = patchdask(client, cluster) - patched_client._shutting_down = False + patched_client._shutting_down = False # type: ignore[attr-defined] # Signal handler with proper exit handling - def signal_handler(sig, frame) -> None: + def signal_handler(sig, frame) -> None: # type: ignore[no-untyped-def] # If already shutting down, force exit - if patched_client._shutting_down: + if patched_client._shutting_down: # type: ignore[attr-defined] import os console.print("[red]Force exit!") os._exit(1) - patched_client._shutting_down = True + patched_client._shutting_down = True # type: ignore[attr-defined] console.print(f"[yellow]Shutting down (signal {sig})...") try: - patched_client.close_all() + patched_client.close_all() # type: ignore[attr-defined] except Exception: pass @@ -280,3 +284,12 @@ def signal_handler(sig, frame) -> None: signal.signal(signal.SIGTERM, signal_handler) return patched_client + + +def wait_exit() -> None: + while True: + try: + time.sleep(1) + except KeyboardInterrupt: + print("exiting...") + return diff --git a/dimos/core/_test_future_annotations_helper.py b/dimos/core/_test_future_annotations_helper.py new file mode 100644 index 0000000000..08c5ec0063 --- /dev/null +++ b/dimos/core/_test_future_annotations_helper.py @@ -0,0 +1,36 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Helper module for testing blueprint handling with PEP 563 (future annotations). + +This file exists because `from __future__ import annotations` affects the entire file. +""" + +from __future__ import annotations + +from dimos.core.module import Module +from dimos.core.stream import In, Out # noqa + + +class FutureData: + pass + + +class FutureModuleOut(Module): + data: Out[FutureData] = None # type: ignore[assignment] + + +class FutureModuleIn(Module): + data: In[FutureData] = None # type: ignore[assignment] diff --git a/dimos/core/blueprints.py b/dimos/core/blueprints.py index fedb05769c..1fa51629bf 100644 --- a/dimos/core/blueprints.py +++ b/dimos/core/blueprints.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,14 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +from abc import ABC from collections import defaultdict -from collections.abc import Mapping +from collections.abc import Callable, Mapping from dataclasses import dataclass, field from functools import cached_property, reduce import inspect import operator +import sys from types import MappingProxyType -from typing import Any, Literal, get_args, get_origin +from typing import Any, Literal, get_args, get_origin, get_type_hints + +import rerun as rr +import rerun.blueprint as rrb from dimos.core.global_config import GlobalConfig from dimos.core.module import Module @@ -27,6 +32,9 @@ from dimos.core.stream import In, Out from dimos.core.transport import LCMTransport, pLCMTransport from dimos.utils.generic import short_id +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() @dataclass(frozen=True) @@ -55,6 +63,7 @@ class ModuleBlueprintSet: remapping_map: Mapping[tuple[type[Module], str], str] = field( default_factory=lambda: MappingProxyType({}) ) + requirement_checks: tuple[Callable[[], str | None], ...] = field(default_factory=tuple) def transports(self, transports: dict[tuple[str, type], Any]) -> "ModuleBlueprintSet": return ModuleBlueprintSet( @@ -62,6 +71,7 @@ def transports(self, transports: dict[tuple[str, type], Any]) -> "ModuleBlueprin transport_map=MappingProxyType({**self.transport_map, **transports}), global_config_overrides=self.global_config_overrides, remapping_map=self.remapping_map, + requirement_checks=self.requirement_checks, ) def global_config(self, **kwargs: Any) -> "ModuleBlueprintSet": @@ -70,6 +80,7 @@ def global_config(self, **kwargs: Any) -> "ModuleBlueprintSet": transport_map=self.transport_map, global_config_overrides=MappingProxyType({**self.global_config_overrides, **kwargs}), remapping_map=self.remapping_map, + requirement_checks=self.requirement_checks, ) def remappings(self, remappings: list[tuple[type[Module], str, str]]) -> "ModuleBlueprintSet": @@ -82,8 +93,37 @@ def remappings(self, remappings: list[tuple[type[Module], str, str]]) -> "Module transport_map=self.transport_map, global_config_overrides=self.global_config_overrides, remapping_map=MappingProxyType(remappings_dict), + requirement_checks=self.requirement_checks, ) + def requirements(self, *checks: Callable[[], str | None]) -> "ModuleBlueprintSet": + return ModuleBlueprintSet( + blueprints=self.blueprints, + transport_map=self.transport_map, + global_config_overrides=self.global_config_overrides, + remapping_map=self.remapping_map, + requirement_checks=self.requirement_checks + tuple(checks), + ) + + def _check_ambiguity( + self, + requested_method_name: str, + interface_methods: Mapping[str, list[tuple[type[Module], Callable[..., Any]]]], + requesting_module: type[Module], + ) -> None: + if ( + requested_method_name in interface_methods + and len(interface_methods[requested_method_name]) > 1 + ): + modules_str = ", ".join( + impl[0].__name__ for impl in interface_methods[requested_method_name] + ) + raise ValueError( + f"Ambiguous RPC method '{requested_method_name}' requested by " + f"{requesting_module.__name__}. Multiple implementations found: " + f"{modules_str}. Please use a concrete class name instead." + ) + def _get_transport_for(self, name: str, type: type) -> Any: transport = self.transport_map.get((name, type), None) if transport: @@ -109,16 +149,59 @@ def _all_name_types(self) -> set[tuple[str, type]]: def _is_name_unique(self, name: str) -> bool: return sum(1 for n, _ in self._all_name_types if n == name) == 1 - def build(self, global_config: GlobalConfig | None = None) -> ModuleCoordinator: - if global_config is None: - global_config = GlobalConfig() - global_config = global_config.model_copy(update=self.global_config_overrides) + def _check_requirements(self) -> None: + errors = [] + red = "\033[31m" + reset = "\033[0m" - module_coordinator = ModuleCoordinator(global_config=global_config) + for check in self.requirement_checks: + error = check() + if error: + errors.append(error) - module_coordinator.start() + if errors: + for error in errors: + print(f"{red}Error: {error}{reset}", file=sys.stderr) + sys.exit(1) - # Deploy all modules. + def _verify_no_name_conflicts(self) -> None: + name_to_types = defaultdict(set) + name_to_modules = defaultdict(list) + + for blueprint in self.blueprints: + for conn in blueprint.connections: + connection_name = self.remapping_map.get((blueprint.module, conn.name), conn.name) + name_to_types[connection_name].add(conn.type) + name_to_modules[connection_name].append((blueprint.module, conn.type)) + + conflicts = {} + for conn_name, types in name_to_types.items(): + if len(types) > 1: + modules_by_type = defaultdict(list) + for module, conn_type in name_to_modules[conn_name]: + modules_by_type[conn_type].append(module) + conflicts[conn_name] = modules_by_type + + if not conflicts: + return + + error_lines = ["Blueprint cannot start because there are conflicting connections."] + for name, modules_by_type in conflicts.items(): + type_entries = [] + for conn_type, modules in modules_by_type.items(): + for module in modules: + type_str = f"{conn_type.__module__}.{conn_type.__name__}" + module_str = module.__name__ + type_entries.append((type_str, module_str)) + if len(type_entries) >= 2: + locations = ", ".join(f"{type_} in {module}" for type_, module in type_entries) + error_lines.append(f" - '{name}' has conflicting types. {locations}") + + raise ValueError("\n".join(error_lines)) + + def _deploy_all_modules( + self, module_coordinator: ModuleCoordinator, global_config: GlobalConfig + ) -> None: for blueprint in self.blueprints: kwargs = {**blueprint.kwargs} sig = inspect.signature(blueprint.module.__init__) @@ -126,10 +209,11 @@ def build(self, global_config: GlobalConfig | None = None) -> ModuleCoordinator: kwargs["global_config"] = global_config module_coordinator.deploy(blueprint.module, *blueprint.args, **kwargs) + def _connect_transports(self, module_coordinator: ModuleCoordinator) -> None: # Gather all the In/Out connections with remapping applied. connections = defaultdict(list) # Track original name -> remapped name for each module - module_conn_mapping = defaultdict(dict) + module_conn_mapping = defaultdict(dict) # type: ignore[var-annotated] for blueprint in self.blueprints: for conn in blueprint.connections: @@ -145,29 +229,172 @@ def build(self, global_config: GlobalConfig | None = None) -> ModuleCoordinator: transport = self._get_transport_for(remapped_name, type) for module, original_name in connections[(remapped_name, type)]: instance = module_coordinator.get_instance(module) - # Use the remote method to set transport on Dask actors - instance.set_transport(original_name, transport) - + instance.set_transport(original_name, transport) # type: ignore[union-attr] + logger.info( + "Transport", + name=remapped_name, + original_name=original_name, + topic=str(getattr(transport, "topic", None)), + type=f"{type.__module__}.{type.__qualname__}", + module=module.__name__, + transport=transport.__class__.__name__, + ) + + def _connect_rpc_methods(self, module_coordinator: ModuleCoordinator) -> None: # Gather all RPC methods. rpc_methods = {} + rpc_methods_dot = {} + + # Track interface methods to detect ambiguity. + interface_methods: defaultdict[str, list[tuple[type[Module], Callable[..., Any]]]] = ( + defaultdict(list) + ) # interface_name_method -> [(module_class, method)] + interface_methods_dot: defaultdict[str, list[tuple[type[Module], Callable[..., Any]]]] = ( + defaultdict(list) + ) # interface_name.method -> [(module_class, method)] + for blueprint in self.blueprints: - for method_name in blueprint.module.rpcs.keys(): + for method_name in blueprint.module.rpcs.keys(): # type: ignore[attr-defined] method = getattr(module_coordinator.get_instance(blueprint.module), method_name) + # Register under concrete class name (backward compatibility) rpc_methods[f"{blueprint.module.__name__}_{method_name}"] = method + rpc_methods_dot[f"{blueprint.module.__name__}.{method_name}"] = method + + # Also register under any interface names + for base in blueprint.module.mro(): + # Check if this base is an abstract interface with the method + if ( + base is not Module + and issubclass(base, ABC) + and hasattr(base, method_name) + and getattr(base, method_name, None) is not None + ): + interface_key = f"{base.__name__}.{method_name}" + interface_methods_dot[interface_key].append((blueprint.module, method)) + interface_key_underscore = f"{base.__name__}_{method_name}" + interface_methods[interface_key_underscore].append( + (blueprint.module, method) + ) + + # Check for ambiguity in interface methods and add non-ambiguous ones + for interface_key, implementations in interface_methods_dot.items(): + if len(implementations) == 1: + rpc_methods_dot[interface_key] = implementations[0][1] + for interface_key, implementations in interface_methods.items(): + if len(implementations) == 1: + rpc_methods[interface_key] = implementations[0][1] # Fulfil method requests (so modules can call each other). for blueprint in self.blueprints: - for method_name in blueprint.module.rpcs.keys(): + instance = module_coordinator.get_instance(blueprint.module) + + for method_name in blueprint.module.rpcs.keys(): # type: ignore[attr-defined] if not method_name.startswith("set_"): continue + linked_name = method_name.removeprefix("set_") + + self._check_ambiguity(linked_name, interface_methods, blueprint.module) + if linked_name not in rpc_methods: continue - instance = module_coordinator.get_instance(blueprint.module) + getattr(instance, method_name)(rpc_methods[linked_name]) + for requested_method_name in instance.get_rpc_method_names(): # type: ignore[union-attr] + self._check_ambiguity( + requested_method_name, interface_methods_dot, blueprint.module + ) + + if requested_method_name not in rpc_methods_dot: + continue + + instance.set_rpc_method( # type: ignore[union-attr] + requested_method_name, rpc_methods_dot[requested_method_name] + ) + + def _init_rerun_blueprint(self, module_coordinator: ModuleCoordinator) -> None: + """Compose and send Rerun blueprint from module contributions. + + Collects rerun_views() from all modules and composes them into a unified layout. + """ + # Collect view contributions from all modules + side_panels = [] + for blueprint in self.blueprints: + if hasattr(blueprint.module, "rerun_views"): + views = blueprint.module.rerun_views() + if views: + side_panels.extend(views) + + # Always include latency panel if we have any panels + if side_panels: + side_panels.append( + rrb.TimeSeriesView( + name="Latency (ms)", + origin="/metrics", + contents=[ + "+ /metrics/voxel_map/latency_ms", + "+ /metrics/costmap/latency_ms", + ], + ) + ) + + # Compose final layout + if side_panels: + composed_blueprint = rrb.Blueprint( + rrb.Horizontal( + rrb.Spatial3DView( + name="3D View", + origin="world", + background=[0, 0, 0], + ), + rrb.Vertical(*side_panels, row_shares=[2] + [1] * (len(side_panels) - 1)), + column_shares=[3, 1], + ), + rrb.TimePanel(state="collapsed"), + rrb.SelectionPanel(state="collapsed"), + rrb.BlueprintPanel(state="collapsed"), + ) + rr.send_blueprint(composed_blueprint) + + def build( + self, + global_config: GlobalConfig | None = None, + cli_config_overrides: Mapping[str, Any] | None = None, + ) -> ModuleCoordinator: + if global_config is None: + global_config = GlobalConfig() + global_config = global_config.model_copy(update=dict(self.global_config_overrides)) + if cli_config_overrides: + global_config = global_config.model_copy(update=dict(cli_config_overrides)) + + self._check_requirements() + self._verify_no_name_conflicts() + + # Initialize Rerun server before deploying modules (if backend is Rerun) + if global_config.rerun_enabled and global_config.viewer_backend.startswith("rerun"): + try: + from dimos.dashboard.rerun_init import init_rerun_server + + server_addr = init_rerun_server(viewer_mode=global_config.viewer_backend) + global_config = global_config.model_copy(update={"rerun_server_addr": server_addr}) + logger.info("Rerun server initialized", addr=server_addr) + except Exception as e: + logger.warning(f"Failed to initialize Rerun server: {e}") + + module_coordinator = ModuleCoordinator(global_config=global_config) + module_coordinator.start() + + self._deploy_all_modules(module_coordinator, global_config) + self._connect_transports(module_coordinator) + self._connect_rpc_methods(module_coordinator) + module_coordinator.start_all_modules() + # Compose and send Rerun blueprint from module contributions + if global_config.viewer_backend.startswith("rerun"): + self._init_rerun_blueprint(module_coordinator) + return module_coordinator @@ -176,10 +403,15 @@ def _make_module_blueprint( ) -> ModuleBlueprint: connections: list[ModuleConnection] = [] - all_annotations = {} - for base_class in reversed(module.__mro__): - if hasattr(base_class, "__annotations__"): - all_annotations.update(base_class.__annotations__) + # Use get_type_hints() to properly resolve string annotations. + try: + all_annotations = get_type_hints(module) + except Exception: + # Fallback to raw annotations if get_type_hints fails. + all_annotations = {} + for base_class in reversed(module.__mro__): + if hasattr(base_class, "__annotations__"): + all_annotations.update(base_class.__annotations__) for name, annotation in all_annotations.items(): origin = get_origin(annotation) @@ -187,7 +419,7 @@ def _make_module_blueprint( continue direction = "in" if origin == In else "out" type_ = get_args(annotation)[0] - connections.append(ModuleConnection(name=name, type=type_, direction=direction)) + connections.append(ModuleConnection(name=name, type=type_, direction=direction)) # type: ignore[arg-type] return ModuleBlueprint(module=module, connections=tuple(connections), args=args, kwargs=kwargs) @@ -199,21 +431,23 @@ def create_module_blueprint(module: type[Module], *args: Any, **kwargs: Any) -> def autoconnect(*blueprints: ModuleBlueprintSet) -> ModuleBlueprintSet: all_blueprints = tuple(_eliminate_duplicates([bp for bs in blueprints for bp in bs.blueprints])) - all_transports = dict( + all_transports = dict( # type: ignore[var-annotated] reduce(operator.iadd, [list(x.transport_map.items()) for x in blueprints], []) ) - all_config_overrides = dict( + all_config_overrides = dict( # type: ignore[var-annotated] reduce(operator.iadd, [list(x.global_config_overrides.items()) for x in blueprints], []) ) - all_remappings = dict( + all_remappings = dict( # type: ignore[var-annotated] reduce(operator.iadd, [list(x.remapping_map.items()) for x in blueprints], []) ) + all_requirement_checks = tuple(check for bs in blueprints for check in bs.requirement_checks) return ModuleBlueprintSet( blueprints=all_blueprints, transport_map=MappingProxyType(all_transports), global_config_overrides=MappingProxyType(all_config_overrides), remapping_map=MappingProxyType(all_remappings), + requirement_checks=all_requirement_checks, ) diff --git a/dimos/core/colors.py b/dimos/core/colors.py index f137523e67..294cf5d43b 100644 --- a/dimos/core/colors.py +++ b/dimos/core/colors.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/core/core.py b/dimos/core/core.py index 57e49e555d..e7a7d09f58 100644 --- a/dimos/core/core.py +++ b/dimos/core/core.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/core/global_config.py b/dimos/core/global_config.py index 64da2a01f2..bfb553a45d 100644 --- a/dimos/core/global_config.py +++ b/dimos/core/global_config.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,15 +13,41 @@ # limitations under the License. from functools import cached_property +import re +from typing import Literal, TypeAlias from pydantic_settings import BaseSettings, SettingsConfigDict +from dimos.mapping.occupancy.path_map import NavigationStrategy + +ViewerBackend: TypeAlias = Literal["rerun-web", "rerun-native", "foxglove"] + + +def _get_all_numbers(s: str) -> list[float]: + return [float(x) for x in re.findall(r"-?\d+\.?\d*", s)] + class GlobalConfig(BaseSettings): robot_ip: str | None = None - use_simulation: bool = False - use_replay: bool = False + simulation: bool = False + replay: bool = False + rerun_enabled: bool = True + rerun_server_addr: str | None = None + viewer_backend: ViewerBackend = "rerun-native" n_dask_workers: int = 2 + memory_limit: str = "auto" + mujoco_camera_position: str | None = None + mujoco_room: str | None = None + mujoco_room_from_occupancy: str | None = None + mujoco_global_costmap_from_occupancy: str | None = None + mujoco_global_map_from_pointcloud: str | None = None + mujoco_start_pos: str = "-1.0, 1.0" + mujoco_steps_per_frame: int = 7 + robot_model: str | None = None + robot_width: float = 0.3 + robot_rotation_diameter: float = 0.6 + planner_strategy: NavigationStrategy = "simple" + planner_robot_speed: float | None = None model_config = SettingsConfigDict( env_file=".env", @@ -32,8 +58,19 @@ class GlobalConfig(BaseSettings): @cached_property def unitree_connection_type(self) -> str: - if self.use_replay: - return "fake" - if self.use_simulation: + if self.replay: + return "replay" + if self.simulation: return "mujoco" return "webrtc" + + @cached_property + def mujoco_start_pos_float(self) -> tuple[float, float]: + x, y = _get_all_numbers(self.mujoco_start_pos) + return (x, y) + + @cached_property + def mujoco_camera_position_float(self) -> tuple[float, ...]: + if self.mujoco_camera_position is None: + return (-0.906, 0.008, 1.101, 4.931, 89.749, -46.378) + return tuple(_get_all_numbers(self.mujoco_camera_position)) diff --git a/dimos/core/introspection/__init__.py b/dimos/core/introspection/__init__.py new file mode 100644 index 0000000000..c40c3d49e6 --- /dev/null +++ b/dimos/core/introspection/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module and blueprint introspection utilities.""" + +from dimos.core.introspection.module import INTERNAL_RPCS, render_module_io +from dimos.core.introspection.svg import to_svg + +__all__ = ["INTERNAL_RPCS", "render_module_io", "to_svg"] diff --git a/dimos/core/introspection/blueprint/__init__.py b/dimos/core/introspection/blueprint/__init__.py new file mode 100644 index 0000000000..6545b39dfa --- /dev/null +++ b/dimos/core/introspection/blueprint/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Blueprint introspection and rendering. + +Renderers: + - dot: Graphviz DOT format (hub-style with type nodes as intermediate hubs) +""" + +from dimos.core.introspection.blueprint import dot +from dimos.core.introspection.blueprint.dot import LayoutAlgo, render_svg + +__all__ = ["LayoutAlgo", "dot", "render_svg"] diff --git a/dimos/core/introspection/blueprint/dot.py b/dimos/core/introspection/blueprint/dot.py new file mode 100644 index 0000000000..4c27c6282d --- /dev/null +++ b/dimos/core/introspection/blueprint/dot.py @@ -0,0 +1,253 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Hub-style Graphviz DOT renderer for blueprint visualization. + +This renderer creates intermediate "type nodes" for data flow, making it clearer +when one output fans out to multiple consumers: + + ModuleA --> [name:Type] --> ModuleB + --> ModuleC +""" + +from collections import defaultdict +from enum import Enum, auto + +from dimos.core.blueprints import ModuleBlueprintSet +from dimos.core.introspection.utils import ( + GROUP_COLORS, + TYPE_COLORS, + color_for_string, + sanitize_id, +) +from dimos.core.module import Module +from dimos.utils.cli import theme + + +class LayoutAlgo(Enum): + """Layout algorithms for controlling graph structure.""" + + STACK_CLUSTERS = auto() # Stack clusters vertically (invisible edges between clusters) + STACK_NODES = auto() # Stack nodes within clusters vertically + FDP = auto() # Use fdp (force-directed) layout engine instead of dot + + +# Connections to ignore (too noisy/common) +DEFAULT_IGNORED_CONNECTIONS = {("odom", "PoseStamped")} + +DEFAULT_IGNORED_MODULES = { + "WebsocketVisModule", + "UtilizationModule", + # "FoxgloveBridge", +} + + +def render( + blueprint_set: ModuleBlueprintSet, + *, + layout: set[LayoutAlgo] | None = None, + ignored_connections: set[tuple[str, str]] | None = None, + ignored_modules: set[str] | None = None, +) -> str: + """Generate a hub-style DOT graph from a ModuleBlueprintSet. + + This creates intermediate "type nodes" that represent data channels, + connecting producers to consumers through a central hub node. + + Args: + blueprint_set: The blueprint set to visualize. + layout: Set of layout algorithms to apply. Default is none (let graphviz decide). + ignored_connections: Set of (name, type_name) tuples to ignore. + ignored_modules: Set of module names to ignore. + + Returns: + A string in DOT format showing modules as nodes, type nodes as + small colored hubs, and edges connecting them. + """ + if layout is None: + layout = set() + if ignored_connections is None: + ignored_connections = DEFAULT_IGNORED_CONNECTIONS + if ignored_modules is None: + ignored_modules = DEFAULT_IGNORED_MODULES + + # Collect all outputs: (name, type) -> list of producer modules + producers: dict[tuple[str, type], list[type[Module]]] = defaultdict(list) + # Collect all inputs: (name, type) -> list of consumer modules + consumers: dict[tuple[str, type], list[type[Module]]] = defaultdict(list) + # Module name -> module class (for getting package info) + module_classes: dict[str, type[Module]] = {} + + for bp in blueprint_set.blueprints: + module_classes[bp.module.__name__] = bp.module + for conn in bp.connections: + # Apply remapping + remapped_name = blueprint_set.remapping_map.get((bp.module, conn.name), conn.name) + key = (remapped_name, conn.type) + if conn.direction == "out": + producers[key].append(bp.module) + else: + consumers[key].append(bp.module) + + # Find all active channels (have both producers AND consumers) + active_channels: dict[tuple[str, type], str] = {} # key -> color + for key in producers: + name, type_ = key + type_name = type_.__name__ + if key not in consumers: + continue + if (name, type_name) in ignored_connections: + continue + # Check if all modules are ignored + valid_producers = [m for m in producers[key] if m.__name__ not in ignored_modules] + valid_consumers = [m for m in consumers[key] if m.__name__ not in ignored_modules] + if not valid_producers or not valid_consumers: + continue + label = f"{name}:{type_name}" + active_channels[key] = color_for_string(TYPE_COLORS, label) + + # Group modules by package + def get_group(mod_class: type[Module]) -> str: + module_path = mod_class.__module__ + parts = module_path.split(".") + if len(parts) >= 2 and parts[0] == "dimos": + return parts[1] + return "other" + + by_group: dict[str, list[str]] = defaultdict(list) + for mod_name, mod_class in module_classes.items(): + if mod_name in ignored_modules: + continue + group = get_group(mod_class) + by_group[group].append(mod_name) + + # Build DOT output + lines = [ + "digraph modules {", + " bgcolor=transparent;", + " rankdir=LR;", + # " nodesep=1;", # horizontal spacing between nodes + # " ranksep=1.5;", # vertical spacing between ranks + " splines=true;", + f' node [shape=box, style=filled, fillcolor="{theme.BACKGROUND}", fontcolor="{theme.FOREGROUND}", color="{theme.BLUE}", fontname=fixed, fontsize=12, margin="0.1,0.1"];', + " edge [fontname=fixed, fontsize=10];", + "", + ] + + # Add subgraphs for each module group + sorted_groups = sorted(by_group.keys()) + for group in sorted_groups: + mods = sorted(by_group[group]) + color = color_for_string(GROUP_COLORS, group) + lines.append(f" subgraph cluster_{group} {{") + lines.append(f' label="{group}";') + lines.append(" labeljust=r;") + lines.append(" fontname=fixed;") + lines.append(" fontsize=14;") + lines.append(f' fontcolor="{theme.FOREGROUND}";') + lines.append(' style="filled,dashed";') + lines.append(f' color="{color}";') + lines.append(" penwidth=1;") + lines.append(f' fillcolor="{color}10";') + for mod in mods: + lines.append(f" {mod};") + # Stack nodes vertically within cluster + if LayoutAlgo.STACK_NODES in layout and len(mods) > 1: + for i in range(len(mods) - 1): + lines.append(f" {mods[i]} -> {mods[i + 1]} [style=invis];") + lines.append(" }") + lines.append("") + + # Add invisible edges between clusters to force vertical stacking + if LayoutAlgo.STACK_CLUSTERS in layout and len(sorted_groups) > 1: + lines.append(" // Force vertical cluster layout") + for i in range(len(sorted_groups) - 1): + group_a = sorted_groups[i] + group_b = sorted_groups[i + 1] + # Pick first node from each cluster + node_a = sorted(by_group[group_a])[0] + node_b = sorted(by_group[group_b])[0] + lines.append(f" {node_a} -> {node_b} [style=invis, weight=10];") + lines.append("") + + # Add type nodes (outside all clusters) + lines.append(" // Type nodes (data channels)") + for key, color in sorted( + active_channels.items(), key=lambda x: f"{x[0][0]}:{x[0][1].__name__}" + ): + name, type_ = key + type_name = type_.__name__ + node_id = sanitize_id(f"chan_{name}_{type_name}") + label = f"{name}:{type_name}" + lines.append( + f' {node_id} [label="{label}", shape=note, style=filled, ' + f'fillcolor="{color}35", color="{color}", fontcolor="{theme.FOREGROUND}", ' + f'width=0, height=0, margin="0.1,0.05", fontsize=10];' + ) + + lines.append("") + + # Add edges: producer -> type_node -> consumer + lines.append(" // Edges") + for key, color in sorted( + active_channels.items(), key=lambda x: f"{x[0][0]}:{x[0][1].__name__}" + ): + name, type_ = key + type_name = type_.__name__ + node_id = sanitize_id(f"chan_{name}_{type_name}") + + # Edges from producers to type node (no arrow, kept close) + for producer in producers[key]: + if producer.__name__ in ignored_modules: + continue + lines.append(f' {producer.__name__} -> {node_id} [color="{color}", arrowhead=none];') + + # Edges from type node to consumers (with arrow) + for consumer in consumers[key]: + if consumer.__name__ in ignored_modules: + continue + lines.append(f' {node_id} -> {consumer.__name__} [color="{color}"];') + + lines.append("}") + return "\n".join(lines) + + +def render_svg( + blueprint_set: ModuleBlueprintSet, + output_path: str, + *, + layout: set[LayoutAlgo] | None = None, +) -> None: + """Generate an SVG file from a ModuleBlueprintSet using graphviz. + + Args: + blueprint_set: The blueprint set to visualize. + output_path: Path to write the SVG file. + layout: Set of layout algorithms to apply. + """ + import subprocess + + if layout is None: + layout = set() + + dot_code = render(blueprint_set, layout=layout) + engine = "fdp" if LayoutAlgo.FDP in layout else "dot" + result = subprocess.run( + [engine, "-Tsvg", "-o", output_path], + input=dot_code, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise RuntimeError(f"graphviz failed: {result.stderr}") diff --git a/dimos/core/introspection/module/__init__.py b/dimos/core/introspection/module/__init__.py new file mode 100644 index 0000000000..444d0e24f3 --- /dev/null +++ b/dimos/core/introspection/module/__init__.py @@ -0,0 +1,45 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module introspection and rendering. + +Renderers: + - ansi: ANSI terminal output (default) + - dot: Graphviz DOT format +""" + +from dimos.core.introspection.module import ansi, dot +from dimos.core.introspection.module.info import ( + INTERNAL_RPCS, + ModuleInfo, + ParamInfo, + RpcInfo, + SkillInfo, + StreamInfo, + extract_module_info, +) +from dimos.core.introspection.module.render import render_module_io + +__all__ = [ + "INTERNAL_RPCS", + "ModuleInfo", + "ParamInfo", + "RpcInfo", + "SkillInfo", + "StreamInfo", + "ansi", + "dot", + "extract_module_info", + "render_module_io", +] diff --git a/dimos/core/introspection/module/ansi.py b/dimos/core/introspection/module/ansi.py new file mode 100644 index 0000000000..6e835d63d3 --- /dev/null +++ b/dimos/core/introspection/module/ansi.py @@ -0,0 +1,96 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ANSI terminal renderer for module IO diagrams.""" + +from dimos.core import colors +from dimos.core.introspection.module.info import ( + ModuleInfo, + ParamInfo, + RpcInfo, + SkillInfo, + StreamInfo, +) + + +def render(info: ModuleInfo, color: bool = True) -> str: + """Render module info as an ANSI terminal diagram. + + Args: + info: ModuleInfo structure to render. + color: Whether to include ANSI color codes. + + Returns: + ASCII/Unicode diagram with optional ANSI colors. + """ + # Color functions that become identity when color=False + _green = colors.green if color else (lambda x: x) + _blue = colors.blue if color else (lambda x: x) + _yellow = colors.yellow if color else (lambda x: x) + _cyan = colors.cyan if color else (lambda x: x) + + def _box(name: str) -> list[str]: + return [ + "ā”Œā”“" + "─" * (len(name) + 1) + "┐", + f"│ {name} │", + "└┬" + "─" * (len(name) + 1) + "ā”˜", + ] + + def format_stream(stream: StreamInfo) -> str: + return f"{_yellow(stream.name)}: {_green(stream.type_name)}" + + def format_param(param: ParamInfo) -> str: + result = param.name + if param.type_name: + result += ": " + _green(param.type_name) + if param.default: + result += f" = {param.default}" + return result + + def format_rpc(rpc: RpcInfo) -> str: + params = ", ".join(format_param(p) for p in rpc.params) + result = _blue(rpc.name) + f"({params})" + if rpc.return_type: + result += " -> " + _green(rpc.return_type) + return result + + def format_skill(skill: SkillInfo) -> str: + info_parts = [] + if skill.stream: + info_parts.append(f"stream={skill.stream}") + if skill.reducer: + info_parts.append(f"reducer={skill.reducer}") + if skill.output: + info_parts.append(f"output={skill.output}") + info = f" ({', '.join(info_parts)})" if info_parts else "" + return _cyan(skill.name) + info + + # Build output + lines = [ + *(f" ā”œā”€ {format_stream(s)}" for s in info.inputs), + *_box(info.name), + *(f" ā”œā”€ {format_stream(s)}" for s in info.outputs), + ] + + if info.rpcs: + lines.append(" │") + for rpc in info.rpcs: + lines.append(f" ā”œā”€ RPC {format_rpc(rpc)}") + + if info.skills: + lines.append(" │") + for skill in info.skills: + lines.append(f" ā”œā”€ Skill {format_skill(skill)}") + + return "\n".join(lines) diff --git a/dimos/core/introspection/module/dot.py b/dimos/core/introspection/module/dot.py new file mode 100644 index 0000000000..829957a8e3 --- /dev/null +++ b/dimos/core/introspection/module/dot.py @@ -0,0 +1,203 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Graphviz DOT renderer for module IO diagrams.""" + +from dimos.core.introspection.module.info import ModuleInfo +from dimos.core.introspection.utils import ( + RPC_COLOR, + SKILL_COLOR, + TYPE_COLORS, + color_for_string, + sanitize_id, +) +from dimos.utils.cli import theme + + +def render(info: ModuleInfo) -> str: + """Render module info as a DOT graph. + + Shows the module as a central node with input streams as nodes + pointing in and output streams as nodes pointing out. + + Args: + info: ModuleInfo structure to render. + + Returns: + DOT format string. + """ + lines = [ + "digraph module {", + " bgcolor=transparent;", + " rankdir=LR;", + " compound=true;", + " splines=true;", + f' node [shape=box, style=filled, fillcolor="{theme.BACKGROUND}", fontcolor="{theme.FOREGROUND}", color="{theme.BLUE}", fontname=fixed, fontsize=12, margin="0.1,0.1"];', + " edge [fontname=fixed, fontsize=10, penwidth=1];", + "", + ] + + # Module node (central, larger) + module_id = sanitize_id(info.name) + lines.append(f' {module_id} [label="{info.name}", width=2, height=0.8];') + lines.append("") + + # Input stream nodes (on the left) + if info.inputs: + lines.append(" // Input streams") + lines.append(" subgraph cluster_inputs {") + lines.append(' label="";') + lines.append(" style=invis;") + lines.append(' rank="same";') + for stream in info.inputs: + label = f"{stream.name}:{stream.type_name}" + color = color_for_string(TYPE_COLORS, label) + node_id = sanitize_id(f"in_{stream.name}") + lines.append( + f' {node_id} [label="{label}", shape=note, style=filled, ' + f'fillcolor="{color}35", color="{color}", ' + f'width=0, height=0, margin="0.1,0.05", fontsize=10];' + ) + lines.append(" }") + lines.append("") + + # Output stream nodes (on the right) + if info.outputs: + lines.append(" // Output streams") + lines.append(" subgraph cluster_outputs {") + lines.append(' label="";') + lines.append(" style=invis;") + lines.append(' rank="same";') + for stream in info.outputs: + label = f"{stream.name}:{stream.type_name}" + color = color_for_string(TYPE_COLORS, label) + node_id = sanitize_id(f"out_{stream.name}") + lines.append( + f' {node_id} [label="{label}", shape=note, style=filled, ' + f'fillcolor="{color}35", color="{color}", ' + f'width=0, height=0, margin="0.1,0.05", fontsize=10];' + ) + lines.append(" }") + lines.append("") + + # RPC nodes (in subgraph) + if info.rpcs: + lines.append(" // RPCs") + lines.append(" subgraph cluster_rpcs {") + lines.append(' label="RPCs";') + lines.append(" labeljust=l;") + lines.append(" fontname=fixed;") + lines.append(" fontsize=14;") + lines.append(f' fontcolor="{theme.FOREGROUND}";') + lines.append(' style="filled,dashed";') + lines.append(f' color="{RPC_COLOR}";') + lines.append(" penwidth=1;") + lines.append(f' fillcolor="{RPC_COLOR}10";') + for rpc in info.rpcs: + params = ", ".join( + f"{p.name}: {p.type_name}" if p.type_name else p.name for p in rpc.params + ) + ret = f" -> {rpc.return_type}" if rpc.return_type else "" + label = f"{rpc.name}({params}){ret}" + node_id = sanitize_id(f"rpc_{rpc.name}") + lines.append( + f' {node_id} [label="{label}", shape=cds, style=filled, ' + f'fillcolor="{RPC_COLOR}35", color="{RPC_COLOR}", ' + f'width=0, height=0, margin="0.1,0.05", fontsize=9];' + ) + lines.append(" }") + lines.append("") + + # Skill nodes (in subgraph) + if info.skills: + lines.append(" // Skills") + lines.append(" subgraph cluster_skills {") + lines.append(' label="Skills";') + lines.append(" labeljust=l;") + lines.append(" fontname=fixed;") + lines.append(" fontsize=14;") + lines.append(f' fontcolor="{theme.FOREGROUND}";') + lines.append(' style="filled,dashed";') + lines.append(f' color="{SKILL_COLOR}";') + lines.append(" penwidth=1;") + lines.append(f' fillcolor="{SKILL_COLOR}20";') + for skill in info.skills: + parts = [skill.name] + if skill.stream: + parts.append(f"stream={skill.stream}") + if skill.reducer: + parts.append(f"reducer={skill.reducer}") + label = " ".join(parts) + node_id = sanitize_id(f"skill_{skill.name}") + lines.append( + f' {node_id} [label="{label}", shape=cds, style=filled, ' + f'fillcolor="{SKILL_COLOR}35", color="{SKILL_COLOR}", ' + f'width=0, height=0, margin="0.1,0.05", fontsize=9];' + ) + lines.append(" }") + lines.append("") + + # Edges: inputs -> module + lines.append(" // Edges") + for stream in info.inputs: + label = f"{stream.name}:{stream.type_name}" + color = color_for_string(TYPE_COLORS, label) + node_id = sanitize_id(f"in_{stream.name}") + lines.append(f' {node_id} -> {module_id} [color="{color}"];') + + # Edges: module -> outputs + for stream in info.outputs: + label = f"{stream.name}:{stream.type_name}" + color = color_for_string(TYPE_COLORS, label) + node_id = sanitize_id(f"out_{stream.name}") + lines.append(f' {module_id} -> {node_id} [color="{color}"];') + + # Edge: module -> RPCs cluster (dashed, no arrow) + if info.rpcs: + first_rpc_id = sanitize_id(f"rpc_{info.rpcs[0].name}") + lines.append( + f" {module_id} -> {first_rpc_id} [lhead=cluster_rpcs, style=filled, weight=3" + f'color="{RPC_COLOR}", arrowhead=none];' + ) + + # Edge: module -> Skills cluster (dashed, no arrow) + if info.skills: + first_skill_id = sanitize_id(f"skill_{info.skills[0].name}") + lines.append( + f" {module_id} -> {first_skill_id} [lhead=cluster_skills, style=filled, weight=3" + f'color="{SKILL_COLOR}", arrowhead=none];' + ) + + lines.append("}") + return "\n".join(lines) + + +def render_svg(info: ModuleInfo, output_path: str) -> None: + """Generate an SVG file from ModuleInfo using graphviz. + + Args: + info: ModuleInfo structure to render. + output_path: Path to write the SVG file. + """ + import subprocess + + dot_code = render(info) + result = subprocess.run( + ["dot", "-Tsvg", "-o", output_path], + input=dot_code, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise RuntimeError(f"graphviz failed: {result.stderr}") diff --git a/dimos/core/introspection/module/info.py b/dimos/core/introspection/module/info.py new file mode 100644 index 0000000000..8fcad76006 --- /dev/null +++ b/dimos/core/introspection/module/info.py @@ -0,0 +1,168 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module introspection data structures.""" + +from collections.abc import Callable +from dataclasses import dataclass, field +import inspect +from typing import Any + +# Internal RPCs to hide from io() output +INTERNAL_RPCS = { + "dynamic_skills", + "get_rpc_method_names", + "set_rpc_method", + "skills", + "_io_instance", +} + + +@dataclass +class StreamInfo: + """Information about a module stream (input or output).""" + + name: str + type_name: str + + +@dataclass +class ParamInfo: + """Information about an RPC parameter.""" + + name: str + type_name: str | None = None + default: str | None = None + + +@dataclass +class RpcInfo: + """Information about an RPC method.""" + + name: str + params: list[ParamInfo] = field(default_factory=list) + return_type: str | None = None + + +@dataclass +class SkillInfo: + """Information about a skill.""" + + name: str + stream: str | None = None # None means "none" + reducer: str | None = None # None means "latest" + output: str | None = None # None means "standard" + + +@dataclass +class ModuleInfo: + """Extracted information about a module's IO interface.""" + + name: str + inputs: list[StreamInfo] = field(default_factory=list) + outputs: list[StreamInfo] = field(default_factory=list) + rpcs: list[RpcInfo] = field(default_factory=list) + skills: list[SkillInfo] = field(default_factory=list) + + +def extract_rpc_info(fn: Callable) -> RpcInfo: # type: ignore[type-arg] + """Extract RPC information from a callable.""" + sig = inspect.signature(fn) + params = [] + + for pname, p in sig.parameters.items(): + if pname == "self": + continue + type_name = None + if p.annotation != inspect.Parameter.empty: + type_name = getattr(p.annotation, "__name__", str(p.annotation)) + default = None + if p.default != inspect.Parameter.empty: + default = str(p.default) + params.append(ParamInfo(name=pname, type_name=type_name, default=default)) + + return_type = None + if sig.return_annotation != inspect.Signature.empty: + return_type = getattr(sig.return_annotation, "__name__", str(sig.return_annotation)) + + return RpcInfo(name=fn.__name__, params=params, return_type=return_type) + + +def extract_skill_info(fn: Callable) -> SkillInfo: # type: ignore[type-arg] + """Extract skill information from a skill-decorated callable.""" + cfg = fn._skill_config # type: ignore[attr-defined] + + stream = cfg.stream.name if cfg.stream.name != "none" else None + reducer_name = getattr(cfg.reducer, "__name__", str(cfg.reducer)) + reducer = reducer_name if reducer_name != "latest" else None + output = cfg.output.name if cfg.output.name != "standard" else None + + return SkillInfo(name=fn.__name__, stream=stream, reducer=reducer, output=output) + + +def extract_module_info( + name: str, + inputs: dict[str, Any], + outputs: dict[str, Any], + rpcs: dict[str, Callable], # type: ignore[type-arg] +) -> ModuleInfo: + """Extract module information into a ModuleInfo structure. + + Args: + name: Module class name. + inputs: Dict of input stream name -> stream object or formatted string. + outputs: Dict of output stream name -> stream object or formatted string. + rpcs: Dict of RPC method name -> callable. + + Returns: + ModuleInfo with extracted data. + """ + + # Extract stream info + def stream_info(stream: Any, stream_name: str) -> StreamInfo: + if isinstance(stream, str): + # Pre-formatted string like "name: Type" - parse it + # Strip ANSI codes for parsing + import re + + clean = re.sub(r"\x1b\[[0-9;]*m", "", stream) + if ": " in clean: + parts = clean.split(": ", 1) + return StreamInfo(name=parts[0], type_name=parts[1]) + return StreamInfo(name=stream_name, type_name=clean) + # Instance stream object + return StreamInfo(name=stream.name, type_name=stream.type.__name__) + + input_infos = [stream_info(s, n) for n, s in inputs.items()] + output_infos = [stream_info(s, n) for n, s in outputs.items()] + + # Separate skills from regular RPCs, filtering internal ones + rpc_infos = [] + skill_infos = [] + + for rpc_name, rpc_fn in rpcs.items(): + if rpc_name in INTERNAL_RPCS: + continue + if hasattr(rpc_fn, "_skill_config"): + skill_infos.append(extract_skill_info(rpc_fn)) + else: + rpc_infos.append(extract_rpc_info(rpc_fn)) + + return ModuleInfo( + name=name, + inputs=input_infos, + outputs=output_infos, + rpcs=rpc_infos, + skills=skill_infos, + ) diff --git a/dimos/core/introspection/module/render.py b/dimos/core/introspection/module/render.py new file mode 100644 index 0000000000..8e87a5b202 --- /dev/null +++ b/dimos/core/introspection/module/render.py @@ -0,0 +1,44 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Convenience rendering functions for module introspection.""" + +from collections.abc import Callable +from typing import Any + +from dimos.core.introspection.module import ansi +from dimos.core.introspection.module.info import extract_module_info + + +def render_module_io( + name: str, + inputs: dict[str, Any], + outputs: dict[str, Any], + rpcs: dict[str, Callable], # type: ignore[type-arg] + color: bool = True, +) -> str: + """Render module IO diagram using the default (ANSI) renderer. + + Args: + name: Module class name. + inputs: Dict of input stream name -> stream object or formatted string. + outputs: Dict of output stream name -> stream object or formatted string. + rpcs: Dict of RPC method name -> callable. + color: Whether to include ANSI color codes. + + Returns: + ASCII diagram showing module inputs, outputs, RPCs, and skills. + """ + info = extract_module_info(name, inputs, outputs, rpcs) + return ansi.render(info, color=color) diff --git a/dimos/core/introspection/svg.py b/dimos/core/introspection/svg.py new file mode 100644 index 0000000000..cdf87cc093 --- /dev/null +++ b/dimos/core/introspection/svg.py @@ -0,0 +1,57 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unified SVG rendering for modules and blueprints.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dimos.core.blueprints import ModuleBlueprintSet + from dimos.core.introspection.blueprint.dot import LayoutAlgo + from dimos.core.introspection.module.info import ModuleInfo + + +def to_svg( + target: ModuleInfo | ModuleBlueprintSet, + output_path: str, + *, + layout: set[LayoutAlgo] | None = None, +) -> None: + """Render a module or blueprint to SVG. + + Dispatches to the appropriate renderer based on input type: + - ModuleInfo -> module/dot.render_svg + - ModuleBlueprintSet -> blueprint/dot.render_svg + + Args: + target: Either a ModuleInfo (single module) or ModuleBlueprintSet (blueprint graph). + output_path: Path to write the SVG file. + layout: Layout algorithms (only used for blueprints). + """ + # Avoid circular imports by importing here + from dimos.core.blueprints import ModuleBlueprintSet + from dimos.core.introspection.module.info import ModuleInfo + + if isinstance(target, ModuleInfo): + from dimos.core.introspection.module import dot as module_dot + + module_dot.render_svg(target, output_path) + elif isinstance(target, ModuleBlueprintSet): + from dimos.core.introspection.blueprint import dot as blueprint_dot + + blueprint_dot.render_svg(target, output_path, layout=layout) + else: + raise TypeError(f"Expected ModuleInfo or ModuleBlueprintSet, got {type(target).__name__}") diff --git a/dimos/core/introspection/utils.py b/dimos/core/introspection/utils.py new file mode 100644 index 0000000000..166933b80c --- /dev/null +++ b/dimos/core/introspection/utils.py @@ -0,0 +1,86 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared utilities for introspection renderers.""" + +import hashlib +import re + +# Colors for type nodes and edges (bright, distinct, good on dark backgrounds) +TYPE_COLORS = [ + "#FF6B6B", # coral red + "#4ECDC4", # teal + "#FFE66D", # yellow + "#95E1D3", # mint + "#F38181", # salmon + "#AA96DA", # lavender + "#81C784", # green + "#64B5F6", # light blue + "#FFB74D", # orange + "#BA68C8", # purple + "#4DD0E1", # cyan + "#AED581", # lime + "#FF8A65", # deep orange + "#7986CB", # indigo + "#F06292", # pink + "#A1887F", # brown + "#90A4AE", # blue grey + "#DCE775", # lime yellow + "#4DB6AC", # teal green + "#9575CD", # deep purple + "#E57373", # light red + "#81D4FA", # sky blue + "#C5E1A5", # light green + "#FFCC80", # light orange + "#B39DDB", # light purple + "#80DEEA", # light cyan + "#FFAB91", # peach + "#CE93D8", # light violet + "#80CBC4", # light teal + "#FFF59D", # light yellow +] + +# Colors for group borders (bright, distinct, good on dark backgrounds) +GROUP_COLORS = [ + "#5C9FF0", # blue + "#FFB74D", # orange + "#81C784", # green + "#BA68C8", # purple + "#4ECDC4", # teal + "#FF6B6B", # coral + "#FFE66D", # yellow + "#7986CB", # indigo + "#F06292", # pink + "#4DB6AC", # teal green + "#9575CD", # deep purple + "#AED581", # lime + "#64B5F6", # light blue + "#FF8A65", # deep orange + "#AA96DA", # lavender +] + +# Colors for RPCs/Skills +RPC_COLOR = "#7986CB" # indigo +SKILL_COLOR = "#4ECDC4" # teal + + +def color_for_string(colors: list[str], s: str) -> str: + """Get a consistent color for a string based on its hash.""" + h = int(hashlib.md5(s.encode()).hexdigest(), 16) + return colors[h % len(colors)] + + +def sanitize_id(s: str) -> str: + """Sanitize a string to be a valid graphviz node ID.""" + return re.sub(r"[^a-zA-Z0-9_]", "_", s) diff --git a/dimos/core/module.py b/dimos/core/module.py index 6ce8480087..08e428d3c7 100644 --- a/dimos/core/module.py +++ b/dimos/core/module.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,24 +15,32 @@ from collections.abc import Callable from dataclasses import dataclass from functools import partial -import inspect +import sys import threading from typing import ( + TYPE_CHECKING, Any, get_args, get_origin, get_type_hints, + overload, ) +if TYPE_CHECKING: + from dimos.core.introspection.module import ModuleInfo + from dask.distributed import Actor, get_worker from reactivex.disposable import CompositeDisposable +from typing_extensions import TypeVar from dimos.core import colors from dimos.core.core import T, rpc +from dimos.core.introspection.module import extract_module_info, render_module_io from dimos.core.resource import Resource +from dimos.core.rpc_client import RpcCall from dimos.core.stream import In, Out, RemoteIn, RemoteOut, Transport from dimos.protocol.rpc import LCMRPC, RPCSpec -from dimos.protocol.service import Configurable +from dimos.protocol.service import Configurable # type: ignore[attr-defined] from dimos.protocol.skill.skill import SkillContainer from dimos.protocol.tf import LCMTF, TFSpec from dimos.utils.generic import classproperty @@ -70,18 +78,26 @@ def get_loop() -> tuple[asyncio.AbstractEventLoop, threading.Thread | None]: class ModuleConfig: rpc_transport: type[RPCSpec] = LCMRPC tf_transport: type[TFSpec] = LCMTF + frame_id_prefix: str | None = None + frame_id: str | None = None + + +ModuleConfigT = TypeVar("ModuleConfigT", bound=ModuleConfig, default=ModuleConfig) -class ModuleBase(Configurable[ModuleConfig], SkillContainer, Resource): +class ModuleBase(Configurable[ModuleConfigT], SkillContainer, Resource): _rpc: RPCSpec | None = None _tf: TFSpec | None = None _loop: asyncio.AbstractEventLoop | None = None _loop_thread: threading.Thread | None _disposables: CompositeDisposable + _bound_rpc_calls: dict[str, RpcCall] = {} - default_config = ModuleConfig + rpc_calls: list[str] = [] - def __init__(self, *args, **kwargs) -> None: + default_config: type[ModuleConfigT] = ModuleConfig # type: ignore[assignment] + + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) self._loop, self._loop_thread = get_loop() self._disposables = CompositeDisposable() @@ -92,10 +108,17 @@ def __init__(self, *args, **kwargs) -> None: # and we register our RPC server self.rpc = self.config.rpc_transport() self.rpc.serve_module_rpc(self) - self.rpc.start() + self.rpc.start() # type: ignore[attr-defined] except ValueError: ... + @property + def frame_id(self) -> str: + base = self.config.frame_id or self.__class__.__name__ + if self.config.frame_id_prefix: + return f"{self.config.frame_id_prefix}/{base}" + return base + @rpc def start(self) -> None: pass @@ -109,7 +132,7 @@ def _close_module(self) -> None: self._close_rpc() if hasattr(self, "_loop") and self._loop_thread: if self._loop_thread.is_alive(): - self._loop.call_soon_threadsafe(self._loop.stop) + self._loop.call_soon_threadsafe(self._loop.stop) # type: ignore[union-attr] self._loop_thread.join(timeout=2) self._loop = None self._loop_thread = None @@ -122,10 +145,10 @@ def _close_module(self) -> None: def _close_rpc(self) -> None: # Using hasattr is needed because SkillCoordinator skips ModuleBase.__init__ and self.rpc is never set. if hasattr(self, "rpc") and self.rpc: - self.rpc.stop() - self.rpc = None + self.rpc.stop() # type: ignore[attr-defined] + self.rpc = None # type: ignore[assignment] - def __getstate__(self): + def __getstate__(self): # type: ignore[no-untyped-def] """Exclude unpicklable runtime attributes when serializing.""" state = self.__dict__.copy() # Remove unpicklable attributes @@ -136,7 +159,7 @@ def __getstate__(self): state.pop("_tf", None) return state - def __setstate__(self, state) -> None: + def __setstate__(self, state) -> None: # type: ignore[no-untyped-def] """Restore object from pickled state.""" self.__dict__.update(state) # Reinitialize runtime attributes @@ -147,14 +170,14 @@ def __setstate__(self, state) -> None: self._tf = None @property - def tf(self): + def tf(self): # type: ignore[no-untyped-def] if self._tf is None: # self._tf = self.config.tf_transport() self._tf = LCMTF() return self._tf @tf.setter - def tf(self, value) -> None: + def tf(self, value) -> None: # type: ignore[no-untyped-def] import warnings warnings.warn( @@ -164,7 +187,7 @@ def tf(self, value) -> None: ) @property - def outputs(self) -> dict[str, Out]: + def outputs(self) -> dict[str, Out]: # type: ignore[type-arg] return { name: s for name, s in self.__dict__.items() @@ -172,86 +195,199 @@ def outputs(self) -> dict[str, Out]: } @property - def inputs(self) -> dict[str, In]: + def inputs(self) -> dict[str, In]: # type: ignore[type-arg] return { name: s for name, s in self.__dict__.items() if isinstance(s, In) and not name.startswith("_") } - @classmethod - @property - def rpcs(cls) -> dict[str, Callable]: + @classproperty + def rpcs(self) -> dict[str, Callable[..., Any]]: return { - name: getattr(cls, name) - for name in dir(cls) + name: getattr(self, name) + for name in dir(self) if not name.startswith("_") and name != "rpcs" # Exclude the rpcs property itself to prevent recursion - and callable(getattr(cls, name, None)) - and hasattr(getattr(cls, name), "__rpc__") + and callable(getattr(self, name, None)) + and hasattr(getattr(self, name), "__rpc__") } @rpc - def io(self) -> str: - def _box(name: str) -> str: - return [ - "ā”Œā”“" + "─" * (len(name) + 1) + "┐", - f"│ {name} │", - "└┬" + "─" * (len(name) + 1) + "ā”˜", - ] - - # can't modify __str__ on a function like we are doing for I/O - # so we have a separate repr function here - def repr_rpc(fn: Callable) -> str: - sig = inspect.signature(fn) - # Remove 'self' parameter - params = [p for name, p in sig.parameters.items() if name != "self"] - - # Format parameters with colored types - param_strs = [] - for param in params: - param_str = param.name - if param.annotation != inspect.Parameter.empty: - type_name = getattr(param.annotation, "__name__", str(param.annotation)) - param_str += ": " + colors.green(type_name) - if param.default != inspect.Parameter.empty: - param_str += f" = {param.default}" - param_strs.append(param_str) - - # Format return type - return_annotation = "" - if sig.return_annotation != inspect.Signature.empty: - return_type = getattr(sig.return_annotation, "__name__", str(sig.return_annotation)) - return_annotation = " -> " + colors.green(return_type) - - return ( - "RPC " + colors.blue(fn.__name__) + f"({', '.join(param_strs)})" + return_annotation - ) + def _io_instance(self, color: bool = True) -> str: + """Instance-level io() - shows actual running streams.""" + return render_module_io( + name=self.__class__.__name__, + inputs=self.inputs, + outputs=self.outputs, + rpcs=self.rpcs, + color=color, + ) + + @classmethod + def _io_class(cls, color: bool = True) -> str: + """Class-level io() - shows declared stream types from annotations.""" + hints = get_type_hints(cls) + + _yellow = colors.yellow if color else (lambda x: x) + _green = colors.green if color else (lambda x: x) + + def is_stream(hint: type, stream_type: type) -> bool: + origin = get_origin(hint) + if origin is stream_type: + return True + if isinstance(hint, type) and issubclass(hint, stream_type): + return True + return False + + def format_stream(name: str, hint: type) -> str: + args = get_args(hint) + type_name = args[0].__name__ if args else "?" + return f"{_yellow(name)}: {_green(type_name)}" + + inputs = { + name: format_stream(name, hint) for name, hint in hints.items() if is_stream(hint, In) + } + outputs = { + name: format_stream(name, hint) for name, hint in hints.items() if is_stream(hint, Out) + } + + return render_module_io( + name=cls.__name__, + inputs=inputs, + outputs=outputs, + rpcs=cls.rpcs, + color=color, + ) + + class _io_descriptor: + """Descriptor that makes io() work on both class and instance.""" + + def __get__( + self, obj: "ModuleBase | None", objtype: type["ModuleBase"] + ) -> Callable[[bool], str]: + if obj is None: + return objtype._io_class + return obj._io_instance + + io = _io_descriptor() + + @classmethod + def _module_info_class(cls) -> "ModuleInfo": + """Class-level module_info() - returns ModuleInfo from annotations.""" + + hints = get_type_hints(cls) + + def is_stream(hint: type, stream_type: type) -> bool: + origin = get_origin(hint) + if origin is stream_type: + return True + if isinstance(hint, type) and issubclass(hint, stream_type): + return True + return False + + def format_stream(name: str, hint: type) -> str: + args = get_args(hint) + type_name = args[0].__name__ if args else "?" + return f"{name}: {type_name}" + + inputs = { + name: format_stream(name, hint) for name, hint in hints.items() if is_stream(hint, In) + } + outputs = { + name: format_stream(name, hint) for name, hint in hints.items() if is_stream(hint, Out) + } - ret = [ - *(f" ā”œā”€ {stream}" for stream in self.inputs.values()), - *_box(self.__class__.__name__), - *(f" ā”œā”€ {stream}" for stream in self.outputs.values()), - " │", - *(f" ā”œā”€ {repr_rpc(rpc)}" for rpc in self.rpcs.values()), - ] + return extract_module_info( + name=cls.__name__, + inputs=inputs, + outputs=outputs, + rpcs=cls.rpcs, + ) + + class _module_info_descriptor: + """Descriptor that makes module_info() work on both class and instance.""" + + def __get__( + self, obj: "ModuleBase | None", objtype: type["ModuleBase"] + ) -> Callable[[], "ModuleInfo"]: + if obj is None: + return objtype._module_info_class + # For instances, extract from actual streams + return lambda: extract_module_info( + name=obj.__class__.__name__, + inputs=obj.inputs, + outputs=obj.outputs, + rpcs=obj.rpcs, + ) - return "\n".join(ret) + module_info = _module_info_descriptor() @classproperty - def blueprint(self): + def blueprint(self): # type: ignore[no-untyped-def] # Here to prevent circular imports. from dimos.core.blueprints import create_module_blueprint - return partial(create_module_blueprint, self) + return partial(create_module_blueprint, self) # type: ignore[arg-type] + + @rpc + def get_rpc_method_names(self) -> list[str]: + return self.rpc_calls + + @rpc + def set_rpc_method(self, method: str, callable: RpcCall) -> None: + callable.set_rpc(self.rpc) # type: ignore[arg-type] + self._bound_rpc_calls[method] = callable + + @overload + def get_rpc_calls(self, method: str) -> RpcCall: ... + + @overload + def get_rpc_calls(self, method1: str, method2: str, *methods: str) -> tuple[RpcCall, ...]: ... + + def get_rpc_calls(self, *methods: str) -> RpcCall | tuple[RpcCall, ...]: # type: ignore[misc] + missing = [m for m in methods if m not in self._bound_rpc_calls] + if missing: + raise ValueError( + f"RPC methods not found. Class: {self.__class__.__name__}, RPC methods: {', '.join(missing)}" + ) + result = tuple(self._bound_rpc_calls[m] for m in methods) + return result[0] if len(result) == 1 else result -class DaskModule(ModuleBase): +class DaskModule(ModuleBase[ModuleConfigT]): ref: Actor worker: int - def __init__(self, *args, **kwargs) -> None: - self.ref = None + def __init_subclass__(cls, **kwargs: Any) -> None: + """Set class-level None attributes for In/Out type annotations. + + This is needed because Dask's Actor proxy looks up attributes on the class + (not instance) when proxying attribute access. Without class-level attributes, + the proxy would fail with AttributeError even though the instance has the attrs. + """ + super().__init_subclass__(**kwargs) + + # Get type hints for this class only (not inherited ones). + globalns = {} + for c in cls.__mro__: + if c.__module__ in sys.modules: + globalns.update(sys.modules[c.__module__].__dict__) + + try: + hints = get_type_hints(cls, globalns=globalns, include_extras=True) + except (NameError, AttributeError, TypeError): + hints = {} + + for name, ann in hints.items(): + origin = get_origin(ann) + if origin in (In, Out): + # Set class-level attribute if not already set. + if not hasattr(cls, name) or getattr(cls, name) is None: + setattr(cls, name, None) + + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + self.ref = None # type: ignore[assignment] # Get type hints with proper namespace resolution for subclasses # Collect namespaces from all classes in the MRO chain @@ -273,25 +409,25 @@ def __init__(self, *args, **kwargs) -> None: origin = get_origin(ann) if origin is Out: inner, *_ = get_args(ann) or (Any,) - stream = Out(inner, name, self) + stream = Out(inner, name, self) # type: ignore[var-annotated] setattr(self, name, stream) elif origin is In: inner, *_ = get_args(ann) or (Any,) - stream = In(inner, name, self) + stream = In(inner, name, self) # type: ignore[assignment] setattr(self, name, stream) super().__init__(*args, **kwargs) - def set_ref(self, ref) -> int: + def set_ref(self, ref) -> int: # type: ignore[no-untyped-def] worker = get_worker() self.ref = ref self.worker = worker.name - return worker.name + return worker.name # type: ignore[no-any-return] def __str__(self) -> str: return f"{self.__class__.__name__}" - # called from remote - def set_transport(self, stream_name: str, transport: Transport) -> bool: + @rpc + def set_transport(self, stream_name: str, transport: Transport) -> bool: # type: ignore[type-arg] stream = getattr(self, stream_name, None) if not stream: raise ValueError(f"{stream_name} not found in {self.__class__.__name__}") @@ -303,7 +439,7 @@ def set_transport(self, stream_name: str, transport: Transport) -> bool: return True # called from remote - def connect_stream(self, input_name: str, remote_stream: RemoteOut[T]): + def connect_stream(self, input_name: str, remote_stream: RemoteOut[T]): # type: ignore[no-untyped-def] input_stream = getattr(self, input_name, None) if not input_stream: raise ValueError(f"{input_name} not found in {self.__class__.__name__}") diff --git a/dimos/core/module_coordinator.py b/dimos/core/module_coordinator.py index a740bef494..9f38fabe05 100644 --- a/dimos/core/module_coordinator.py +++ b/dimos/core/module_coordinator.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,12 +32,11 @@ class ModuleCoordinator(Resource): def __init__( self, n: int | None = None, - memory_limit: str = "auto", global_config: GlobalConfig | None = None, ) -> None: cfg = global_config or GlobalConfig() self._n = n if n is not None else cfg.n_dask_workers - self._memory_limit = memory_limit + self._memory_limit = cfg.memory_limit def start(self) -> None: self._client = core.start(self._n, self._memory_limit) @@ -46,28 +45,28 @@ def stop(self) -> None: for module in reversed(self._deployed_modules.values()): module.stop() - self._client.close_all() + self._client.close_all() # type: ignore[union-attr] - def deploy(self, module_class: type[T], *args, **kwargs) -> T: + def deploy(self, module_class: type[T], *args, **kwargs) -> T: # type: ignore[no-untyped-def] if not self._client: raise ValueError("Not started") - module = self._client.deploy(module_class, *args, **kwargs) + module = self._client.deploy(module_class, *args, **kwargs) # type: ignore[attr-defined] self._deployed_modules[module_class] = module - return module + return module # type: ignore[no-any-return] def start_all_modules(self) -> None: for module in self._deployed_modules.values(): module.start() def get_instance(self, module: type[T]) -> T | None: - return self._deployed_modules.get(module) + return self._deployed_modules.get(module) # type: ignore[return-value] def loop(self) -> None: try: while True: time.sleep(0.1) except KeyboardInterrupt: - pass + return finally: self.stop() diff --git a/dimos/core/o3dpickle.py b/dimos/core/o3dpickle.py index 8e0f13dbf0..1912ab7739 100644 --- a/dimos/core/o3dpickle.py +++ b/dimos/core/o3dpickle.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,16 +15,16 @@ import copyreg import numpy as np -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] -def reduce_external(obj): +def reduce_external(obj): # type: ignore[no-untyped-def] # Convert Vector3dVector to numpy array for pickling points_array = np.asarray(obj.points) return (reconstruct_pointcloud, (points_array,)) -def reconstruct_pointcloud(points_array): +def reconstruct_pointcloud(points_array): # type: ignore[no-untyped-def] # Create new PointCloud and assign the points pc = o3d.geometry.PointCloud() pc.points = o3d.utility.Vector3dVector(points_array) diff --git a/dimos/core/resource.py b/dimos/core/resource.py index 3d69f50bb4..21cdec6322 100644 --- a/dimos/core/resource.py +++ b/dimos/core/resource.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/core/rpc_client.py b/dimos/core/rpc_client.py index bfcec5bb71..a3d1a2da0c 100644 --- a/dimos/core/rpc_client.py +++ b/dimos/core/rpc_client.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,10 @@ from collections.abc import Callable from typing import Any -from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.protocol.rpc import LCMRPC from dimos.utils.logging_config import setup_logger -logger = setup_logger(__file__) +logger = setup_logger() class RpcCall: @@ -26,7 +26,7 @@ class RpcCall: _rpc: LCMRPC | None _name: str _remote_name: str - _unsub_fns: list + _unsub_fns: list # type: ignore[type-arg] _stop_rpc_client: Callable[[], None] | None = None def __init__( @@ -35,7 +35,7 @@ def __init__( rpc: LCMRPC, name: str, remote_name: str, - unsub_fns: list, + unsub_fns: list, # type: ignore[type-arg] stop_client: Callable[[], None] | None = None, ) -> None: self._original_method = original_method @@ -53,7 +53,7 @@ def __init__( def set_rpc(self, rpc: LCMRPC) -> None: self._rpc = rpc - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs): # type: ignore[no-untyped-def] if not self._rpc: logger.warning("RPC client not initialized") return None @@ -61,19 +61,19 @@ def __call__(self, *args, **kwargs): # For stop, use call_nowait to avoid deadlock # (the remote side stops its RPC service before responding) if self._name == "stop": - self._rpc.call_nowait(f"{self._remote_name}/{self._name}", (args, kwargs)) + self._rpc.call_nowait(f"{self._remote_name}/{self._name}", (args, kwargs)) # type: ignore[arg-type] if self._stop_rpc_client: self._stop_rpc_client() return None - result, unsub_fn = self._rpc.call_sync(f"{self._remote_name}/{self._name}", (args, kwargs)) + result, unsub_fn = self._rpc.call_sync(f"{self._remote_name}/{self._name}", (args, kwargs)) # type: ignore[arg-type] self._unsub_fns.append(unsub_fn) return result - def __getstate__(self): + def __getstate__(self): # type: ignore[no-untyped-def] return (self._original_method, self._name, self._remote_name) - def __setstate__(self, state) -> None: + def __setstate__(self, state) -> None: # type: ignore[no-untyped-def] self._original_method, self._name, self._remote_name = state self._unsub_fns = [] self._rpc = None @@ -81,14 +81,14 @@ def __setstate__(self, state) -> None: class RPCClient: - def __init__(self, actor_instance, actor_class) -> None: + def __init__(self, actor_instance, actor_class) -> None: # type: ignore[no-untyped-def] self.rpc = LCMRPC() self.actor_class = actor_class self.remote_name = actor_class.__name__ self.actor_instance = actor_instance self.rpcs = actor_class.rpcs.keys() self.rpc.start() - self._unsub_fns = [] + self._unsub_fns = [] # type: ignore[var-annotated] def stop_rpc_client(self) -> None: for unsub in self._unsub_fns: @@ -101,9 +101,9 @@ def stop_rpc_client(self) -> None: if self.rpc: self.rpc.stop() - self.rpc = None + self.rpc = None # type: ignore[assignment] - def __reduce__(self): + def __reduce__(self): # type: ignore[no-untyped-def] # Return the class and the arguments needed to reconstruct the object return ( self.__class__, @@ -111,7 +111,7 @@ def __reduce__(self): ) # passthrough - def __getattr__(self, name: str): + def __getattr__(self, name: str): # type: ignore[no-untyped-def] # Check if accessing a known safe attribute to avoid recursion if name in { "__class__", diff --git a/dimos/core/skill_module.py b/dimos/core/skill_module.py index 4c6a42fa5b..fa5abd381f 100644 --- a/dimos/core/skill_module.py +++ b/dimos/core/skill_module.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,15 +18,20 @@ class SkillModule(Module): - """Use this module if you want to auto-register skills to an LlmAgent.""" + """Use this module if you want to auto-register skills to an AgentSpec.""" @rpc - def set_LlmAgent_register_skills(self, callable: RpcCall) -> None: - callable.set_rpc(self.rpc) + def set_AgentSpec_register_skills(self, callable: RpcCall) -> None: + callable.set_rpc(self.rpc) # type: ignore[arg-type] + callable(RPCClient(self, self.__class__)) + + @rpc + def set_MCPModule_register_skills(self, callable: RpcCall) -> None: + callable.set_rpc(self.rpc) # type: ignore[arg-type] callable(RPCClient(self, self.__class__)) def __getstate__(self) -> None: pass - def __setstate__(self, _state) -> None: + def __setstate__(self, _state) -> None: # type: ignore[no-untyped-def] pass diff --git a/dimos/core/stream.py b/dimos/core/stream.py index 1868ed6dbd..66d8cf4ef5 100644 --- a/dimos/core/stream.py +++ b/dimos/core/stream.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,22 +28,29 @@ from reactivex.disposable import Disposable import dimos.core.colors as colors +from dimos.core.resource import Resource +from dimos.utils.logging_config import setup_logger import dimos.utils.reactive as reactive from dimos.utils.reactive import backpressure if TYPE_CHECKING: from collections.abc import Callable + from reactivex.observable import Observable + T = TypeVar("T") +logger = setup_logger() + + class ObservableMixin(Generic[T]): # subscribes and returns the first value it receives # might be nicer to write without rxpy but had this snippet ready def get_next(self, timeout: float = 10.0) -> T: try: - return ( - self.observable() + return ( # type: ignore[no-any-return] + self.observable() # type: ignore[no-untyped-call] .pipe(ops.first(), *([ops.timeout(timeout)] if timeout is not None else [])) .run() ) @@ -51,18 +58,18 @@ def get_next(self, timeout: float = 10.0) -> T: raise Exception(f"No value received after {timeout} seconds") from e def hot_latest(self) -> Callable[[], T]: - return reactive.getter_streaming(self.observable()) + return reactive.getter_streaming(self.observable()) # type: ignore[no-untyped-call] - def pure_observable(self): - def _subscribe(observer, scheduler=None): - unsubscribe = self.subscribe(observer.on_next) + def pure_observable(self) -> Observable[T]: + def _subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] + unsubscribe = self.subscribe(observer.on_next) # type: ignore[attr-defined] return Disposable(unsubscribe) return rx.create(_subscribe) # default return is backpressured because most # use cases will want this by default - def observable(self): + def observable(self): # type: ignore[no-untyped-def] return backpressure(self.pure_observable()) @@ -73,26 +80,28 @@ class State(enum.Enum): FLOWING = "flowing" # runtime: data observed -class Transport(ObservableMixin[T]): +class Transport(Resource, ObservableMixin[T]): # used by local Output - def broadcast(self, selfstream: Out[T], value: T) -> None: ... - - def publish(self, msg: T) -> None: - self.broadcast(None, msg) + def broadcast(self, selfstream: Out[T], value: T) -> None: + raise NotImplementedError # used by local Input - def subscribe(self, selfstream: In[T], callback: Callable[[T], any]) -> None: ... + def subscribe(self, callback: Callable[[T], Any], selfstream: Stream[T]) -> Callable[[], None]: + raise NotImplementedError + + def publish(self, msg: T) -> None: + self.broadcast(None, msg) # type: ignore[arg-type] class Stream(Generic[T]): - _transport: Transport | None + _transport: Transport | None # type: ignore[type-arg] def __init__( self, type: type[T], name: str, owner: Any | None = None, - transport: Transport | None = None, + transport: Transport | None = None, # type: ignore[type-arg] ) -> None: self.name = name self.owner = owner @@ -107,11 +116,11 @@ def type_name(self) -> str: return getattr(self.type, "__name__", repr(self.type)) def _color_fn(self) -> Callable[[str], str]: - if self.state == State.UNBOUND: + if self.state == State.UNBOUND: # type: ignore[attr-defined] return colors.orange - if self.state == State.READY: + if self.state == State.READY: # type: ignore[attr-defined] return colors.blue - if self.state == State.CONNECTED: + if self.state == State.CONNECTED: # type: ignore[attr-defined] return colors.green return lambda s: s @@ -122,29 +131,35 @@ def __str__(self) -> str: + self._color_fn()(f"{self.name}[{self.type_name}]") + " @ " + ( - colors.orange(self.owner) + colors.orange(self.owner) # type: ignore[arg-type] if isinstance(self.owner, Actor) - else colors.green(self.owner) + else colors.green(self.owner) # type: ignore[arg-type] ) + ("" if not self._transport else " via " + str(self._transport)) ) -class Out(Stream[T]): - _transport: Transport +class Out(Stream[T], ObservableMixin[T]): + _transport: Transport # type: ignore[type-arg] + _subscribers: list[Callable[[T], Any]] - def __init__(self, *argv, **kwargs) -> None: + def __init__(self, *argv, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*argv, **kwargs) + self._subscribers = [] @property def transport(self) -> Transport[T]: return self._transport + @transport.setter + def transport(self, value: Transport[T]) -> None: + self._transport = value + @property def state(self) -> State: return State.UNBOUND if self.owner is None else State.READY - def __reduce__(self): + def __reduce__(self): # type: ignore[no-untyped-def] if self.owner is None or not hasattr(self.owner, "ref"): raise ValueError("Cannot serialise Out without an owner ref") return ( @@ -157,10 +172,19 @@ def __reduce__(self): ), ) - def publish(self, msg): - if not hasattr(self, "_transport") or self._transport is None: - raise Exception(f"{self} transport for stream is not specified,") - self._transport.broadcast(self, msg) + def publish(self, msg: T) -> None: + if hasattr(self, "_transport") and self._transport is not None: + self._transport.broadcast(self, msg) + for cb in self._subscribers: + cb(msg) + + def subscribe(self, cb: Callable[[T], Any]) -> Callable[[], None]: + self._subscribers.append(cb) + + def unsubscribe() -> None: + self._subscribers.remove(cb) + + return unsubscribe class RemoteStream(Stream[T]): @@ -171,19 +195,19 @@ def state(self) -> State: # this won't work but nvm @property def transport(self) -> Transport[T]: - return self._transport + return self._transport # type: ignore[return-value] @transport.setter def transport(self, value: Transport[T]) -> None: - self.owner.set_transport(self.name, value).result() + self.owner.set_transport(self.name, value).result() # type: ignore[union-attr] self._transport = value class RemoteOut(RemoteStream[T]): - def connect(self, other: RemoteIn[T]): + def connect(self, other: RemoteIn[T]): # type: ignore[no-untyped-def] return other.connect(self) - def subscribe(self, cb) -> Callable[[], None]: + def subscribe(self, cb: Callable[[T], Any]) -> Callable[[], None]: return self.transport.subscribe(cb, self) @@ -191,7 +215,7 @@ def subscribe(self, cb) -> Callable[[], None]: # as views from inside of the module class In(Stream[T], ObservableMixin[T]): connection: RemoteOut[T] | None = None - _transport: Transport + _transport: Transport # type: ignore[type-arg] def __str__(self) -> str: mystr = super().__str__() @@ -201,23 +225,30 @@ def __str__(self) -> str: return (mystr + " ◀─").ljust(60, "─") + f" {self.connection}" - def __reduce__(self): + def __reduce__(self): # type: ignore[no-untyped-def] if self.owner is None or not hasattr(self.owner, "ref"): raise ValueError("Cannot serialise Out without an owner ref") return (RemoteIn, (self.type, self.name, self.owner.ref, self._transport)) @property def transport(self) -> Transport[T]: - if not self._transport: + if not self._transport and self.connection: self._transport = self.connection.transport return self._transport + @transport.setter + def transport(self, value: Transport[T]) -> None: + self._transport = value + + def connect(self, value: Out[T]) -> None: + value.subscribe(self.transport.publish) # type: ignore[arg-type] + @property def state(self) -> State: return State.UNBOUND if self.owner is None else State.READY # returns unsubscribe function - def subscribe(self, cb) -> Callable[[], None]: + def subscribe(self, cb: Callable[[T], Any]) -> Callable[[], None]: return self.transport.subscribe(cb, self) @@ -225,17 +256,17 @@ def subscribe(self, cb) -> Callable[[], None]: # used for configuring connections, setting a transport class RemoteIn(RemoteStream[T]): def connect(self, other: RemoteOut[T]) -> None: - return self.owner.connect_stream(self.name, other).result() + return self.owner.connect_stream(self.name, other).result() # type: ignore[no-any-return, union-attr] # this won't work but that's ok - @property + @property # type: ignore[misc] def transport(self) -> Transport[T]: - return self._transport + return self._transport # type: ignore[return-value] - def publish(self, msg) -> None: - self.transport.broadcast(self, msg) + def publish(self, msg) -> None: # type: ignore[no-untyped-def] + self.transport.broadcast(self, msg) # type: ignore[arg-type] - @transport.setter + @transport.setter # type: ignore[attr-defined, no-redef, untyped-decorator] def transport(self, value: Transport[T]) -> None: - self.owner.set_transport(self.name, value).result() + self.owner.set_transport(self.name, value).result() # type: ignore[union-attr] self._transport = value diff --git a/dimos/core/test_blueprints.py b/dimos/core/test_blueprints.py index 59f541aa58..54313f1a84 100644 --- a/dimos/core/test_blueprints.py +++ b/dimos/core/test_blueprints.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,6 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + +from dimos.core._test_future_annotations_helper import ( + FutureData, + FutureModuleIn, + FutureModuleOut, +) from dimos.core.blueprints import ( ModuleBlueprint, ModuleBlueprintSet, @@ -20,7 +27,6 @@ autoconnect, ) from dimos.core.core import rpc -from dimos.core.global_config import GlobalConfig from dimos.core.module import Module from dimos.core.module_coordinator import ModuleCoordinator from dimos.core.rpc_client import RpcCall @@ -55,8 +61,8 @@ class Data3: class ModuleA(Module): - data1: Out[Data1] = None - data2: Out[Data2] = None + data1: Out[Data1] + data2: Out[Data2] @rpc def get_name(self) -> str: @@ -64,9 +70,9 @@ def get_name(self) -> str: class ModuleB(Module): - data1: In[Data1] = None - data2: In[Data2] = None - data3: Out[Data3] = None + data1: In[Data1] + data2: In[Data2] + data3: Out[Data3] _module_a_get_name: callable = None @@ -83,7 +89,7 @@ def what_is_as_name(self) -> str: class ModuleC(Module): - data3: In[Data3] = None + data3: In[Data3] module_a = ModuleA.blueprint @@ -155,7 +161,7 @@ def test_build_happy_path() -> None: blueprint_set = autoconnect(module_a(), module_b(), module_c()) - coordinator = blueprint_set.build(GlobalConfig()) + coordinator = blueprint_set.build() try: assert isinstance(coordinator, ModuleCoordinator) @@ -185,16 +191,91 @@ def test_build_happy_path() -> None: coordinator.stop() -def test_remapping(): +def test_name_conflicts_are_reported() -> None: + class ModuleA(Module): + shared_data: Out[Data1] + + class ModuleB(Module): + shared_data: In[Data2] + + blueprint_set = autoconnect(ModuleA.blueprint(), ModuleB.blueprint()) + + try: + blueprint_set._verify_no_name_conflicts() + pytest.fail("Expected ValueError to be raised") + except ValueError as e: + error_message = str(e) + assert "Blueprint cannot start because there are conflicting connections" in error_message + assert "'shared_data' has conflicting types" in error_message + assert "Data1 in ModuleA" in error_message + assert "Data2 in ModuleB" in error_message + + +def test_multiple_name_conflicts_are_reported() -> None: + class Module1(Module): + sensor_data: Out[Data1] + control_signal: Out[Data2] + + class Module2(Module): + sensor_data: In[Data2] + control_signal: In[Data3] + + blueprint_set = autoconnect(Module1.blueprint(), Module2.blueprint()) + + try: + blueprint_set._verify_no_name_conflicts() + pytest.fail("Expected ValueError to be raised") + except ValueError as e: + error_message = str(e) + assert "Blueprint cannot start because there are conflicting connections" in error_message + assert "'sensor_data' has conflicting types" in error_message + assert "'control_signal' has conflicting types" in error_message + + +def test_that_remapping_can_resolve_conflicts() -> None: + class Module1(Module): + data: Out[Data1] + + class Module2(Module): + data: Out[Data2] # Would conflict with Module1.data + + class Module3(Module): + data1: In[Data1] + data2: In[Data2] + + # Without remapping, should raise conflict error + blueprint_set = autoconnect(Module1.blueprint(), Module2.blueprint(), Module3.blueprint()) + + try: + blueprint_set._verify_no_name_conflicts() + pytest.fail("Expected ValueError due to conflict") + except ValueError as e: + assert "'data' has conflicting types" in str(e) + + # With remapping to resolve the conflict + blueprint_set_remapped = autoconnect( + Module1.blueprint(), Module2.blueprint(), Module3.blueprint() + ).remappings( + [ + (Module1, "data", "data1"), + (Module2, "data", "data2"), + ] + ) + + # Should not raise any exception after remapping + blueprint_set_remapped._verify_no_name_conflicts() + + +def test_remapping() -> None: """Test that remapping connections works correctly.""" pubsub.lcm.autoconf() # Define test modules with connections that will be remapped class SourceModule(Module): - color_image: Out[Data1] = None # Will be remapped to 'remapped_data' + color_image: Out[Data1] # Will be remapped to 'remapped_data' class TargetModule(Module): - remapped_data: In[Data1] = None # Receives the remapped connection + remapped_data: In[Data1] # Receives the remapped connection # Create blueprint with remapping blueprint_set = autoconnect( @@ -216,7 +297,7 @@ class TargetModule(Module): assert ("color_image", Data1) not in blueprint_set._all_name_types # Build and verify connections work - coordinator = blueprint_set.build(GlobalConfig()) + coordinator = blueprint_set.build() try: source_instance = coordinator.get_instance(SourceModule) @@ -240,3 +321,50 @@ class TargetModule(Module): finally: coordinator.stop() + + +def test_future_annotations_support() -> None: + """Test that modules using `from __future__ import annotations` work correctly. + + PEP 563 (future annotations) stores annotations as strings instead of actual types. + This test verifies that _make_module_blueprint properly resolves string annotations + to the actual In/Out types. + """ + + # Test that connections are properly extracted from modules with future annotations + out_blueprint = _make_module_blueprint(FutureModuleOut, args=(), kwargs={}) + assert len(out_blueprint.connections) == 1 + assert out_blueprint.connections[0] == ModuleConnection( + name="data", type=FutureData, direction="out" + ) + + in_blueprint = _make_module_blueprint(FutureModuleIn, args=(), kwargs={}) + assert len(in_blueprint.connections) == 1 + assert in_blueprint.connections[0] == ModuleConnection( + name="data", type=FutureData, direction="in" + ) + + +def test_future_annotations_autoconnect() -> None: + """Test that autoconnect works with modules using `from __future__ import annotations`.""" + + blueprint_set = autoconnect(FutureModuleOut.blueprint(), FutureModuleIn.blueprint()) + + coordinator = blueprint_set.build() + + try: + out_instance = coordinator.get_instance(FutureModuleOut) + in_instance = coordinator.get_instance(FutureModuleIn) + + assert out_instance is not None + assert in_instance is not None + + # Both should have transports set + assert out_instance.data.transport is not None + assert in_instance.data.transport is not None + + # They should be connected via the same transport + assert out_instance.data.transport.topic == in_instance.data.transport.topic + + finally: + coordinator.stop() diff --git a/dimos/core/test_core.py b/dimos/core/test_core.py index 97f09a4182..597b580c5c 100644 --- a/dimos/core/test_core.py +++ b/dimos/core/test_core.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -35,10 +35,10 @@ class Navigation(Module): - mov: Out[Vector3] = None - lidar: In[LidarMessage] = None - target_position: In[Vector3] = None - odometry: In[Odometry] = None + mov: Out[Vector3] + lidar: In[LidarMessage] + target_position: In[Vector3] + odometry: In[Odometry] odom_msg_count = 0 lidar_msg_count = 0 @@ -87,7 +87,7 @@ def test_classmethods() -> None: # Check that we have the expected RPC methods assert "navigate_to" in class_rpcs, "navigate_to should be in rpcs" assert "start" in class_rpcs, "start should be in rpcs" - assert len(class_rpcs) == 6 + assert len(class_rpcs) == 8 # Check that the values are callable assert callable(class_rpcs["navigate_to"]), "navigate_to should be callable" diff --git a/dimos/core/test_modules.py b/dimos/core/test_modules.py index d1f925aff2..7bd995c857 100644 --- a/dimos/core/test_modules.py +++ b/dimos/core/test_modules.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/core/test_rpcstress.py b/dimos/core/test_rpcstress.py index fc00a95854..1d09f3e210 100644 --- a/dimos/core/test_rpcstress.py +++ b/dimos/core/test_rpcstress.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ class Counter(Module): current_count: int = 0 - count_stream: Out[int] = None + count_stream: Out[int] def __init__(self) -> None: super().__init__() @@ -38,7 +38,7 @@ def increment(self): class CounterValidator(Module): """Calls counter.increment() as fast as possible and validates no numbers are skipped.""" - count_in: In[int] = None + count_in: In[int] def __init__(self, increment_func) -> None: super().__init__() diff --git a/dimos/core/test_stream.py b/dimos/core/test_stream.py index 91091e42af..4909cd8cc5 100644 --- a/dimos/core/test_stream.py +++ b/dimos/core/test_stream.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ def sub2_msgs_len(self) -> int: class ClassicSubscriber(SubscriberBase): - odom: In[Odometry] = None + odom: In[Odometry] unsub: Callable[[], None] | None = None unsub2: Callable[[], None] | None = None @@ -82,7 +82,7 @@ def stop(self) -> None: class RXPYSubscriber(SubscriberBase): - odom: In[Odometry] = None + odom: In[Odometry] unsub: Callable[[], None] | None = None unsub2: Callable[[], None] | None = None diff --git a/dimos/core/testing.py b/dimos/core/testing.py index 92f6d6b497..832f1f985b 100644 --- a/dimos/core/testing.py +++ b/dimos/core/testing.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ from threading import Event, Thread import time -import pytest +import pytest # type: ignore[import-not-found] from dimos.core import In, Module, Out, rpc, start from dimos.msgs.geometry_msgs import Vector3 @@ -25,21 +25,21 @@ @pytest.fixture -def dimos(): +def dimos(): # type: ignore[no-untyped-def] """Fixture to create a Dimos client for testing.""" client = start(2) yield client - client.stop() + client.stop() # type: ignore[attr-defined] class MockRobotClient(Module): - odometry: Out[Odometry] = None - lidar: Out[LidarMessage] = None - mov: In[Vector3] = None + odometry: Out[Odometry] + lidar: Out[LidarMessage] + mov: In[Vector3] mov_msg_count = 0 - def mov_callback(self, msg) -> None: + def mov_callback(self, msg) -> None: # type: ignore[no-untyped-def] self.mov_msg_count += 1 def __init__(self) -> None: @@ -51,8 +51,8 @@ def __init__(self) -> None: def start(self) -> None: super().start() - self._thread = Thread(target=self.odomloop) - self._thread.start() + self._thread = Thread(target=self.odomloop) # type: ignore[assignment] + self._thread.start() # type: ignore[attr-defined] self.mov.subscribe(self.mov_callback) @rpc @@ -78,6 +78,6 @@ def odomloop(self) -> None: self.odometry.publish(odom) lidarmsg = next(lidariter) - lidarmsg.pubtime = time.perf_counter() + lidarmsg.pubtime = time.perf_counter() # type: ignore[union-attr] self.lidar.publish(lidarmsg) time.sleep(0.1) diff --git a/dimos/core/transport.py b/dimos/core/transport.py index 32f75e6c33..8ffbfc91f4 100644 --- a/dimos/core/transport.py +++ b/dimos/core/transport.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,8 +14,7 @@ from __future__ import annotations -import traceback -from typing import TypeVar +from typing import Any, TypeVar import dimos.core.colors as colors @@ -26,7 +25,7 @@ TypeVar, ) -from dimos.core.stream import In, RemoteIn, Transport +from dimos.core.stream import In, Out, Stream, Transport from dimos.protocol.pubsub.jpeg_shm import JpegSharedMemory from dimos.protocol.pubsub.lcmpubsub import LCM, JpegLCM, PickleLCM, Topic as LCMTopic from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory, SharedMemory @@ -34,13 +33,13 @@ if TYPE_CHECKING: from collections.abc import Callable -T = TypeVar("T") +T = TypeVar("T") # type: ignore[misc] class PubSubTransport(Transport[T]): - topic: any + topic: Any - def __init__(self, topic: any) -> None: + def __init__(self, topic: Any) -> None: self.topic = topic def __str__(self) -> str: @@ -54,179 +53,163 @@ def __str__(self) -> str: class pLCMTransport(PubSubTransport[T]): _started: bool = False - def __init__(self, topic: str, **kwargs) -> None: + def __init__(self, topic: str, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(topic) self.lcm = PickleLCM(**kwargs) - def __reduce__(self): + def __reduce__(self): # type: ignore[no-untyped-def] return (pLCMTransport, (self.topic,)) - def broadcast(self, _, msg) -> None: + def broadcast(self, _: Out[T] | None, msg: T) -> None: if not self._started: self.lcm.start() self._started = True self.lcm.publish(self.topic, msg) - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + def subscribe( + self, callback: Callable[[T], Any], selfstream: Stream[T] | None = None + ) -> Callable[[], None]: if not self._started: self.lcm.start() self._started = True return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) + def start(self) -> None: ... + + def stop(self) -> None: + self.lcm.stop() + class LCMTransport(PubSubTransport[T]): _started: bool = False - def __init__(self, topic: str, type: type, **kwargs) -> None: + def __init__(self, topic: str, type: type, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(LCMTopic(topic, type)) if not hasattr(self, "lcm"): self.lcm = LCM(**kwargs) - def __reduce__(self): + def start(self) -> None: ... + + def stop(self) -> None: + self.lcm.stop() + + def __reduce__(self): # type: ignore[no-untyped-def] return (LCMTransport, (self.topic.topic, self.topic.lcm_type)) - def broadcast(self, _, msg) -> None: + def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] if not self._started: self.lcm.start() self._started = True self.lcm.publish(self.topic, msg) - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: # type: ignore[assignment, override] if not self._started: self.lcm.start() self._started = True - return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) + return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[return-value] -class JpegLcmTransport(LCMTransport): - def __init__(self, topic: str, type: type, **kwargs): - self.lcm = JpegLCM(**kwargs) +class JpegLcmTransport(LCMTransport): # type: ignore[type-arg] + def __init__(self, topic: str, type: type, **kwargs) -> None: # type: ignore[no-untyped-def] + self.lcm = JpegLCM(**kwargs) # type: ignore[assignment] super().__init__(topic, type) - def __reduce__(self): + def __reduce__(self): # type: ignore[no-untyped-def] return (JpegLcmTransport, (self.topic.topic, self.topic.lcm_type)) + def start(self) -> None: ... + + def stop(self) -> None: + self.lcm.stop() + class pSHMTransport(PubSubTransport[T]): _started: bool = False - def __init__(self, topic: str, **kwargs) -> None: + def __init__(self, topic: str, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(topic) self.shm = PickleSharedMemory(**kwargs) - def __reduce__(self): + def __reduce__(self): # type: ignore[no-untyped-def] return (pSHMTransport, (self.topic,)) - def broadcast(self, _, msg) -> None: + def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] if not self._started: self.shm.start() self._started = True self.shm.publish(self.topic, msg) - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: # type: ignore[assignment, override] if not self._started: self.shm.start() self._started = True - return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) + return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[return-value] + + def start(self) -> None: ... + + def stop(self) -> None: + self.shm.stop() class SHMTransport(PubSubTransport[T]): _started: bool = False - def __init__(self, topic: str, **kwargs) -> None: + def __init__(self, topic: str, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(topic) self.shm = SharedMemory(**kwargs) - def __reduce__(self): + def __reduce__(self): # type: ignore[no-untyped-def] return (SHMTransport, (self.topic,)) - def broadcast(self, _, msg) -> None: + def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] if not self._started: self.shm.start() self._started = True self.shm.publish(self.topic, msg) - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] | None = None) -> None: # type: ignore[override] if not self._started: self.shm.start() self._started = True - return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) + return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[arg-type, return-value] + + def start(self) -> None: ... + + def stop(self) -> None: + self.shm.stop() class JpegShmTransport(PubSubTransport[T]): _started: bool = False - def __init__(self, topic: str, quality: int = 75, **kwargs): + def __init__(self, topic: str, quality: int = 75, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(topic) self.shm = JpegSharedMemory(quality=quality, **kwargs) self.quality = quality - def __reduce__(self): + def __reduce__(self): # type: ignore[no-untyped-def] return (JpegShmTransport, (self.topic, self.quality)) - def broadcast(self, _, msg): + def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] if not self._started: self.shm.start() self._started = True self.shm.publish(self.topic, msg) - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] | None = None) -> None: # type: ignore[override] if not self._started: self.shm.start() self._started = True - return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) + return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[arg-type, return-value] + def start(self) -> None: ... -class DaskTransport(Transport[T]): - subscribers: list[Callable[[T], None]] - _started: bool = False - - def __init__(self) -> None: - self.subscribers = [] - - def __str__(self) -> str: - return colors.yellow("DaskTransport") - - def __reduce__(self): - return (DaskTransport, ()) - - def broadcast(self, selfstream: RemoteIn[T], msg: T) -> None: - for subscriber in self.subscribers: - # there is some sort of a bug here with losing worker loop - # print(subscriber.owner, subscriber.owner._worker, subscriber.owner._client) - # subscriber.owner._try_bind_worker_client() - # print(subscriber.owner, subscriber.owner._worker, subscriber.owner._client) - - subscriber.owner.dask_receive_msg(subscriber.name, msg).result() - - def dask_receive_msg(self, msg) -> None: - for subscriber in self.subscribers: - try: - subscriber(msg) - except Exception as e: - print( - colors.red("Error in DaskTransport subscriber callback:"), - e, - traceback.format_exc(), - ) - - # for outputs - def dask_register_subscriber(self, remoteInput: RemoteIn[T]) -> None: - self.subscribers.append(remoteInput) - - # for inputs - def subscribe(self, callback: Callable[[T], None], selfstream: In[T]) -> None: - if not self._started: - selfstream.connection.owner.dask_register_subscriber( - selfstream.connection.name, selfstream - ).result() - self._started = True - self.subscribers.append(callback) + def stop(self) -> None: ... class ZenohTransport(PubSubTransport[T]): ... diff --git a/dimos/dashboard/__init__.py b/dimos/dashboard/__init__.py new file mode 100644 index 0000000000..fc97805936 --- /dev/null +++ b/dimos/dashboard/__init__.py @@ -0,0 +1,34 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Dashboard module for visualization and monitoring. + +Rerun Initialization: + Main process (e.g., blueprints.build) starts Rerun server automatically. + Worker modules connect to the server via connect_rerun(). + +Usage in modules: + import rerun as rr + from dimos.dashboard.rerun_init import connect_rerun + + class MyModule(Module): + def start(self): + super().start() + connect_rerun() # Connect to Rerun server + rr.log("my/entity", my_data.to_rerun()) +""" + +from dimos.dashboard.rerun_init import connect_rerun, init_rerun_server, shutdown_rerun + +__all__ = ["connect_rerun", "init_rerun_server", "shutdown_rerun"] diff --git a/dimos/dashboard/dimos.rbl b/dimos/dashboard/dimos.rbl new file mode 100644 index 0000000000..160180e27a Binary files /dev/null and b/dimos/dashboard/dimos.rbl differ diff --git a/dimos/dashboard/rerun_init.py b/dimos/dashboard/rerun_init.py new file mode 100644 index 0000000000..4ccec8209d --- /dev/null +++ b/dimos/dashboard/rerun_init.py @@ -0,0 +1,166 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rerun initialization with multi-process support. + +Architecture: + - Main process calls init_rerun_server() to start gRPC server + viewer + - Worker processes call connect_rerun() to connect to the server + - All processes share the same Rerun recording stream + +Viewer modes (set via VIEWER_BACKEND config or environment variable): + - "rerun-web" (default): Web viewer on port 9090 + - "rerun-native": Native Rerun viewer (requires display) + - "foxglove": Use Foxglove instead of Rerun + +Usage: + # Set via environment: + VIEWER_BACKEND=rerun-web # or rerun-native or foxglove + + # Or via .env file: + viewer_backend=rerun-native + + # In main process (blueprints.py handles this automatically): + from dimos.dashboard.rerun_init import init_rerun_server + server_addr = init_rerun_server(viewer_mode="rerun-web") + + # In worker modules: + from dimos.dashboard.rerun_init import connect_rerun + connect_rerun() + + # On shutdown: + from dimos.dashboard.rerun_init import shutdown_rerun + shutdown_rerun() +""" + +import atexit +import threading + +import rerun as rr + +from dimos.core.global_config import GlobalConfig +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + +RERUN_GRPC_PORT = 9876 +RERUN_WEB_PORT = 9090 +RERUN_GRPC_ADDR = f"rerun+http://127.0.0.1:{RERUN_GRPC_PORT}/proxy" + +# Track initialization state +_server_started = False +_connected = False +_rerun_init_lock = threading.Lock() + + +def init_rerun_server(viewer_mode: str = "rerun-web", memory_limit: str = "4GB") -> str: + """Initialize Rerun server in the main process. + + Starts the gRPC server and optionally the web/native viewer. + Should only be called once from the main process. + + Args: + viewer_mode: One of "rerun-web", "rerun-native", or "rerun-grpc-only" + memory_limit: Maximum memory for Rerun viewer (e.g., "16GB", "25%"). Default 16GB. + + Returns: + Server address for workers to connect to. + + Raises: + RuntimeError: If server initialization fails. + """ + global _server_started + + if _server_started: + logger.debug("Rerun server already started") + return RERUN_GRPC_ADDR + + rr.init("dimos") + + if viewer_mode == "rerun-native": + # Spawn native viewer (requires display) + rr.spawn(port=RERUN_GRPC_PORT, connect=True, memory_limit=memory_limit) + logger.info("Rerun: spawned native viewer", port=RERUN_GRPC_PORT, memory_limit=memory_limit) + elif viewer_mode == "rerun-web": + # Start gRPC + web viewer (headless friendly) + server_uri = rr.serve_grpc(grpc_port=RERUN_GRPC_PORT) + rr.serve_web_viewer(web_port=RERUN_WEB_PORT, open_browser=False, connect_to=server_uri) + logger.info( + "Rerun: web viewer started", + web_port=RERUN_WEB_PORT, + url=f"http://localhost:{RERUN_WEB_PORT}", + ) + else: + # Just gRPC server, no viewer (connect externally) + rr.serve_grpc(grpc_port=RERUN_GRPC_PORT) + logger.info( + "Rerun: gRPC server only", + port=RERUN_GRPC_PORT, + connect_command=f"rerun --connect {RERUN_GRPC_ADDR}", + ) + + _server_started = True + + # Register shutdown handler + atexit.register(shutdown_rerun) + + return RERUN_GRPC_ADDR + + +def connect_rerun( + global_config: GlobalConfig | None = None, + server_addr: str | None = None, +) -> None: + """Connect to Rerun server from a worker process. + + Modules should check global_config.viewer_backend before calling this. + + Args: + global_config: Global configuration (checks viewer_backend) + server_addr: Server address to connect to. Defaults to RERUN_GRPC_ADDR. + """ + global _connected + + with _rerun_init_lock: + if _connected: + logger.debug("Already connected to Rerun server") + return + + # Skip if foxglove backend selected + if global_config and not global_config.viewer_backend.startswith("rerun"): + logger.debug("Rerun connection skipped", viewer_backend=global_config.viewer_backend) + return + + addr = server_addr or RERUN_GRPC_ADDR + + rr.init("dimos") + rr.connect_grpc(addr) + logger.info("Rerun: connected to server", addr=addr) + + _connected = True + + +def shutdown_rerun() -> None: + """Disconnect from Rerun and cleanup resources.""" + global _server_started, _connected + + if _server_started or _connected: + try: + rr.disconnect() + logger.info("Rerun: disconnected") + except Exception as e: + logger.warning("Rerun: error during disconnect", error=str(e)) + + _server_started = False + _connected = False diff --git a/dimos/dashboard/tf_rerun_module.py b/dimos/dashboard/tf_rerun_module.py new file mode 100644 index 0000000000..c862778cad --- /dev/null +++ b/dimos/dashboard/tf_rerun_module.py @@ -0,0 +1,112 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""TF Rerun Module - Automatically visualize all transforms in Rerun. + +This module subscribes to the /tf LCM topic and logs ALL transforms +to Rerun, providing automatic visualization of the robot's TF tree. + +Usage: + # In blueprints: + from dimos.dashboard.tf_rerun_module import tf_rerun + + def my_robot(): + return ( + robot_connection() + + tf_rerun() # Add TF visualization + + other_modules() + ) +""" + +from typing import Any + +import rerun as rr + +from dimos.core import Module, rpc +from dimos.core.global_config import GlobalConfig +from dimos.dashboard.rerun_init import connect_rerun +from dimos.msgs.tf2_msgs import TFMessage +from dimos.protocol.pubsub.lcmpubsub import LCM, Topic +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class TFRerunModule(Module): + """Subscribes to /tf LCM topic and logs all transforms to Rerun. + + This module automatically visualizes the TF tree in Rerun by: + - Subscribing to the /tf LCM topic (captures ALL transforms in the system) + - Logging each transform to its derived entity path (world/{child_frame_id}) + """ + + _global_config: GlobalConfig + _lcm: LCM | None = None + _unsubscribe: Any = None + + def __init__( + self, + global_config: GlobalConfig | None = None, + **kwargs: Any, + ) -> None: + """Initialize TFRerunModule. + + Args: + global_config: Optional global configuration for viewer backend settings + **kwargs: Additional arguments passed to parent Module + """ + super().__init__(**kwargs) + self._global_config = global_config or GlobalConfig() + + @rpc + def start(self) -> None: + """Start the TF visualization module.""" + super().start() + + # Only connect if Rerun backend is selected + if self._global_config.viewer_backend.startswith("rerun"): + connect_rerun(global_config=self._global_config) + + # Subscribe directly to LCM /tf topic (captures ALL transforms) + self._lcm = LCM() + self._lcm.start() + topic = Topic("/tf", TFMessage) + self._unsubscribe = self._lcm.subscribe(topic, self._on_tf_message) + logger.info("TFRerunModule: subscribed to /tf, logging all transforms to Rerun") + + def _on_tf_message(self, msg: TFMessage, topic: Topic) -> None: + """Log all transforms in TFMessage to Rerun. + + Args: + msg: TFMessage containing transforms to visualize + topic: The LCM topic (unused but required by callback signature) + """ + for entity_path, transform in msg.to_rerun(): # type: ignore[no-untyped-call] + rr.log(entity_path, transform) + + @rpc + def stop(self) -> None: + """Stop the TF visualization module and cleanup LCM subscription.""" + if self._unsubscribe: + self._unsubscribe() + self._unsubscribe = None + + if self._lcm: + self._lcm.stop() + self._lcm = None + + super().stop() + + +tf_rerun = TFRerunModule.blueprint diff --git a/dimos/e2e_tests/conftest.py b/dimos/e2e_tests/conftest.py new file mode 100644 index 0000000000..12d3e407ae --- /dev/null +++ b/dimos/e2e_tests/conftest.py @@ -0,0 +1,86 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable, Iterator + +import pytest + +from dimos.core.transport import pLCMTransport +from dimos.e2e_tests.dimos_cli_call import DimosCliCall +from dimos.e2e_tests.lcm_spy import LcmSpy +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion +from dimos.msgs.geometry_msgs.Vector3 import make_vector3 +from dimos.msgs.std_msgs.Bool import Bool + + +def _pose(x: float, y: float, theta: float) -> PoseStamped: + return PoseStamped( + position=make_vector3(x, y, 0), + orientation=Quaternion.from_euler(make_vector3(0, 0, theta)), + frame_id="map", + ) + + +@pytest.fixture +def lcm_spy() -> Iterator[LcmSpy]: + lcm_spy = LcmSpy() + lcm_spy.start() + yield lcm_spy + lcm_spy.stop() + + +@pytest.fixture +def follow_points(lcm_spy: LcmSpy): + def fun(*, points: list[tuple[float, float, float]], fail_message: str) -> None: + topic = "/goal_reached#std_msgs.Bool" + lcm_spy.save_topic(topic) + + for x, y, theta in points: + lcm_spy.publish("/goal_request#geometry_msgs.PoseStamped", _pose(x, y, theta)) + lcm_spy.wait_for_message_result( + topic, + Bool, + predicate=lambda v: bool(v), + fail_message=fail_message, + timeout=60.0, + ) + + yield fun + + +@pytest.fixture +def start_blueprint() -> Iterator[Callable[[str], DimosCliCall]]: + dimos_robot_call = DimosCliCall() + + def set_name_and_start(demo_name: str) -> DimosCliCall: + dimos_robot_call.demo_name = demo_name + dimos_robot_call.start() + return dimos_robot_call + + yield set_name_and_start + + dimos_robot_call.stop() + + +@pytest.fixture +def human_input(): + transport = pLCMTransport("/human_input") + transport.lcm.start() + + def send_human_input(message: str) -> None: + transport.publish(message) + + yield send_human_input + + transport.lcm.stop() diff --git a/dimos/e2e_tests/dimos_cli_call.py b/dimos/e2e_tests/dimos_cli_call.py new file mode 100644 index 0000000000..07def58782 --- /dev/null +++ b/dimos/e2e_tests/dimos_cli_call.py @@ -0,0 +1,69 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import signal +import subprocess +import time + + +class DimosCliCall: + process: subprocess.Popen[bytes] | None + demo_name: str | None = None + + def __init__(self) -> None: + self.process = None + + def start(self) -> None: + if self.demo_name is None: + raise ValueError("Demo name must be set before starting the process.") + + self.process = subprocess.Popen( + ["dimos", "--simulation", "run", self.demo_name], + ) + + def stop(self) -> None: + if self.process is None: + return + + try: + # Send the kill signal (SIGTERM for graceful shutdown) + self.process.send_signal(signal.SIGTERM) + + # Record the time when we sent the kill signal + shutdown_start = time.time() + + # Wait for the process to terminate with a 30-second timeout + try: + self.process.wait(timeout=30) + shutdown_duration = time.time() - shutdown_start + + # Verify it shut down in time + assert shutdown_duration <= 30, ( + f"Process took {shutdown_duration:.2f} seconds to shut down, " + f"which exceeds the 30-second limit" + ) + except subprocess.TimeoutExpired: + # If we reach here, the process didn't terminate in 30 seconds + self.process.kill() # Force kill + self.process.wait() # Clean up + raise AssertionError( + "Process did not shut down within 30 seconds after receiving SIGTERM" + ) + + except Exception: + # Clean up if something goes wrong + if self.process.poll() is None: # Process still running + self.process.kill() + self.process.wait() + raise diff --git a/dimos/e2e_tests/lcm_spy.py b/dimos/e2e_tests/lcm_spy.py new file mode 100644 index 0000000000..de0864dcd2 --- /dev/null +++ b/dimos/e2e_tests/lcm_spy.py @@ -0,0 +1,191 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable, Iterator +from contextlib import contextmanager +import math +import pickle +import threading +import time +from typing import Any + +import lcm + +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.protocol.service.lcmservice import LCMMsg, LCMService + + +class LcmSpy(LCMService): + l: lcm.LCM + messages: dict[str, list[bytes]] + _messages_lock: threading.Lock + _saved_topics: set[str] + _saved_topics_lock: threading.Lock + _topic_listeners: dict[str, list[Callable[[bytes], None]]] + _topic_listeners_lock: threading.Lock + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.l = lcm.LCM() + self.messages = {} + self._messages_lock = threading.Lock() + self._saved_topics = set() + self._saved_topics_lock = threading.Lock() + self._topic_listeners = {} + self._topic_listeners_lock = threading.Lock() + + def start(self) -> None: + super().start() + if self.l: + self.l.subscribe(".*", self.msg) + + def stop(self) -> None: + super().stop() + + def msg(self, topic: str, data: bytes) -> None: + with self._saved_topics_lock: + if topic in self._saved_topics: + with self._messages_lock: + self.messages.setdefault(topic, []).append(data) + + with self._topic_listeners_lock: + listeners = self._topic_listeners.get(topic) + if listeners: + for listener in listeners: + listener(data) + + def publish(self, topic: str, msg: Any) -> None: + self.l.publish(topic, msg.lcm_encode()) + + def save_topic(self, topic: str) -> None: + with self._saved_topics_lock: + self._saved_topics.add(topic) + + def register_topic_listener(self, topic: str, listener: Callable[[bytes], None]) -> int: + with self._topic_listeners_lock: + listeners = self._topic_listeners.setdefault(topic, []) + listener_index = len(listeners) + listeners.append(listener) + return listener_index + + def unregister_topic_listener(self, topic: str, listener_index: int) -> None: + with self._topic_listeners_lock: + listeners = self._topic_listeners[topic] + listeners.pop(listener_index) + + @contextmanager + def topic_listener(self, topic: str, listener: Callable[[bytes], None]) -> Iterator[None]: + listener_index = self.register_topic_listener(topic, listener) + try: + yield + finally: + self.unregister_topic_listener(topic, listener_index) + + def wait_until( + self, + *, + condition: Callable[[], bool], + timeout: float, + error_message: str, + poll_interval: float = 0.1, + ) -> None: + start_time = time.time() + while time.time() - start_time < timeout: + if condition(): + return + time.sleep(poll_interval) + raise TimeoutError(error_message) + + def wait_for_saved_topic(self, topic: str, timeout: float = 30.0) -> None: + def condition() -> bool: + with self._messages_lock: + return topic in self.messages + + self.wait_until( + condition=condition, + timeout=timeout, + error_message=f"Timeout waiting for topic {topic}", + ) + + def wait_for_saved_topic_content( + self, topic: str, content_contains: bytes, timeout: float = 30.0 + ) -> None: + def condition() -> bool: + with self._messages_lock: + return any(content_contains in msg for msg in self.messages.get(topic, [])) + + self.wait_until( + condition=condition, + timeout=timeout, + error_message=f"Timeout waiting for '{topic}' to contain '{content_contains!r}'", + ) + + def wait_for_message_pickle_result( + self, + topic: str, + predicate: Callable[[Any], bool], + fail_message: str, + timeout: float = 30.0, + ) -> None: + event = threading.Event() + + def listener(msg: bytes) -> None: + data = pickle.loads(msg) + if predicate(data["res"]): + event.set() + + with self.topic_listener(topic, listener): + self.wait_until( + condition=event.is_set, + timeout=timeout, + error_message=fail_message, + ) + + def wait_for_message_result( + self, + topic: str, + type: type[LCMMsg], + predicate: Callable[[Any], bool], + fail_message: str, + timeout: float = 30.0, + ) -> None: + event = threading.Event() + + def listener(msg: bytes) -> None: + data = type.lcm_decode(msg) + if predicate(data): + event.set() + + with self.topic_listener(topic, listener): + self.wait_until( + condition=event.is_set, + timeout=timeout, + error_message=fail_message, + ) + + def wait_until_odom_position( + self, x: float, y: float, threshold: float = 1, timeout: float = 60 + ) -> None: + def predicate(msg: PoseStamped) -> bool: + pos = msg.position + distance = math.sqrt((pos.x - x) ** 2 + (pos.y - y) ** 2) + return distance < threshold + + self.wait_for_message_result( + "/odom#geometry_msgs.PoseStamped", + PoseStamped, + predicate, + f"Failed to get to position x={x}, y={y}", + timeout, + ) diff --git a/dimos/e2e_tests/test_dimos_cli_e2e.py b/dimos/e2e_tests/test_dimos_cli_e2e.py new file mode 100644 index 0000000000..7571e113ad --- /dev/null +++ b/dimos/e2e_tests/test_dimos_cli_e2e.py @@ -0,0 +1,40 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import pytest + + +@pytest.mark.skipif(bool(os.getenv("CI")), reason="LCM spy doesn't work in CI.") +@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set.") +def test_dimos_skills(lcm_spy, start_blueprint, human_input) -> None: + lcm_spy.save_topic("/rpc/DemoCalculatorSkill/set_AgentSpec_register_skills/res") + lcm_spy.save_topic("/rpc/HumanInput/start/res") + lcm_spy.save_topic("/agent") + lcm_spy.save_topic("/rpc/DemoCalculatorSkill/sum_numbers/req") + lcm_spy.save_topic("/rpc/DemoCalculatorSkill/sum_numbers/res") + + start_blueprint("demo-skill") + + lcm_spy.wait_for_saved_topic("/rpc/DemoCalculatorSkill/set_AgentSpec_register_skills/res") + lcm_spy.wait_for_saved_topic("/rpc/HumanInput/start/res") + lcm_spy.wait_for_saved_topic_content("/agent", b"AIMessage") + + human_input("what is 52983 + 587237") + + lcm_spy.wait_for_saved_topic_content("/agent", b"640220") + + assert "/rpc/DemoCalculatorSkill/sum_numbers/req" in lcm_spy.messages + assert "/rpc/DemoCalculatorSkill/sum_numbers/res" in lcm_spy.messages diff --git a/dimos/e2e_tests/test_spatial_memory.py b/dimos/e2e_tests/test_spatial_memory.py new file mode 100644 index 0000000000..5029f46525 --- /dev/null +++ b/dimos/e2e_tests/test_spatial_memory.py @@ -0,0 +1,62 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +import math +import os +import time + +import pytest + +from dimos.e2e_tests.dimos_cli_call import DimosCliCall +from dimos.e2e_tests.lcm_spy import LcmSpy + + +@pytest.mark.skipif(bool(os.getenv("CI")), reason="LCM spy doesn't work in CI.") +@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set.") +@pytest.mark.e2e +def test_spatial_memory_navigation( + lcm_spy: LcmSpy, + start_blueprint: Callable[[str], DimosCliCall], + human_input: Callable[[str], None], + follow_points: Callable[..., None], +) -> None: + start_blueprint("unitree-go2-agentic") + + lcm_spy.save_topic("/rpc/HumanInput/start/res") + lcm_spy.wait_for_saved_topic("/rpc/HumanInput/start/res", timeout=120.0) + lcm_spy.save_topic("/agent") + lcm_spy.wait_for_saved_topic_content("/agent", b"AIMessage", timeout=120.0) + + time.sleep(5) + + follow_points( + points=[ + # Navigate to the bookcase. + (1, 1, 0), + (4, 1, 0), + (4.2, -1.1, -math.pi / 2), + (4.2, -3, -math.pi / 2), + (4.2, -5, -math.pi / 2), + # Move away, until it's not visible. + (1, 1, math.pi / 2), + ], + fail_message="Failed to get to the bookcase.", + ) + + time.sleep(5) + + human_input("go to the bookcase") + + lcm_spy.wait_until_odom_position(4.2, -5, threshold=2.0) diff --git a/dimos/environment/agent_environment.py b/dimos/environment/agent_environment.py deleted file mode 100644 index a5dab0e272..0000000000 --- a/dimos/environment/agent_environment.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path - -import cv2 -import numpy as np - -from .environment import Environment - - -class AgentEnvironment(Environment): - def __init__(self) -> None: - super().__init__() - self.environment_type = "agent" - self.frames = [] - self.current_frame_idx = 0 - self._depth_maps = [] - self._segmentations = [] - self._point_clouds = [] - - def initialize_from_images(self, images: list[str] | list[np.ndarray]) -> bool: - """Initialize environment from a list of image paths or numpy arrays. - - Args: - images: List of image paths or numpy arrays representing frames - - Returns: - bool: True if initialization successful, False otherwise - """ - try: - self.frames = [] - for img in images: - if isinstance(img, str): - frame = cv2.imread(img) - if frame is None: - raise ValueError(f"Failed to load image: {img}") - self.frames.append(frame) - elif isinstance(img, np.ndarray): - self.frames.append(img.copy()) - else: - raise ValueError(f"Unsupported image type: {type(img)}") - return True - except Exception as e: - print(f"Failed to initialize from images: {e}") - return False - - def initialize_from_file(self, file_path: str) -> bool: - """Initialize environment from a video file. - - Args: - file_path: Path to the video file - - Returns: - bool: True if initialization successful, False otherwise - """ - try: - if not Path(file_path).exists(): - raise FileNotFoundError(f"Video file not found: {file_path}") - - cap = cv2.VideoCapture(file_path) - self.frames = [] - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - self.frames.append(frame) - - cap.release() - return len(self.frames) > 0 - except Exception as e: - print(f"Failed to initialize from video: {e}") - return False - - def initialize_from_directory(self, directory_path: str) -> bool: - """Initialize environment from a directory of images.""" - # TODO: Implement directory initialization - raise NotImplementedError("Directory initialization not yet implemented") - - def label_objects(self) -> list[str]: - """Implementation of abstract method to label objects.""" - # TODO: Implement object labeling using a detection model - raise NotImplementedError("Object labeling not yet implemented") - - def generate_segmentations( - self, model: str | None = None, objects: list[str] | None = None, *args, **kwargs - ) -> list[np.ndarray]: - """Generate segmentations for the current frame.""" - # TODO: Implement segmentation generation using specified model - raise NotImplementedError("Segmentation generation not yet implemented") - - def get_segmentations(self) -> list[np.ndarray]: - """Return pre-computed segmentations for the current frame.""" - if self._segmentations: - return self._segmentations[self.current_frame_idx] - return [] - - def generate_point_cloud(self, object: str | None = None, *args, **kwargs) -> np.ndarray: - """Generate point cloud from the current frame.""" - # TODO: Implement point cloud generation - raise NotImplementedError("Point cloud generation not yet implemented") - - def get_point_cloud(self, object: str | None = None) -> np.ndarray: - """Return pre-computed point cloud.""" - if self._point_clouds: - return self._point_clouds[self.current_frame_idx] - return np.array([]) - - def generate_depth_map( - self, - stereo: bool | None = None, - monocular: bool | None = None, - model: str | None = None, - *args, - **kwargs, - ) -> np.ndarray: - """Generate depth map for the current frame.""" - # TODO: Implement depth map generation using specified method - raise NotImplementedError("Depth map generation not yet implemented") - - def get_depth_map(self) -> np.ndarray: - """Return pre-computed depth map for the current frame.""" - if self._depth_maps: - return self._depth_maps[self.current_frame_idx] - return np.array([]) - - def get_frame_count(self) -> int: - """Return the total number of frames.""" - return len(self.frames) - - def get_current_frame_index(self) -> int: - """Return the current frame index.""" - return self.current_frame_idx diff --git a/dimos/environment/colmap_environment.py b/dimos/environment/colmap_environment.py deleted file mode 100644 index f1b0986c77..0000000000 --- a/dimos/environment/colmap_environment.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# UNDER DEVELOPMENT 🚧🚧🚧 - -from pathlib import Path - -import cv2 -import pycolmap - -from dimos.environment.environment import Environment - - -class COLMAPEnvironment(Environment): - def initialize_from_images(self, image_dir): - """Initialize the environment from a set of image frames or video.""" - image_dir = Path(image_dir) - output_path = Path("colmap_output") - output_path.mkdir(exist_ok=True) - mvs_path = output_path / "mvs" - database_path = output_path / "database.db" - - # Step 1: Feature extraction - pycolmap.extract_features(database_path, image_dir) - - # Step 2: Feature matching - pycolmap.match_exhaustive(database_path) - - # Step 3: Sparse reconstruction - maps = pycolmap.incremental_mapping(database_path, image_dir, output_path) - maps[0].write(output_path) - - # Step 4: Dense reconstruction (optional) - pycolmap.undistort_images(mvs_path, output_path, image_dir) - pycolmap.patch_match_stereo(mvs_path) # Requires compilation with CUDA - pycolmap.stereo_fusion(mvs_path / "dense.ply", mvs_path) - - return maps - - def initialize_from_video(self, video_path, frame_output_dir): - """Extract frames from a video and initialize the environment.""" - video_path = Path(video_path) - frame_output_dir = Path(frame_output_dir) - frame_output_dir.mkdir(exist_ok=True) - - # Extract frames from the video - self._extract_frames_from_video(video_path, frame_output_dir) - - # Initialize from the extracted frames - return self.initialize_from_images(frame_output_dir) - - def _extract_frames_from_video(self, video_path, frame_output_dir) -> None: - """Extract frames from a video and save them to a directory.""" - cap = cv2.VideoCapture(str(video_path)) - frame_count = 0 - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - frame_filename = frame_output_dir / f"frame_{frame_count:04d}.jpg" - cv2.imwrite(str(frame_filename), frame) - frame_count += 1 - - cap.release() - - def label_objects(self) -> None: - pass - - def get_visualization(self, format_type) -> None: - pass - - def get_segmentations(self) -> None: - pass - - def get_point_cloud(self, object_id=None) -> None: - pass - - def get_depth_map(self) -> None: - pass diff --git a/dimos/environment/environment.py b/dimos/environment/environment.py index 8b0068cbae..ba1923b765 100644 --- a/dimos/environment/environment.py +++ b/dimos/environment/environment.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -33,14 +33,14 @@ def label_objects(self) -> list[str]: pass @abstractmethod - def get_visualization(self, format_type): + def get_visualization(self, format_type): # type: ignore[no-untyped-def] """Return different visualization formats like images, NERFs, or other 3D file types.""" pass @abstractmethod - def generate_segmentations( + def generate_segmentations( # type: ignore[no-untyped-def] self, model: str | None = None, objects: list[str] | None = None, *args, **kwargs - ) -> list[np.ndarray]: + ) -> list[np.ndarray]: # type: ignore[type-arg] """ Generate object segmentations of objects[] using neural methods. @@ -60,7 +60,7 @@ def generate_segmentations( pass @abstractmethod - def get_segmentations(self) -> list[np.ndarray]: + def get_segmentations(self) -> list[np.ndarray]: # type: ignore[type-arg] """ Get segmentations using a method like 'segment anything'. @@ -71,7 +71,7 @@ def get_segmentations(self) -> list[np.ndarray]: pass @abstractmethod - def generate_point_cloud(self, object: str | None = None, *args, **kwargs) -> np.ndarray: + def generate_point_cloud(self, object: str | None = None, *args, **kwargs) -> np.ndarray: # type: ignore[no-untyped-def, type-arg] """ Generate a point cloud for the entire environment or a specific object. @@ -91,7 +91,7 @@ def generate_point_cloud(self, object: str | None = None, *args, **kwargs) -> np pass @abstractmethod - def get_point_cloud(self, object: str | None = None) -> np.ndarray: + def get_point_cloud(self, object: str | None = None) -> np.ndarray: # type: ignore[type-arg] """ Return point clouds of the entire environment or a specific object. @@ -105,14 +105,14 @@ def get_point_cloud(self, object: str | None = None) -> np.ndarray: pass @abstractmethod - def generate_depth_map( + def generate_depth_map( # type: ignore[no-untyped-def] self, stereo: bool | None = None, monocular: bool | None = None, model: str | None = None, *args, **kwargs, - ) -> np.ndarray: + ) -> np.ndarray: # type: ignore[type-arg] """ Generate a depth map using monocular or stereo camera methods. @@ -134,7 +134,7 @@ def generate_depth_map( pass @abstractmethod - def get_depth_map(self) -> np.ndarray: + def get_depth_map(self) -> np.ndarray: # type: ignore[type-arg] """ Return a depth map of the environment. @@ -150,11 +150,11 @@ def get_depth_map(self) -> np.ndarray: """ pass - def initialize_from_images(self, images): + def initialize_from_images(self, images): # type: ignore[no-untyped-def] """Initialize the environment from a set of image frames or video.""" raise NotImplementedError("This method is not implemented for this environment type.") - def initialize_from_file(self, file_path): + def initialize_from_file(self, file_path): # type: ignore[no-untyped-def] """Initialize the environment from a spatial file type. Supported file types include: diff --git a/dimos/exceptions/agent_memory_exceptions.py b/dimos/exceptions/agent_memory_exceptions.py index 073e56c643..eec80be83c 100644 --- a/dimos/exceptions/agent_memory_exceptions.py +++ b/dimos/exceptions/agent_memory_exceptions.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -38,14 +38,14 @@ class AgentMemoryConnectionError(AgentMemoryError): cause (Exception, optional): Original exception, if any, that led to this error. """ - def __init__(self, message: str = "Failed to connect to the database", cause=None) -> None: + def __init__(self, message: str = "Failed to connect to the database", cause=None) -> None: # type: ignore[no-untyped-def] super().__init__(message) if cause: self.cause = cause self.traceback = traceback.format_exc() if cause else None def __str__(self) -> str: - return f"{self.message}\nCaused by: {self.cause!r}" if self.cause else self.message + return f"{self.message}\nCaused by: {self.cause!r}" if self.cause else self.message # type: ignore[attr-defined] class UnknownConnectionTypeError(AgentMemoryConnectionError): @@ -87,7 +87,7 @@ class DataNotFoundError(DataRetrievalError): message (str, optional): Human-readable message providing more detail. If not provided, a default message is generated. """ - def __init__(self, vector_id, message=None) -> None: + def __init__(self, vector_id, message=None) -> None: # type: ignore[no-untyped-def] message = message or f"Requested data for vector ID {vector_id} was not found." super().__init__(message) self.vector_id = vector_id diff --git a/dimos/hardware/README.md b/dimos/hardware/README.md index fb598e82cf..2587e3595d 100644 --- a/dimos/hardware/README.md +++ b/dimos/hardware/README.md @@ -26,4 +26,4 @@ On receiver machine: ```bash python3 dimos/hardware/gstreamer_camera_test_script.py --host 10.0.0.227 --port 5000 -``` \ No newline at end of file +``` diff --git a/dimos/hardware/camera/module.py b/dimos/hardware/camera/module.py deleted file mode 100644 index 0f0791650b..0000000000 --- a/dimos/hardware/camera/module.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from dataclasses import dataclass, field -import queue -import time - -from dimos_lcm.sensor_msgs import CameraInfo -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import Disposable -from reactivex.observable import Observable - -from dimos.agents2 import Output, Reducer, Stream, skill -from dimos.core import Module, Out, rpc -from dimos.core.module import Module, ModuleConfig -from dimos.hardware.camera.spec import ( - CameraHardware, -) -from dimos.hardware.camera.webcam import Webcam -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import Image -from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier - - -def default_transform(): - return Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ) - - -@dataclass -class CameraModuleConfig(ModuleConfig): - frame_id: str = "camera_link" - transform: Transform | None = field(default_factory=default_transform) - hardware: Callable[[], CameraHardware] | CameraHardware = Webcam - - -class CameraModule(Module): - image: Out[Image] = None - camera_info: Out[CameraInfo] = None - - hardware: CameraHardware = None - _module_subscription: Disposable | None = None - _camera_info_subscription: Disposable | None = None - _skill_stream: Observable[Image] | None = None - - default_config = CameraModuleConfig - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - @rpc - def start(self) -> str: - if callable(self.config.hardware): - self.hardware = self.config.hardware() - else: - self.hardware = self.config.hardware - - if self._module_subscription: - return "already started" - - stream = self.hardware.image_stream().pipe(sharpness_barrier(5)) - - # camera_info_stream = self.camera_info_stream(frequency=5.0) - - def publish_info(camera_info: CameraInfo) -> None: - self.camera_info.publish(camera_info) - - if self.config.transform is None: - return - - camera_link = self.config.transform - camera_link.ts = camera_info.ts - camera_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), - frame_id="camera_link", - child_frame_id="camera_optical", - ts=camera_link.ts, - ) - - self.tf.publish(camera_link, camera_optical) - - self._camera_info_subscription = self.camera_info_stream().subscribe(publish_info) - self._module_subscription = stream.subscribe(self.image.publish) - - @skill(stream=Stream.passive, output=Output.image, reducer=Reducer.latest) - def video_stream(self) -> Image: - """implicit video stream skill""" - _queue = queue.Queue(maxsize=1) - self.hardware.image_stream().subscribe(_queue.put) - - yield from iter(_queue.get, None) - - def camera_info_stream(self, frequency: float = 5.0) -> Observable[CameraInfo]: - def camera_info(_) -> CameraInfo: - self.hardware.camera_info.ts = time.time() - return self.hardware.camera_info - - return rx.interval(1.0 / frequency).pipe(ops.map(camera_info)) - - def stop(self) -> None: - if self._module_subscription: - self._module_subscription.dispose() - self._module_subscription = None - if self._camera_info_subscription: - self._camera_info_subscription.dispose() - self._camera_info_subscription = None - # Also stop the hardware if it has a stop method - if self.hardware and hasattr(self.hardware, "stop"): - self.hardware.stop() - super().stop() diff --git a/dimos/hardware/camera/spec.py b/dimos/hardware/camera/spec.py deleted file mode 100644 index b9722d6cd2..0000000000 --- a/dimos/hardware/camera/spec.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod, abstractproperty -from typing import Generic, Protocol, TypeVar - -from dimos_lcm.sensor_msgs import CameraInfo -from reactivex.observable import Observable - -from dimos.msgs.sensor_msgs import Image -from dimos.protocol.service import Configurable - - -class CameraConfig(Protocol): - frame_id_prefix: str | None - - -CameraConfigT = TypeVar("CameraConfigT", bound=CameraConfig) - - -class CameraHardware(ABC, Configurable[CameraConfigT], Generic[CameraConfigT]): - @abstractmethod - def image_stream(self) -> Observable[Image]: - pass - - @abstractproperty - def camera_info(self) -> CameraInfo: - pass - - -# This is an example, feel free to change spec for stereo cameras -# e.g., separate camera_info or streams for left/right, etc. -class StereoCameraHardware(ABC, Configurable[CameraConfigT], Generic[CameraConfigT]): - @abstractmethod - def image_stream(self) -> Observable[Image]: - pass - - @abstractmethod - def depth_stream(self) -> Observable[Image]: - pass - - @abstractproperty - def camera_info(self) -> CameraInfo: - pass diff --git a/dimos/hardware/camera/test_webcam.py b/dimos/hardware/camera/test_webcam.py deleted file mode 100644 index e2f99e85dd..0000000000 --- a/dimos/hardware/camera/test_webcam.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import pytest - -from dimos import core -from dimos.hardware.camera import zed -from dimos.hardware.camera.module import CameraModule -from dimos.hardware.camera.webcam import Webcam -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo, Image - - -@pytest.mark.tool -def test_streaming_single() -> None: - dimos = core.start(1) - - camera = dimos.deploy( - CameraModule, - transform=Transform( - translation=Vector3(0.05, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="sensor", - child_frame_id="camera_link", - ), - hardware=lambda: Webcam( - stereo_slice="left", - camera_index=0, - frequency=15, - camera_info=zed.CameraInfo.SingleWebcam, - ), - ) - - camera.image.transport = core.LCMTransport("/image1", Image) - camera.camera_info.transport = core.LCMTransport("/image1/camera_info", CameraInfo) - camera.start() - - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - camera.stop() - dimos.stop() - - -@pytest.mark.tool -def test_streaming_double() -> None: - dimos = core.start(2) - - camera1 = dimos.deploy( - CameraModule, - transform=Transform( - translation=Vector3(0.05, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="sensor", - child_frame_id="camera_link", - ), - hardware=lambda: Webcam( - stereo_slice="left", - camera_index=0, - frequency=15, - camera_info=zed.CameraInfo.SingleWebcam, - ), - ) - - camera2 = dimos.deploy( - CameraModule, - transform=Transform( - translation=Vector3(0.05, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="sensor", - child_frame_id="camera_link", - ), - hardware=lambda: Webcam( - camera_index=4, - frequency=15, - stereo_slice="left", - camera_info=zed.CameraInfo.SingleWebcam, - ), - ) - - camera1.image.transport = core.LCMTransport("/image1", Image) - camera1.camera_info.transport = core.LCMTransport("/image1/camera_info", CameraInfo) - camera1.start() - camera2.image.transport = core.LCMTransport("/image2", Image) - camera2.camera_info.transport = core.LCMTransport("/image2/camera_info", CameraInfo) - camera2.start() - - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - camera1.stop() - camera2.stop() - dimos.stop() diff --git a/dimos/hardware/camera/zed/__init__.py b/dimos/hardware/camera/zed/__init__.py deleted file mode 100644 index d7b70a1319..0000000000 --- a/dimos/hardware/camera/zed/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""ZED camera hardware interfaces.""" - -from pathlib import Path - -from dimos.msgs.sensor_msgs.CameraInfo import CalibrationProvider - -# Check if ZED SDK is available -try: - import pyzed.sl as sl - - HAS_ZED_SDK = True -except ImportError: - HAS_ZED_SDK = False - -# Only import ZED classes if SDK is available -if HAS_ZED_SDK: - from dimos.hardware.camera.zed.camera import ZEDCamera, ZEDModule -else: - # Provide stub classes when SDK is not available - class ZEDCamera: - def __init__(self, *args, **kwargs) -> None: - raise ImportError( - "ZED SDK not installed. Please install pyzed package to use ZED camera functionality." - ) - - class ZEDModule: - def __init__(self, *args, **kwargs) -> None: - raise ImportError( - "ZED SDK not installed. Please install pyzed package to use ZED camera functionality." - ) - - -# Set up camera calibration provider (always available) -CALIBRATION_DIR = Path(__file__).parent -CameraInfo = CalibrationProvider(CALIBRATION_DIR) - -__all__ = [ - "HAS_ZED_SDK", - "CameraInfo", - "ZEDCamera", - "ZEDModule", -] diff --git a/dimos/hardware/camera/zed/camera.py b/dimos/hardware/camera/zed/camera.py deleted file mode 100644 index fdcd93f731..0000000000 --- a/dimos/hardware/camera/zed/camera.py +++ /dev/null @@ -1,874 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from types import TracebackType -from typing import Any - -import cv2 -from dimos_lcm.sensor_msgs import CameraInfo -import numpy as np -import open3d as o3d -import pyzed.sl as sl -from reactivex import interval - -from dimos.core import Module, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 - -# Import LCM message types -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.msgs.std_msgs import Header -from dimos.protocol.tf import TF -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(__name__) - - -class ZEDCamera: - """ZED Camera capture node with neural depth processing.""" - - def __init__( - self, - camera_id: int = 0, - resolution: sl.RESOLUTION = sl.RESOLUTION.HD720, - depth_mode: sl.DEPTH_MODE = sl.DEPTH_MODE.NEURAL, - fps: int = 30, - **kwargs, - ) -> None: - """ - Initialize ZED Camera. - - Args: - camera_id: Camera ID (0 for first ZED) - resolution: ZED camera resolution - depth_mode: Depth computation mode - fps: Camera frame rate (default: 30) - """ - if sl is None: - raise ImportError("ZED SDK not installed. Please install pyzed package.") - - super().__init__(**kwargs) - - self.camera_id = camera_id - self.resolution = resolution - self.depth_mode = depth_mode - self.fps = fps - - # Initialize ZED camera - self.zed = sl.Camera() - self.init_params = sl.InitParameters() - self.init_params.camera_resolution = resolution - self.init_params.depth_mode = depth_mode - self.init_params.coordinate_system = sl.COORDINATE_SYSTEM.RIGHT_HANDED_Z_UP_X_FWD - self.init_params.coordinate_units = sl.UNIT.METER - self.init_params.camera_fps = fps - - # Set camera ID using the correct parameter name - if hasattr(self.init_params, "set_from_camera_id"): - self.init_params.set_from_camera_id(camera_id) - elif hasattr(self.init_params, "input"): - self.init_params.input.set_from_camera_id(camera_id) - - # Use enable_fill_mode instead of SENSING_MODE.STANDARD - self.runtime_params = sl.RuntimeParameters() - self.runtime_params.enable_fill_mode = True # False = STANDARD mode, True = FILL mode - - # Image containers - self.image_left = sl.Mat() - self.image_right = sl.Mat() - self.depth_map = sl.Mat() - self.point_cloud = sl.Mat() - self.confidence_map = sl.Mat() - - # Positional tracking - self.tracking_enabled = False - self.tracking_params = sl.PositionalTrackingParameters() - self.camera_pose = sl.Pose() - self.sensors_data = sl.SensorsData() - - self.is_opened = False - - def open(self) -> bool: - """Open the ZED camera.""" - try: - err = self.zed.open(self.init_params) - if err != sl.ERROR_CODE.SUCCESS: - logger.error(f"Failed to open ZED camera: {err}") - return False - - self.is_opened = True - logger.info("ZED camera opened successfully") - - # Get camera information - info = self.zed.get_camera_information() - logger.info(f"ZED Camera Model: {info.camera_model}") - logger.info(f"Serial Number: {info.serial_number}") - logger.info(f"Firmware: {info.camera_configuration.firmware_version}") - - return True - - except Exception as e: - logger.error(f"Error opening ZED camera: {e}") - return False - - def enable_positional_tracking( - self, - enable_area_memory: bool = False, - enable_pose_smoothing: bool = True, - enable_imu_fusion: bool = True, - set_floor_as_origin: bool = False, - initial_world_transform: sl.Transform | None = None, - ) -> bool: - """ - Enable positional tracking on the ZED camera. - - Args: - enable_area_memory: Enable area learning to correct tracking drift - enable_pose_smoothing: Enable pose smoothing - enable_imu_fusion: Enable IMU fusion if available - set_floor_as_origin: Set the floor as origin (useful for robotics) - initial_world_transform: Initial world transform - - Returns: - True if tracking enabled successfully - """ - if not self.is_opened: - logger.error("ZED camera not opened") - return False - - try: - # Configure tracking parameters - self.tracking_params.enable_area_memory = enable_area_memory - self.tracking_params.enable_pose_smoothing = enable_pose_smoothing - self.tracking_params.enable_imu_fusion = enable_imu_fusion - self.tracking_params.set_floor_as_origin = set_floor_as_origin - - if initial_world_transform is not None: - self.tracking_params.initial_world_transform = initial_world_transform - - # Enable tracking - err = self.zed.enable_positional_tracking(self.tracking_params) - if err != sl.ERROR_CODE.SUCCESS: - logger.error(f"Failed to enable positional tracking: {err}") - return False - - self.tracking_enabled = True - logger.info("Positional tracking enabled successfully") - return True - - except Exception as e: - logger.error(f"Error enabling positional tracking: {e}") - return False - - def disable_positional_tracking(self) -> None: - """Disable positional tracking.""" - if self.tracking_enabled: - self.zed.disable_positional_tracking() - self.tracking_enabled = False - logger.info("Positional tracking disabled") - - def get_pose( - self, reference_frame: sl.REFERENCE_FRAME = sl.REFERENCE_FRAME.WORLD - ) -> dict[str, Any] | None: - """ - Get the current camera pose. - - Args: - reference_frame: Reference frame (WORLD or CAMERA) - - Returns: - Dictionary containing: - - position: [x, y, z] in meters - - rotation: [x, y, z, w] quaternion - - euler_angles: [roll, pitch, yaw] in radians - - timestamp: Pose timestamp in nanoseconds - - confidence: Tracking confidence (0-100) - - valid: Whether pose is valid - """ - if not self.tracking_enabled: - logger.error("Positional tracking not enabled") - return None - - try: - # Get current pose - tracking_state = self.zed.get_position(self.camera_pose, reference_frame) - - if tracking_state == sl.POSITIONAL_TRACKING_STATE.OK: - # Extract translation - translation = self.camera_pose.get_translation().get() - - # Extract rotation (quaternion) - rotation = self.camera_pose.get_orientation().get() - - # Get Euler angles - euler = self.camera_pose.get_euler_angles() - - return { - "position": translation.tolist(), - "rotation": rotation.tolist(), # [x, y, z, w] - "euler_angles": euler.tolist(), # [roll, pitch, yaw] - "timestamp": self.camera_pose.timestamp.get_nanoseconds(), - "confidence": self.camera_pose.pose_confidence, - "valid": True, - "tracking_state": str(tracking_state), - } - else: - logger.warning(f"Tracking state: {tracking_state}") - return {"valid": False, "tracking_state": str(tracking_state)} - - except Exception as e: - logger.error(f"Error getting pose: {e}") - return None - - def get_imu_data(self) -> dict[str, Any] | None: - """ - Get IMU sensor data if available. - - Returns: - Dictionary containing: - - orientation: IMU orientation quaternion [x, y, z, w] - - angular_velocity: [x, y, z] in rad/s - - linear_acceleration: [x, y, z] in m/s² - - timestamp: IMU data timestamp - """ - if not self.is_opened: - logger.error("ZED camera not opened") - return None - - try: - # Get sensors data synchronized with images - if ( - self.zed.get_sensors_data(self.sensors_data, sl.TIME_REFERENCE.IMAGE) - == sl.ERROR_CODE.SUCCESS - ): - imu = self.sensors_data.get_imu_data() - - # Get IMU orientation - imu_orientation = imu.get_pose().get_orientation().get() - - # Get angular velocity - angular_vel = imu.get_angular_velocity() - - # Get linear acceleration - linear_accel = imu.get_linear_acceleration() - - return { - "orientation": imu_orientation.tolist(), - "angular_velocity": angular_vel.tolist(), - "linear_acceleration": linear_accel.tolist(), - "timestamp": self.sensors_data.timestamp.get_nanoseconds(), - "temperature": self.sensors_data.temperature.get(sl.SENSOR_LOCATION.IMU), - } - else: - return None - - except Exception as e: - logger.error(f"Error getting IMU data: {e}") - return None - - def capture_frame( - self, - ) -> tuple[np.ndarray | None, np.ndarray | None, np.ndarray | None]: - """ - Capture a frame from ZED camera. - - Returns: - Tuple of (left_image, right_image, depth_map) as numpy arrays - """ - if not self.is_opened: - logger.error("ZED camera not opened") - return None, None, None - - try: - # Grab frame - if self.zed.grab(self.runtime_params) == sl.ERROR_CODE.SUCCESS: - # Retrieve left image - self.zed.retrieve_image(self.image_left, sl.VIEW.LEFT) - left_img = self.image_left.get_data()[:, :, :3] # Remove alpha channel - - # Retrieve right image - self.zed.retrieve_image(self.image_right, sl.VIEW.RIGHT) - right_img = self.image_right.get_data()[:, :, :3] # Remove alpha channel - - # Retrieve depth map - self.zed.retrieve_measure(self.depth_map, sl.MEASURE.DEPTH) - depth = self.depth_map.get_data() - - return left_img, right_img, depth - else: - logger.warning("Failed to grab frame from ZED camera") - return None, None, None - - except Exception as e: - logger.error(f"Error capturing frame: {e}") - return None, None, None - - def capture_pointcloud(self) -> o3d.geometry.PointCloud | None: - """ - Capture point cloud from ZED camera. - - Returns: - Open3D point cloud with XYZ coordinates and RGB colors - """ - if not self.is_opened: - logger.error("ZED camera not opened") - return None - - try: - if self.zed.grab(self.runtime_params) == sl.ERROR_CODE.SUCCESS: - # Retrieve point cloud with RGBA data - self.zed.retrieve_measure(self.point_cloud, sl.MEASURE.XYZRGBA) - point_cloud_data = self.point_cloud.get_data() - - # Convert to numpy array format - _height, _width = point_cloud_data.shape[:2] - points = point_cloud_data.reshape(-1, 4) - - # Extract XYZ coordinates - xyz = points[:, :3] - - # Extract and unpack RGBA color data from 4th channel - rgba_packed = points[:, 3].view(np.uint32) - - # Unpack RGBA: each 32-bit value contains 4 bytes (R, G, B, A) - colors_rgba = np.zeros((len(rgba_packed), 4), dtype=np.uint8) - colors_rgba[:, 0] = rgba_packed & 0xFF # R - colors_rgba[:, 1] = (rgba_packed >> 8) & 0xFF # G - colors_rgba[:, 2] = (rgba_packed >> 16) & 0xFF # B - colors_rgba[:, 3] = (rgba_packed >> 24) & 0xFF # A - - # Extract RGB (ignore alpha) and normalize to [0, 1] - colors_rgb = colors_rgba[:, :3].astype(np.float64) / 255.0 - - # Filter out invalid points (NaN or inf) - valid = np.isfinite(xyz).all(axis=1) - valid_xyz = xyz[valid] - valid_colors = colors_rgb[valid] - - # Create Open3D point cloud - pcd = o3d.geometry.PointCloud() - - if len(valid_xyz) > 0: - pcd.points = o3d.utility.Vector3dVector(valid_xyz) - pcd.colors = o3d.utility.Vector3dVector(valid_colors) - - return pcd - else: - logger.warning("Failed to grab frame for point cloud") - return None - - except Exception as e: - logger.error(f"Error capturing point cloud: {e}") - return None - - def capture_frame_with_pose( - self, - ) -> tuple[np.ndarray | None, np.ndarray | None, np.ndarray | None, dict[str, Any] | None]: - """ - Capture a frame with synchronized pose data. - - Returns: - Tuple of (left_image, right_image, depth_map, pose_data) - """ - if not self.is_opened: - logger.error("ZED camera not opened") - return None, None, None, None - - try: - # Grab frame - if self.zed.grab(self.runtime_params) == sl.ERROR_CODE.SUCCESS: - # Get images and depth - left_img, right_img, depth = self.capture_frame() - - # Get synchronized pose if tracking is enabled - pose_data = None - if self.tracking_enabled: - pose_data = self.get_pose() - - return left_img, right_img, depth, pose_data - else: - logger.warning("Failed to grab frame from ZED camera") - return None, None, None, None - - except Exception as e: - logger.error(f"Error capturing frame with pose: {e}") - return None, None, None, None - - def close(self) -> None: - """Close the ZED camera.""" - if self.is_opened: - # Disable tracking if enabled - if self.tracking_enabled: - self.disable_positional_tracking() - - self.zed.close() - self.is_opened = False - logger.info("ZED camera closed") - - def get_camera_info(self) -> dict[str, Any]: - """Get ZED camera information and calibration parameters.""" - if not self.is_opened: - return {} - - try: - info = self.zed.get_camera_information() - calibration = info.camera_configuration.calibration_parameters - - # In ZED SDK 4.0+, the baseline calculation has changed - # Try to get baseline from the stereo parameters - try: - # Method 1: Try to get from stereo parameters if available - if hasattr(calibration, "getCameraBaseline"): - baseline = calibration.getCameraBaseline() - else: - # Method 2: Calculate from left and right camera positions - # The baseline is the distance between left and right cameras - - # Try different ways to get baseline in SDK 4.0+ - if hasattr(info.camera_configuration, "calibration_parameters_raw"): - # Use raw calibration if available - raw_calib = info.camera_configuration.calibration_parameters_raw - if hasattr(raw_calib, "T"): - baseline = abs(raw_calib.T[0]) - else: - baseline = 0.12 # Default ZED-M baseline approximation - else: - # Use default baseline for ZED-M - baseline = 0.12 # ZED-M baseline is approximately 120mm - except: - baseline = 0.12 # Fallback to approximate ZED-M baseline - - return { - "model": str(info.camera_model), - "serial_number": info.serial_number, - "firmware": info.camera_configuration.firmware_version, - "resolution": { - "width": info.camera_configuration.resolution.width, - "height": info.camera_configuration.resolution.height, - }, - "fps": info.camera_configuration.fps, - "left_cam": { - "fx": calibration.left_cam.fx, - "fy": calibration.left_cam.fy, - "cx": calibration.left_cam.cx, - "cy": calibration.left_cam.cy, - "k1": calibration.left_cam.disto[0], - "k2": calibration.left_cam.disto[1], - "p1": calibration.left_cam.disto[2], - "p2": calibration.left_cam.disto[3], - "k3": calibration.left_cam.disto[4], - }, - "right_cam": { - "fx": calibration.right_cam.fx, - "fy": calibration.right_cam.fy, - "cx": calibration.right_cam.cx, - "cy": calibration.right_cam.cy, - "k1": calibration.right_cam.disto[0], - "k2": calibration.right_cam.disto[1], - "p1": calibration.right_cam.disto[2], - "p2": calibration.right_cam.disto[3], - "k3": calibration.right_cam.disto[4], - }, - "baseline": baseline, - } - except Exception as e: - logger.error(f"Error getting camera info: {e}") - return {} - - def calculate_intrinsics(self): - """Calculate camera intrinsics from ZED calibration.""" - info = self.get_camera_info() - if not info: - return super().calculate_intrinsics() - - left_cam = info.get("left_cam", {}) - resolution = info.get("resolution", {}) - - return { - "focal_length_x": left_cam.get("fx", 0), - "focal_length_y": left_cam.get("fy", 0), - "principal_point_x": left_cam.get("cx", 0), - "principal_point_y": left_cam.get("cy", 0), - "baseline": info.get("baseline", 0), - "resolution_width": resolution.get("width", 0), - "resolution_height": resolution.get("height", 0), - } - - def __enter__(self): - """Context manager entry.""" - if not self.open(): - raise RuntimeError("Failed to open ZED camera") - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - """Context manager exit.""" - self.close() - - -class ZEDModule(Module): - """ - Dask module for ZED camera that publishes sensor data via LCM. - - Publishes: - - /zed/color_image: RGB camera images - - /zed/depth_image: Depth images - - /zed/camera_info: Camera calibration information - - /zed/pose: Camera pose (if tracking enabled) - """ - - # Define LCM outputs - color_image: Out[Image] = None - depth_image: Out[Image] = None - camera_info: Out[CameraInfo] = None - pose: Out[PoseStamped] = None - - def __init__( - self, - camera_id: int = 0, - resolution: str = "HD720", - depth_mode: str = "NEURAL", - fps: int = 30, - enable_tracking: bool = True, - enable_imu_fusion: bool = True, - set_floor_as_origin: bool = True, - publish_rate: float = 30.0, - frame_id: str = "zed_camera", - recording_path: str | None = None, - **kwargs, - ) -> None: - """ - Initialize ZED Module. - - Args: - camera_id: Camera ID (0 for first ZED) - resolution: Resolution string ("HD720", "HD1080", "HD2K", "VGA") - depth_mode: Depth mode string ("NEURAL", "ULTRA", "QUALITY", "PERFORMANCE") - fps: Camera frame rate - enable_tracking: Enable positional tracking - enable_imu_fusion: Enable IMU fusion for tracking - set_floor_as_origin: Set floor as origin for tracking - publish_rate: Rate to publish messages (Hz) - frame_id: TF frame ID for messages - recording_path: Path to save recorded data - """ - super().__init__(**kwargs) - - self.camera_id = camera_id - self.fps = fps - self.enable_tracking = enable_tracking - self.enable_imu_fusion = enable_imu_fusion - self.set_floor_as_origin = set_floor_as_origin - self.publish_rate = publish_rate - self.frame_id = frame_id - self.recording_path = recording_path - - # Convert string parameters to ZED enums - self.resolution = getattr(sl.RESOLUTION, resolution, sl.RESOLUTION.HD720) - self.depth_mode = getattr(sl.DEPTH_MODE, depth_mode, sl.DEPTH_MODE.NEURAL) - - # Internal state - self.zed_camera = None - self._running = False - self._subscription = None - self._sequence = 0 - - # Initialize TF publisher - self.tf = TF() - - # Initialize storage for recording if path provided - self.storages = None - if self.recording_path: - from dimos.utils.testing import TimedSensorStorage - - self.storages = { - "color": TimedSensorStorage(f"{self.recording_path}/color"), - "depth": TimedSensorStorage(f"{self.recording_path}/depth"), - "pose": TimedSensorStorage(f"{self.recording_path}/pose"), - "camera_info": TimedSensorStorage(f"{self.recording_path}/camera_info"), - } - logger.info(f"Recording enabled - saving to {self.recording_path}") - - logger.info(f"ZEDModule initialized for camera {camera_id}") - - @rpc - def start(self) -> None: - """Start the ZED module and begin publishing data.""" - if self._running: - logger.warning("ZED module already running") - return - - super().start() - - try: - # Initialize ZED camera - self.zed_camera = ZEDCamera( - camera_id=self.camera_id, - resolution=self.resolution, - depth_mode=self.depth_mode, - fps=self.fps, - ) - - # Open camera - if not self.zed_camera.open(): - logger.error("Failed to open ZED camera") - return - - # Enable tracking if requested - if self.enable_tracking: - success = self.zed_camera.enable_positional_tracking( - enable_imu_fusion=self.enable_imu_fusion, - set_floor_as_origin=self.set_floor_as_origin, - enable_pose_smoothing=True, - enable_area_memory=True, - ) - if not success: - logger.warning("Failed to enable positional tracking") - self.enable_tracking = False - - # Publish camera info once at startup - self._publish_camera_info() - - # Start periodic frame capture and publishing - self._running = True - publish_interval = 1.0 / self.publish_rate - - self._subscription = interval(publish_interval).subscribe( - lambda _: self._capture_and_publish() - ) - - logger.info(f"ZED module started, publishing at {self.publish_rate} Hz") - - except Exception as e: - logger.error(f"Error starting ZED module: {e}") - self._running = False - - @rpc - def stop(self) -> None: - """Stop the ZED module.""" - if not self._running: - return - - self._running = False - - # Stop subscription - if self._subscription: - self._subscription.dispose() - self._subscription = None - - # Close camera - if self.zed_camera: - self.zed_camera.close() - self.zed_camera = None - - super().stop() - - def _capture_and_publish(self) -> None: - """Capture frame and publish all data.""" - if not self._running or not self.zed_camera: - return - - try: - # Capture frame with pose - left_img, _, depth, pose_data = self.zed_camera.capture_frame_with_pose() - - if left_img is None or depth is None: - return - - # Save raw color data if recording - if self.storages and left_img is not None: - self.storages["color"].save_one(left_img) - - # Save raw depth data if recording - if self.storages and depth is not None: - self.storages["depth"].save_one(depth) - - # Save raw pose data if recording - if self.storages and pose_data: - self.storages["pose"].save_one(pose_data) - - # Create header - header = Header(self.frame_id) - self._sequence += 1 - - # Publish color image - self._publish_color_image(left_img, header) - - # Publish depth image - self._publish_depth_image(depth, header) - - # Publish camera info periodically - self._publish_camera_info() - - # Publish pose if tracking enabled and valid - if self.enable_tracking and pose_data and pose_data.get("valid", False): - self._publish_pose(pose_data, header) - - except Exception as e: - logger.error(f"Error in capture and publish: {e}") - - def _publish_color_image(self, image: np.ndarray, header: Header) -> None: - """Publish color image as LCM message.""" - try: - # Convert BGR to RGB if needed - if len(image.shape) == 3 and image.shape[2] == 3: - image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - else: - image_rgb = image - - # Create LCM Image message - msg = Image( - data=image_rgb, - format=ImageFormat.RGB, - frame_id=header.frame_id, - ts=header.ts, - ) - - self.color_image.publish(msg) - - except Exception as e: - logger.error(f"Error publishing color image: {e}") - - def _publish_depth_image(self, depth: np.ndarray, header: Header) -> None: - """Publish depth image as LCM message.""" - try: - # Depth is float32 in meters - msg = Image( - data=depth, - format=ImageFormat.DEPTH, - frame_id=header.frame_id, - ts=header.ts, - ) - self.depth_image.publish(msg) - - except Exception as e: - logger.error(f"Error publishing depth image: {e}") - - def _publish_camera_info(self) -> None: - """Publish camera calibration information.""" - try: - info = self.zed_camera.get_camera_info() - if not info: - return - - # Save raw camera info if recording - if self.storages: - self.storages["camera_info"].save_one(info) - - # Get calibration parameters - left_cam = info.get("left_cam", {}) - resolution = info.get("resolution", {}) - - # Create CameraInfo message - header = Header(self.frame_id) - - # Create camera matrix K (3x3) - K = [ - left_cam.get("fx", 0), - 0, - left_cam.get("cx", 0), - 0, - left_cam.get("fy", 0), - left_cam.get("cy", 0), - 0, - 0, - 1, - ] - - # Distortion coefficients - D = [ - left_cam.get("k1", 0), - left_cam.get("k2", 0), - left_cam.get("p1", 0), - left_cam.get("p2", 0), - left_cam.get("k3", 0), - ] - - # Identity rotation matrix - R = [1, 0, 0, 0, 1, 0, 0, 0, 1] - - # Projection matrix P (3x4) - P = [ - left_cam.get("fx", 0), - 0, - left_cam.get("cx", 0), - 0, - 0, - left_cam.get("fy", 0), - left_cam.get("cy", 0), - 0, - 0, - 0, - 1, - 0, - ] - - msg = CameraInfo( - D_length=len(D), - header=header, - height=resolution.get("height", 0), - width=resolution.get("width", 0), - distortion_model="plumb_bob", - D=D, - K=K, - R=R, - P=P, - binning_x=0, - binning_y=0, - ) - - self.camera_info.publish(msg) - - except Exception as e: - logger.error(f"Error publishing camera info: {e}") - - def _publish_pose(self, pose_data: dict[str, Any], header: Header) -> None: - """Publish camera pose as PoseStamped message and TF transform.""" - try: - position = pose_data.get("position", [0, 0, 0]) - rotation = pose_data.get("rotation", [0, 0, 0, 1]) # quaternion [x,y,z,w] - - # Create PoseStamped message - msg = PoseStamped(ts=header.ts, position=position, orientation=rotation) - self.pose.publish(msg) - - # Publish TF transform - camera_tf = Transform( - translation=Vector3(position), - rotation=Quaternion(rotation), - frame_id="zed_world", - child_frame_id="zed_camera_link", - ts=header.ts, - ) - self.tf.publish(camera_tf) - - except Exception as e: - logger.error(f"Error publishing pose: {e}") - - @rpc - def get_camera_info(self) -> dict[str, Any]: - """Get camera information and calibration parameters.""" - if self.zed_camera: - return self.zed_camera.get_camera_info() - return {} - - @rpc - def get_pose(self) -> dict[str, Any] | None: - """Get current camera pose if tracking is enabled.""" - if self.zed_camera and self.enable_tracking: - return self.zed_camera.get_pose() - return None diff --git a/dimos/hardware/end_effectors/__init__.py b/dimos/hardware/end_effectors/__init__.py new file mode 100644 index 0000000000..9a7aa9759a --- /dev/null +++ b/dimos/hardware/end_effectors/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .end_effector import EndEffector + +__all__ = ["EndEffector"] diff --git a/dimos/hardware/end_effector.py b/dimos/hardware/end_effectors/end_effector.py similarity index 77% rename from dimos/hardware/end_effector.py rename to dimos/hardware/end_effectors/end_effector.py index 1c5eb08281..e958261b91 100644 --- a/dimos/hardware/end_effector.py +++ b/dimos/hardware/end_effectors/end_effector.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,8 +14,8 @@ class EndEffector: - def __init__(self, effector_type=None) -> None: + def __init__(self, effector_type=None) -> None: # type: ignore[no-untyped-def] self.effector_type = effector_type - def get_effector_type(self): + def get_effector_type(self): # type: ignore[no-untyped-def] return self.effector_type diff --git a/dimos/hardware/manipulators/README.md b/dimos/hardware/manipulators/README.md new file mode 100644 index 0000000000..d4bb1cdba7 --- /dev/null +++ b/dimos/hardware/manipulators/README.md @@ -0,0 +1,173 @@ +# Manipulator Drivers + +Component-based framework for integrating robotic manipulators into DIMOS. + +## Quick Start: Adding a New Manipulator + +Adding support for a new robot arm requires **two files**: +1. **SDK Wrapper** (~200-500 lines) - Translates vendor SDK to standard interface +2. **Driver** (~30-50 lines) - Assembles components and configuration + +## Directory Structure + +``` +manipulators/ +ā”œā”€ā”€ base/ # Framework (don't modify) +│ ā”œā”€ā”€ sdk_interface.py # BaseManipulatorSDK abstract class +│ ā”œā”€ā”€ driver.py # BaseManipulatorDriver base class +│ ā”œā”€ā”€ spec.py # ManipulatorCapabilities dataclass +│ └── components/ # Reusable standard components +ā”œā”€ā”€ xarm/ # XArm implementation (reference) +└── piper/ # Piper implementation (reference) +``` + +## Hardware Requirements + +Your manipulator **must** support: + +| Requirement | Description | +|-------------|-------------| +| Joint Position Feedback | Read current joint angles | +| Joint Position Control | Command target joint positions | +| Servo Enable/Disable | Enable and disable motor power | +| Error Reporting | Report error codes/states | +| Emergency Stop | Hardware or software e-stop | + +**Optional:** velocity control, torque control, cartesian control, F/T sensor, gripper + +## Step 1: Implement SDK Wrapper + +Create `your_arm/your_arm_wrapper.py` implementing `BaseManipulatorSDK`: + +```python +from dimos.hardware.manipulators.base.sdk_interface import BaseManipulatorSDK, ManipulatorInfo + +class YourArmSDKWrapper(BaseManipulatorSDK): + def __init__(self): + self._sdk = None + + def connect(self, config: dict) -> bool: + self._sdk = YourNativeSDK(config['ip']) + return self._sdk.connect() + + def get_joint_positions(self) -> list[float]: + """Return positions in RADIANS.""" + degrees = self._sdk.get_angles() + return [math.radians(d) for d in degrees] + + def set_joint_positions(self, positions: list[float], + velocity: float, acceleration: float) -> bool: + return self._sdk.move_joints(positions, velocity) + + def enable_servos(self) -> bool: + return self._sdk.motor_on() + + # ... implement remaining required methods (see sdk_interface.py) +``` + +### Unit Conventions + +**All SDK wrappers must use these standard units:** + +| Quantity | Unit | +|----------|------| +| Joint positions | radians | +| Joint velocities | rad/s | +| Joint accelerations | rad/s^2 | +| Joint torques | Nm | +| Cartesian positions | meters | +| Forces | N | + +## Step 2: Create Driver Assembly + +Create `your_arm/your_arm_driver.py`: + +```python +from dimos.hardware.manipulators.base.driver import BaseManipulatorDriver +from dimos.hardware.manipulators.base.spec import ManipulatorCapabilities +from dimos.hardware.manipulators.base.components import ( + StandardMotionComponent, + StandardServoComponent, + StandardStatusComponent, +) +from .your_arm_wrapper import YourArmSDKWrapper + +class YourArmDriver(BaseManipulatorDriver): + def __init__(self, config: dict): + sdk = YourArmSDKWrapper() + + capabilities = ManipulatorCapabilities( + dof=6, + has_gripper=False, + has_force_torque=False, + joint_limits_lower=[-3.14, -2.09, -3.14, -3.14, -3.14, -3.14], + joint_limits_upper=[3.14, 2.09, 3.14, 3.14, 3.14, 3.14], + max_joint_velocity=[2.0] * 6, + max_joint_acceleration=[4.0] * 6, + ) + + components = [ + StandardMotionComponent(), + StandardServoComponent(), + StandardStatusComponent(), + ] + + super().__init__(sdk, components, config, capabilities) +``` + +## Component API Decorator + +Use `@component_api` to expose methods as RPC endpoints: + +```python +from dimos.hardware.manipulators.base.components import component_api + +class StandardMotionComponent: + @component_api + def move_joint(self, positions: list[float], velocity: float = 1.0): + """Auto-exposed as driver.move_joint()""" + ... +``` + +## Threading Architecture + +The driver runs **2 threads**: +1. **Control Loop (100Hz)** - Processes commands, reads joint state, publishes feedback +2. **Monitor Loop (10Hz)** - Reads robot state, errors, optional sensors + +``` +RPC Call → Command Queue → Control Loop → SDK → Hardware + ↓ + SharedState → LCM Publisher +``` + +## Testing Your Driver + +```python +driver = YourArmDriver({"ip": "192.168.1.100"}) +driver.start() +driver.enable_servo() +driver.move_joint([0, 0, 0, 0, 0, 0], velocity=0.5) +state = driver.get_joint_state() +driver.stop() +``` + +## Common Issues + +| Issue | Solution | +|-------|----------| +| Unit mismatch | Verify wrapper converts to radians/meters | +| Commands ignored | Ensure servos are enabled before commanding | +| Velocity not working | Some arms need mode switch via `set_control_mode()` | + +## Architecture Details + +For complete architecture documentation including full SDK interface specification, +component details, and testing strategies, see: + +**[component_based_architecture.md](base/component_based_architecture.md)** + +## Reference Implementations + +- **XArm**: [xarm/xarm_wrapper.py](xarm/xarm_wrapper.py) - Full-featured wrapper +- **Piper**: [piper/piper_wrapper.py](piper/piper_wrapper.py) - Shows velocity workaround diff --git a/dimos/hardware/manipulators/__init__.py b/dimos/hardware/manipulators/__init__.py new file mode 100644 index 0000000000..a54a846afc --- /dev/null +++ b/dimos/hardware/manipulators/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manipulator Hardware Drivers + +Drivers for various robotic manipulator arms. +""" + +__all__ = [] diff --git a/dimos/hardware/manipulators/base/__init__.py b/dimos/hardware/manipulators/base/__init__.py new file mode 100644 index 0000000000..3ed58d9819 --- /dev/null +++ b/dimos/hardware/manipulators/base/__init__.py @@ -0,0 +1,44 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base framework for generalized manipulator drivers. + +This package provides the foundation for building manipulator drivers +that work with any robotic arm (XArm, Piper, UR, Franka, etc.). +""" + +from .components import StandardMotionComponent, StandardServoComponent, StandardStatusComponent +from .driver import BaseManipulatorDriver, Command +from .sdk_interface import BaseManipulatorSDK, ManipulatorInfo +from .spec import ManipulatorCapabilities, ManipulatorDriverSpec, RobotState +from .utils import SharedState + +__all__ = [ + # Driver + "BaseManipulatorDriver", + # SDK Interface + "BaseManipulatorSDK", + "Command", + "ManipulatorCapabilities", + # Spec + "ManipulatorDriverSpec", + "ManipulatorInfo", + "RobotState", + # Utils + "SharedState", + # Components + "StandardMotionComponent", + "StandardServoComponent", + "StandardStatusComponent", +] diff --git a/dimos/hardware/manipulators/base/component_based_architecture.md b/dimos/hardware/manipulators/base/component_based_architecture.md new file mode 100644 index 0000000000..893ebf1276 --- /dev/null +++ b/dimos/hardware/manipulators/base/component_based_architecture.md @@ -0,0 +1,208 @@ +# Component-Based Architecture for Manipulator Drivers + +## Overview + +This architecture provides maximum code reuse through standardized SDK wrappers and reusable components. Each new manipulator requires only an SDK wrapper (~200-500 lines) and a thin driver assembly (~30-50 lines). + +## Architecture Layers + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ RPC Interface │ +│ (Standardized across all arms) │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ā–² +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Driver Instance (XArmDriver) │ +│ Extends DIMOS Module, assembles components │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ā–² +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Standard Components │ +│ (Motion, Servo, Status) - reused everywhere │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ā–² +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ SDK Wrapper (XArmSDKWrapper) │ +│ Implements BaseManipulatorSDK interface │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ā–² +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Native Vendor SDK (XArmAPI) │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +## Core Interfaces + +### BaseManipulatorSDK + +Abstract interface that all SDK wrappers must implement. See `sdk_interface.py` for full specification. + +**Required methods:** `connect()`, `disconnect()`, `is_connected()`, `get_joint_positions()`, `get_joint_velocities()`, `set_joint_positions()`, `enable_servos()`, `disable_servos()`, `emergency_stop()`, `get_error_code()`, `clear_errors()`, `get_info()` + +**Optional methods:** `get_force_torque()`, `get_gripper_position()`, `set_cartesian_position()`, etc. + +### ManipulatorCapabilities + +Dataclass defining arm properties: DOF, joint limits, velocity limits, feature flags. + +## Component System + +### @component_api Decorator + +Methods marked with `@component_api` are automatically exposed as RPC endpoints on the driver: + +```python +from dimos.hardware.manipulators.base.components import component_api + +class StandardMotionComponent: + @component_api + def move_joint(self, positions: list[float], velocity: float = 1.0) -> dict: + """Auto-exposed as driver.move_joint()""" + ... +``` + +### Dependency Injection + +Components receive dependencies via setter methods, not constructor: + +```python +class StandardMotionComponent: + def __init__(self): + self.sdk = None + self.shared_state = None + self.command_queue = None + self.capabilities = None + + def set_sdk(self, sdk): self.sdk = sdk + def set_shared_state(self, state): self.shared_state = state + def set_command_queue(self, queue): self.command_queue = queue + def set_capabilities(self, caps): self.capabilities = caps + def initialize(self): pass # Called after all setters +``` + +### Standard Components + +| Component | Purpose | Key Methods | +|-----------|---------|-------------| +| `StandardMotionComponent` | Joint/cartesian motion | `move_joint()`, `move_joint_velocity()`, `get_joint_state()`, `stop_motion()` | +| `StandardServoComponent` | Motor control | `enable_servo()`, `disable_servo()`, `emergency_stop()`, `set_control_mode()` | +| `StandardStatusComponent` | Monitoring | `get_robot_state()`, `get_error_state()`, `get_health_metrics()` | + +## Threading Model + +The driver runs **2 threads**: + +1. **Control Loop (100Hz)** - Process commands, read joint state, publish feedback +2. **Monitor Loop (10Hz)** - Read robot state, errors, optional sensors (F/T, gripper) + +``` +RPC Call → Command Queue → Control Loop → SDK → Hardware + ↓ + SharedState (thread-safe) + ↓ + LCM Publisher → External Systems +``` + +## DIMOS Module Integration + +The driver extends `Module` for pub/sub integration: + +```python +class BaseManipulatorDriver(Module): + def __init__(self, sdk, components, config, capabilities): + super().__init__() + self.shared_state = SharedState() + self.command_queue = Queue(maxsize=10) + + # Inject dependencies into components + for component in components: + component.set_sdk(sdk) + component.set_shared_state(self.shared_state) + component.set_command_queue(self.command_queue) + component.set_capabilities(capabilities) + component.initialize() + + # Auto-expose @component_api methods + self._auto_expose_component_apis() +``` + +## Adding a New Manipulator + +### Step 1: SDK Wrapper + +```python +class YourArmSDKWrapper(BaseManipulatorSDK): + def get_joint_positions(self) -> list[float]: + degrees = self._sdk.get_angles() + return [math.radians(d) for d in degrees] # Convert to radians + + def set_joint_positions(self, positions, velocity, acceleration) -> bool: + return self._sdk.move_joints(positions, velocity) + + # ... implement remaining required methods +``` + +### Step 2: Driver Assembly + +```python +class YourArmDriver(BaseManipulatorDriver): + def __init__(self, config: dict): + sdk = YourArmSDKWrapper() + capabilities = ManipulatorCapabilities( + dof=6, + joint_limits_lower=[-3.14] * 6, + joint_limits_upper=[3.14] * 6, + ) + components = [ + StandardMotionComponent(), + StandardServoComponent(), + StandardStatusComponent(), + ] + super().__init__(sdk, components, config, capabilities) +``` + +## Unit Conventions + +All SDK wrappers must convert to standard units: + +| Quantity | Unit | +|----------|------| +| Positions | radians | +| Velocities | rad/s | +| Accelerations | rad/s^2 | +| Torques | Nm | +| Cartesian | meters | + +## Testing Strategy + +```python +# Test SDK wrapper with mocked native SDK +def test_wrapper_positions(): + mock = Mock() + mock.get_angles.return_value = [0, 90, 180] + wrapper = YourArmSDKWrapper() + wrapper._sdk = mock + assert wrapper.get_joint_positions() == [0, math.pi/2, math.pi] + +# Test component with mocked SDK wrapper +def test_motion_component(): + mock_sdk = Mock(spec=BaseManipulatorSDK) + component = StandardMotionComponent() + component.set_sdk(mock_sdk) + component.move_joint([0, 0, 0]) + # Verify command was queued +``` + +## Advantages + +- **Maximum reuse**: Components tested once, used by 100+ arms +- **Consistent behavior**: All arms identical at RPC level +- **Centralized fixes**: Fix once in component, all arms benefit +- **Team scalability**: Developers work on wrappers independently +- **Strong contracts**: SDK interface defines exact requirements + +## Reference Implementations + +- **XArm**: `xarm/xarm_wrapper.py` - Full-featured, converts degrees→radians +- **Piper**: `piper/piper_wrapper.py` - Shows velocity integration workaround diff --git a/dimos/hardware/manipulators/base/components/__init__.py b/dimos/hardware/manipulators/base/components/__init__.py new file mode 100644 index 0000000000..b04f60f691 --- /dev/null +++ b/dimos/hardware/manipulators/base/components/__init__.py @@ -0,0 +1,59 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Standard components for manipulator drivers.""" + +from collections.abc import Callable +from typing import Any, TypeVar + +F = TypeVar("F", bound=Callable[..., Any]) + + +def component_api(fn: F) -> F: + """Decorator to mark component methods that should be exposed as driver RPCs. + + Methods decorated with @component_api will be automatically discovered by the + driver and exposed as @rpc methods on the driver instance. This allows external + code to call these methods via the standard Module RPC system. + + Example: + class MyComponent: + @component_api + def enable_servo(self): + '''Enable servo motors.''' + return self.sdk.enable_servos() + + # The driver will auto-generate: + # @rpc + # def enable_servo(self): + # return component.enable_servo() + + # External code can then call: + # driver.enable_servo() + """ + fn.__component_api__ = True # type: ignore[attr-defined] + return fn + + +# Import components AFTER defining component_api to avoid circular imports +from .motion import StandardMotionComponent +from .servo import StandardServoComponent +from .status import StandardStatusComponent + +__all__ = [ + "StandardMotionComponent", + "StandardServoComponent", + "StandardStatusComponent", + "component_api", +] diff --git a/dimos/hardware/manipulators/base/components/motion.py b/dimos/hardware/manipulators/base/components/motion.py new file mode 100644 index 0000000000..f3205acb01 --- /dev/null +++ b/dimos/hardware/manipulators/base/components/motion.py @@ -0,0 +1,591 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Standard motion control component for manipulator drivers.""" + +import logging +from queue import Queue +import time +from typing import Any + +from ..driver import Command +from ..sdk_interface import BaseManipulatorSDK +from ..spec import ManipulatorCapabilities +from ..utils import SharedState, scale_velocities, validate_joint_limits, validate_velocity_limits +from . import component_api + + +class StandardMotionComponent: + """Motion control component that works with any SDK wrapper. + + This component provides standard motion control methods that work + consistently across all manipulator types. Methods decorated with @component_api + are automatically exposed as RPC methods on the driver. It handles: + - Joint position control + - Joint velocity control + - Joint effort/torque control (if supported) + - Trajectory execution (if supported) + - Motion safety validation + """ + + def __init__( + self, + sdk: BaseManipulatorSDK | None = None, + shared_state: SharedState | None = None, + command_queue: Queue[Any] | None = None, + capabilities: ManipulatorCapabilities | None = None, + ) -> None: + """Initialize the motion component. + + Args: + sdk: SDK wrapper instance (can be set later) + shared_state: Shared state instance (can be set later) + command_queue: Command queue (can be set later) + capabilities: Manipulator capabilities (can be set later) + """ + self.sdk = sdk + self.shared_state = shared_state + self.command_queue = command_queue + self.capabilities = capabilities + self.logger = logging.getLogger(self.__class__.__name__) + + # Motion limits + self.velocity_scale = 1.0 # Global velocity scaling (0-1) + self.acceleration_scale = 1.0 # Global acceleration scaling (0-1) + + # ============= Initialization Methods (called by BaseDriver) ============= + + def set_sdk(self, sdk: BaseManipulatorSDK) -> None: + """Set the SDK wrapper instance.""" + self.sdk = sdk + + def set_shared_state(self, shared_state: SharedState) -> None: + """Set the shared state instance.""" + self.shared_state = shared_state + + def set_command_queue(self, command_queue: "Queue[Any]") -> None: + """Set the command queue instance.""" + self.command_queue = command_queue + + def set_capabilities(self, capabilities: ManipulatorCapabilities) -> None: + """Set the capabilities instance.""" + self.capabilities = capabilities + + def initialize(self) -> None: + """Initialize the component after all resources are set.""" + self.logger.debug("Motion component initialized") + + # ============= Component API Methods ============= + + @component_api + def move_joint( + self, + positions: list[float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + validate: bool = True, + ) -> dict[str, Any]: + """Move joints to target positions. + + Args: + positions: Target joint positions in radians + velocity: Velocity scaling factor (0-1) + acceleration: Acceleration scaling factor (0-1) + wait: If True, block until motion completes + validate: If True, validate against joint limits + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # Validate inputs + if validate and self.capabilities: + if len(positions) != self.capabilities.dof: + return { + "success": False, + "error": f"Expected {self.capabilities.dof} positions, got {len(positions)}", + } + + # Check joint limits + if self.capabilities.joint_limits_lower and self.capabilities.joint_limits_upper: + valid, error = validate_joint_limits( + positions, + self.capabilities.joint_limits_lower, + self.capabilities.joint_limits_upper, + ) + if not valid: + return {"success": False, "error": error} + + # Apply global scaling + velocity = velocity * self.velocity_scale + acceleration = acceleration * self.acceleration_scale + + # Queue command for async execution + if self.command_queue and not wait: + command = Command( + type="position", + data={ + "positions": positions, + "velocity": velocity, + "acceleration": acceleration, + "wait": False, + }, + timestamp=time.time(), + ) + self.command_queue.put(command) + return {"success": True, "queued": True} + + # Execute directly (blocking or wait mode) + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + success = self.sdk.set_joint_positions(positions, velocity, acceleration, wait) + + if success and self.shared_state: + self.shared_state.set_target_joints(positions=positions) + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in move_joint: {e}") + return {"success": False, "error": str(e)} + + @component_api + def move_joint_velocity( + self, velocities: list[float], acceleration: float = 1.0, validate: bool = True + ) -> dict[str, Any]: + """Set joint velocities. + + Args: + velocities: Target joint velocities in rad/s + acceleration: Acceleration scaling factor (0-1) + validate: If True, validate against velocity limits + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # Validate inputs + if validate and self.capabilities: + if len(velocities) != self.capabilities.dof: + return { + "success": False, + "error": f"Expected {self.capabilities.dof} velocities, got {len(velocities)}", + } + + # Check velocity limits + if self.capabilities.max_joint_velocity: + valid, _error = validate_velocity_limits( + velocities, self.capabilities.max_joint_velocity, self.velocity_scale + ) + if not valid: + # Scale velocities to stay within limits + velocities = scale_velocities( + velocities, self.capabilities.max_joint_velocity, self.velocity_scale + ) + self.logger.warning("Velocities scaled to stay within limits") + + # Queue command for async execution + if self.command_queue: + command = Command( + type="velocity", data={"velocities": velocities}, timestamp=time.time() + ) + self.command_queue.put(command) + return {"success": True, "queued": True} + + # Execute directly + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + success = self.sdk.set_joint_velocities(velocities) + + if success and self.shared_state: + self.shared_state.set_target_joints(velocities=velocities) + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in move_joint_velocity: {e}") + return {"success": False, "error": str(e)} + + @component_api + def move_joint_effort(self, efforts: list[float], validate: bool = True) -> dict[str, Any]: + """Set joint efforts/torques. + + Args: + efforts: Target joint efforts in Nm + validate: If True, validate inputs + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # Check if effort control is supported + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + if not hasattr(self.sdk, "set_joint_efforts"): + return {"success": False, "error": "Effort control not supported"} + + # Validate inputs + if validate and self.capabilities: + if len(efforts) != self.capabilities.dof: + return { + "success": False, + "error": f"Expected {self.capabilities.dof} efforts, got {len(efforts)}", + } + + # Queue command for async execution + if self.command_queue: + command = Command(type="effort", data={"efforts": efforts}, timestamp=time.time()) + self.command_queue.put(command) + return {"success": True, "queued": True} + + # Execute directly + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + success = self.sdk.set_joint_efforts(efforts) + + if success and self.shared_state: + self.shared_state.set_target_joints(efforts=efforts) + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in move_joint_effort: {e}") + return {"success": False, "error": str(e)} + + @component_api + def stop_motion(self) -> dict[str, Any]: + """Stop all ongoing motion immediately. + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # Queue stop command with high priority + if self.command_queue: + command = Command(type="stop", data={}, timestamp=time.time()) + # Clear queue and add stop command + while not self.command_queue.empty(): + try: + self.command_queue.get_nowait() + except: + break + self.command_queue.put(command) + + # Also execute directly for immediate stop + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + success = self.sdk.stop_motion() + + # Clear targets + if self.shared_state: + self.shared_state.set_target_joints(positions=None, velocities=None, efforts=None) + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in stop_motion: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_joint_state(self) -> dict[str, Any]: + """Get current joint state. + + Returns: + Dict with joint positions, velocities, efforts, and timestamp + """ + try: + if self.shared_state: + # Get from shared state (updated by reader thread) + positions, velocities, efforts = self.shared_state.get_joint_state() + else: + # Get directly from SDK + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + positions = self.sdk.get_joint_positions() + velocities = self.sdk.get_joint_velocities() + efforts = self.sdk.get_joint_efforts() + + return { + "positions": positions, + "velocities": velocities, + "efforts": efforts, + "timestamp": time.time(), + "success": True, + } + + except Exception as e: + self.logger.error(f"Error in get_joint_state: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_joint_limits(self) -> dict[str, Any]: + """Get joint position limits. + + Returns: + Dict with lower and upper limits in radians + """ + try: + if self.capabilities: + return { + "lower": self.capabilities.joint_limits_lower, + "upper": self.capabilities.joint_limits_upper, + "success": True, + } + else: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + lower, upper = self.sdk.get_joint_limits() + return {"lower": lower, "upper": upper, "success": True} + + except Exception as e: + self.logger.error(f"Error in get_joint_limits: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_velocity_limits(self) -> dict[str, Any]: + """Get joint velocity limits. + + Returns: + Dict with maximum velocities in rad/s + """ + try: + if self.capabilities: + return {"limits": self.capabilities.max_joint_velocity, "success": True} + else: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + limits = self.sdk.get_velocity_limits() + return {"limits": limits, "success": True} + + except Exception as e: + self.logger.error(f"Error in get_velocity_limits: {e}") + return {"success": False, "error": str(e)} + + @component_api + def set_velocity_scale(self, scale: float) -> dict[str, Any]: + """Set global velocity scaling factor. + + Args: + scale: Velocity scale factor (0-1) + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if scale <= 0 or scale > 1: + return {"success": False, "error": f"Invalid scale {scale}, must be in (0, 1]"} + + self.velocity_scale = scale + return {"success": True, "scale": scale} + + except Exception as e: + self.logger.error(f"Error in set_velocity_scale: {e}") + return {"success": False, "error": str(e)} + + @component_api + def set_acceleration_scale(self, scale: float) -> dict[str, Any]: + """Set global acceleration scaling factor. + + Args: + scale: Acceleration scale factor (0-1) + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if scale <= 0 or scale > 1: + return {"success": False, "error": f"Invalid scale {scale}, must be in (0, 1]"} + + self.acceleration_scale = scale + return {"success": True, "scale": scale} + + except Exception as e: + self.logger.error(f"Error in set_acceleration_scale: {e}") + return {"success": False, "error": str(e)} + + # ============= Cartesian Control (Optional) ============= + + @component_api + def move_cartesian( + self, + pose: dict[str, float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + ) -> dict[str, Any]: + """Move end-effector to target pose. + + Args: + pose: Target pose with keys: x, y, z (meters), roll, pitch, yaw (radians) + velocity: Velocity scaling factor (0-1) + acceleration: Acceleration scaling factor (0-1) + wait: If True, block until motion completes + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # Check if Cartesian control is supported + if not self.capabilities or not self.capabilities.has_cartesian_control: + return {"success": False, "error": "Cartesian control not supported"} + + # Apply global scaling + velocity = velocity * self.velocity_scale + acceleration = acceleration * self.acceleration_scale + + # Queue command for async execution + if self.command_queue and not wait: + command = Command( + type="cartesian", + data={ + "pose": pose, + "velocity": velocity, + "acceleration": acceleration, + "wait": False, + }, + timestamp=time.time(), + ) + self.command_queue.put(command) + return {"success": True, "queued": True} + + # Execute directly + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + success = self.sdk.set_cartesian_position(pose, velocity, acceleration, wait) + + if success and self.shared_state: + self.shared_state.target_cartesian_position = pose + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in move_cartesian: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_cartesian_state(self) -> dict[str, Any]: + """Get current end-effector pose. + + Returns: + Dict with pose (x, y, z, roll, pitch, yaw) and timestamp + """ + try: + # Check if Cartesian control is supported + if not self.capabilities or not self.capabilities.has_cartesian_control: + return {"success": False, "error": "Cartesian control not supported"} + + pose: dict[str, float] | None = None + if self.shared_state and self.shared_state.cartesian_position: + # Get from shared state + pose = self.shared_state.cartesian_position + else: + # Get directly from SDK + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + pose = self.sdk.get_cartesian_position() + + if pose: + return {"pose": pose, "timestamp": time.time(), "success": True} + else: + return {"success": False, "error": "Failed to get Cartesian state"} + + except Exception as e: + self.logger.error(f"Error in get_cartesian_state: {e}") + return {"success": False, "error": str(e)} + + # ============= Trajectory Execution (Optional) ============= + + @component_api + def execute_trajectory( + self, trajectory: list[dict[str, Any]], wait: bool = True + ) -> dict[str, Any]: + """Execute a joint trajectory. + + Args: + trajectory: List of waypoints, each with: + - 'positions': list[float] in radians + - 'velocities': Optional list[float] in rad/s + - 'time': float seconds from start + wait: If True, block until trajectory completes + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # Check if trajectory execution is supported + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + if not hasattr(self.sdk, "execute_trajectory"): + return {"success": False, "error": "Trajectory execution not supported"} + + # Validate trajectory if capabilities available + if self.capabilities: + from ..utils import validate_trajectory + + # Only validate if all required capability fields are present + jl_lower = self.capabilities.joint_limits_lower + jl_upper = self.capabilities.joint_limits_upper + max_vel = self.capabilities.max_joint_velocity + max_acc = self.capabilities.max_joint_acceleration + + if ( + jl_lower is not None + and jl_upper is not None + and max_vel is not None + and max_acc is not None + ): + valid, error = validate_trajectory( + trajectory, + jl_lower, + jl_upper, + max_vel, + max_acc, + ) + if not valid: + return {"success": False, "error": error} + else: + self.logger.debug("Skipping trajectory validation; capabilities incomplete") + + # Execute trajectory + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + success = self.sdk.execute_trajectory(trajectory, wait) + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in execute_trajectory: {e}") + return {"success": False, "error": str(e)} + + @component_api + def stop_trajectory(self) -> dict[str, Any]: + """Stop any executing trajectory. + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # Check if trajectory execution is supported + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + if not hasattr(self.sdk, "stop_trajectory"): + return {"success": False, "error": "Trajectory execution not supported"} + + success = self.sdk.stop_trajectory() + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in stop_trajectory: {e}") + return {"success": False, "error": str(e)} diff --git a/dimos/hardware/manipulators/base/components/servo.py b/dimos/hardware/manipulators/base/components/servo.py new file mode 100644 index 0000000000..c773f10723 --- /dev/null +++ b/dimos/hardware/manipulators/base/components/servo.py @@ -0,0 +1,522 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Standard servo control component for manipulator drivers.""" + +import logging +import time +from typing import Any + +from ..sdk_interface import BaseManipulatorSDK +from ..spec import ManipulatorCapabilities +from ..utils import SharedState +from . import component_api + + +class StandardServoComponent: + """Servo control component that works with any SDK wrapper. + + This component provides standard servo/motor control methods that work + consistently across all manipulator types. Methods decorated with @component_api + are automatically exposed as RPC methods on the driver. It handles: + - Servo enable/disable + - Control mode switching + - Emergency stop + - Error recovery + - Homing operations + """ + + def __init__( + self, + sdk: BaseManipulatorSDK | None = None, + shared_state: SharedState | None = None, + capabilities: ManipulatorCapabilities | None = None, + ): + """Initialize the servo component. + + Args: + sdk: SDK wrapper instance (can be set later) + shared_state: Shared state instance (can be set later) + capabilities: Manipulator capabilities (can be set later) + """ + self.sdk = sdk + self.shared_state = shared_state + self.capabilities = capabilities + self.logger = logging.getLogger(self.__class__.__name__) + + # State tracking + self.last_enable_time = 0.0 + self.last_disable_time = 0.0 + + # ============= Initialization Methods (called by BaseDriver) ============= + + def set_sdk(self, sdk: BaseManipulatorSDK) -> None: + """Set the SDK wrapper instance.""" + self.sdk = sdk + + def set_shared_state(self, shared_state: SharedState) -> None: + """Set the shared state instance.""" + self.shared_state = shared_state + + def set_capabilities(self, capabilities: ManipulatorCapabilities) -> None: + """Set the capabilities instance.""" + self.capabilities = capabilities + + def initialize(self) -> None: + """Initialize the component after all resources are set.""" + self.logger.debug("Servo component initialized") + + # ============= Component API Methods ============= + + @component_api + def enable_servo(self, check_errors: bool = True) -> dict[str, Any]: + """Enable servo/motor control. + + Args: + check_errors: If True, check for errors before enabling + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + # Check if already enabled + if self.sdk.are_servos_enabled(): + return {"success": True, "message": "Servos already enabled"} + + # Check for errors if requested + if check_errors: + error_code = self.sdk.get_error_code() + if error_code != 0: + error_msg = self.sdk.get_error_message() + return { + "success": False, + "error": f"Cannot enable servos with active error: {error_msg} (code: {error_code})", + } + + # Enable servos + success = self.sdk.enable_servos() + + if success: + self.last_enable_time = time.time() + if self.shared_state: + self.shared_state.is_enabled = True + self.logger.info("Servos enabled successfully") + else: + self.logger.error("Failed to enable servos") + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in enable_servo: {e}") + return {"success": False, "error": str(e)} + + @component_api + def disable_servo(self, stop_motion: bool = True) -> dict[str, Any]: + """Disable servo/motor control. + + Args: + stop_motion: If True, stop any ongoing motion first + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + # Check if already disabled + if not self.sdk.are_servos_enabled(): + return {"success": True, "message": "Servos already disabled"} + + # Stop motion if requested + if stop_motion: + self.sdk.stop_motion() + time.sleep(0.1) # Brief delay to ensure motion stopped + + # Disable servos + success = self.sdk.disable_servos() + + if success: + self.last_disable_time = time.time() + if self.shared_state: + self.shared_state.is_enabled = False + self.logger.info("Servos disabled successfully") + else: + self.logger.error("Failed to disable servos") + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in disable_servo: {e}") + return {"success": False, "error": str(e)} + + @component_api + def toggle_servo(self) -> dict[str, Any]: + """Toggle servo enable/disable state. + + Returns: + Dict with 'success', 'enabled' state, and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + current_state = self.sdk.are_servos_enabled() + + if current_state: + result = self.disable_servo() + else: + result = self.enable_servo() + + if result["success"]: + result["enabled"] = not current_state + + return result + + except Exception as e: + self.logger.error(f"Error in toggle_servo: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_servo_state(self) -> dict[str, Any]: + """Get current servo state. + + Returns: + Dict with servo state information + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + enabled = self.sdk.are_servos_enabled() + robot_state = self.sdk.get_robot_state() + + return { + "enabled": enabled, + "mode": robot_state.get("mode", 0), + "state": robot_state.get("state", 0), + "is_moving": robot_state.get("is_moving", False), + "last_enable_time": self.last_enable_time, + "last_disable_time": self.last_disable_time, + "success": True, + } + + except Exception as e: + self.logger.error(f"Error in get_servo_state: {e}") + return {"success": False, "error": str(e)} + + @component_api + def emergency_stop(self) -> dict[str, Any]: + """Execute emergency stop. + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + # Execute e-stop + success = self.sdk.emergency_stop() + + if success: + # Update shared state + if self.shared_state: + self.shared_state.update_robot_state(state=3) # 3 = e-stop state + self.shared_state.is_enabled = False + self.shared_state.is_moving = False + + self.logger.warning("Emergency stop executed") + else: + self.logger.error("Failed to execute emergency stop") + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in emergency_stop: {e}") + # Try to stop motion as fallback + try: + if self.sdk is not None: + self.sdk.stop_motion() + except: + pass + return {"success": False, "error": str(e)} + + @component_api + def reset_emergency_stop(self) -> dict[str, Any]: + """Reset from emergency stop state. + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + # Clear errors first + self.sdk.clear_errors() + + # Re-enable servos + success = self.sdk.enable_servos() + + if success: + if self.shared_state: + self.shared_state.update_robot_state(state=0) # 0 = idle + self.shared_state.is_enabled = True + + self.logger.info("Emergency stop reset successfully") + else: + self.logger.error("Failed to reset emergency stop") + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in reset_emergency_stop: {e}") + return {"success": False, "error": str(e)} + + @component_api + def set_control_mode(self, mode: str) -> dict[str, Any]: + """Set control mode. + + Args: + mode: Control mode ('position', 'velocity', 'torque', 'impedance') + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + # Validate mode + valid_modes = ["position", "velocity", "torque", "impedance"] + if mode not in valid_modes: + return { + "success": False, + "error": f"Invalid mode '{mode}'. Valid modes: {valid_modes}", + } + + # Check if mode is supported + if mode == "impedance" and self.capabilities: + if not self.capabilities.has_impedance_control: + return {"success": False, "error": "Impedance control not supported"} + + # Set control mode + success = self.sdk.set_control_mode(mode) + + if success: + # Map mode string to integer + mode_map = {"position": 0, "velocity": 1, "torque": 2, "impedance": 3} + if self.shared_state: + self.shared_state.update_robot_state(mode=mode_map.get(mode, 0)) + + self.logger.info(f"Control mode set to '{mode}'") + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in set_control_mode: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_control_mode(self) -> dict[str, Any]: + """Get current control mode. + + Returns: + Dict with current mode and success status + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + mode = self.sdk.get_control_mode() + + if mode: + return {"mode": mode, "success": True} + else: + # Try to get from robot state + robot_state = self.sdk.get_robot_state() + mode_int = robot_state.get("mode", 0) + + # Map integer to string + mode_map = {0: "position", 1: "velocity", 2: "torque", 3: "impedance"} + mode_str = mode_map.get(mode_int, "unknown") + + return {"mode": mode_str, "success": True} + + except Exception as e: + self.logger.error(f"Error in get_control_mode: {e}") + return {"success": False, "error": str(e)} + + @component_api + def clear_errors(self) -> dict[str, Any]: + """Clear any error states. + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + # Clear errors via SDK + success = self.sdk.clear_errors() + + if success: + # Update shared state + if self.shared_state: + self.shared_state.clear_errors() + + self.logger.info("Errors cleared successfully") + else: + self.logger.error("Failed to clear errors") + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in clear_errors: {e}") + return {"success": False, "error": str(e)} + + @component_api + def reset_fault(self) -> dict[str, Any]: + """Reset from fault state. + + This typically involves: + 1. Clearing errors + 2. Disabling servos + 3. Brief delay + 4. Re-enabling servos + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + self.logger.info("Resetting fault state...") + + # Step 1: Clear errors + if not self.sdk.clear_errors(): + return {"success": False, "error": "Failed to clear errors"} + + # Step 2: Disable servos if enabled + if self.sdk.are_servos_enabled(): + if not self.sdk.disable_servos(): + return {"success": False, "error": "Failed to disable servos"} + + # Step 3: Brief delay + time.sleep(0.5) + + # Step 4: Re-enable servos + if not self.sdk.enable_servos(): + return {"success": False, "error": "Failed to re-enable servos"} + + # Update shared state + if self.shared_state: + self.shared_state.update_robot_state( + state=0, # idle + error_code=0, + error_message="", + ) + self.shared_state.is_enabled = True + + self.logger.info("Fault reset successfully") + return {"success": True} + + except Exception as e: + self.logger.error(f"Error in reset_fault: {e}") + return {"success": False, "error": str(e)} + + @component_api + def home_robot(self, position: list[float] | None = None) -> dict[str, Any]: + """Move robot to home position. + + Args: + position: Optional home position in radians. + If None, uses zero position or configured home. + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + # Determine home position + if position is None: + # Use configured home or zero position + if self.capabilities: + position = [0.0] * self.capabilities.dof + else: + # Get current DOF from joint state + current = self.sdk.get_joint_positions() + position = [0.0] * len(current) + + # Enable servos if needed + if not self.sdk.are_servos_enabled(): + if not self.sdk.enable_servos(): + return {"success": False, "error": "Failed to enable servos"} + + # Move to home position + success = self.sdk.set_joint_positions( + position, + velocity=0.3, # Slower speed for homing + acceleration=0.3, + wait=True, # Wait for completion + ) + + if success: + if self.shared_state: + self.shared_state.is_homed = True + self.logger.info("Robot homed successfully") + + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in home_robot: {e}") + return {"success": False, "error": str(e)} + + @component_api + def brake_release(self) -> dict[str, Any]: + """Release motor brakes (if applicable). + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # This is typically the same as enabling servos + return self.enable_servo() + + except Exception as e: + self.logger.error(f"Error in brake_release: {e}") + return {"success": False, "error": str(e)} + + @component_api + def brake_engage(self) -> dict[str, Any]: + """Engage motor brakes (if applicable). + + Returns: + Dict with 'success' and optional 'error' keys + """ + try: + # This is typically the same as disabling servos + return self.disable_servo() + + except Exception as e: + self.logger.error(f"Error in brake_engage: {e}") + return {"success": False, "error": str(e)} diff --git a/dimos/hardware/manipulators/base/components/status.py b/dimos/hardware/manipulators/base/components/status.py new file mode 100644 index 0000000000..b20897ac65 --- /dev/null +++ b/dimos/hardware/manipulators/base/components/status.py @@ -0,0 +1,595 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Standard status monitoring component for manipulator drivers.""" + +from collections import deque +from dataclasses import dataclass +import logging +import time +from typing import Any + +from ..sdk_interface import BaseManipulatorSDK +from ..spec import ManipulatorCapabilities +from ..utils import SharedState +from . import component_api + + +@dataclass +class HealthMetrics: + """Health metrics for monitoring.""" + + update_rate: float = 0.0 # Hz + command_rate: float = 0.0 # Hz + error_rate: float = 0.0 # errors/minute + uptime: float = 0.0 # seconds + total_errors: int = 0 + total_commands: int = 0 + total_updates: int = 0 + + +class StandardStatusComponent: + """Status monitoring component that works with any SDK wrapper. + + This component provides standard status monitoring methods that work + consistently across all manipulator types. Methods decorated with @component_api + are automatically exposed as RPC methods on the driver. It handles: + - Robot state queries + - Error monitoring + - Health metrics + - System information + - Force/torque monitoring (if supported) + - Temperature monitoring (if supported) + """ + + def __init__( + self, + sdk: BaseManipulatorSDK | None = None, + shared_state: SharedState | None = None, + capabilities: ManipulatorCapabilities | None = None, + ): + """Initialize the status component. + + Args: + sdk: SDK wrapper instance (can be set later) + shared_state: Shared state instance (can be set later) + capabilities: Manipulator capabilities (can be set later) + """ + self.sdk = sdk + self.shared_state = shared_state + self.capabilities = capabilities + self.logger = logging.getLogger(self.__class__.__name__) + + # Health monitoring + self.start_time = time.time() + self.health_metrics = HealthMetrics() + + # Rate calculation + self.update_timestamps: deque[float] = deque(maxlen=100) + self.command_timestamps: deque[float] = deque(maxlen=100) + self.error_timestamps: deque[float] = deque(maxlen=100) + + # Error history + self.error_history: deque[dict[str, Any]] = deque(maxlen=50) + + # ============= Initialization Methods (called by BaseDriver) ============= + + def set_sdk(self, sdk: BaseManipulatorSDK) -> None: + """Set the SDK wrapper instance.""" + self.sdk = sdk + + def set_shared_state(self, shared_state: SharedState) -> None: + """Set the shared state instance.""" + self.shared_state = shared_state + + def set_capabilities(self, capabilities: ManipulatorCapabilities) -> None: + """Set the capabilities instance.""" + self.capabilities = capabilities + + def initialize(self) -> None: + """Initialize the component after all resources are set.""" + self.start_time = time.time() + self.logger.debug("Status component initialized") + + def publish_state(self) -> None: + """Called periodically to update metrics (by publisher thread).""" + current_time = time.time() + self.update_timestamps.append(current_time) + self._update_health_metrics() + + # ============= Component API Methods ============= + + @component_api + def get_robot_state(self) -> dict[str, Any]: + """Get comprehensive robot state. + + Returns: + Dict with complete state information + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + current_time = time.time() + + # Get state from SDK + robot_state = self.sdk.get_robot_state() + + # Get additional info + error_msg = ( + self.sdk.get_error_message() if robot_state.get("error_code", 0) != 0 else "" + ) + + # Map state integer to string + state_map = {0: "idle", 1: "moving", 2: "error", 3: "emergency_stop"} + state_str = state_map.get(robot_state.get("state", 0), "unknown") + + # Map mode integer to string + mode_map = {0: "position", 1: "velocity", 2: "torque", 3: "impedance"} + mode_str = mode_map.get(robot_state.get("mode", 0), "unknown") + + result = { + "state": state_str, + "state_code": robot_state.get("state", 0), + "mode": mode_str, + "mode_code": robot_state.get("mode", 0), + "error_code": robot_state.get("error_code", 0), + "error_message": error_msg, + "is_moving": robot_state.get("is_moving", False), + "is_connected": self.sdk.is_connected(), + "is_enabled": self.sdk.are_servos_enabled(), + "timestamp": current_time, + "success": True, + } + + # Add shared state info if available + if self.shared_state: + result["is_homed"] = self.shared_state.is_homed + result["last_update"] = self.shared_state.last_state_update + result["last_command"] = self.shared_state.last_command_sent + + return result + + except Exception as e: + self.logger.error(f"Error in get_robot_state: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_system_info(self) -> dict[str, Any]: + """Get system information. + + Returns: + Dict with system information + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + # Get manipulator info + info = self.sdk.get_info() + + result = { + "vendor": info.vendor, + "model": info.model, + "dof": info.dof, + "firmware_version": info.firmware_version, + "serial_number": info.serial_number, + "success": True, + } + + # Add capabilities if available + if self.capabilities: + result["capabilities"] = { + "dof": self.capabilities.dof, + "has_gripper": self.capabilities.has_gripper, + "has_force_torque": self.capabilities.has_force_torque, + "has_impedance_control": self.capabilities.has_impedance_control, + "has_cartesian_control": self.capabilities.has_cartesian_control, + "payload_mass": self.capabilities.payload_mass, + "reach": self.capabilities.reach, + } + + return result + + except Exception as e: + self.logger.error(f"Error in get_system_info: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_capabilities(self) -> dict[str, Any]: + """Get manipulator capabilities. + + Returns: + Dict with capability information + """ + try: + if not self.capabilities: + return {"success": False, "error": "Capabilities not available"} + + return { + "dof": self.capabilities.dof, + "has_gripper": self.capabilities.has_gripper, + "has_force_torque": self.capabilities.has_force_torque, + "has_impedance_control": self.capabilities.has_impedance_control, + "has_cartesian_control": self.capabilities.has_cartesian_control, + "joint_limits_lower": self.capabilities.joint_limits_lower, + "joint_limits_upper": self.capabilities.joint_limits_upper, + "max_joint_velocity": self.capabilities.max_joint_velocity, + "max_joint_acceleration": self.capabilities.max_joint_acceleration, + "payload_mass": self.capabilities.payload_mass, + "reach": self.capabilities.reach, + "success": True, + } + + except Exception as e: + self.logger.error(f"Error in get_capabilities: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_error_state(self) -> dict[str, Any]: + """Get detailed error state. + + Returns: + Dict with error information + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + error_code = self.sdk.get_error_code() + error_msg = self.sdk.get_error_message() + + result = { + "has_error": error_code != 0, + "error_code": error_code, + "error_message": error_msg, + "error_history": list(self.error_history), + "total_errors": self.health_metrics.total_errors, + "success": True, + } + + # Add last error time from shared state + if self.shared_state: + result["last_error_time"] = self.shared_state.last_error_time + + return result + + except Exception as e: + self.logger.error(f"Error in get_error_state: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_health_metrics(self) -> dict[str, Any]: + """Get health metrics. + + Returns: + Dict with health metrics + """ + try: + self._update_health_metrics() + + return { + "uptime": self.health_metrics.uptime, + "update_rate": self.health_metrics.update_rate, + "command_rate": self.health_metrics.command_rate, + "error_rate": self.health_metrics.error_rate, + "total_updates": self.health_metrics.total_updates, + "total_commands": self.health_metrics.total_commands, + "total_errors": self.health_metrics.total_errors, + "is_healthy": self._is_healthy(), + "timestamp": time.time(), + "success": True, + } + + except Exception as e: + self.logger.error(f"Error in get_health_metrics: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_statistics(self) -> dict[str, Any]: + """Get operation statistics. + + Returns: + Dict with statistics + """ + try: + stats = {} + + # Get stats from shared state + if self.shared_state: + stats.update(self.shared_state.get_statistics()) + + # Add component stats + stats["uptime"] = time.time() - self.start_time + stats["health_metrics"] = { + "update_rate": self.health_metrics.update_rate, + "command_rate": self.health_metrics.command_rate, + "error_rate": self.health_metrics.error_rate, + } + + stats["success"] = True + return stats + + except Exception as e: + self.logger.error(f"Error in get_statistics: {e}") + return {"success": False, "error": str(e)} + + @component_api + def check_connection(self) -> dict[str, Any]: + """Check connection status. + + Returns: + Dict with connection status + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + connected = self.sdk.is_connected() + + result: dict[str, Any] = { + "connected": connected, + "timestamp": time.time(), + "success": True, + } + + # Try to get more info if connected + if connected: + try: + # Try a simple query to verify connection + self.sdk.get_error_code() + result["verified"] = True + except: + result["verified"] = False + result["message"] = "Connected but cannot communicate" + + return result + + except Exception as e: + self.logger.error(f"Error in check_connection: {e}") + return {"success": False, "error": str(e)} + + # ============= Force/Torque Monitoring (Optional) ============= + + @component_api + def get_force_torque(self) -> dict[str, Any]: + """Get force/torque sensor data. + + Returns: + Dict with F/T data if available + """ + try: + # Check if F/T is supported + if not self.capabilities or not self.capabilities.has_force_torque: + return {"success": False, "error": "Force/torque sensor not available"} + + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + ft_data = self.sdk.get_force_torque() + + if ft_data: + return { + "force": ft_data[:3] if len(ft_data) >= 3 else None, # [fx, fy, fz] + "torque": ft_data[3:6] if len(ft_data) >= 6 else None, # [tx, ty, tz] + "data": ft_data, + "timestamp": time.time(), + "success": True, + } + else: + return {"success": False, "error": "Failed to read F/T sensor"} + + except Exception as e: + self.logger.error(f"Error in get_force_torque: {e}") + return {"success": False, "error": str(e)} + + @component_api + def zero_force_torque(self) -> dict[str, Any]: + """Zero the force/torque sensor. + + Returns: + Dict with success status + """ + try: + # Check if F/T is supported + if not self.capabilities or not self.capabilities.has_force_torque: + return {"success": False, "error": "Force/torque sensor not available"} + + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + success = self.sdk.zero_force_torque() + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in zero_force_torque: {e}") + return {"success": False, "error": str(e)} + + # ============= I/O Monitoring (Optional) ============= + + @component_api + def get_digital_inputs(self) -> dict[str, Any]: + """Get digital input states. + + Returns: + Dict with digital input states + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + inputs = self.sdk.get_digital_inputs() + + if inputs is not None: + return {"inputs": inputs, "timestamp": time.time(), "success": True} + else: + return {"success": False, "error": "Digital inputs not available"} + + except Exception as e: + self.logger.error(f"Error in get_digital_inputs: {e}") + return {"success": False, "error": str(e)} + + @component_api + def set_digital_outputs(self, outputs: dict[str, bool]) -> dict[str, Any]: + """Set digital output states. + + Args: + outputs: Dict of output_id: bool + + Returns: + Dict with success status + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + success = self.sdk.set_digital_outputs(outputs) + return {"success": success} + + except Exception as e: + self.logger.error(f"Error in set_digital_outputs: {e}") + return {"success": False, "error": str(e)} + + @component_api + def get_analog_inputs(self) -> dict[str, Any]: + """Get analog input values. + + Returns: + Dict with analog input values + """ + try: + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + inputs = self.sdk.get_analog_inputs() + + if inputs is not None: + return {"inputs": inputs, "timestamp": time.time(), "success": True} + else: + return {"success": False, "error": "Analog inputs not available"} + + except Exception as e: + self.logger.error(f"Error in get_analog_inputs: {e}") + return {"success": False, "error": str(e)} + + # ============= Gripper Status (Optional) ============= + + @component_api + def get_gripper_state(self) -> dict[str, Any]: + """Get gripper state. + + Returns: + Dict with gripper state + """ + try: + # Check if gripper is supported + if not self.capabilities or not self.capabilities.has_gripper: + return {"success": False, "error": "Gripper not available"} + + if self.sdk is None: + return {"success": False, "error": "SDK not configured"} + + position = self.sdk.get_gripper_position() + + if position is not None: + result: dict[str, Any] = { + "position": position, # meters + "timestamp": time.time(), + "success": True, + } + + # Add from shared state if available + if self.shared_state and self.shared_state.gripper_force is not None: + result["force"] = self.shared_state.gripper_force + + return result + else: + return {"success": False, "error": "Failed to get gripper state"} + + except Exception as e: + self.logger.error(f"Error in get_gripper_state: {e}") + return {"success": False, "error": str(e)} + + # ============= Helper Methods ============= + + def _update_health_metrics(self) -> None: + """Update health metrics based on recent data.""" + current_time = time.time() + + # Update uptime + self.health_metrics.uptime = current_time - self.start_time + + # Calculate update rate + if len(self.update_timestamps) > 1: + time_span = self.update_timestamps[-1] - self.update_timestamps[0] + if time_span > 0: + self.health_metrics.update_rate = len(self.update_timestamps) / time_span + + # Calculate command rate + if len(self.command_timestamps) > 1: + time_span = self.command_timestamps[-1] - self.command_timestamps[0] + if time_span > 0: + self.health_metrics.command_rate = len(self.command_timestamps) / time_span + + # Calculate error rate (errors per minute) + recent_errors = [t for t in self.error_timestamps if current_time - t < 60] + self.health_metrics.error_rate = len(recent_errors) + + # Update totals from shared state + if self.shared_state: + stats = self.shared_state.get_statistics() + self.health_metrics.total_updates = stats.get("state_read_count", 0) + self.health_metrics.total_commands = stats.get("command_sent_count", 0) + self.health_metrics.total_errors = stats.get("error_count", 0) + + def _is_healthy(self) -> bool: + """Check if system is healthy based on metrics.""" + # Check update rate (should be > 10 Hz) + if self.health_metrics.update_rate < 10: + return False + + # Check error rate (should be < 10 per minute) + if self.health_metrics.error_rate > 10: + return False + + # Check SDK is configured + if self.sdk is None: + return False + + # Check connection + if not self.sdk.is_connected(): + return False + + # Check for persistent errors + if self.sdk.get_error_code() != 0: + return False + + return True + + def record_error(self, error_code: int, error_msg: str) -> None: + """Record an error occurrence. + + Args: + error_code: Error code + error_msg: Error message + """ + current_time = time.time() + self.error_timestamps.append(current_time) + self.error_history.append( + {"code": error_code, "message": error_msg, "timestamp": current_time} + ) + + def record_command(self) -> None: + """Record a command occurrence.""" + self.command_timestamps.append(time.time()) diff --git a/dimos/hardware/manipulators/base/driver.py b/dimos/hardware/manipulators/base/driver.py new file mode 100644 index 0000000000..be68be5a23 --- /dev/null +++ b/dimos/hardware/manipulators/base/driver.py @@ -0,0 +1,637 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base manipulator driver with threading and component management.""" + +from dataclasses import dataclass +import logging +from queue import Empty, Queue +from threading import Event, Thread +import time +from typing import Any + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import WrenchStamped +from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState + +from .sdk_interface import BaseManipulatorSDK +from .spec import ManipulatorCapabilities +from .utils import SharedState + + +@dataclass +class Command: + """Command to be sent to the manipulator.""" + + type: str # 'position', 'velocity', 'effort', 'cartesian', etc. + data: Any + timestamp: float = 0.0 + + +class BaseManipulatorDriver(Module): + """Base driver providing threading and component management. + + This class handles: + - Thread management (state reader, command sender, state publisher) + - Component registration and lifecycle + - RPC method registration + - Shared state management + - Error handling and recovery + - Pub/Sub with LCM transport for real-time control + """ + + # Input topics (commands from controllers - initialized by Module) + joint_position_command: In[JointCommand] = None # type: ignore[assignment] + joint_velocity_command: In[JointCommand] = None # type: ignore[assignment] + + # Output topics (state publishing - initialized by Module) + joint_state: Out[JointState] = None # type: ignore[assignment] + robot_state: Out[RobotState] = None # type: ignore[assignment] + ft_sensor: Out[WrenchStamped] = None # type: ignore[assignment] + + def __init__( + self, + sdk: BaseManipulatorSDK, + components: list[Any], + config: dict[str, Any], + name: str | None = None, + *args: Any, + **kwargs: Any, + ) -> None: + """Initialize the base manipulator driver. + + Args: + sdk: SDK wrapper instance + components: List of component instances + config: Configuration dictionary + name: Optional driver name for logging + *args, **kwargs: Additional arguments for Module + """ + # Initialize Module parent class + super().__init__(*args, **kwargs) + + self.sdk = sdk + self.components = components + self.config: Any = config # Config dict accessed as object + self.name = name or self.__class__.__name__ + + # Logging + self.logger = logging.getLogger(self.name) + + # Shared state + self.shared_state = SharedState() + + # Threading + self.stop_event = Event() + self.threads: list[Thread] = [] + self.command_queue: Queue[Any] = Queue(maxsize=10) + + # RPC registry + self.rpc_methods: dict[str, Any] = {} + self._exposed_component_apis: set[str] = set() # Track auto-exposed method names + + # Capabilities + self.capabilities = self._get_capabilities() + + # Rate control + self.control_rate = config.get("control_rate", 100) # Hz - control loop + joint feedback + self.monitor_rate = config.get("monitor_rate", 10) # Hz - robot state monitoring + + # Pre-allocate reusable objects (optimization: avoid per-cycle allocation) + # Note: _joint_names is populated after _get_capabilities() sets self.capabilities + self._joint_names: list[str] = [f"joint{i + 1}" for i in range(self.capabilities.dof)] + + # Initialize components with shared resources + self._initialize_components() + + # Auto-expose component API methods as RPCs on the driver + self._auto_expose_component_apis() + + # Connect to hardware + self._connect() + + def _get_capabilities(self) -> ManipulatorCapabilities: + """Get manipulator capabilities from config or SDK. + + Returns: + ManipulatorCapabilities instance + """ + # Try to get from SDK info + info = self.sdk.get_info() + + # Get joint limits + lower_limits, upper_limits = self.sdk.get_joint_limits() + velocity_limits = self.sdk.get_velocity_limits() + acceleration_limits = self.sdk.get_acceleration_limits() + + return ManipulatorCapabilities( + dof=info.dof, + has_gripper=self.config.get("has_gripper", False), + has_force_torque=self.config.get("has_force_torque", False), + has_impedance_control=self.config.get("has_impedance_control", False), + has_cartesian_control=self.config.get("has_cartesian_control", False), + max_joint_velocity=velocity_limits, + max_joint_acceleration=acceleration_limits, + joint_limits_lower=lower_limits, + joint_limits_upper=upper_limits, + payload_mass=self.config.get("payload_mass", 0.0), + reach=self.config.get("reach", 0.0), + ) + + def _initialize_components(self) -> None: + """Initialize components with shared resources.""" + for component in self.components: + # Provide access to shared state + if hasattr(component, "set_shared_state"): + component.set_shared_state(self.shared_state) + + # Provide access to SDK + if hasattr(component, "set_sdk"): + component.set_sdk(self.sdk) + + # Provide access to command queue + if hasattr(component, "set_command_queue"): + component.set_command_queue(self.command_queue) + + # Provide access to capabilities + if hasattr(component, "set_capabilities"): + component.set_capabilities(self.capabilities) + + # Initialize component + if hasattr(component, "initialize"): + component.initialize() + + def _auto_expose_component_apis(self) -> None: + """Auto-expose @component_api methods from components as RPC methods on the driver. + + This scans all components for methods decorated with @component_api and creates + corresponding @rpc wrapper methods on the driver instance. This allows external + code to call these methods via the standard Module RPC system. + + Example: + # Component defines: + @component_api + def enable_servo(self): ... + + # Driver auto-generates an RPC wrapper, so external code can call: + driver.enable_servo() + + # And the method is discoverable via: + driver.rpcs # Lists 'enable_servo' among available RPCs + """ + for component in self.components: + for method_name in dir(component): + if method_name.startswith("_"): + continue + + method = getattr(component, method_name, None) + if not callable(method) or not getattr(method, "__component_api__", False): + continue + + # Skip if driver already has a non-wrapper method with this name + existing = getattr(self, method_name, None) + if existing is not None and not getattr( + existing, "__component_api_wrapper__", False + ): + self.logger.warning( + f"Driver already has method '{method_name}', skipping component API" + ) + continue + + # Create RPC wrapper - use factory to properly capture method reference + wrapper = self._create_component_api_wrapper(method) + + # Attach to driver instance + setattr(self, method_name, wrapper) + + # Store in rpc_methods dict for backward compatibility + self.rpc_methods[method_name] = wrapper + + # Track exposed method name for cleanup + self._exposed_component_apis.add(method_name) + + self.logger.debug(f"Exposed component API as RPC: {method_name}") + + def _create_component_api_wrapper(self, component_method: Any) -> Any: + """Create an RPC wrapper for a component API method. + + Args: + component_method: The component method to wrap + + Returns: + RPC-decorated wrapper function + """ + import functools + + @rpc + @functools.wraps(component_method) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return component_method(*args, **kwargs) + + wrapper.__component_api_wrapper__ = True # type: ignore[attr-defined] + return wrapper + + def _connect(self) -> None: + """Connect to the manipulator hardware.""" + self.logger.info(f"Connecting to {self.name}...") + + # Connect via SDK + if not self.sdk.connect(self.config): + raise RuntimeError(f"Failed to connect to {self.name}") + + self.shared_state.is_connected = True + self.logger.info(f"Successfully connected to {self.name}") + + # Get initial state + self._update_joint_state() + self._update_robot_state() + + def _update_joint_state(self) -> None: + """Update joint state from hardware (high frequency - 100Hz). + + Reads joint positions, velocities, efforts and publishes to LCM immediately. + """ + try: + # Get joint state feedback + positions = self.sdk.get_joint_positions() + velocities = self.sdk.get_joint_velocities() + efforts = self.sdk.get_joint_efforts() + + self.shared_state.update_joint_state( + positions=positions, velocities=velocities, efforts=efforts + ) + + # Publish joint state immediately at control rate + if self.joint_state and hasattr(self.joint_state, "publish"): + joint_state_msg = JointState( + ts=time.time(), + frame_id="joint-state", + name=self._joint_names, # Pre-allocated list (optimization) + position=positions or [0.0] * self.capabilities.dof, + velocity=velocities or [0.0] * self.capabilities.dof, + effort=efforts or [0.0] * self.capabilities.dof, + ) + self.joint_state.publish(joint_state_msg) + + except Exception as e: + self.logger.error(f"Error updating joint state: {e}") + + def _update_robot_state(self) -> None: + """Update robot state from hardware (low frequency - 10Hz). + + Reads robot mode, errors, warnings, optional states and publishes to LCM immediately. + """ + try: + # Get robot state (mode, errors, warnings) + robot_state = self.sdk.get_robot_state() + self.shared_state.update_robot_state( + state=robot_state.get("state", 0), + mode=robot_state.get("mode", 0), + error_code=robot_state.get("error_code", 0), + error_message=self.sdk.get_error_message(), + ) + + # Update status flags + self.shared_state.is_moving = robot_state.get("is_moving", False) + self.shared_state.is_enabled = self.sdk.are_servos_enabled() + + # Get optional states (cartesian, force/torque, gripper) + if self.capabilities.has_cartesian_control: + cart_pos = self.sdk.get_cartesian_position() + if cart_pos: + self.shared_state.cartesian_position = cart_pos + + if self.capabilities.has_force_torque: + ft = self.sdk.get_force_torque() + if ft: + self.shared_state.force_torque = ft + + if self.capabilities.has_gripper: + gripper_pos = self.sdk.get_gripper_position() + if gripper_pos is not None: + self.shared_state.gripper_position = gripper_pos + + # Publish robot state immediately at monitor rate + if self.robot_state and hasattr(self.robot_state, "publish"): + robot_state_msg = RobotState( + state=self.shared_state.robot_state, + mode=self.shared_state.control_mode, + error_code=self.shared_state.error_code, + warn_code=0, + ) + self.robot_state.publish(robot_state_msg) + + # Publish force/torque if available + if ( + self.ft_sensor + and hasattr(self.ft_sensor, "publish") + and self.capabilities.has_force_torque + ): + if self.shared_state.force_torque: + ft_msg = WrenchStamped.from_force_torque_array( + ft_data=self.shared_state.force_torque, + frame_id="ft_sensor", + ts=time.time(), + ) + self.ft_sensor.publish(ft_msg) + + except Exception as e: + self.logger.error(f"Error updating robot state: {e}") + self.shared_state.update_robot_state(error_code=999, error_message=str(e)) + + # ============= Threading ============= + + @rpc + def start(self) -> None: + """Start all driver threads and subscribe to input topics.""" + super().start() + self.logger.info(f"Starting {self.name} driver threads...") + + # Subscribe to input topics if they have transports + try: + if self.joint_position_command and hasattr(self.joint_position_command, "subscribe"): + self.joint_position_command.subscribe(self._on_joint_position_command) + self.logger.debug("Subscribed to joint_position_command") + except (AttributeError, ValueError) as e: + self.logger.debug(f"joint_position_command transport not configured: {e}") + + try: + if self.joint_velocity_command and hasattr(self.joint_velocity_command, "subscribe"): + self.joint_velocity_command.subscribe(self._on_joint_velocity_command) + self.logger.debug("Subscribed to joint_velocity_command") + except (AttributeError, ValueError) as e: + self.logger.debug(f"joint_velocity_command transport not configured: {e}") + + self.threads = [ + Thread(target=self._control_loop_thread, name=f"{self.name}-ControlLoop", daemon=True), + Thread( + target=self._robot_state_monitor_thread, + name=f"{self.name}-StateMonitor", + daemon=True, + ), + ] + + for thread in self.threads: + thread.start() + self.logger.debug(f"Started thread: {thread.name}") + + self.logger.info(f"{self.name} driver started successfully") + + def _control_loop_thread(self) -> None: + """Control loop: send commands AND read joint feedback (100Hz). + + This tight loop ensures synchronized command/feedback for real-time control. + """ + self.logger.debug("Control loop thread started") + period = 1.0 / self.control_rate + next_time = time.perf_counter() + period # perf_counter for precise timing + + while not self.stop_event.is_set(): + try: + # 1. Process all pending commands (non-blocking) + while True: + try: + command = self.command_queue.get_nowait() # Non-blocking (optimization) + self._process_command(command) + except Empty: + break # No more commands + + # 2. Read joint state feedback (critical for control) + self._update_joint_state() + + except Exception as e: + self.logger.error(f"Control loop error: {e}") + + # Rate control - maintain precise timing + next_time += period + sleep_time = next_time - time.perf_counter() + if sleep_time > 0: + time.sleep(sleep_time) + else: + # Fell behind - reset timing + next_time = time.perf_counter() + period + if sleep_time < -period: + self.logger.warning(f"Control loop fell behind by {-sleep_time:.3f}s") + + self.logger.debug("Control loop thread stopped") + + def _robot_state_monitor_thread(self) -> None: + """Monitor robot state: mode, errors, warnings (10-20Hz). + + Lower frequency monitoring for high-level planning and error handling. + """ + self.logger.debug("Robot state monitor thread started") + period = 1.0 / self.monitor_rate + next_time = time.perf_counter() + period # perf_counter for precise timing + + while not self.stop_event.is_set(): + try: + # Read robot state, mode, errors, optional states + self._update_robot_state() + except Exception as e: + self.logger.error(f"Robot state monitor error: {e}") + + # Rate control + next_time += period + sleep_time = next_time - time.perf_counter() + if sleep_time > 0: + time.sleep(sleep_time) + else: + next_time = time.perf_counter() + period + + self.logger.debug("Robot state monitor thread stopped") + + def _process_command(self, command: Command) -> None: + """Process a command from the queue. + + Args: + command: Command to process + """ + try: + if command.type == "position": + success = self.sdk.set_joint_positions( + command.data["positions"], + command.data.get("velocity", 1.0), + command.data.get("acceleration", 1.0), + command.data.get("wait", False), + ) + if success: + self.shared_state.target_positions = command.data["positions"] + + elif command.type == "velocity": + success = self.sdk.set_joint_velocities(command.data["velocities"]) + if success: + self.shared_state.target_velocities = command.data["velocities"] + + elif command.type == "effort": + success = self.sdk.set_joint_efforts(command.data["efforts"]) + if success: + self.shared_state.target_efforts = command.data["efforts"] + + elif command.type == "cartesian": + success = self.sdk.set_cartesian_position( + command.data["pose"], + command.data.get("velocity", 1.0), + command.data.get("acceleration", 1.0), + command.data.get("wait", False), + ) + if success: + self.shared_state.target_cartesian_position = command.data["pose"] + + elif command.type == "stop": + self.sdk.stop_motion() + + else: + self.logger.warning(f"Unknown command type: {command.type}") + + except Exception as e: + self.logger.error(f"Error processing command {command.type}: {e}") + + # ============= Input Callbacks ============= + + def _on_joint_position_command(self, cmd_msg: JointCommand) -> None: + """Callback when joint position command is received. + + Args: + cmd_msg: JointCommand message containing positions + """ + command = Command( + type="position", data={"positions": list(cmd_msg.positions)}, timestamp=time.time() + ) + try: + self.command_queue.put_nowait(command) + except: + self.logger.warning("Command queue full, dropping position command") + + def _on_joint_velocity_command(self, cmd_msg: JointCommand) -> None: + """Callback when joint velocity command is received. + + Args: + cmd_msg: JointCommand message containing velocities + """ + command = Command( + type="velocity", + data={"velocities": list(cmd_msg.positions)}, # JointCommand uses 'positions' field + timestamp=time.time(), + ) + try: + self.command_queue.put_nowait(command) + except: + self.logger.warning("Command queue full, dropping velocity command") + + # ============= Lifecycle Management ============= + + @rpc + def stop(self) -> None: + """Stop all threads and disconnect from hardware.""" + self.logger.info(f"Stopping {self.name} driver...") + + # Signal threads to stop + self.stop_event.set() + + # Stop any ongoing motion + try: + self.sdk.stop_motion() + except: + pass + + # Wait for threads to stop + for thread in self.threads: + thread.join(timeout=2.0) + if thread.is_alive(): + self.logger.warning(f"Thread {thread.name} did not stop cleanly") + + # Disconnect from hardware + try: + self.sdk.disconnect() + except: + pass + + self.shared_state.is_connected = False + self.logger.info(f"{self.name} driver stopped") + + # Call Module's stop + super().stop() + + def __del__(self) -> None: + """Cleanup on deletion.""" + if self.shared_state.is_connected: + self.stop() + + # ============= RPC Method Access ============= + + def get_rpc_method(self, method_name: str) -> Any: + """Get an RPC method by name. + + Args: + method_name: Name of the RPC method + + Returns: + The method if found, None otherwise + """ + return self.rpc_methods.get(method_name) + + def list_rpc_methods(self) -> list[str]: + """List all available RPC methods. + + Returns: + List of RPC method names + """ + return list(self.rpc_methods.keys()) + + # ============= Component Access ============= + + def get_component(self, component_type: type[Any]) -> Any: + """Get a component by type. + + Args: + component_type: Type of component to find + + Returns: + The component if found, None otherwise + """ + for component in self.components: + if isinstance(component, component_type): + return component + return None + + def add_component(self, component: Any) -> None: + """Add a component at runtime. + + Args: + component: Component instance to add + """ + self.components.append(component) + self._initialize_components() + self._auto_expose_component_apis() + + def remove_component(self, component: Any) -> None: + """Remove a component at runtime. + + Args: + component: Component instance to remove + """ + if component in self.components: + self.components.remove(component) + # Clean up old exposed methods and re-expose for remaining components + self._cleanup_exposed_component_apis() + self._auto_expose_component_apis() + + def _cleanup_exposed_component_apis(self) -> None: + """Remove all auto-exposed component API methods from the driver.""" + for method_name in self._exposed_component_apis: + if hasattr(self, method_name): + delattr(self, method_name) + self._exposed_component_apis.clear() + self.rpc_methods.clear() diff --git a/dimos/hardware/manipulators/base/sdk_interface.py b/dimos/hardware/manipulators/base/sdk_interface.py new file mode 100644 index 0000000000..f20d35bd50 --- /dev/null +++ b/dimos/hardware/manipulators/base/sdk_interface.py @@ -0,0 +1,471 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base SDK interface that all manipulator SDK wrappers must implement. + +This interface defines the standard methods and units that all SDK wrappers +must provide, ensuring consistent behavior across different manipulator types. + +Standard Units: +- Angles: radians +- Angular velocity: rad/s +- Linear position: meters +- Linear velocity: m/s +- Force: Newtons +- Torque: Nm +- Time: seconds +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any + + +@dataclass +class ManipulatorInfo: + """Information about the manipulator.""" + + vendor: str + model: str + dof: int + firmware_version: str | None = None + serial_number: str | None = None + + +class BaseManipulatorSDK(ABC): + """Abstract base class for manipulator SDK wrappers. + + All SDK wrappers must implement this interface to ensure compatibility + with the standard components. Methods should handle unit conversions + internally to always work with standard units. + """ + + # ============= Connection Management ============= + + @abstractmethod + def connect(self, config: dict[str, Any]) -> bool: + """Establish connection to the manipulator. + + Args: + config: Configuration dict with connection parameters + (e.g., ip, port, can_interface, etc.) + + Returns: + True if connection successful, False otherwise + """ + pass + + @abstractmethod + def disconnect(self) -> None: + """Disconnect from the manipulator. + + Should cleanly close all connections and free resources. + """ + pass + + @abstractmethod + def is_connected(self) -> bool: + """Check if currently connected to the manipulator. + + Returns: + True if connected, False otherwise + """ + pass + + # ============= Joint State Query ============= + + @abstractmethod + def get_joint_positions(self) -> list[float]: + """Get current joint positions. + + Returns: + Joint positions in RADIANS + """ + pass + + @abstractmethod + def get_joint_velocities(self) -> list[float]: + """Get current joint velocities. + + Returns: + Joint velocities in RAD/S + """ + pass + + @abstractmethod + def get_joint_efforts(self) -> list[float]: + """Get current joint efforts/torques. + + Returns: + Joint efforts in Nm (torque) or N (force) + """ + pass + + # ============= Joint Motion Control ============= + + @abstractmethod + def set_joint_positions( + self, + positions: list[float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + ) -> bool: + """Move joints to target positions. + + Args: + positions: Target positions in RADIANS + velocity: Max velocity as fraction of maximum (0-1) + acceleration: Max acceleration as fraction of maximum (0-1) + wait: If True, block until motion completes + + Returns: + True if command accepted, False otherwise + """ + pass + + @abstractmethod + def set_joint_velocities(self, velocities: list[float]) -> bool: + """Set joint velocity targets. + + Args: + velocities: Target velocities in RAD/S + + Returns: + True if command accepted, False otherwise + """ + pass + + @abstractmethod + def set_joint_efforts(self, efforts: list[float]) -> bool: + """Set joint effort/torque targets. + + Args: + efforts: Target efforts in Nm (torque) or N (force) + + Returns: + True if command accepted, False otherwise + """ + pass + + @abstractmethod + def stop_motion(self) -> bool: + """Stop all ongoing motion immediately. + + Returns: + True if stop successful, False otherwise + """ + pass + + # ============= Servo Control ============= + + @abstractmethod + def enable_servos(self) -> bool: + """Enable motor control (servos/brakes released). + + Returns: + True if servos enabled, False otherwise + """ + pass + + @abstractmethod + def disable_servos(self) -> bool: + """Disable motor control (servos/brakes engaged). + + Returns: + True if servos disabled, False otherwise + """ + pass + + @abstractmethod + def are_servos_enabled(self) -> bool: + """Check if servos are currently enabled. + + Returns: + True if enabled, False if disabled + """ + pass + + # ============= System State ============= + + @abstractmethod + def get_robot_state(self) -> dict[str, Any]: + """Get current robot state information. + + Returns: + Dict with at least these keys: + - 'state': int (0=idle, 1=moving, 2=error, 3=e-stop) + - 'mode': int (0=position, 1=velocity, 2=torque) + - 'error_code': int (0 = no error) + - 'is_moving': bool + """ + pass + + @abstractmethod + def get_error_code(self) -> int: + """Get current error code. + + Returns: + Error code (0 = no error) + """ + pass + + @abstractmethod + def get_error_message(self) -> str: + """Get human-readable error message. + + Returns: + Error message string (empty if no error) + """ + pass + + @abstractmethod + def clear_errors(self) -> bool: + """Clear any error states. + + Returns: + True if errors cleared, False otherwise + """ + pass + + @abstractmethod + def emergency_stop(self) -> bool: + """Execute emergency stop. + + Returns: + True if e-stop executed, False otherwise + """ + pass + + # ============= Information ============= + + @abstractmethod + def get_info(self) -> ManipulatorInfo: + """Get manipulator information. + + Returns: + ManipulatorInfo object with vendor, model, DOF, etc. + """ + pass + + @abstractmethod + def get_joint_limits(self) -> tuple[list[float], list[float]]: + """Get joint position limits. + + Returns: + Tuple of (lower_limits, upper_limits) in RADIANS + """ + pass + + @abstractmethod + def get_velocity_limits(self) -> list[float]: + """Get joint velocity limits. + + Returns: + Maximum velocities in RAD/S + """ + pass + + @abstractmethod + def get_acceleration_limits(self) -> list[float]: + """Get joint acceleration limits. + + Returns: + Maximum accelerations in RAD/S² + """ + pass + + # ============= Optional Methods (Override if Supported) ============= + # These have default implementations that indicate feature not available + + def get_cartesian_position(self) -> dict[str, float] | None: + """Get current end-effector pose. + + Returns: + Dict with keys: x, y, z (meters), roll, pitch, yaw (radians) + None if not supported + """ + return None + + def set_cartesian_position( + self, + pose: dict[str, float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + ) -> bool: + """Move end-effector to target pose. + + Args: + pose: Target pose with keys: x, y, z (meters), roll, pitch, yaw (radians) + velocity: Max velocity as fraction (0-1) + acceleration: Max acceleration as fraction (0-1) + wait: If True, block until motion completes + + Returns: + False (not supported by default) + """ + return False + + def get_cartesian_velocity(self) -> dict[str, float] | None: + """Get current end-effector velocity. + + Returns: + Dict with keys: vx, vy, vz (m/s), wx, wy, wz (rad/s) + None if not supported + """ + return None + + def set_cartesian_velocity(self, twist: dict[str, float]) -> bool: + """Set end-effector velocity. + + Args: + twist: Velocity with keys: vx, vy, vz (m/s), wx, wy, wz (rad/s) + + Returns: + False (not supported by default) + """ + return False + + def get_force_torque(self) -> list[float] | None: + """Get force/torque sensor reading. + + Returns: + List of [fx, fy, fz (N), tx, ty, tz (Nm)] + None if not supported + """ + return None + + def zero_force_torque(self) -> bool: + """Zero the force/torque sensor. + + Returns: + False (not supported by default) + """ + return False + + def set_impedance_parameters(self, stiffness: list[float], damping: list[float]) -> bool: + """Set impedance control parameters. + + Args: + stiffness: Stiffness values [x, y, z, rx, ry, rz] + damping: Damping values [x, y, z, rx, ry, rz] + + Returns: + False (not supported by default) + """ + return False + + def get_digital_inputs(self) -> dict[str, bool] | None: + """Get digital input states. + + Returns: + Dict of input_id: bool + None if not supported + """ + return None + + def set_digital_outputs(self, outputs: dict[str, bool]) -> bool: + """Set digital output states. + + Args: + outputs: Dict of output_id: bool + + Returns: + False (not supported by default) + """ + return False + + def get_analog_inputs(self) -> dict[str, float] | None: + """Get analog input values. + + Returns: + Dict of input_id: float + None if not supported + """ + return None + + def set_analog_outputs(self, outputs: dict[str, float]) -> bool: + """Set analog output values. + + Args: + outputs: Dict of output_id: float + + Returns: + False (not supported by default) + """ + return False + + def execute_trajectory(self, trajectory: list[dict[str, Any]], wait: bool = True) -> bool: + """Execute a joint trajectory. + + Args: + trajectory: List of waypoints, each with: + - 'positions': list[float] in radians + - 'velocities': Optional list[float] in rad/s + - 'time': float seconds from start + wait: If True, block until trajectory completes + + Returns: + False (not supported by default) + """ + return False + + def stop_trajectory(self) -> bool: + """Stop any executing trajectory. + + Returns: + False (not supported by default) + """ + return False + + def get_gripper_position(self) -> float | None: + """Get gripper position. + + Returns: + Position in meters (0=closed, max=fully open) + None if no gripper + """ + return None + + def set_gripper_position(self, position: float, force: float = 1.0) -> bool: + """Set gripper position. + + Args: + position: Target position in meters + force: Gripping force as fraction (0-1) + + Returns: + False (not supported by default) + """ + return False + + def set_control_mode(self, mode: str) -> bool: + """Set control mode. + + Args: + mode: One of 'position', 'velocity', 'torque', 'impedance' + + Returns: + False (not supported by default) + """ + return False + + def get_control_mode(self) -> str | None: + """Get current control mode. + + Returns: + Current mode string or None if not supported + """ + return None diff --git a/dimos/hardware/manipulators/base/spec.py b/dimos/hardware/manipulators/base/spec.py new file mode 100644 index 0000000000..8a0722cf09 --- /dev/null +++ b/dimos/hardware/manipulators/base/spec.py @@ -0,0 +1,195 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Any, Protocol + +from dimos.core import In, Out +from dimos.msgs.geometry_msgs import WrenchStamped +from dimos.msgs.sensor_msgs import JointCommand, JointState + + +@dataclass +class RobotState: + """Universal robot state compatible with all manipulators.""" + + # Core state fields (all manipulators must provide these) + state: int = 0 # 0: idle, 1: moving, 2: error, 3: e-stop + mode: int = 0 # 0: position, 1: velocity, 2: torque, 3: impedance + error_code: int = 0 # Standardized error codes across all arms + warn_code: int = 0 # Standardized warning codes + + # Extended state (optional, arm-specific) + is_connected: bool = False + is_enabled: bool = False + is_moving: bool = False + is_collision: bool = False + + # Vendor-specific data (if needed) + vendor_data: dict[str, Any] | None = None + + +@dataclass +class ManipulatorCapabilities: + """Describes what a manipulator can do.""" + + dof: int # Degrees of freedom + has_gripper: bool = False + has_force_torque: bool = False + has_impedance_control: bool = False + has_cartesian_control: bool = False + max_joint_velocity: list[float] | None = None # rad/s + max_joint_acceleration: list[float] | None = None # rad/s² + joint_limits_lower: list[float] | None = None # rad + joint_limits_upper: list[float] | None = None # rad + payload_mass: float = 0.0 # kg + reach: float = 0.0 # meters + + +class ManipulatorDriverSpec(Protocol): + """Universal protocol specification for ALL manipulator drivers. + + This defines the standard interface that every manipulator driver + must implement, regardless of the underlying hardware (XArm, Piper, + UR, Franka, etc.). + + ## Component-Based Architecture + + Drivers use a **component-based architecture** where functionality is provided + by composable components: + + - **StandardMotionComponent**: Joint/cartesian motion, trajectory execution + - **StandardServoComponent**: Servo control, modes, emergency stop, error handling + - **StandardStatusComponent**: State monitoring, capabilities, diagnostics + + RPC methods are provided by components and registered with the driver. + Access them via: + + ```python + # Method 1: Via component (direct access) + motion = driver.get_component(StandardMotionComponent) + motion.rpc_move_joint(positions=[0, 0, 0, 0, 0, 0]) + + # Method 2: Via driver's RPC registry + move_fn = driver.get_rpc_method('rpc_move_joint') + move_fn(positions=[0, 0, 0, 0, 0, 0]) + + # Method 3: Via blueprints (recommended - automatic routing) + # Commands sent to input topics are automatically routed to components + driver.joint_position_command.publish(JointCommand(positions=[0, 0, 0, 0, 0, 0])) + ``` + + ## Required Components + + Every driver must include these standard components: + - `StandardMotionComponent` - Provides motion control RPC methods + - `StandardServoComponent` - Provides servo control RPC methods + - `StandardStatusComponent` - Provides status monitoring RPC methods + + ## Available RPC Methods (via Components) + + ### Motion Control (StandardMotionComponent) + - `rpc_move_joint()` - Move to joint positions + - `rpc_move_joint_velocity()` - Set joint velocities + - `rpc_move_joint_effort()` - Set joint efforts (optional) + - `rpc_stop_motion()` - Stop all motion + - `rpc_get_joint_state()` - Get current joint state + - `rpc_get_joint_limits()` - Get joint limits + - `rpc_move_cartesian()` - Cartesian motion (optional) + - `rpc_execute_trajectory()` - Execute trajectory (optional) + + ### Servo Control (StandardServoComponent) + - `rpc_enable_servo()` - Enable motor control + - `rpc_disable_servo()` - Disable motor control + - `rpc_set_control_mode()` - Set control mode + - `rpc_emergency_stop()` - Execute emergency stop + - `rpc_clear_errors()` - Clear error states + - `rpc_home_robot()` - Home the robot + + ### Status Monitoring (StandardStatusComponent) + - `rpc_get_robot_state()` - Get robot state + - `rpc_get_capabilities()` - Get capabilities + - `rpc_get_system_info()` - Get system information + - `rpc_check_connection()` - Check connection status + + ## Standardized Units + + All units are standardized: + - Angles: radians + - Angular velocity: rad/s + - Linear position: meters + - Linear velocity: m/s + - Force: Newtons + - Torque: Nm + - Time: seconds + """ + + # ============= Capabilities Declaration ============= + capabilities: ManipulatorCapabilities + + # ============= Input Topics (Commands) ============= + # Core control inputs (all manipulators must support these) + joint_position_command: In[JointCommand] # Target joint positions (rad) + joint_velocity_command: In[JointCommand] # Target joint velocities (rad/s) + + # ============= Output Topics (Feedback) ============= + # Core feedback (all manipulators must provide these) + joint_state: Out[JointState] # Current positions, velocities, efforts + robot_state: Out[RobotState] # System state and health + + # Optional feedback (capability-dependent) + ft_sensor: Out[WrenchStamped] | None # Force/torque sensor data + + # ============= Component Access ============= + def get_component(self, component_type: type) -> Any: + """Get a component by type. + + Args: + component_type: Type of component to retrieve + + Returns: + Component instance if found, None otherwise + + Example: + motion = driver.get_component(StandardMotionComponent) + motion.rpc_move_joint([0, 0, 0, 0, 0, 0]) + """ + pass + + def get_rpc_method(self, method_name: str) -> Any: + """Get an RPC method by name. + + Args: + method_name: Name of the RPC method (e.g., 'rpc_move_joint') + + Returns: + Callable method if found, None otherwise + + Example: + move_fn = driver.get_rpc_method('rpc_move_joint') + result = move_fn(positions=[0, 0, 0, 0, 0, 0]) + """ + ... + + def list_rpc_methods(self) -> list[str]: + """List all available RPC methods from all components. + + Returns: + List of RPC method names + + Example: + methods = driver.list_rpc_methods() + # ['rpc_move_joint', 'rpc_enable_servo', 'rpc_get_robot_state', ...] + """ + ... diff --git a/dimos/hardware/manipulators/base/tests/__init__.py b/dimos/hardware/manipulators/base/tests/__init__.py new file mode 100644 index 0000000000..f863fa5120 --- /dev/null +++ b/dimos/hardware/manipulators/base/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for manipulator base module.""" diff --git a/dimos/hardware/manipulators/base/tests/conftest.py b/dimos/hardware/manipulators/base/tests/conftest.py new file mode 100644 index 0000000000..d3e6a4c66d --- /dev/null +++ b/dimos/hardware/manipulators/base/tests/conftest.py @@ -0,0 +1,362 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pytest fixtures and mocks for manipulator driver tests. + +This module contains MockSDK which implements BaseManipulatorSDK with controllable +behavior for testing driver logic without requiring hardware. + +Features: +- Configurable initial state (positions, DOF, vendor, model) +- Call tracking for verification +- Configurable error injection +- Simulated behavior (e.g., position updates) +""" + +from dataclasses import dataclass, field +import math + +import pytest + +from ..sdk_interface import BaseManipulatorSDK, ManipulatorInfo + + +@dataclass +class MockSDKConfig: + """Configuration for MockSDK behavior.""" + + dof: int = 6 + vendor: str = "Mock" + model: str = "TestArm" + initial_positions: list[float] | None = None + initial_velocities: list[float] | None = None + initial_efforts: list[float] | None = None + + # Error injection + connect_fails: bool = False + enable_fails: bool = False + motion_fails: bool = False + error_code: int = 0 + + # Behavior options + simulate_motion: bool = False # If True, set_joint_positions updates internal state + + +@dataclass +class CallRecord: + """Record of a method call for verification.""" + + method: str + args: tuple = field(default_factory=tuple) + kwargs: dict = field(default_factory=dict) + + +class MockSDK(BaseManipulatorSDK): + """Mock SDK for unit testing. Implements BaseManipulatorSDK interface. + + Usage: + # Basic usage + mock = MockSDK() + driver = create_driver_with_sdk(mock) + driver.enable_servo() + assert mock.enable_servos_called + + # With custom config + config = MockSDKConfig(dof=7, connect_fails=True) + mock = MockSDK(config=config) + + # With initial positions + mock = MockSDK(positions=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) + + # Verify calls + mock.set_joint_positions([0.1] * 6) + assert mock.was_called("set_joint_positions") + assert mock.call_count("set_joint_positions") == 1 + """ + + def __init__( + self, + config: MockSDKConfig | None = None, + *, + dof: int = 6, + vendor: str = "Mock", + model: str = "TestArm", + positions: list[float] | None = None, + ): + """Initialize MockSDK. + + Args: + config: Full configuration object (takes precedence) + dof: Degrees of freedom (ignored if config provided) + vendor: Vendor name (ignored if config provided) + model: Model name (ignored if config provided) + positions: Initial joint positions (ignored if config provided) + """ + if config is None: + config = MockSDKConfig( + dof=dof, + vendor=vendor, + model=model, + initial_positions=positions, + ) + + self._config = config + self._dof = config.dof + self._vendor = config.vendor + self._model = config.model + + # State + self._connected = False + self._servos_enabled = False + self._positions = list(config.initial_positions or [0.0] * self._dof) + self._velocities = list(config.initial_velocities or [0.0] * self._dof) + self._efforts = list(config.initial_efforts or [0.0] * self._dof) + self._mode = 0 + self._state = 0 + self._error_code = config.error_code + + # Call tracking + self._calls: list[CallRecord] = [] + + # Convenience flags for simple assertions + self.connect_called = False + self.disconnect_called = False + self.enable_servos_called = False + self.disable_servos_called = False + self.set_joint_positions_called = False + self.set_joint_velocities_called = False + self.stop_motion_called = False + self.emergency_stop_called = False + self.clear_errors_called = False + + def _record_call(self, method: str, *args, **kwargs): + """Record a method call.""" + self._calls.append(CallRecord(method=method, args=args, kwargs=kwargs)) + + def was_called(self, method: str) -> bool: + """Check if a method was called.""" + return any(c.method == method for c in self._calls) + + def call_count(self, method: str) -> int: + """Get the number of times a method was called.""" + return sum(1 for c in self._calls if c.method == method) + + def get_calls(self, method: str) -> list[CallRecord]: + """Get all calls to a specific method.""" + return [c for c in self._calls if c.method == method] + + def get_last_call(self, method: str) -> CallRecord | None: + """Get the last call to a specific method.""" + calls = self.get_calls(method) + return calls[-1] if calls else None + + def reset_calls(self): + """Reset call tracking.""" + self._calls.clear() + self.connect_called = False + self.disconnect_called = False + self.enable_servos_called = False + self.disable_servos_called = False + self.set_joint_positions_called = False + self.set_joint_velocities_called = False + self.stop_motion_called = False + self.emergency_stop_called = False + self.clear_errors_called = False + + # ============= State Manipulation (for test setup) ============= + + def set_positions(self, positions: list[float]): + """Set internal positions (test helper).""" + self._positions = list(positions) + + def set_error(self, code: int, message: str = ""): + """Inject an error state (test helper).""" + self._error_code = code + + def set_enabled(self, enabled: bool): + """Set servo enabled state (test helper).""" + self._servos_enabled = enabled + + # ============= BaseManipulatorSDK Implementation ============= + + def connect(self, config: dict) -> bool: + self._record_call("connect", config) + self.connect_called = True + + if self._config.connect_fails: + return False + + self._connected = True + return True + + def disconnect(self) -> None: + self._record_call("disconnect") + self.disconnect_called = True + self._connected = False + + def is_connected(self) -> bool: + self._record_call("is_connected") + return self._connected + + def get_joint_positions(self) -> list[float]: + self._record_call("get_joint_positions") + return self._positions.copy() + + def get_joint_velocities(self) -> list[float]: + self._record_call("get_joint_velocities") + return self._velocities.copy() + + def get_joint_efforts(self) -> list[float]: + self._record_call("get_joint_efforts") + return self._efforts.copy() + + def set_joint_positions( + self, + positions: list[float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + ) -> bool: + self._record_call( + "set_joint_positions", + positions, + velocity=velocity, + acceleration=acceleration, + wait=wait, + ) + self.set_joint_positions_called = True + + if self._config.motion_fails: + return False + + if not self._servos_enabled: + return False + + if self._config.simulate_motion: + self._positions = list(positions) + + return True + + def set_joint_velocities(self, velocities: list[float]) -> bool: + self._record_call("set_joint_velocities", velocities) + self.set_joint_velocities_called = True + + if self._config.motion_fails: + return False + + if not self._servos_enabled: + return False + + self._velocities = list(velocities) + return True + + def set_joint_efforts(self, efforts: list[float]) -> bool: + self._record_call("set_joint_efforts", efforts) + return False # Not supported in mock + + def stop_motion(self) -> bool: + self._record_call("stop_motion") + self.stop_motion_called = True + self._velocities = [0.0] * self._dof + return True + + def enable_servos(self) -> bool: + self._record_call("enable_servos") + self.enable_servos_called = True + + if self._config.enable_fails: + return False + + self._servos_enabled = True + return True + + def disable_servos(self) -> bool: + self._record_call("disable_servos") + self.disable_servos_called = True + self._servos_enabled = False + return True + + def are_servos_enabled(self) -> bool: + self._record_call("are_servos_enabled") + return self._servos_enabled + + def get_robot_state(self) -> dict: + self._record_call("get_robot_state") + return { + "state": self._state, + "mode": self._mode, + "error_code": self._error_code, + "is_moving": any(v != 0 for v in self._velocities), + } + + def get_error_code(self) -> int: + self._record_call("get_error_code") + return self._error_code + + def get_error_message(self) -> str: + self._record_call("get_error_message") + return "" if self._error_code == 0 else f"Mock error {self._error_code}" + + def clear_errors(self) -> bool: + self._record_call("clear_errors") + self.clear_errors_called = True + self._error_code = 0 + return True + + def emergency_stop(self) -> bool: + self._record_call("emergency_stop") + self.emergency_stop_called = True + self._velocities = [0.0] * self._dof + self._servos_enabled = False + return True + + def get_info(self) -> ManipulatorInfo: + self._record_call("get_info") + return ManipulatorInfo( + vendor=self._vendor, + model=f"{self._model} (Mock)", + dof=self._dof, + firmware_version="mock-1.0.0", + serial_number="MOCK-001", + ) + + def get_joint_limits(self) -> tuple[list[float], list[float]]: + self._record_call("get_joint_limits") + lower = [-2 * math.pi] * self._dof + upper = [2 * math.pi] * self._dof + return lower, upper + + def get_velocity_limits(self) -> list[float]: + self._record_call("get_velocity_limits") + return [math.pi] * self._dof + + def get_acceleration_limits(self) -> list[float]: + self._record_call("get_acceleration_limits") + return [math.pi * 2] * self._dof + + +# ============= Pytest Fixtures ============= + + +@pytest.fixture +def mock_sdk(): + """Create a basic MockSDK.""" + return MockSDK(dof=6) + + +@pytest.fixture +def mock_sdk_with_positions(): + """Create MockSDK with initial positions.""" + positions = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + return MockSDK(positions=positions) diff --git a/dimos/hardware/manipulators/base/tests/test_driver_unit.py b/dimos/hardware/manipulators/base/tests/test_driver_unit.py new file mode 100644 index 0000000000..b305d8cd15 --- /dev/null +++ b/dimos/hardware/manipulators/base/tests/test_driver_unit.py @@ -0,0 +1,577 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for BaseManipulatorDriver. + +These tests use MockSDK to test driver logic in isolation without hardware. +Run with: pytest dimos/hardware/manipulators/base/tests/test_driver_unit.py -v +""" + +import math +import time + +import pytest + +from ..components import ( + StandardMotionComponent, + StandardServoComponent, + StandardStatusComponent, +) +from ..driver import BaseManipulatorDriver +from .conftest import MockSDK, MockSDKConfig + +# ============================================================================= +# Fixtures +# ============================================================================= +# Note: mock_sdk and mock_sdk_with_positions fixtures are defined in conftest.py + + +@pytest.fixture +def standard_components(): + """Create standard component set.""" + return [ + StandardMotionComponent(), + StandardServoComponent(), + StandardStatusComponent(), + ] + + +@pytest.fixture +def driver(mock_sdk, standard_components): + """Create a driver with MockSDK and standard components.""" + config = {"dof": 6} + driver = BaseManipulatorDriver( + sdk=mock_sdk, + components=standard_components, + config=config, + name="TestDriver", + ) + yield driver + # Cleanup - stop driver if running + try: + driver.stop() + except Exception: + pass + + +@pytest.fixture +def started_driver(driver): + """Create and start a driver.""" + driver.start() + time.sleep(0.05) # Allow threads to start + yield driver + + +# ============================================================================= +# Connection Tests +# ============================================================================= + + +class TestConnection: + """Tests for driver connection behavior.""" + + def test_driver_connects_on_init(self, mock_sdk, standard_components): + """Driver should connect to SDK during initialization.""" + config = {"dof": 6} + driver = BaseManipulatorDriver( + sdk=mock_sdk, + components=standard_components, + config=config, + name="TestDriver", + ) + + assert mock_sdk.connect_called + assert mock_sdk.is_connected() + assert driver.shared_state.is_connected + + driver.stop() + + @pytest.mark.skip( + reason="Driver init failure leaks LCM threads - needs cleanup fix in Module base class" + ) + def test_connection_failure_raises(self, standard_components): + """Driver should raise if SDK connection fails.""" + config_fail = MockSDKConfig(connect_fails=True) + mock_sdk = MockSDK(config=config_fail) + + with pytest.raises(RuntimeError, match="Failed to connect"): + BaseManipulatorDriver( + sdk=mock_sdk, + components=standard_components, + config={"dof": 6}, + name="TestDriver", + ) + + def test_disconnect_on_stop(self, started_driver, mock_sdk): + """Driver should disconnect SDK on stop.""" + started_driver.stop() + + assert mock_sdk.disconnect_called + assert not started_driver.shared_state.is_connected + + +# ============================================================================= +# Joint State Tests +# ============================================================================= + + +class TestJointState: + """Tests for joint state reading.""" + + def test_get_joint_state_returns_positions(self, driver): + """get_joint_state should return current positions.""" + result = driver.get_joint_state() + + assert result["success"] is True + assert len(result["positions"]) == 6 + assert len(result["velocities"]) == 6 + assert len(result["efforts"]) == 6 + + def test_get_joint_state_with_custom_positions(self, standard_components): + """get_joint_state should return SDK positions.""" + expected_positions = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + mock_sdk = MockSDK(positions=expected_positions) + + driver = BaseManipulatorDriver( + sdk=mock_sdk, + components=standard_components, + config={"dof": 6}, + name="TestDriver", + ) + + result = driver.get_joint_state() + + assert result["positions"] == expected_positions + + driver.stop() + + def test_shared_state_updated_on_joint_read(self, driver): + """Shared state should be updated when reading joints.""" + # Manually trigger joint state update + driver._update_joint_state() + + assert driver.shared_state.joint_positions is not None + assert len(driver.shared_state.joint_positions) == 6 + + +# ============================================================================= +# Servo Control Tests +# ============================================================================= + + +class TestServoControl: + """Tests for servo enable/disable.""" + + def test_enable_servo_calls_sdk(self, driver, mock_sdk): + """enable_servo should call SDK's enable_servos.""" + result = driver.enable_servo() + + assert result["success"] is True + assert mock_sdk.enable_servos_called + + def test_enable_servo_updates_shared_state(self, driver): + """enable_servo should update shared state.""" + driver.enable_servo() + + # Trigger state update to sync + driver._update_robot_state() + + assert driver.shared_state.is_enabled is True + + def test_disable_servo_calls_sdk(self, driver, mock_sdk): + """disable_servo should call SDK's disable_servos.""" + driver.enable_servo() # Enable first + result = driver.disable_servo() + + assert result["success"] is True + assert mock_sdk.disable_servos_called + + def test_enable_fails_with_error(self, standard_components): + """enable_servo should return failure when SDK fails.""" + config = MockSDKConfig(enable_fails=True) + mock_sdk = MockSDK(config=config) + + driver = BaseManipulatorDriver( + sdk=mock_sdk, + components=standard_components, + config={"dof": 6}, + name="TestDriver", + ) + + result = driver.enable_servo() + + assert result["success"] is False + + driver.stop() + + +# ============================================================================= +# Motion Control Tests +# ============================================================================= + + +class TestMotionControl: + """Tests for motion commands.""" + + def test_move_joint_blocking_calls_sdk(self, driver, mock_sdk): + """move_joint with wait=True should call SDK directly.""" + # Enable servos first (required for motion) + driver.enable_servo() + + target = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + # Use wait=True to bypass queue and call SDK directly + result = driver.move_joint(target, velocity=0.5, wait=True) + + assert result["success"] is True + assert mock_sdk.set_joint_positions_called + + # Verify arguments + call = mock_sdk.get_last_call("set_joint_positions") + assert call is not None + assert list(call.args[0]) == target + assert call.kwargs["velocity"] == 0.5 + + def test_move_joint_async_queues_command(self, driver, mock_sdk): + """move_joint with wait=False should queue command.""" + # Enable servos first + driver.enable_servo() + + target = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + # Default wait=False queues command + result = driver.move_joint(target, velocity=0.5) + + assert result["success"] is True + assert result.get("queued") is True + # SDK not called yet (command is in queue) + assert not mock_sdk.set_joint_positions_called + # But command is in the queue + assert not driver.command_queue.empty() + + def test_move_joint_fails_without_enable(self, driver, mock_sdk): + """move_joint should fail if servos not enabled (blocking mode).""" + target = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + # Use wait=True to test synchronous failure + result = driver.move_joint(target, wait=True) + + assert result["success"] is False + + def test_move_joint_with_simulated_motion(self, standard_components): + """With simulate_motion, positions should update (blocking mode).""" + config = MockSDKConfig(simulate_motion=True) + mock_sdk = MockSDK(config=config) + + driver = BaseManipulatorDriver( + sdk=mock_sdk, + components=standard_components, + config={"dof": 6}, + name="TestDriver", + ) + + driver.enable_servo() + target = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + # Use wait=True to execute directly + driver.move_joint(target, wait=True) + + # Check SDK internal state updated + assert mock_sdk.get_joint_positions() == target + + driver.stop() + + def test_stop_motion_calls_sdk(self, driver, mock_sdk): + """stop_motion should call SDK's stop_motion.""" + result = driver.stop_motion() + + # stop_motion may return success=False if not moving, but should not error + assert result is not None + assert mock_sdk.stop_motion_called + + def test_process_command_calls_sdk(self, driver, mock_sdk): + """_process_command should execute queued commands.""" + from ..driver import Command + + driver.enable_servo() + + # Create a position command directly + command = Command( + type="position", + data={"positions": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], "velocity": 0.5}, + ) + + # Process it directly + driver._process_command(command) + + assert mock_sdk.set_joint_positions_called + + +# ============================================================================= +# Robot State Tests +# ============================================================================= + + +class TestRobotState: + """Tests for robot state reading.""" + + def test_get_robot_state_returns_state(self, driver): + """get_robot_state should return state info.""" + result = driver.get_robot_state() + + assert result["success"] is True + assert "state" in result + assert "mode" in result + assert "error_code" in result + + def test_get_robot_state_with_error(self, standard_components): + """get_robot_state should report errors from SDK.""" + config = MockSDKConfig(error_code=42) + mock_sdk = MockSDK(config=config) + + driver = BaseManipulatorDriver( + sdk=mock_sdk, + components=standard_components, + config={"dof": 6}, + name="TestDriver", + ) + + result = driver.get_robot_state() + + assert result["error_code"] == 42 + + driver.stop() + + def test_clear_errors_calls_sdk(self, driver, mock_sdk): + """clear_errors should call SDK's clear_errors.""" + result = driver.clear_errors() + + assert result["success"] is True + assert mock_sdk.clear_errors_called + + +# ============================================================================= +# Joint Limits Tests +# ============================================================================= + + +class TestJointLimits: + """Tests for joint limit queries.""" + + def test_get_joint_limits_returns_limits(self, driver): + """get_joint_limits should return lower and upper limits.""" + result = driver.get_joint_limits() + + assert result["success"] is True + assert len(result["lower"]) == 6 + assert len(result["upper"]) == 6 + + def test_joint_limits_are_reasonable(self, driver): + """Joint limits should be reasonable values.""" + result = driver.get_joint_limits() + + for lower, upper in zip(result["lower"], result["upper"], strict=False): + assert lower < upper + assert lower >= -2 * math.pi + assert upper <= 2 * math.pi + + +# ============================================================================= +# Capabilities Tests +# ============================================================================= + + +class TestCapabilities: + """Tests for driver capabilities.""" + + def test_capabilities_from_sdk(self, driver): + """Driver should get capabilities from SDK.""" + assert driver.capabilities.dof == 6 + assert len(driver.capabilities.max_joint_velocity) == 6 + assert len(driver.capabilities.joint_limits_lower) == 6 + + def test_capabilities_with_different_dof(self, standard_components): + """Driver should support different DOF arms.""" + mock_sdk = MockSDK(dof=7) + + driver = BaseManipulatorDriver( + sdk=mock_sdk, + components=standard_components, + config={"dof": 7}, + name="TestDriver", + ) + + assert driver.capabilities.dof == 7 + assert len(driver.capabilities.max_joint_velocity) == 7 + + driver.stop() + + +# ============================================================================= +# Component API Exposure Tests +# ============================================================================= + + +class TestComponentAPIExposure: + """Tests for auto-exposed component APIs.""" + + def test_motion_component_api_exposed(self, driver): + """Motion component APIs should be exposed on driver.""" + assert hasattr(driver, "move_joint") + assert hasattr(driver, "stop_motion") + assert callable(driver.move_joint) + + def test_servo_component_api_exposed(self, driver): + """Servo component APIs should be exposed on driver.""" + assert hasattr(driver, "enable_servo") + assert hasattr(driver, "disable_servo") + assert callable(driver.enable_servo) + + def test_status_component_api_exposed(self, driver): + """Status component APIs should be exposed on driver.""" + assert hasattr(driver, "get_joint_state") + assert hasattr(driver, "get_robot_state") + assert hasattr(driver, "get_joint_limits") + assert callable(driver.get_joint_state) + + +# ============================================================================= +# Threading Tests +# ============================================================================= + + +class TestThreading: + """Tests for driver threading behavior.""" + + def test_start_creates_threads(self, driver): + """start() should create control threads.""" + driver.start() + time.sleep(0.05) + + assert len(driver.threads) >= 2 + assert all(t.is_alive() for t in driver.threads) + + driver.stop() + + def test_stop_terminates_threads(self, started_driver): + """stop() should terminate all threads.""" + started_driver.stop() + time.sleep(0.1) + + assert all(not t.is_alive() for t in started_driver.threads) + + def test_stop_calls_sdk_stop_motion(self, started_driver, mock_sdk): + """stop() should call SDK stop_motion.""" + started_driver.stop() + + assert mock_sdk.stop_motion_called + + +# ============================================================================= +# Call Verification Tests (MockSDK features) +# ============================================================================= + + +class TestMockSDKCallTracking: + """Tests for MockSDK call tracking features.""" + + def test_call_count(self, mock_sdk): + """MockSDK should count method calls.""" + mock_sdk.get_joint_positions() + mock_sdk.get_joint_positions() + mock_sdk.get_joint_positions() + + assert mock_sdk.call_count("get_joint_positions") == 3 + + def test_was_called(self, mock_sdk): + """MockSDK.was_called should report if method called.""" + assert not mock_sdk.was_called("enable_servos") + + mock_sdk.enable_servos() + + assert mock_sdk.was_called("enable_servos") + + def test_get_last_call_args(self, mock_sdk): + """MockSDK should record call arguments.""" + positions = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + mock_sdk.enable_servos() + mock_sdk.set_joint_positions(positions, velocity=0.5, wait=True) + + call = mock_sdk.get_last_call("set_joint_positions") + + assert call is not None + assert list(call.args[0]) == positions + assert call.kwargs["velocity"] == 0.5 + assert call.kwargs["wait"] is True + + def test_reset_calls(self, mock_sdk): + """MockSDK.reset_calls should clear call history.""" + mock_sdk.enable_servos() + mock_sdk.get_joint_positions() + + mock_sdk.reset_calls() + + assert mock_sdk.call_count("enable_servos") == 0 + assert mock_sdk.call_count("get_joint_positions") == 0 + assert not mock_sdk.enable_servos_called + + +# ============================================================================= +# Edge Case Tests +# ============================================================================= + + +class TestEdgeCases: + """Tests for edge cases and error handling.""" + + def test_multiple_enable_calls_optimized(self, driver): + """Multiple enable calls should only call SDK once (optimization).""" + result1 = driver.enable_servo() + result2 = driver.enable_servo() + result3 = driver.enable_servo() + + # All calls succeed + assert result1["success"] is True + assert result2["success"] is True + assert result3["success"] is True + + # But SDK only called once (component optimizes redundant calls) + assert driver.sdk.call_count("enable_servos") == 1 + + # Second and third calls should indicate already enabled + assert result2.get("message") == "Servos already enabled" + assert result3.get("message") == "Servos already enabled" + + def test_disable_when_already_disabled(self, driver): + """Disable when already disabled should return success without SDK call.""" + # MockSDK starts with servos disabled + result = driver.disable_servo() + + assert result["success"] is True + assert result.get("message") == "Servos already disabled" + # SDK not called since already disabled + assert not driver.sdk.disable_servos_called + + def test_disable_after_enable(self, driver): + """Disable after enable should call SDK.""" + driver.enable_servo() + result = driver.disable_servo() + + assert result["success"] is True + assert driver.sdk.disable_servos_called + + def test_emergency_stop(self, driver): + """emergency_stop should disable servos.""" + driver.enable_servo() + + driver.sdk.emergency_stop() + + assert driver.sdk.emergency_stop_called + assert not driver.sdk.are_servos_enabled() diff --git a/dimos/hardware/manipulators/base/utils/__init__.py b/dimos/hardware/manipulators/base/utils/__init__.py new file mode 100644 index 0000000000..a2dcb2f82e --- /dev/null +++ b/dimos/hardware/manipulators/base/utils/__init__.py @@ -0,0 +1,40 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared utilities for manipulator drivers.""" + +from .converters import degrees_to_radians, meters_to_mm, mm_to_meters, radians_to_degrees +from .shared_state import SharedState +from .validators import ( + clamp_positions, + scale_velocities, + validate_acceleration_limits, + validate_joint_limits, + validate_trajectory, + validate_velocity_limits, +) + +__all__ = [ + "SharedState", + "clamp_positions", + "degrees_to_radians", + "meters_to_mm", + "mm_to_meters", + "radians_to_degrees", + "scale_velocities", + "validate_acceleration_limits", + "validate_joint_limits", + "validate_trajectory", + "validate_velocity_limits", +] diff --git a/dimos/hardware/manipulators/base/utils/converters.py b/dimos/hardware/manipulators/base/utils/converters.py new file mode 100644 index 0000000000..dff5956f8e --- /dev/null +++ b/dimos/hardware/manipulators/base/utils/converters.py @@ -0,0 +1,266 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit conversion utilities for manipulator drivers.""" + +import math + + +def degrees_to_radians(degrees: float | list[float]) -> float | list[float]: + """Convert degrees to radians. + + Args: + degrees: Angle(s) in degrees + + Returns: + Angle(s) in radians + """ + if isinstance(degrees, list): + return [math.radians(d) for d in degrees] + return math.radians(degrees) + + +def radians_to_degrees(radians: float | list[float]) -> float | list[float]: + """Convert radians to degrees. + + Args: + radians: Angle(s) in radians + + Returns: + Angle(s) in degrees + """ + if isinstance(radians, list): + return [math.degrees(r) for r in radians] + return math.degrees(radians) + + +def mm_to_meters(mm: float | list[float]) -> float | list[float]: + """Convert millimeters to meters. + + Args: + mm: Distance(s) in millimeters + + Returns: + Distance(s) in meters + """ + if isinstance(mm, list): + return [m / 1000.0 for m in mm] + return mm / 1000.0 + + +def meters_to_mm(meters: float | list[float]) -> float | list[float]: + """Convert meters to millimeters. + + Args: + meters: Distance(s) in meters + + Returns: + Distance(s) in millimeters + """ + if isinstance(meters, list): + return [m * 1000.0 for m in meters] + return meters * 1000.0 + + +def rpm_to_rad_per_sec(rpm: float | list[float]) -> float | list[float]: + """Convert RPM to rad/s. + + Args: + rpm: Angular velocity in RPM + + Returns: + Angular velocity in rad/s + """ + factor = (2 * math.pi) / 60.0 + if isinstance(rpm, list): + return [r * factor for r in rpm] + return rpm * factor + + +def rad_per_sec_to_rpm(rad_per_sec: float | list[float]) -> float | list[float]: + """Convert rad/s to RPM. + + Args: + rad_per_sec: Angular velocity in rad/s + + Returns: + Angular velocity in RPM + """ + factor = 60.0 / (2 * math.pi) + if isinstance(rad_per_sec, list): + return [r * factor for r in rad_per_sec] + return rad_per_sec * factor + + +def quaternion_to_euler(qx: float, qy: float, qz: float, qw: float) -> tuple[float, float, float]: + """Convert quaternion to Euler angles (roll, pitch, yaw). + + Args: + qx, qy, qz, qw: Quaternion components + + Returns: + Tuple of (roll, pitch, yaw) in radians + """ + # Roll (x-axis rotation) + sinr_cosp = 2 * (qw * qx + qy * qz) + cosr_cosp = 1 - 2 * (qx * qx + qy * qy) + roll = math.atan2(sinr_cosp, cosr_cosp) + + # Pitch (y-axis rotation) + sinp = 2 * (qw * qy - qz * qx) + if abs(sinp) >= 1: + pitch = math.copysign(math.pi / 2, sinp) # Use 90 degrees if out of range + else: + pitch = math.asin(sinp) + + # Yaw (z-axis rotation) + siny_cosp = 2 * (qw * qz + qx * qy) + cosy_cosp = 1 - 2 * (qy * qy + qz * qz) + yaw = math.atan2(siny_cosp, cosy_cosp) + + return roll, pitch, yaw + + +def euler_to_quaternion(roll: float, pitch: float, yaw: float) -> tuple[float, float, float, float]: + """Convert Euler angles to quaternion. + + Args: + roll, pitch, yaw: Euler angles in radians + + Returns: + Tuple of (qx, qy, qz, qw) quaternion components + """ + cy = math.cos(yaw * 0.5) + sy = math.sin(yaw * 0.5) + cp = math.cos(pitch * 0.5) + sp = math.sin(pitch * 0.5) + cr = math.cos(roll * 0.5) + sr = math.sin(roll * 0.5) + + qw = cr * cp * cy + sr * sp * sy + qx = sr * cp * cy - cr * sp * sy + qy = cr * sp * cy + sr * cp * sy + qz = cr * cp * sy - sr * sp * cy + + return qx, qy, qz, qw + + +def pose_dict_to_list(pose: dict[str, float]) -> list[float]: + """Convert pose dictionary to list format. + + Args: + pose: Dict with keys: x, y, z, roll, pitch, yaw + + Returns: + List [x, y, z, roll, pitch, yaw] + """ + return [ + pose.get("x", 0.0), + pose.get("y", 0.0), + pose.get("z", 0.0), + pose.get("roll", 0.0), + pose.get("pitch", 0.0), + pose.get("yaw", 0.0), + ] + + +def pose_list_to_dict(pose: list[float]) -> dict[str, float]: + """Convert pose list to dictionary format. + + Args: + pose: List [x, y, z, roll, pitch, yaw] + + Returns: + Dict with keys: x, y, z, roll, pitch, yaw + """ + if len(pose) < 6: + raise ValueError(f"Pose list must have 6 elements, got {len(pose)}") + + return { + "x": pose[0], + "y": pose[1], + "z": pose[2], + "roll": pose[3], + "pitch": pose[4], + "yaw": pose[5], + } + + +def twist_dict_to_list(twist: dict[str, float]) -> list[float]: + """Convert twist dictionary to list format. + + Args: + twist: Dict with keys: vx, vy, vz, wx, wy, wz + + Returns: + List [vx, vy, vz, wx, wy, wz] + """ + return [ + twist.get("vx", 0.0), + twist.get("vy", 0.0), + twist.get("vz", 0.0), + twist.get("wx", 0.0), + twist.get("wy", 0.0), + twist.get("wz", 0.0), + ] + + +def twist_list_to_dict(twist: list[float]) -> dict[str, float]: + """Convert twist list to dictionary format. + + Args: + twist: List [vx, vy, vz, wx, wy, wz] + + Returns: + Dict with keys: vx, vy, vz, wx, wy, wz + """ + if len(twist) < 6: + raise ValueError(f"Twist list must have 6 elements, got {len(twist)}") + + return { + "vx": twist[0], + "vy": twist[1], + "vz": twist[2], + "wx": twist[3], + "wy": twist[4], + "wz": twist[5], + } + + +def normalize_angle(angle: float) -> float: + """Normalize angle to [-pi, pi]. + + Args: + angle: Angle in radians + + Returns: + Normalized angle in [-pi, pi] + """ + while angle > math.pi: + angle -= 2 * math.pi + while angle < -math.pi: + angle += 2 * math.pi + return angle + + +def normalize_angles(angles: list[float]) -> list[float]: + """Normalize angles to [-pi, pi]. + + Args: + angles: Angles in radians + + Returns: + Normalized angles in [-pi, pi] + """ + return [normalize_angle(a) for a in angles] diff --git a/dimos/hardware/manipulators/base/utils/shared_state.py b/dimos/hardware/manipulators/base/utils/shared_state.py new file mode 100644 index 0000000000..8af275ea17 --- /dev/null +++ b/dimos/hardware/manipulators/base/utils/shared_state.py @@ -0,0 +1,255 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Thread-safe shared state for manipulator drivers.""" + +from dataclasses import dataclass, field +from threading import Lock +import time +from typing import Any + + +@dataclass +class SharedState: + """Thread-safe shared state for manipulator drivers. + + This class holds the current state of the manipulator that needs to be + shared between multiple threads (state reader, command sender, publisher). + All access should be protected by the lock. + """ + + # Thread synchronization + lock: Lock = field(default_factory=Lock) + + # Joint state (current values from hardware) + joint_positions: list[float] | None = None # radians + joint_velocities: list[float] | None = None # rad/s + joint_efforts: list[float] | None = None # Nm + + # Joint targets (commanded values) + target_positions: list[float] | None = None # radians + target_velocities: list[float] | None = None # rad/s + target_efforts: list[float] | None = None # Nm + + # Cartesian state (if available) + cartesian_position: dict[str, float] | None = None # x,y,z,roll,pitch,yaw + cartesian_velocity: dict[str, float] | None = None # vx,vy,vz,wx,wy,wz + + # Cartesian targets + target_cartesian_position: dict[str, float] | None = None + target_cartesian_velocity: dict[str, float] | None = None + + # Force/torque sensor (if available) + force_torque: list[float] | None = None # [fx,fy,fz,tx,ty,tz] + + # System state + robot_state: int = 0 # 0=idle, 1=moving, 2=error, 3=e-stop + control_mode: int = 0 # 0=position, 1=velocity, 2=torque + error_code: int = 0 # 0 = no error + error_message: str = "" # Human-readable error + + # Connection and enable status + is_connected: bool = False + is_enabled: bool = False + is_moving: bool = False + is_homed: bool = False + + # Gripper state (if available) + gripper_position: float | None = None # meters + gripper_force: float | None = None # Newtons + + # Timestamps + last_state_update: float = 0.0 + last_command_sent: float = 0.0 + last_error_time: float = 0.0 + + # Statistics + state_read_count: int = 0 + command_sent_count: int = 0 + error_count: int = 0 + + def update_joint_state( + self, + positions: list[float] | None = None, + velocities: list[float] | None = None, + efforts: list[float] | None = None, + ) -> None: + """Thread-safe update of joint state. + + Args: + positions: Joint positions in radians + velocities: Joint velocities in rad/s + efforts: Joint efforts in Nm + """ + with self.lock: + if positions is not None: + self.joint_positions = positions + if velocities is not None: + self.joint_velocities = velocities + if efforts is not None: + self.joint_efforts = efforts + self.last_state_update = time.time() + self.state_read_count += 1 + + def update_robot_state( + self, + state: int | None = None, + mode: int | None = None, + error_code: int | None = None, + error_message: str | None = None, + ) -> None: + """Thread-safe update of robot state. + + Args: + state: Robot state code + mode: Control mode code + error_code: Error code (0 = no error) + error_message: Human-readable error message + """ + with self.lock: + if state is not None: + self.robot_state = state + if mode is not None: + self.control_mode = mode + if error_code is not None: + self.error_code = error_code + if error_code != 0: + self.error_count += 1 + self.last_error_time = time.time() + if error_message is not None: + self.error_message = error_message + + def update_cartesian_state( + self, position: dict[str, float] | None = None, velocity: dict[str, float] | None = None + ) -> None: + """Thread-safe update of Cartesian state. + + Args: + position: End-effector pose (x,y,z,roll,pitch,yaw) + velocity: End-effector twist (vx,vy,vz,wx,wy,wz) + """ + with self.lock: + if position is not None: + self.cartesian_position = position + if velocity is not None: + self.cartesian_velocity = velocity + + def set_target_joints( + self, + positions: list[float] | None = None, + velocities: list[float] | None = None, + efforts: list[float] | None = None, + ) -> None: + """Thread-safe update of joint targets. + + Args: + positions: Target positions in radians + velocities: Target velocities in rad/s + efforts: Target efforts in Nm + """ + with self.lock: + if positions is not None: + self.target_positions = positions + if velocities is not None: + self.target_velocities = velocities + if efforts is not None: + self.target_efforts = efforts + self.last_command_sent = time.time() + self.command_sent_count += 1 + + def get_joint_state( + self, + ) -> tuple[list[float] | None, list[float] | None, list[float] | None]: + """Thread-safe read of joint state. + + Returns: + Tuple of (positions, velocities, efforts) + """ + with self.lock: + return ( + self.joint_positions.copy() if self.joint_positions else None, + self.joint_velocities.copy() if self.joint_velocities else None, + self.joint_efforts.copy() if self.joint_efforts else None, + ) + + def get_robot_state(self) -> dict[str, Any]: + """Thread-safe read of robot state. + + Returns: + Dict with state information + """ + with self.lock: + return { + "state": self.robot_state, + "mode": self.control_mode, + "error_code": self.error_code, + "error_message": self.error_message, + "is_connected": self.is_connected, + "is_enabled": self.is_enabled, + "is_moving": self.is_moving, + "last_update": self.last_state_update, + } + + def get_statistics(self) -> dict[str, Any]: + """Get statistics about state updates. + + Returns: + Dict with statistics + """ + with self.lock: + return { + "state_read_count": self.state_read_count, + "command_sent_count": self.command_sent_count, + "error_count": self.error_count, + "last_state_update": self.last_state_update, + "last_command_sent": self.last_command_sent, + "last_error_time": self.last_error_time, + } + + def clear_errors(self) -> None: + """Clear error state.""" + with self.lock: + self.error_code = 0 + self.error_message = "" + + def reset(self) -> None: + """Reset all state to initial values.""" + with self.lock: + self.joint_positions = None + self.joint_velocities = None + self.joint_efforts = None + self.target_positions = None + self.target_velocities = None + self.target_efforts = None + self.cartesian_position = None + self.cartesian_velocity = None + self.target_cartesian_position = None + self.target_cartesian_velocity = None + self.force_torque = None + self.robot_state = 0 + self.control_mode = 0 + self.error_code = 0 + self.error_message = "" + self.is_connected = False + self.is_enabled = False + self.is_moving = False + self.is_homed = False + self.gripper_position = None + self.gripper_force = None + self.last_state_update = 0.0 + self.last_command_sent = 0.0 + self.last_error_time = 0.0 + self.state_read_count = 0 + self.command_sent_count = 0 + self.error_count = 0 diff --git a/dimos/hardware/manipulators/base/utils/validators.py b/dimos/hardware/manipulators/base/utils/validators.py new file mode 100644 index 0000000000..3fabdcd306 --- /dev/null +++ b/dimos/hardware/manipulators/base/utils/validators.py @@ -0,0 +1,254 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Validation utilities for manipulator drivers.""" + +from typing import cast + + +def validate_joint_limits( + positions: list[float], + lower_limits: list[float], + upper_limits: list[float], + tolerance: float = 0.0, +) -> tuple[bool, str | None]: + """Validate joint positions are within limits. + + Args: + positions: Joint positions to validate (radians) + lower_limits: Lower joint limits (radians) + upper_limits: Upper joint limits (radians) + tolerance: Optional tolerance for soft limits (radians) + + Returns: + Tuple of (is_valid, error_message) + If valid, error_message is None + """ + if len(positions) != len(lower_limits) or len(positions) != len(upper_limits): + return False, f"Dimension mismatch: {len(positions)} positions, {len(lower_limits)} limits" + + for i, pos in enumerate(positions): + lower = lower_limits[i] - tolerance + upper = upper_limits[i] + tolerance + + if pos < lower: + return False, f"Joint {i} position {pos:.3f} below limit {lower_limits[i]:.3f}" + + if pos > upper: + return False, f"Joint {i} position {pos:.3f} above limit {upper_limits[i]:.3f}" + + return True, None + + +def validate_velocity_limits( + velocities: list[float], max_velocities: list[float], scale_factor: float = 1.0 +) -> tuple[bool, str | None]: + """Validate joint velocities are within limits. + + Args: + velocities: Joint velocities to validate (rad/s) + max_velocities: Maximum allowed velocities (rad/s) + scale_factor: Optional scaling factor (0-1) to reduce max velocity + + Returns: + Tuple of (is_valid, error_message) + If valid, error_message is None + """ + if len(velocities) != len(max_velocities): + return ( + False, + f"Dimension mismatch: {len(velocities)} velocities, {len(max_velocities)} limits", + ) + + if scale_factor <= 0 or scale_factor > 1: + return False, f"Invalid scale factor: {scale_factor} (must be in (0, 1])" + + for i, vel in enumerate(velocities): + max_vel = max_velocities[i] * scale_factor + + if abs(vel) > max_vel: + return False, f"Joint {i} velocity {abs(vel):.3f} exceeds limit {max_vel:.3f}" + + return True, None + + +def validate_acceleration_limits( + accelerations: list[float], max_accelerations: list[float], scale_factor: float = 1.0 +) -> tuple[bool, str | None]: + """Validate joint accelerations are within limits. + + Args: + accelerations: Joint accelerations to validate (rad/s²) + max_accelerations: Maximum allowed accelerations (rad/s²) + scale_factor: Optional scaling factor (0-1) to reduce max acceleration + + Returns: + Tuple of (is_valid, error_message) + If valid, error_message is None + """ + if len(accelerations) != len(max_accelerations): + return ( + False, + f"Dimension mismatch: {len(accelerations)} accelerations, {len(max_accelerations)} limits", + ) + + if scale_factor <= 0 or scale_factor > 1: + return False, f"Invalid scale factor: {scale_factor} (must be in (0, 1])" + + for i, acc in enumerate(accelerations): + max_acc = max_accelerations[i] * scale_factor + + if abs(acc) > max_acc: + return False, f"Joint {i} acceleration {abs(acc):.3f} exceeds limit {max_acc:.3f}" + + return True, None + + +def validate_trajectory( + trajectory: list[dict[str, float | list[float]]], + lower_limits: list[float], + upper_limits: list[float], + max_velocities: list[float] | None = None, + max_accelerations: list[float] | None = None, +) -> tuple[bool, str | None]: + """Validate a joint trajectory. + + Args: + trajectory: List of waypoints, each with: + - 'positions': list[float] in radians + - 'velocities': Optional list[float] in rad/s + - 'time': float seconds from start + lower_limits: Lower joint limits (radians) + upper_limits: Upper joint limits (radians) + max_velocities: Optional maximum velocities (rad/s) + max_accelerations: Optional maximum accelerations (rad/s²) + + Returns: + Tuple of (is_valid, error_message) + If valid, error_message is None + """ + if not trajectory: + return False, "Empty trajectory" + + # Check first waypoint starts at time 0 + if trajectory[0].get("time", 0) != 0: + return False, "Trajectory must start at time 0" + + # Check waypoints are time-ordered + prev_time: float = -1.0 + for i, waypoint in enumerate(trajectory): + curr_time = cast("float", waypoint.get("time", 0)) + if curr_time <= prev_time: + return False, f"Waypoint {i} time {curr_time} not after previous {prev_time}" + prev_time = curr_time + + # Validate each waypoint + for i, waypoint in enumerate(trajectory): + # Check required fields + if "positions" not in waypoint: + return False, f"Waypoint {i} missing positions" + + positions = cast("list[float]", waypoint["positions"]) + + # Validate position limits + valid, error = validate_joint_limits(positions, lower_limits, upper_limits) + if not valid: + return False, f"Waypoint {i}: {error}" + + # Validate velocity limits if provided + if "velocities" in waypoint and max_velocities: + velocities = cast("list[float]", waypoint["velocities"]) + valid, error = validate_velocity_limits(velocities, max_velocities) + if not valid: + return False, f"Waypoint {i}: {error}" + + # Check acceleration limits between waypoints + if max_accelerations and len(trajectory) > 1: + for i in range(1, len(trajectory)): + prev = trajectory[i - 1] + curr = trajectory[i] + + dt = cast("float", curr["time"]) - cast("float", prev["time"]) + if dt <= 0: + continue + + # Estimate acceleration from position change + prev_pos = cast("list[float]", prev["positions"]) + curr_pos = cast("list[float]", curr["positions"]) + for j in range(len(prev_pos)): + pos_change = curr_pos[j] - prev_pos[j] + pos_change / dt + + # If velocities provided, use them for better estimate + if "velocities" in prev and "velocities" in curr: + prev_vel = cast("list[float]", prev["velocities"]) + curr_vel = cast("list[float]", curr["velocities"]) + vel_change = curr_vel[j] - prev_vel[j] + acc = vel_change / dt + if abs(acc) > max_accelerations[j]: + return ( + False, + f"Acceleration between waypoint {i - 1} and {i} joint {j}: {abs(acc):.3f} exceeds limit {max_accelerations[j]:.3f}", + ) + + return True, None + + +def scale_velocities( + velocities: list[float], max_velocities: list[float], scale_factor: float = 0.8 +) -> list[float]: + """Scale velocities to stay within limits. + + Args: + velocities: Desired velocities (rad/s) + max_velocities: Maximum allowed velocities (rad/s) + scale_factor: Safety factor (0-1) to stay below limits + + Returns: + Scaled velocities that respect limits + """ + if not velocities or not max_velocities: + return velocities + + # Find the joint that requires most scaling + max_scale = 1.0 + for vel, max_vel in zip(velocities, max_velocities, strict=False): + if max_vel > 0 and abs(vel) > 0: + required_scale = abs(vel) / (max_vel * scale_factor) + max_scale = max(max_scale, required_scale) + + # Apply uniform scaling to maintain direction + if max_scale > 1.0: + return [v / max_scale for v in velocities] + + return velocities + + +def clamp_positions( + positions: list[float], lower_limits: list[float], upper_limits: list[float] +) -> list[float]: + """Clamp positions to stay within limits. + + Args: + positions: Desired positions (radians) + lower_limits: Lower joint limits (radians) + upper_limits: Upper joint limits (radians) + + Returns: + Clamped positions within limits + """ + clamped = [] + for pos, lower, upper in zip(positions, lower_limits, upper_limits, strict=False): + clamped.append(max(lower, min(upper, pos))) + return clamped diff --git a/dimos/hardware/manipulators/piper/README.md b/dimos/hardware/manipulators/piper/README.md new file mode 100644 index 0000000000..89ff2161ac --- /dev/null +++ b/dimos/hardware/manipulators/piper/README.md @@ -0,0 +1,35 @@ +# Piper Driver + +Driver for the Piper 6-DOF manipulator with CAN bus communication. + +## Supported Features + +āœ… **Joint Control** +- Position control +- Velocity control (integration-based) +- Joint state feedback at 100Hz + +āœ… **System Control** +- Enable/disable motors +- Emergency stop +- Error recovery + +āœ… **Gripper Control** +- Position and force control +- Gripper state feedback + +## Cartesian Control Limitation + +āš ļø **Cartesian control is currently NOT available for the Piper arm.** + +### Why? +The Piper SDK doesn't expose an inverse kinematics (IK) solver that can be called without moving the robot. While the robot can execute Cartesian commands internally, we cannot: +- Pre-compute joint trajectories for Cartesian paths +- Validate if a pose is reachable without trying to move there +- Plan complex Cartesian trajectories offline + +### Future Solution +We will implement a universal IK solver that sits outside the driver layer and works with all arms (XArm, Piper, and future robots), regardless of whether they expose internal IK. + +### Current Workaround +Use joint-space control for now. If you need Cartesian planning, consider using external IK libraries like ikpy or robotics-toolbox-python with the Piper's URDF file. diff --git a/dimos/hardware/manipulators/piper/__init__.py b/dimos/hardware/manipulators/piper/__init__.py new file mode 100644 index 0000000000..acead9f7fb --- /dev/null +++ b/dimos/hardware/manipulators/piper/__init__.py @@ -0,0 +1,32 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Piper Arm Driver + +Real-time driver for Piper manipulator with CAN bus communication. +""" + +from .piper_blueprints import piper_cartesian, piper_servo, piper_trajectory +from .piper_driver import PiperDriver, piper_driver +from .piper_wrapper import PiperSDKWrapper + +__all__ = [ + "PiperDriver", + "PiperSDKWrapper", + "piper_cartesian", + "piper_driver", + "piper_servo", + "piper_trajectory", +] diff --git a/dimos/hardware/can_activate.sh b/dimos/hardware/manipulators/piper/can_activate.sh similarity index 99% rename from dimos/hardware/can_activate.sh rename to dimos/hardware/manipulators/piper/can_activate.sh index 60cc95e7ea..addb892557 100644 --- a/dimos/hardware/can_activate.sh +++ b/dimos/hardware/manipulators/piper/can_activate.sh @@ -37,12 +37,12 @@ if [ "$CURRENT_CAN_COUNT" -ne "1" ]; then for iface in $(ip -br link show type can | awk '{print $1}'); do # Use ethtool to retrieve bus-info. BUS_INFO=$(sudo ethtool -i "$iface" | grep "bus-info" | awk '{print $2}') - + if [ -z "$BUS_INFO" ];then echo "Error: Unable to retrieve bus-info for interface $iface." continue fi - + echo "Interface $iface is inserted into USB port $BUS_INFO" done echo -e " \e[31m Error: The number of CAN modules detected by the system ($CURRENT_CAN_COUNT) does not match the expected number (1). \e[0m" @@ -62,7 +62,7 @@ fi if [ -n "$USB_ADDRESS" ]; then echo "Detected USB hardware address parameter: $USB_ADDRESS" - + # Use ethtool to find the CAN interface corresponding to the USB hardware address. INTERFACE_NAME="" for iface in $(ip -br link show type can | awk '{print $1}'); do @@ -72,7 +72,7 @@ if [ -n "$USB_ADDRESS" ]; then break fi done - + if [ -z "$INTERFACE_NAME" ]; then echo "Error: Unable to find CAN interface corresponding to USB hardware address $USB_ADDRESS." exit 1 @@ -82,7 +82,7 @@ if [ -n "$USB_ADDRESS" ]; then else # Retrieve the unique CAN interface. INTERFACE_NAME=$(ip -br link show type can | awk '{print $1}') - + # Check if the interface name has been retrieved. if [ -z "$INTERFACE_NAME" ]; then echo "Error: Unable to detect CAN interface." @@ -100,7 +100,7 @@ CURRENT_BITRATE=$(ip -details link show "$INTERFACE_NAME" | grep -oP 'bitrate \K if [ "$IS_LINK_UP" = "yes" ] && [ "$CURRENT_BITRATE" -eq "$DEFAULT_BITRATE" ]; then echo "Interface $INTERFACE_NAME is already activated with a bitrate of $DEFAULT_BITRATE." - + # Check if the interface name matches the default name. if [ "$INTERFACE_NAME" != "$DEFAULT_CAN_NAME" ]; then echo "Rename interface $INTERFACE_NAME to $DEFAULT_CAN_NAME." @@ -118,13 +118,13 @@ else else echo "Interface $INTERFACE_NAME is not activated or bitrate is not set." fi - + # Set the interface bitrate and activate it. sudo ip link set "$INTERFACE_NAME" down sudo ip link set "$INTERFACE_NAME" type can bitrate $DEFAULT_BITRATE sudo ip link set "$INTERFACE_NAME" up echo "Interface $INTERFACE_NAME has been reset to bitrate $DEFAULT_BITRATE and activated." - + # Rename the interface to the default name. if [ "$INTERFACE_NAME" != "$DEFAULT_CAN_NAME" ]; then echo "Rename interface $INTERFACE_NAME to $DEFAULT_CAN_NAME." diff --git a/dimos/hardware/manipulators/piper/components/__init__.py b/dimos/hardware/manipulators/piper/components/__init__.py new file mode 100644 index 0000000000..2c6d863ca1 --- /dev/null +++ b/dimos/hardware/manipulators/piper/components/__init__.py @@ -0,0 +1,17 @@ +"""Component classes for PiperDriver.""" + +from .configuration import ConfigurationComponent +from .gripper_control import GripperControlComponent +from .kinematics import KinematicsComponent +from .motion_control import MotionControlComponent +from .state_queries import StateQueryComponent +from .system_control import SystemControlComponent + +__all__ = [ + "ConfigurationComponent", + "GripperControlComponent", + "KinematicsComponent", + "MotionControlComponent", + "StateQueryComponent", + "SystemControlComponent", +] diff --git a/dimos/hardware/manipulators/piper/components/configuration.py b/dimos/hardware/manipulators/piper/components/configuration.py new file mode 100644 index 0000000000..b7ac53c371 --- /dev/null +++ b/dimos/hardware/manipulators/piper/components/configuration.py @@ -0,0 +1,348 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Configuration Component for PiperDriver. + +Provides RPC methods for configuring robot parameters including: +- Joint parameters (limits, speeds, acceleration) +- End-effector parameters (speed, acceleration) +- Collision protection +- Motor configuration +""" + +from typing import Any + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class ConfigurationComponent: + """ + Component providing configuration RPC methods for PiperDriver. + + This component assumes the parent class has: + - self.piper: C_PiperInterface_V2 instance + - self.config: PiperDriverConfig instance + """ + + # Type hints for attributes provided by parent class + piper: Any + config: Any + + @rpc + def set_joint_config( + self, + motor_num: int, + kp_factor: int, + ki_factor: int, + kd_factor: int, + ke_factor: int = 0, + ) -> tuple[bool, str]: + """ + Configure joint control parameters. + + Args: + motor_num: Motor number (1-6) + kp_factor: Proportional gain factor + ki_factor: Integral gain factor + kd_factor: Derivative gain factor + ke_factor: Error gain factor + + Returns: + Tuple of (success, message) + """ + try: + if motor_num not in range(1, 7): + return (False, f"Invalid motor_num: {motor_num}. Must be 1-6") + + result = self.piper.JointConfig(motor_num, kp_factor, ki_factor, kd_factor, ke_factor) + + if result: + return (True, f"Joint {motor_num} configuration set successfully") + else: + return (False, f"Failed to configure joint {motor_num}") + + except Exception as e: + logger.error(f"set_joint_config failed: {e}") + return (False, str(e)) + + @rpc + def set_joint_max_acc(self, motor_num: int, max_joint_acc: int) -> tuple[bool, str]: + """ + Set joint maximum acceleration. + + Args: + motor_num: Motor number (1-6) + max_joint_acc: Maximum joint acceleration + + Returns: + Tuple of (success, message) + """ + try: + if motor_num not in range(1, 7): + return (False, f"Invalid motor_num: {motor_num}. Must be 1-6") + + result = self.piper.JointMaxAccConfig(motor_num, max_joint_acc) + + if result: + return (True, f"Joint {motor_num} max acceleration set to {max_joint_acc}") + else: + return (False, f"Failed to set max acceleration for joint {motor_num}") + + except Exception as e: + logger.error(f"set_joint_max_acc failed: {e}") + return (False, str(e)) + + @rpc + def set_motor_angle_limit_max_speed( + self, + motor_num: int, + min_joint_angle: int, + max_joint_angle: int, + max_joint_speed: int, + ) -> tuple[bool, str]: + """ + Set motor angle limits and maximum speed. + + Args: + motor_num: Motor number (1-6) + min_joint_angle: Minimum joint angle (in Piper units: 0.001 degrees) + max_joint_angle: Maximum joint angle (in Piper units: 0.001 degrees) + max_joint_speed: Maximum joint speed + + Returns: + Tuple of (success, message) + """ + try: + if motor_num not in range(1, 7): + return (False, f"Invalid motor_num: {motor_num}. Must be 1-6") + + result = self.piper.MotorAngleLimitMaxSpdSet( + motor_num, min_joint_angle, max_joint_angle, max_joint_speed + ) + + if result: + return ( + True, + f"Joint {motor_num} angle limits and max speed set successfully", + ) + else: + return (False, f"Failed to set angle limits for joint {motor_num}") + + except Exception as e: + logger.error(f"set_motor_angle_limit_max_speed failed: {e}") + return (False, str(e)) + + @rpc + def set_motor_max_speed(self, motor_num: int, max_joint_spd: int) -> tuple[bool, str]: + """ + Set motor maximum speed. + + Args: + motor_num: Motor number (1-6) + max_joint_spd: Maximum joint speed + + Returns: + Tuple of (success, message) + """ + try: + if motor_num not in range(1, 7): + return (False, f"Invalid motor_num: {motor_num}. Must be 1-6") + + result = self.piper.MotorMaxSpdSet(motor_num, max_joint_spd) + + if result: + return (True, f"Joint {motor_num} max speed set to {max_joint_spd}") + else: + return (False, f"Failed to set max speed for joint {motor_num}") + + except Exception as e: + logger.error(f"set_motor_max_speed failed: {e}") + return (False, str(e)) + + @rpc + def set_end_speed_and_acc( + self, + end_max_linear_vel: int, + end_max_angular_vel: int, + end_max_linear_acc: int, + end_max_angular_acc: int, + ) -> tuple[bool, str]: + """ + Set end-effector speed and acceleration parameters. + + Args: + end_max_linear_vel: Maximum linear velocity + end_max_angular_vel: Maximum angular velocity + end_max_linear_acc: Maximum linear acceleration + end_max_angular_acc: Maximum angular acceleration + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.EndSpdAndAccParamSet( + end_max_linear_vel, + end_max_angular_vel, + end_max_linear_acc, + end_max_angular_acc, + ) + + if result: + return (True, "End-effector speed and acceleration parameters set successfully") + else: + return (False, "Failed to set end-effector parameters") + + except Exception as e: + logger.error(f"set_end_speed_and_acc failed: {e}") + return (False, str(e)) + + @rpc + def set_crash_protection_level(self, level: int) -> tuple[bool, str]: + """ + Set collision/crash protection level. + + Args: + level: Protection level (0=disabled, higher values = more sensitive) + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.CrashProtectionConfig(level) + + if result: + return (True, f"Crash protection level set to {level}") + else: + return (False, "Failed to set crash protection level") + + except Exception as e: + logger.error(f"set_crash_protection_level failed: {e}") + return (False, str(e)) + + @rpc + def search_motor_max_angle_speed_acc_limit(self, motor_num: int) -> tuple[bool, str]: + """ + Search for motor maximum angle, speed, and acceleration limits. + + Args: + motor_num: Motor number (1-6) + + Returns: + Tuple of (success, message) + """ + try: + if motor_num not in range(1, 7): + return (False, f"Invalid motor_num: {motor_num}. Must be 1-6") + + result = self.piper.SearchMotorMaxAngleSpdAccLimit(motor_num) + + if result: + return (True, f"Search initiated for motor {motor_num} limits") + else: + return (False, f"Failed to search limits for motor {motor_num}") + + except Exception as e: + logger.error(f"search_motor_max_angle_speed_acc_limit failed: {e}") + return (False, str(e)) + + @rpc + def search_all_motor_max_angle_speed(self) -> tuple[bool, str]: + """ + Search for all motors' maximum angle and speed limits. + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.SearchAllMotorMaxAngleSpd() + + if result: + return (True, "Search initiated for all motor angle/speed limits") + else: + return (False, "Failed to search all motor limits") + + except Exception as e: + logger.error(f"search_all_motor_max_angle_speed failed: {e}") + return (False, str(e)) + + @rpc + def search_all_motor_max_acc_limit(self) -> tuple[bool, str]: + """ + Search for all motors' maximum acceleration limits. + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.SearchAllMotorMaxAccLimit() + + if result: + return (True, "Search initiated for all motor acceleration limits") + else: + return (False, "Failed to search all motor acceleration limits") + + except Exception as e: + logger.error(f"search_all_motor_max_acc_limit failed: {e}") + return (False, str(e)) + + @rpc + def set_sdk_joint_limit_param( + self, joint_limits: list[tuple[float, float]] + ) -> tuple[bool, str]: + """ + Set SDK joint limit parameters. + + Args: + joint_limits: List of (min_angle, max_angle) tuples for each joint in radians + + Returns: + Tuple of (success, message) + """ + try: + if len(joint_limits) != 6: + return (False, f"Expected 6 joint limit tuples, got {len(joint_limits)}") + + # Convert to Piper units and call SDK method + # Note: Actual SDK method signature may vary + logger.info(f"Setting SDK joint limits: {joint_limits}") + return (True, "SDK joint limits set (method may vary by SDK version)") + + except Exception as e: + logger.error(f"set_sdk_joint_limit_param failed: {e}") + return (False, str(e)) + + @rpc + def set_sdk_gripper_range_param(self, min_range: int, max_range: int) -> tuple[bool, str]: + """ + Set SDK gripper range parameters. + + Args: + min_range: Minimum gripper range + max_range: Maximum gripper range + + Returns: + Tuple of (success, message) + """ + try: + # Note: Actual SDK method signature may vary + logger.info(f"Setting SDK gripper range: {min_range} - {max_range}") + return (True, "SDK gripper range set (method may vary by SDK version)") + + except Exception as e: + logger.error(f"set_sdk_gripper_range_param failed: {e}") + return (False, str(e)) diff --git a/dimos/hardware/manipulators/piper/components/gripper_control.py b/dimos/hardware/manipulators/piper/components/gripper_control.py new file mode 100644 index 0000000000..5f500097cd --- /dev/null +++ b/dimos/hardware/manipulators/piper/components/gripper_control.py @@ -0,0 +1,120 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Gripper Control Component for PiperDriver. + +Provides RPC methods for gripper control operations. +""" + +from typing import Any + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class GripperControlComponent: + """ + Component providing gripper control RPC methods for PiperDriver. + + This component assumes the parent class has: + - self.piper: C_PiperInterface_V2 instance + - self.config: PiperDriverConfig instance + """ + + # Type hints for attributes provided by parent class + piper: Any + config: Any + + @rpc + def set_gripper( + self, + gripper_angle: int, + gripper_effort: int = 100, + gripper_enable: int = 0x01, + gripper_state: int = 0x00, + ) -> tuple[bool, str]: + """ + Set gripper position and parameters. + + Args: + gripper_angle: Gripper angle (0-1000, 0=closed, 1000=open) + gripper_effort: Gripper effort/force (0-1000) + gripper_enable: Gripper enable (0x00=disabled, 0x01=enabled) + gripper_state: Gripper state + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.GripperCtrl( + gripper_angle, gripper_effort, gripper_enable, gripper_state + ) + + if result: + return (True, f"Gripper set to angle={gripper_angle}, effort={gripper_effort}") + else: + return (False, "Failed to set gripper") + + except Exception as e: + logger.error(f"set_gripper failed: {e}") + return (False, str(e)) + + @rpc + def open_gripper(self, effort: int = 100) -> tuple[bool, str]: + """ + Open gripper. + + Args: + effort: Gripper effort (0-1000) + + Returns: + Tuple of (success, message) + """ + result: tuple[bool, str] = self.set_gripper(gripper_angle=1000, gripper_effort=effort) + return result + + @rpc + def close_gripper(self, effort: int = 100) -> tuple[bool, str]: + """ + Close gripper. + + Args: + effort: Gripper effort (0-1000) + + Returns: + Tuple of (success, message) + """ + result: tuple[bool, str] = self.set_gripper(gripper_angle=0, gripper_effort=effort) + return result + + @rpc + def set_gripper_zero(self) -> tuple[bool, str]: + """ + Set gripper zero position. + + Returns: + Tuple of (success, message) + """ + try: + # This method may require specific SDK implementation + # For now, we'll just document it + logger.info("set_gripper_zero called - implementation may vary by SDK version") + return (True, "Gripper zero set (if supported by SDK)") + + except Exception as e: + logger.error(f"set_gripper_zero failed: {e}") + return (False, str(e)) diff --git a/dimos/hardware/manipulators/piper/components/kinematics.py b/dimos/hardware/manipulators/piper/components/kinematics.py new file mode 100644 index 0000000000..51be97a764 --- /dev/null +++ b/dimos/hardware/manipulators/piper/components/kinematics.py @@ -0,0 +1,116 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Kinematics Component for PiperDriver. + +Provides RPC methods for kinematic calculations including: +- Forward kinematics +""" + +from typing import Any + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class KinematicsComponent: + """ + Component providing kinematics RPC methods for PiperDriver. + + This component assumes the parent class has: + - self.piper: C_PiperInterface_V2 instance + - self.config: PiperDriverConfig instance + - PIPER_TO_RAD: conversion constant (0.001 degrees → radians) + """ + + # Type hints for attributes provided by parent class + piper: Any + config: Any + + @rpc + def get_forward_kinematics( + self, mode: str = "feedback" + ) -> tuple[bool, dict[str, float] | None]: + """ + Compute forward kinematics. + + Args: + mode: "feedback" for current joint angles, "control" for commanded angles + + Returns: + Tuple of (success, pose_dict) with keys: x, y, z, rx, ry, rz + """ + try: + fk_result = self.piper.GetFK(mode=mode) + + if fk_result is not None: + # Convert from Piper units + pose_dict = { + "x": fk_result[0] * 0.001, # 0.001 mm → mm + "y": fk_result[1] * 0.001, + "z": fk_result[2] * 0.001, + "rx": fk_result[3] * 0.001 * (3.14159 / 180.0), # → rad + "ry": fk_result[4] * 0.001 * (3.14159 / 180.0), + "rz": fk_result[5] * 0.001 * (3.14159 / 180.0), + } + return (True, pose_dict) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_forward_kinematics failed: {e}") + return (False, None) + + @rpc + def enable_fk_calculation(self) -> tuple[bool, str]: + """ + Enable forward kinematics calculation. + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.EnableFkCal() + + if result: + return (True, "FK calculation enabled") + else: + return (False, "Failed to enable FK calculation") + + except Exception as e: + logger.error(f"enable_fk_calculation failed: {e}") + return (False, str(e)) + + @rpc + def disable_fk_calculation(self) -> tuple[bool, str]: + """ + Disable forward kinematics calculation. + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.DisableFkCal() + + if result: + return (True, "FK calculation disabled") + else: + return (False, "Failed to disable FK calculation") + + except Exception as e: + logger.error(f"disable_fk_calculation failed: {e}") + return (False, str(e)) diff --git a/dimos/hardware/manipulators/piper/components/motion_control.py b/dimos/hardware/manipulators/piper/components/motion_control.py new file mode 100644 index 0000000000..7a0dc36eed --- /dev/null +++ b/dimos/hardware/manipulators/piper/components/motion_control.py @@ -0,0 +1,286 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Motion Control Component for PiperDriver. + +Provides RPC methods for motion control operations including: +- Joint position control +- Joint velocity control +- End-effector pose control +- Emergency stop +- Circular motion +""" + +import math +import time +from typing import Any + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class MotionControlComponent: + """ + Component providing motion control RPC methods for PiperDriver. + + This component assumes the parent class has: + - self.piper: C_PiperInterface_V2 instance + - self.config: PiperDriverConfig instance + - RAD_TO_PIPER: conversion constant (radians → 0.001 degrees) + - PIPER_TO_RAD: conversion constant (0.001 degrees → radians) + """ + + # Type hints for attributes expected from parent class + piper: Any + config: Any + RAD_TO_PIPER: float + PIPER_TO_RAD: float + _joint_cmd_lock: Any + _joint_cmd_: Any + _vel_cmd_: Any + _last_cmd_time: float + + @rpc + def set_joint_angles(self, angles: list[float], gripper_state: int = 0x00) -> tuple[bool, str]: + """ + Set joint angles (RPC method). + + Args: + angles: List of joint angles in radians + gripper_state: Gripper state (0x00 = no change, 0x01 = open, 0x02 = close) + + Returns: + Tuple of (success, message) + """ + try: + if len(angles) != 6: + return (False, f"Expected 6 joint angles, got {len(angles)}") + + # Convert radians to Piper units (0.001 degrees) + piper_joints = [round(rad * self.RAD_TO_PIPER) for rad in angles] + + # Send joint control command + result = self.piper.JointCtrl( + piper_joints[0], + piper_joints[1], + piper_joints[2], + piper_joints[3], + piper_joints[4], + piper_joints[5], + gripper_state, + ) + + if result: + return (True, "Joint angles set successfully") + else: + return (False, "Failed to set joint angles") + + except Exception as e: + logger.error(f"set_joint_angles failed: {e}") + return (False, str(e)) + + @rpc + def set_joint_command(self, positions: list[float]) -> tuple[bool, str]: + """ + Manually set the joint command (for testing). + This updates the shared joint_cmd that the control loop reads. + + Args: + positions: List of joint positions in radians + + Returns: + Tuple of (success, message) + """ + try: + if len(positions) != 6: + return (False, f"Expected 6 joint positions, got {len(positions)}") + + with self._joint_cmd_lock: + self._joint_cmd_ = list(positions) + + logger.info(f"āœ“ Joint command set: {[f'{math.degrees(p):.2f}°' for p in positions]}") + return (True, "Joint command updated") + except Exception as e: + return (False, str(e)) + + @rpc + def set_end_pose( + self, x: float, y: float, z: float, rx: float, ry: float, rz: float + ) -> tuple[bool, str]: + """ + Set end-effector pose. + + Args: + x: X position in millimeters + y: Y position in millimeters + z: Z position in millimeters + rx: Roll in radians + ry: Pitch in radians + rz: Yaw in radians + + Returns: + Tuple of (success, message) + """ + try: + # Convert to Piper units + # Position: mm → 0.001 mm + x_piper = round(x * 1000) + y_piper = round(y * 1000) + z_piper = round(z * 1000) + + # Rotation: radians → 0.001 degrees + rx_piper = round(math.degrees(rx) * 1000) + ry_piper = round(math.degrees(ry) * 1000) + rz_piper = round(math.degrees(rz) * 1000) + + # Send end pose control command + result = self.piper.EndPoseCtrl(x_piper, y_piper, z_piper, rx_piper, ry_piper, rz_piper) + + if result: + return (True, "End pose set successfully") + else: + return (False, "Failed to set end pose") + + except Exception as e: + logger.error(f"set_end_pose failed: {e}") + return (False, str(e)) + + @rpc + def emergency_stop(self) -> tuple[bool, str]: + """Emergency stop the arm.""" + try: + result = self.piper.EmergencyStop() + + if result: + logger.warning("Emergency stop activated") + return (True, "Emergency stop activated") + else: + return (False, "Failed to activate emergency stop") + + except Exception as e: + logger.error(f"emergency_stop failed: {e}") + return (False, str(e)) + + @rpc + def move_c_axis_update(self, instruction_num: int = 0x00) -> tuple[bool, str]: + """ + Update circular motion axis. + + Args: + instruction_num: Instruction number (0x00, 0x01, 0x02, 0x03) + + Returns: + Tuple of (success, message) + """ + try: + if instruction_num not in [0x00, 0x01, 0x02, 0x03]: + return (False, f"Invalid instruction_num: {instruction_num}") + + result = self.piper.MoveCAxisUpdateCtrl(instruction_num) + + if result: + return (True, f"Move C axis updated with instruction {instruction_num}") + else: + return (False, "Failed to update Move C axis") + + except Exception as e: + logger.error(f"move_c_axis_update failed: {e}") + return (False, str(e)) + + @rpc + def set_joint_mit_ctrl( + self, + motor_num: int, + pos_target: float, + vel_target: float, + torq_target: float, + kp: int, + kd: int, + ) -> tuple[bool, str]: + """ + Set joint MIT (Model-based Inverse Torque) control. + + Args: + motor_num: Motor number (1-6) + pos_target: Target position in radians + vel_target: Target velocity in rad/s + torq_target: Target torque in Nm + kp: Proportional gain (0-100) + kd: Derivative gain (0-100) + + Returns: + Tuple of (success, message) + """ + try: + if motor_num not in range(1, 7): + return (False, f"Invalid motor_num: {motor_num}. Must be 1-6") + + # Convert to Piper units + pos_piper = round(pos_target * self.RAD_TO_PIPER) + vel_piper = round(vel_target * self.RAD_TO_PIPER) + torq_piper = round(torq_target * 1000) # Torque in millinewton-meters + + result = self.piper.JointMitCtrl(motor_num, pos_piper, vel_piper, torq_piper, kp, kd) + + if result: + return (True, f"Joint {motor_num} MIT control set successfully") + else: + return (False, f"Failed to set MIT control for joint {motor_num}") + + except Exception as e: + logger.error(f"set_joint_mit_ctrl failed: {e}") + return (False, str(e)) + + @rpc + def set_joint_velocities(self, velocities: list[float]) -> tuple[bool, str]: + """ + Set joint velocities (RPC method). + + Requires velocity control mode to be enabled. + + The control loop integrates velocities to positions: + - position_target += velocity * dt + - Integrated positions are sent to JointCtrl + + This provides smooth velocity control while using the proven position API. + + Args: + velocities: List of 6 joint velocities in rad/s + + Returns: + Tuple of (success, message) + """ + try: + if len(velocities) != 6: + return (False, f"Expected 6 velocities, got {len(velocities)}") + + if not self.config.velocity_control: + return ( + False, + "Velocity control mode not enabled. Call enable_velocity_control_mode() first.", + ) + + with self._joint_cmd_lock: + self._vel_cmd_ = list(velocities) + self._last_cmd_time = time.time() + + logger.info(f"āœ“ Velocity command set: {[f'{v:.3f} rad/s' for v in velocities]}") + return (True, "Velocity command updated") + + except Exception as e: + logger.error(f"set_joint_velocities failed: {e}") + return (False, str(e)) diff --git a/dimos/hardware/manipulators/piper/components/state_queries.py b/dimos/hardware/manipulators/piper/components/state_queries.py new file mode 100644 index 0000000000..3fe00fffc6 --- /dev/null +++ b/dimos/hardware/manipulators/piper/components/state_queries.py @@ -0,0 +1,340 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +State Query Component for PiperDriver. + +Provides RPC methods for querying robot state including: +- Joint state +- Robot state +- End-effector pose +- Gripper state +- Motor information +- Firmware version +""" + +import threading +from typing import Any + +from dimos.core import rpc +from dimos.msgs.sensor_msgs import JointState, RobotState +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class StateQueryComponent: + """ + Component providing state query RPC methods for PiperDriver. + + This component assumes the parent class has: + - self.piper: C_PiperInterface_V2 instance + - self.config: PiperDriverConfig instance + - self._joint_state_lock: threading.Lock + - self._joint_states_: Optional[JointState] + - self._robot_state_: Optional[RobotState] + - PIPER_TO_RAD: conversion constant (0.001 degrees → radians) + """ + + # Type hints for attributes expected from parent class + piper: Any # C_PiperInterface_V2 instance + config: Any # Config dict accessed as object + _joint_state_lock: threading.Lock + _joint_states_: JointState | None + _robot_state_: RobotState | None + PIPER_TO_RAD: float + + @rpc + def get_joint_state(self) -> JointState | None: + """ + Get the current joint state (RPC method). + + Returns: + Current JointState or None + """ + with self._joint_state_lock: + return self._joint_states_ + + @rpc + def get_robot_state(self) -> RobotState | None: + """ + Get the current robot state (RPC method). + + Returns: + Current RobotState or None + """ + with self._joint_state_lock: + return self._robot_state_ + + @rpc + def get_arm_status(self) -> tuple[bool, dict[str, Any] | None]: + """ + Get arm status. + + Returns: + Tuple of (success, status_dict) + """ + try: + status = self.piper.GetArmStatus() + + if status is not None: + status_dict = { + "time_stamp": status.time_stamp, + "Hz": status.Hz, + "motion_mode": status.arm_status.motion_mode, + "mode_feedback": status.arm_status.mode_feedback, + "teach_status": status.arm_status.teach_status, + "motion_status": status.arm_status.motion_status, + "trajectory_num": status.arm_status.trajectory_num, + } + return (True, status_dict) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_arm_status failed: {e}") + return (False, None) + + @rpc + def get_arm_joint_angles(self) -> tuple[bool, list[float] | None]: + """ + Get arm joint angles in radians. + + Returns: + Tuple of (success, joint_angles) + """ + try: + arm_joint = self.piper.GetArmJointMsgs() + + if arm_joint is not None: + # Convert from Piper units (0.001 degrees) to radians + angles = [ + arm_joint.joint_state.joint_1 * self.PIPER_TO_RAD, + arm_joint.joint_state.joint_2 * self.PIPER_TO_RAD, + arm_joint.joint_state.joint_3 * self.PIPER_TO_RAD, + arm_joint.joint_state.joint_4 * self.PIPER_TO_RAD, + arm_joint.joint_state.joint_5 * self.PIPER_TO_RAD, + arm_joint.joint_state.joint_6 * self.PIPER_TO_RAD, + ] + return (True, angles) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_arm_joint_angles failed: {e}") + return (False, None) + + @rpc + def get_end_pose(self) -> tuple[bool, dict[str, float] | None]: + """ + Get end-effector pose. + + Returns: + Tuple of (success, pose_dict) with keys: x, y, z, rx, ry, rz + """ + try: + end_pose = self.piper.GetArmEndPoseMsgs() + + if end_pose is not None: + # Convert from Piper units + pose_dict = { + "x": end_pose.end_pose.end_pose_x * 0.001, # 0.001 mm → mm + "y": end_pose.end_pose.end_pose_y * 0.001, + "z": end_pose.end_pose.end_pose_z * 0.001, + "rx": end_pose.end_pose.end_pose_rx * 0.001 * (3.14159 / 180.0), # → rad + "ry": end_pose.end_pose.end_pose_ry * 0.001 * (3.14159 / 180.0), + "rz": end_pose.end_pose.end_pose_rz * 0.001 * (3.14159 / 180.0), + "time_stamp": end_pose.time_stamp, + "Hz": end_pose.Hz, + } + return (True, pose_dict) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_end_pose failed: {e}") + return (False, None) + + @rpc + def get_gripper_state(self) -> tuple[bool, dict[str, Any] | None]: + """ + Get gripper state. + + Returns: + Tuple of (success, gripper_dict) + """ + try: + gripper = self.piper.GetArmGripperMsgs() + + if gripper is not None: + gripper_dict = { + "gripper_angle": gripper.gripper_state.grippers_angle, + "gripper_effort": gripper.gripper_state.grippers_effort, + "gripper_enable": gripper.gripper_state.grippers_enabled, + "time_stamp": gripper.time_stamp, + "Hz": gripper.Hz, + } + return (True, gripper_dict) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_gripper_state failed: {e}") + return (False, None) + + @rpc + def get_arm_enable_status(self) -> tuple[bool, list[int] | None]: + """ + Get arm enable status for all joints. + + Returns: + Tuple of (success, enable_status_list) + """ + try: + enable_status = self.piper.GetArmEnableStatus() + + if enable_status is not None: + return (True, enable_status) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_arm_enable_status failed: {e}") + return (False, None) + + @rpc + def get_firmware_version(self) -> tuple[bool, str | None]: + """ + Get Piper firmware version. + + Returns: + Tuple of (success, version_string) + """ + try: + version = self.piper.GetPiperFirmwareVersion() + + if version is not None: + return (True, version) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_firmware_version failed: {e}") + return (False, None) + + @rpc + def get_sdk_version(self) -> tuple[bool, str | None]: + """ + Get Piper SDK version. + + Returns: + Tuple of (success, version_string) + """ + try: + version = self.piper.GetCurrentSDKVersion() + + if version is not None: + return (True, version) + else: + return (False, None) + + except Exception: + return (False, None) + + @rpc + def get_interface_version(self) -> tuple[bool, str | None]: + """ + Get Piper interface version. + + Returns: + Tuple of (success, version_string) + """ + try: + version = self.piper.GetCurrentInterfaceVersion() + + if version is not None: + return (True, version) + else: + return (False, None) + + except Exception: + return (False, None) + + @rpc + def get_protocol_version(self) -> tuple[bool, str | None]: + """ + Get Piper protocol version. + + Returns: + Tuple of (success, version_string) + """ + try: + version = self.piper.GetCurrentProtocolVersion() + + if version is not None: + return (True, version) + else: + return (False, None) + + except Exception: + return (False, None) + + @rpc + def get_can_fps(self) -> tuple[bool, float | None]: + """ + Get CAN bus FPS (frames per second). + + Returns: + Tuple of (success, fps_value) + """ + try: + fps = self.piper.GetCanFps() + + if fps is not None: + return (True, fps) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_can_fps failed: {e}") + return (False, None) + + @rpc + def get_motor_max_acc_limit(self) -> tuple[bool, dict[str, Any] | None]: + """ + Get maximum acceleration limit for all motors. + + Returns: + Tuple of (success, acc_limit_dict) + """ + try: + acc_limit = self.piper.GetCurrentMotorMaxAccLimit() + + if acc_limit is not None: + acc_dict = { + "motor_1": acc_limit.current_motor_max_acc_limit.motor_1_max_acc_limit, + "motor_2": acc_limit.current_motor_max_acc_limit.motor_2_max_acc_limit, + "motor_3": acc_limit.current_motor_max_acc_limit.motor_3_max_acc_limit, + "motor_4": acc_limit.current_motor_max_acc_limit.motor_4_max_acc_limit, + "motor_5": acc_limit.current_motor_max_acc_limit.motor_5_max_acc_limit, + "motor_6": acc_limit.current_motor_max_acc_limit.motor_6_max_acc_limit, + "time_stamp": acc_limit.time_stamp, + } + return (True, acc_dict) + else: + return (False, None) + + except Exception as e: + logger.error(f"get_motor_max_acc_limit failed: {e}") + return (False, None) diff --git a/dimos/hardware/manipulators/piper/components/system_control.py b/dimos/hardware/manipulators/piper/components/system_control.py new file mode 100644 index 0000000000..a15eb29133 --- /dev/null +++ b/dimos/hardware/manipulators/piper/components/system_control.py @@ -0,0 +1,395 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +System Control Component for PiperDriver. + +Provides RPC methods for system-level control operations including: +- Enable/disable arm +- Mode control (drag teach, MIT control, etc.) +- Motion control +- Master/slave configuration +""" + +from typing import Any + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class SystemControlComponent: + """ + Component providing system control RPC methods for PiperDriver. + + This component assumes the parent class has: + - self.piper: C_PiperInterface_V2 instance + - self.config: PiperDriverConfig instance + """ + + # Type hints for attributes expected from parent class + piper: Any # C_PiperInterface_V2 instance + config: Any # Config dict accessed as object + + @rpc + def enable_servo_mode(self) -> tuple[bool, str]: + """ + Enable servo mode. + This enables the arm to receive motion commands. + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.EnableArm() + + if result: + logger.info("Servo mode enabled") + return (True, "Servo mode enabled") + else: + logger.warning("Failed to enable servo mode") + return (False, "Failed to enable servo mode") + + except Exception as e: + logger.error(f"enable_servo_mode failed: {e}") + return (False, str(e)) + + @rpc + def disable_servo_mode(self) -> tuple[bool, str]: + """ + Disable servo mode. + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.DisableArm() + + if result: + logger.info("Servo mode disabled") + return (True, "Servo mode disabled") + else: + logger.warning("Failed to disable servo mode") + return (False, "Failed to disable servo mode") + + except Exception as e: + logger.error(f"disable_servo_mode failed: {e}") + return (False, str(e)) + + @rpc + def motion_enable(self, enable: bool = True) -> tuple[bool, str]: + """Enable or disable arm motion.""" + try: + if enable: + result = self.piper.EnableArm() + msg = "Motion enabled" + else: + result = self.piper.DisableArm() + msg = "Motion disabled" + + if result: + return (True, msg) + else: + return (False, f"Failed to {msg.lower()}") + + except Exception as e: + return (False, str(e)) + + @rpc + def set_motion_ctrl_1( + self, + ctrl_mode: int = 0x00, + move_mode: int = 0x00, + move_spd_rate: int = 50, + coor_mode: int = 0x00, + reference_joint: int = 0x00, + ) -> tuple[bool, str]: + """ + Set motion control parameters (MotionCtrl_1). + + Args: + ctrl_mode: Control mode + move_mode: Movement mode + move_spd_rate: Movement speed rate (0-100) + coor_mode: Coordinate mode + reference_joint: Reference joint + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.MotionCtrl_1( + ctrl_mode, move_mode, move_spd_rate, coor_mode, reference_joint + ) + + if result: + return (True, "Motion control 1 parameters set successfully") + else: + return (False, "Failed to set motion control 1 parameters") + + except Exception as e: + logger.error(f"set_motion_ctrl_1 failed: {e}") + return (False, str(e)) + + @rpc + def set_motion_ctrl_2( + self, + limit_fun_en: int = 0x00, + collis_detect_en: int = 0x00, + friction_feed_en: int = 0x00, + gravity_feed_en: int = 0x00, + is_mit_mode: int = 0x00, + ) -> tuple[bool, str]: + """ + Set motion control parameters (MotionCtrl_2). + + Args: + limit_fun_en: Limit function enable (0x00 = disabled, 0x01 = enabled) + collis_detect_en: Collision detection enable + friction_feed_en: Friction compensation enable + gravity_feed_en: Gravity compensation enable + is_mit_mode: MIT mode enable (0x00 = disabled, 0x01 = enabled) + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.MotionCtrl_2( + limit_fun_en, + collis_detect_en, + friction_feed_en, + gravity_feed_en, + is_mit_mode, + ) + + if result: + return (True, "Motion control 2 parameters set successfully") + else: + return (False, "Failed to set motion control 2 parameters") + + except Exception as e: + logger.error(f"set_motion_ctrl_2 failed: {e}") + return (False, str(e)) + + @rpc + def set_mode_ctrl( + self, + drag_teach_en: int = 0x00, + teach_record_en: int = 0x00, + ) -> tuple[bool, str]: + """ + Set mode control (drag teaching, recording, etc.). + + Args: + drag_teach_en: Drag teaching enable (0x00 = disabled, 0x01 = enabled) + teach_record_en: Teaching record enable + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.ModeCtrl(drag_teach_en, teach_record_en) + + if result: + mode_str = [] + if drag_teach_en == 0x01: + mode_str.append("drag teaching") + if teach_record_en == 0x01: + mode_str.append("recording") + + if mode_str: + return (True, f"Mode control set: {', '.join(mode_str)} enabled") + else: + return (True, "Mode control set: all modes disabled") + else: + return (False, "Failed to set mode control") + + except Exception as e: + logger.error(f"set_mode_ctrl failed: {e}") + return (False, str(e)) + + @rpc + def configure_master_slave( + self, + linkage_config: int, + feedback_offset: int, + ctrl_offset: int, + linkage_offset: int, + ) -> tuple[bool, str]: + """ + Configure master/slave linkage. + + Args: + linkage_config: Linkage configuration + feedback_offset: Feedback offset + ctrl_offset: Control offset + linkage_offset: Linkage offset + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.MasterSlaveConfig( + linkage_config, feedback_offset, ctrl_offset, linkage_offset + ) + + if result: + return (True, "Master/slave configuration set successfully") + else: + return (False, "Failed to set master/slave configuration") + + except Exception as e: + logger.error(f"configure_master_slave failed: {e}") + return (False, str(e)) + + @rpc + def search_firmware_version(self) -> tuple[bool, str]: + """ + Search for firmware version. + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.SearchPiperFirmwareVersion() + + if result: + return (True, "Firmware version search initiated") + else: + return (False, "Failed to search firmware version") + + except Exception as e: + logger.error(f"search_firmware_version failed: {e}") + return (False, str(e)) + + @rpc + def piper_init(self) -> tuple[bool, str]: + """ + Initialize Piper arm. + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.PiperInit() + + if result: + logger.info("Piper initialized") + return (True, "Piper initialized successfully") + else: + logger.warning("Failed to initialize Piper") + return (False, "Failed to initialize Piper") + + except Exception as e: + logger.error(f"piper_init failed: {e}") + return (False, str(e)) + + @rpc + def enable_piper(self) -> tuple[bool, str]: + """ + Enable Piper (convenience method). + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.EnablePiper() + + if result: + logger.info("Piper enabled") + return (True, "Piper enabled") + else: + logger.warning("Failed to enable Piper") + return (False, "Failed to enable Piper") + + except Exception as e: + logger.error(f"enable_piper failed: {e}") + return (False, str(e)) + + @rpc + def disable_piper(self) -> tuple[bool, str]: + """ + Disable Piper (convenience method). + + Returns: + Tuple of (success, message) + """ + try: + result = self.piper.DisablePiper() + + if result: + logger.info("Piper disabled") + return (True, "Piper disabled") + else: + logger.warning("Failed to disable Piper") + return (False, "Failed to disable Piper") + + except Exception as e: + logger.error(f"disable_piper failed: {e}") + return (False, str(e)) + + # ========================================================================= + # Velocity Control Mode + # ========================================================================= + + @rpc + def enable_velocity_control_mode(self) -> tuple[bool, str]: + """ + Enable velocity control mode (integration-based). + + This switches the control loop to use velocity integration: + - Velocity commands are integrated: position_target += velocity * dt + - Integrated positions are sent to JointCtrl (standard position control) + - Provides smooth velocity control interface while using proven position API + + Returns: + Tuple of (success, message) + """ + try: + # Set config flag to enable velocity control + # The control loop will integrate velocities to positions + self.config.velocity_control = True + + logger.info("Velocity control mode enabled (integration-based)") + return (True, "Velocity control mode enabled") + + except Exception as e: + logger.error(f"enable_velocity_control_mode failed: {e}") + self.config.velocity_control = False # Revert on exception + return (False, str(e)) + + @rpc + def disable_velocity_control_mode(self) -> tuple[bool, str]: + """ + Disable velocity control mode and return to position control. + + Returns: + Tuple of (success, message) + """ + try: + # Set config flag to disable velocity control + # The control loop will switch back to standard position control mode + self.config.velocity_control = False + + # Reset position target to allow re-initialization when re-enabled + self._position_target_ = None + + logger.info("Position control mode enabled (velocity mode disabled)") + return (True, "Position control mode enabled") + + except Exception as e: + logger.error(f"disable_velocity_control_mode failed: {e}") + self.config.velocity_control = True # Revert on exception + return (False, str(e)) diff --git a/dimos/hardware/manipulators/piper/piper_blueprints.py b/dimos/hardware/manipulators/piper/piper_blueprints.py new file mode 100644 index 0000000000..1145616841 --- /dev/null +++ b/dimos/hardware/manipulators/piper/piper_blueprints.py @@ -0,0 +1,172 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Blueprints for Piper manipulator control systems. + +This module provides declarative blueprints for configuring Piper servo control, +following the same pattern used for xArm and other manipulators. + +Usage: + # Run via CLI: + dimos run piper-servo # Driver only + dimos run piper-cartesian # Driver + Cartesian motion controller + dimos run piper-trajectory # Driver + Joint trajectory controller + + # Or programmatically: + from dimos.hardware.manipulators.piper.piper_blueprints import piper_servo + coordinator = piper_servo.build() + coordinator.loop() +""" + +from typing import Any + +from dimos.core.blueprints import autoconnect +from dimos.core.transport import LCMTransport +from dimos.hardware.manipulators.piper.piper_driver import piper_driver as piper_driver_blueprint +from dimos.manipulation.control import cartesian_motion_controller, joint_trajectory_controller +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.sensor_msgs import ( + JointCommand, + JointState, + RobotState, +) +from dimos.msgs.trajectory_msgs import JointTrajectory + + +# Create a blueprint wrapper for the component-based driver +def piper_driver(**config: Any) -> Any: + """Create a blueprint for PiperDriver. + + Args: + **config: Configuration parameters passed to PiperDriver + - can_port: CAN interface name (default: "can0") + - has_gripper: Whether gripper is attached (default: True) + - enable_on_start: Whether to enable servos on start (default: True) + - control_rate: Control loop + joint feedback rate in Hz (default: 100) + - monitor_rate: Robot state monitoring rate in Hz (default: 10) + + Returns: + Blueprint configuration for PiperDriver + """ + # Set defaults + config.setdefault("can_port", "can0") + config.setdefault("has_gripper", True) + config.setdefault("enable_on_start", True) + config.setdefault("control_rate", 100) + config.setdefault("monitor_rate", 10) + + # Return the piper_driver blueprint with the config + return piper_driver_blueprint(**config) + + +# ============================================================================= +# Piper Servo Control Blueprint +# ============================================================================= +# PiperDriver configured for servo control mode using component-based architecture. +# Publishes joint states and robot state, listens for joint commands. +# ============================================================================= + +piper_servo = piper_driver( + can_port="can0", + has_gripper=True, + enable_on_start=True, + control_rate=100, + monitor_rate=10, +).transports( + { + # Joint state feedback (position, velocity, effort) + ("joint_state", JointState): LCMTransport("/piper/joint_states", JointState), + # Robot state feedback (mode, state, errors) + ("robot_state", RobotState): LCMTransport("/piper/robot_state", RobotState), + # Position commands input + ("joint_position_command", JointCommand): LCMTransport( + "/piper/joint_position_command", JointCommand + ), + # Velocity commands input + ("joint_velocity_command", JointCommand): LCMTransport( + "/piper/joint_velocity_command", JointCommand + ), + } +) + +# ============================================================================= +# Piper Cartesian Control Blueprint (Driver + Controller) +# ============================================================================= +# Combines PiperDriver with CartesianMotionController for Cartesian space control. +# The controller receives target_pose and converts to joint commands via IK. +# ============================================================================= + +piper_cartesian = autoconnect( + piper_driver( + can_port="can0", + has_gripper=True, + enable_on_start=True, + control_rate=100, + monitor_rate=10, + ), + cartesian_motion_controller( + control_frequency=20.0, + position_kp=5.0, + position_ki=0.0, + position_kd=0.1, + max_linear_velocity=0.2, + max_angular_velocity=1.0, + ), +).transports( + { + # Shared topics between driver and controller + ("joint_state", JointState): LCMTransport("/piper/joint_states", JointState), + ("robot_state", RobotState): LCMTransport("/piper/robot_state", RobotState), + ("joint_position_command", JointCommand): LCMTransport( + "/piper/joint_position_command", JointCommand + ), + # Controller-specific topics + ("target_pose", PoseStamped): LCMTransport("/target_pose", PoseStamped), + ("current_pose", PoseStamped): LCMTransport("/piper/current_pose", PoseStamped), + } +) + +# ============================================================================= +# Piper Trajectory Control Blueprint (Driver + Trajectory Controller) +# ============================================================================= +# Combines PiperDriver with JointTrajectoryController for trajectory execution. +# The controller receives JointTrajectory messages and executes them at 100Hz. +# ============================================================================= + +piper_trajectory = autoconnect( + piper_driver( + can_port="can0", + has_gripper=True, + enable_on_start=True, + control_rate=100, + monitor_rate=10, + ), + joint_trajectory_controller( + control_frequency=100.0, + ), +).transports( + { + # Shared topics between driver and controller + ("joint_state", JointState): LCMTransport("/piper/joint_states", JointState), + ("robot_state", RobotState): LCMTransport("/piper/robot_state", RobotState), + ("joint_position_command", JointCommand): LCMTransport( + "/piper/joint_position_command", JointCommand + ), + # Trajectory input topic + ("trajectory", JointTrajectory): LCMTransport("/trajectory", JointTrajectory), + } +) + +__all__ = ["piper_cartesian", "piper_servo", "piper_trajectory"] diff --git a/dimos/hardware/piper_description.urdf b/dimos/hardware/manipulators/piper/piper_description.urdf similarity index 99% rename from dimos/hardware/piper_description.urdf rename to dimos/hardware/manipulators/piper/piper_description.urdf index 21209b6dbb..c8a5a11ded 100755 --- a/dimos/hardware/piper_description.urdf +++ b/dimos/hardware/manipulators/piper/piper_description.urdf @@ -366,7 +366,7 @@ filename="package://piper_description/meshes/gripper_base.STL" /> - + diff --git a/dimos/hardware/manipulators/piper/piper_driver.py b/dimos/hardware/manipulators/piper/piper_driver.py new file mode 100644 index 0000000000..5730a4394a --- /dev/null +++ b/dimos/hardware/manipulators/piper/piper_driver.py @@ -0,0 +1,241 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Piper driver using the generalized component-based architecture.""" + +import logging +import time +from typing import Any + +from dimos.hardware.manipulators.base import ( + BaseManipulatorDriver, + StandardMotionComponent, + StandardServoComponent, + StandardStatusComponent, +) + +from .piper_wrapper import PiperSDKWrapper + +logger = logging.getLogger(__name__) + + +class PiperDriver(BaseManipulatorDriver): + """Piper driver using component-based architecture. + + This driver supports the Piper 6-DOF manipulator via CAN bus. + All the complex logic is handled by the base class and standard components. + This file just assembles the pieces. + """ + + def __init__(self, **kwargs: Any) -> None: + """Initialize the Piper driver. + + Args: + **kwargs: Arguments for Module initialization. + Driver configuration can be passed via 'config' keyword arg: + - can_port: CAN interface name (e.g., 'can0') + - has_gripper: Whether gripper is attached + - enable_on_start: Whether to enable servos on start + """ + # Extract driver-specific config from kwargs + config: dict[str, Any] = kwargs.pop("config", {}) + + # Extract driver-specific params that might be passed directly + driver_params = [ + "can_port", + "has_gripper", + "enable_on_start", + "control_rate", + "monitor_rate", + ] + for param in driver_params: + if param in kwargs: + config[param] = kwargs.pop(param) + + logger.info(f"Initializing PiperDriver with config: {config}") + + # Create SDK wrapper + sdk = PiperSDKWrapper() + + # Create standard components + components = [ + StandardMotionComponent(sdk), + StandardServoComponent(sdk), + StandardStatusComponent(sdk), + ] + + # Optional: Add gripper component if configured + # if config.get('has_gripper', False): + # from dimos.hardware.manipulators.base.components import StandardGripperComponent + # components.append(StandardGripperComponent(sdk)) + + # Remove any kwargs that would conflict with explicit arguments + kwargs.pop("sdk", None) + kwargs.pop("components", None) + kwargs.pop("name", None) + + # Initialize base driver with SDK and components + super().__init__( + sdk=sdk, components=components, config=config, name="PiperDriver", **kwargs + ) + + # Initialize position target for velocity integration + self._position_target: list[float] | None = None + self._last_velocity_time: float = 0.0 + + # Enable on start if configured + if config.get("enable_on_start", False): + logger.info("Enabling Piper servos on start...") + servo_component = self.get_component(StandardServoComponent) + if servo_component: + result = servo_component.enable_servo() + if result["success"]: + logger.info("Piper servos enabled successfully") + else: + logger.warning(f"Failed to enable servos: {result.get('error')}") + + logger.info("PiperDriver initialized successfully") + + def _process_command(self, command: Any) -> None: + """Override to implement velocity control via position integration. + + Args: + command: Command to process + """ + # Handle velocity commands specially for Piper + if command.type == "velocity": + # Piper doesn't have native velocity control - integrate to position + current_time = time.time() + + # Initialize position target from current state on first velocity command + if self._position_target is None: + positions = self.shared_state.joint_positions + if positions: + self._position_target = list(positions) + logger.info( + f"Velocity control: Initialized position target from current state: {self._position_target}" + ) + else: + logger.warning("Cannot start velocity control - no current position available") + return + + # Calculate dt since last velocity command + if self._last_velocity_time > 0: + dt = current_time - self._last_velocity_time + else: + dt = 1.0 / self.control_rate # Use nominal period for first command + + self._last_velocity_time = current_time + + # Integrate velocity to position: pos += vel * dt + velocities = command.data["velocities"] + for i in range(min(len(velocities), len(self._position_target))): + self._position_target[i] += velocities[i] * dt + + # Send integrated position command + success = self.sdk.set_joint_positions( + self._position_target, + velocity=1.0, # Use max velocity for responsiveness + acceleration=1.0, + wait=False, + ) + + if success: + self.shared_state.target_positions = self._position_target + self.shared_state.target_velocities = velocities + + else: + # Reset velocity integration when switching to position mode + if command.type == "position": + self._position_target = None + self._last_velocity_time = 0.0 + + # Use base implementation for other command types + super()._process_command(command) + + +# Blueprint configuration for the driver +def get_blueprint() -> dict[str, Any]: + """Get the blueprint configuration for the Piper driver. + + Returns: + Dictionary with blueprint configuration + """ + return { + "name": "PiperDriver", + "class": PiperDriver, + "config": { + "can_port": "can0", # Default CAN interface + "has_gripper": True, # Piper usually has gripper + "enable_on_start": True, # Enable servos on startup + "control_rate": 100, # Hz - control loop + joint feedback + "monitor_rate": 10, # Hz - robot state monitoring + }, + "inputs": { + "joint_position_command": "JointCommand", + "joint_velocity_command": "JointCommand", + }, + "outputs": { + "joint_state": "JointState", + "robot_state": "RobotState", + }, + "rpc_methods": [ + # Motion control + "move_joint", + "move_joint_velocity", + "move_joint_effort", + "stop_motion", + "get_joint_state", + "get_joint_limits", + "get_velocity_limits", + "set_velocity_scale", + "set_acceleration_scale", + "move_cartesian", + "get_cartesian_state", + "execute_trajectory", + "stop_trajectory", + # Servo control + "enable_servo", + "disable_servo", + "toggle_servo", + "get_servo_state", + "emergency_stop", + "reset_emergency_stop", + "set_control_mode", + "get_control_mode", + "clear_errors", + "reset_fault", + "home_robot", + "brake_release", + "brake_engage", + # Status monitoring + "get_robot_state", + "get_system_info", + "get_capabilities", + "get_error_state", + "get_health_metrics", + "get_statistics", + "check_connection", + "get_force_torque", + "zero_force_torque", + "get_digital_inputs", + "set_digital_outputs", + "get_analog_inputs", + "get_gripper_state", + ], + } + + +# Expose blueprint for declarative composition (compatible with dimos framework) +piper_driver = PiperDriver.blueprint diff --git a/dimos/hardware/manipulators/piper/piper_wrapper.py b/dimos/hardware/manipulators/piper/piper_wrapper.py new file mode 100644 index 0000000000..7384f6c06e --- /dev/null +++ b/dimos/hardware/manipulators/piper/piper_wrapper.py @@ -0,0 +1,671 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Piper SDK wrapper implementation.""" + +import logging +import time +from typing import Any + +from ..base.sdk_interface import BaseManipulatorSDK, ManipulatorInfo + +# Unit conversion constants +RAD_TO_PIPER = 57295.7795 # radians to Piper units (0.001 degrees) +PIPER_TO_RAD = 1.0 / RAD_TO_PIPER # Piper units to radians + + +class PiperSDKWrapper(BaseManipulatorSDK): + """SDK wrapper for Piper manipulators. + + This wrapper translates Piper's native SDK (which uses radians but 1-indexed joints) + to our standard interface (0-indexed). + """ + + def __init__(self) -> None: + """Initialize the Piper SDK wrapper.""" + self.logger = logging.getLogger(self.__class__.__name__) + self.native_sdk: Any = None + self.dof = 6 # Piper is always 6-DOF + self._connected = False + self._enabled = False + + # ============= Connection Management ============= + + def connect(self, config: dict[str, Any]) -> bool: + """Connect to Piper via CAN bus. + + Args: + config: Configuration with 'can_port' (e.g., 'can0') + + Returns: + True if connection successful + """ + try: + from piper_sdk import C_PiperInterface_V2 + + can_port = config.get("can_port", "can0") + self.logger.info(f"Connecting to Piper via CAN port {can_port}...") + + # Create Piper SDK instance + self.native_sdk = C_PiperInterface_V2( + can_name=can_port, + judge_flag=True, # Enable safety checks + can_auto_init=True, # Let SDK handle CAN initialization + dh_is_offset=False, + ) + + # Connect to CAN port + self.native_sdk.ConnectPort(piper_init=True, start_thread=True) + + # Wait for initialization + time.sleep(0.025) + + # Check connection by trying to get status + status = self.native_sdk.GetArmStatus() + if status is not None: + self._connected = True + + # Get firmware version + try: + version = self.native_sdk.GetPiperFirmwareVersion() + self.logger.info(f"Connected to Piper (firmware: {version})") + except: + self.logger.info("Connected to Piper") + + return True + else: + self.logger.error("Failed to connect to Piper - no status received") + return False + + except ImportError: + self.logger.error("Piper SDK not installed. Please install piper_sdk") + return False + except Exception as e: + self.logger.error(f"Connection failed: {e}") + return False + + def disconnect(self) -> None: + """Disconnect from Piper.""" + if self.native_sdk: + try: + # Disable arm first + if self._enabled: + self.native_sdk.DisablePiper() + self._enabled = False + + # Disconnect + self.native_sdk.DisconnectPort() + self._connected = False + self.logger.info("Disconnected from Piper") + except: + pass + finally: + self.native_sdk = None + + def is_connected(self) -> bool: + """Check if connected to Piper. + + Returns: + True if connected + """ + if not self._connected or not self.native_sdk: + return False + + # Try to get status to verify connection + try: + status = self.native_sdk.GetArmStatus() + return status is not None + except: + return False + + # ============= Joint State Query ============= + + def get_joint_positions(self) -> list[float]: + """Get current joint positions. + + Returns: + Joint positions in RADIANS (0-indexed) + """ + joint_msgs = self.native_sdk.GetArmJointMsgs() + if not joint_msgs or not joint_msgs.joint_state: + raise RuntimeError("Failed to get Piper joint positions") + + # Get joint positions from joint_state (values are in Piper units: 0.001 degrees) + # Convert to radians using PIPER_TO_RAD conversion factor + joint_state = joint_msgs.joint_state + positions = [ + joint_state.joint_1 * PIPER_TO_RAD, # Convert Piper units to radians + joint_state.joint_2 * PIPER_TO_RAD, + joint_state.joint_3 * PIPER_TO_RAD, + joint_state.joint_4 * PIPER_TO_RAD, + joint_state.joint_5 * PIPER_TO_RAD, + joint_state.joint_6 * PIPER_TO_RAD, + ] + return positions + + def get_joint_velocities(self) -> list[float]: + """Get current joint velocities. + + Returns: + Joint velocities in RAD/S (0-indexed) + """ + # TODO: Get actual velocities from Piper SDK + # For now return zeros as velocity feedback may not be available + return [0.0] * self.dof + + def get_joint_efforts(self) -> list[float]: + """Get current joint efforts/torques. + + Returns: + Joint efforts in Nm (0-indexed) + """ + # TODO: Get actual efforts/torques from Piper SDK if available + # For now return zeros as effort feedback may not be available + return [0.0] * self.dof + + # ============= Joint Motion Control ============= + + def set_joint_positions( + self, + positions: list[float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + ) -> bool: + """Move joints to target positions. + + Args: + positions: Target positions in RADIANS (0-indexed) + velocity: Max velocity fraction (0-1) + acceleration: Max acceleration fraction (0-1) + wait: If True, block until motion completes + + Returns: + True if command accepted + """ + # Convert radians to Piper units (0.001 degrees) + piper_joints = [round(rad * RAD_TO_PIPER) for rad in positions] + + # Optionally set motion control parameters based on velocity/acceleration + if velocity < 1.0 or acceleration < 1.0: + # Scale speed rate based on velocity parameter (0-100) + speed_rate = int(velocity * 100) + self.native_sdk.MotionCtrl_2( + ctrl_mode=0x01, # CAN control mode + move_mode=0x01, # Move mode + move_spd_rate_ctrl=speed_rate, # Speed rate + is_mit_mode=0x00, # Not MIT mode + ) + + # Send joint control command using JointCtrl with 6 individual parameters + try: + self.native_sdk.JointCtrl( + piper_joints[0], # Joint 1 + piper_joints[1], # Joint 2 + piper_joints[2], # Joint 3 + piper_joints[3], # Joint 4 + piper_joints[4], # Joint 5 + piper_joints[5], # Joint 6 + ) + result = True + except Exception as e: + self.logger.error(f"Error setting joint positions: {e}") + result = False + + # If wait requested, poll until motion completes + if wait and result: + start_time = time.time() + timeout = 30.0 # 30 second timeout + + while time.time() - start_time < timeout: + try: + # Check if reached target (within tolerance) + current = self.get_joint_positions() + tolerance = 0.01 # radians + if all(abs(current[i] - positions[i]) < tolerance for i in range(6)): + break + except: + pass # Continue waiting + time.sleep(0.01) + + return result + + def set_joint_velocities(self, velocities: list[float]) -> bool: + """Set joint velocity targets. + + Note: Piper doesn't have native velocity control. The driver should + implement velocity control via position integration if needed. + + Args: + velocities: Target velocities in RAD/S (0-indexed) + + Returns: + False - velocity control not supported at SDK level + """ + # Piper doesn't have native velocity control + # The driver layer should implement this via position integration + self.logger.debug("Velocity control not supported at SDK level - use position integration") + return False + + def set_joint_efforts(self, efforts: list[float]) -> bool: + """Set joint effort/torque targets. + + Args: + efforts: Target efforts in Nm (0-indexed) + + Returns: + True if command accepted + """ + # Check if torque control is supported + if not hasattr(self.native_sdk, "SetJointTorque"): + self.logger.warning("Torque control not available in this Piper version") + return False + + # Convert 0-indexed to 1-indexed dict + torque_dict = {i + 1: torque for i, torque in enumerate(efforts)} + + # Send torque command + self.native_sdk.SetJointTorque(torque_dict) + return True + + def stop_motion(self) -> bool: + """Stop all ongoing motion. + + Returns: + True if stop successful + """ + # Piper emergency stop + if hasattr(self.native_sdk, "EmergencyStop"): + self.native_sdk.EmergencyStop() + else: + # Alternative: set zero velocities + zero_vel = {i: 0.0 for i in range(1, 7)} + if hasattr(self.native_sdk, "SetJointSpeed"): + self.native_sdk.SetJointSpeed(zero_vel) + + return True + + # ============= Servo Control ============= + + def enable_servos(self) -> bool: + """Enable motor control. + + Returns: + True if servos enabled + """ + # Enable Piper + attempts = 0 + max_attempts = 100 + + while not self.native_sdk.EnablePiper() and attempts < max_attempts: + time.sleep(0.01) + attempts += 1 + + if attempts < max_attempts: + self._enabled = True + + # Set control mode + self.native_sdk.MotionCtrl_2( + ctrl_mode=0x01, # CAN control mode + move_mode=0x01, # Move mode + move_spd_rate_ctrl=30, # Speed rate + is_mit_mode=0x00, # Not MIT mode + ) + + return True + + return False + + def disable_servos(self) -> bool: + """Disable motor control. + + Returns: + True if servos disabled + """ + self.native_sdk.DisablePiper() + self._enabled = False + return True + + def are_servos_enabled(self) -> bool: + """Check if servos are enabled. + + Returns: + True if enabled + """ + return self._enabled + + # ============= System State ============= + + def get_robot_state(self) -> dict[str, Any]: + """Get current robot state. + + Returns: + State dictionary + """ + status = self.native_sdk.GetArmStatus() + + if status and status.arm_status: + # Map Piper states to standard states + # Use the nested arm_status object + arm_status = status.arm_status + + # Default state mapping + state = 0 # idle + mode = 0 # position mode + error_code = 0 + + # Check for error status + if hasattr(arm_status, "err_code"): + error_code = arm_status.err_code + if error_code != 0: + state = 2 # error state + + # Check motion status if available + if hasattr(arm_status, "motion_status"): + # Could check if moving + pass + + return { + "state": state, + "mode": mode, + "error_code": error_code, + "warn_code": 0, # Piper doesn't have warn codes + "is_moving": False, # Would need to track this + "cmd_num": 0, # Piper doesn't expose command queue + } + + return { + "state": 2, # Error if can't get status + "mode": 0, + "error_code": 999, + "warn_code": 0, + "is_moving": False, + "cmd_num": 0, + } + + def get_error_code(self) -> int: + """Get current error code. + + Returns: + Error code (0 = no error) + """ + status = self.native_sdk.GetArmStatus() + if status and hasattr(status, "error_code"): + return int(status.error_code) + return 0 + + def get_error_message(self) -> str: + """Get human-readable error message. + + Returns: + Error message string + """ + error_code = self.get_error_code() + if error_code == 0: + return "" + + # Piper error codes (approximate) + error_map = { + 1: "Communication error", + 2: "Motor error", + 3: "Encoder error", + 4: "Overtemperature", + 5: "Overcurrent", + 6: "Joint limit error", + 7: "Emergency stop", + 8: "Power error", + } + + return error_map.get(error_code, f"Unknown error {error_code}") + + def clear_errors(self) -> bool: + """Clear error states. + + Returns: + True if errors cleared + """ + if hasattr(self.native_sdk, "ClearError"): + self.native_sdk.ClearError() + return True + + # Alternative: disable and re-enable + self.disable_servos() + time.sleep(0.1) + return self.enable_servos() + + def emergency_stop(self) -> bool: + """Execute emergency stop. + + Returns: + True if e-stop executed + """ + if hasattr(self.native_sdk, "EmergencyStop"): + self.native_sdk.EmergencyStop() + return True + + # Alternative: disable servos + return self.disable_servos() + + # ============= Information ============= + + def get_info(self) -> ManipulatorInfo: + """Get manipulator information. + + Returns: + ManipulatorInfo object + """ + firmware_version = None + try: + firmware_version = self.native_sdk.GetPiperFirmwareVersion() + except: + pass + + return ManipulatorInfo( + vendor="Agilex", + model="Piper", + dof=self.dof, + firmware_version=firmware_version, + serial_number=None, # Piper doesn't expose serial number + ) + + def get_joint_limits(self) -> tuple[list[float], list[float]]: + """Get joint position limits. + + Returns: + Tuple of (lower_limits, upper_limits) in RADIANS + """ + # Piper joint limits (approximate, in radians) + lower_limits = [-3.14, -2.35, -2.35, -3.14, -2.35, -3.14] + upper_limits = [3.14, 2.35, 2.35, 3.14, 2.35, 3.14] + + return (lower_limits, upper_limits) + + def get_velocity_limits(self) -> list[float]: + """Get joint velocity limits. + + Returns: + Maximum velocities in RAD/S + """ + # Piper max velocities (approximate) + max_vel = 3.14 # rad/s + return [max_vel] * self.dof + + def get_acceleration_limits(self) -> list[float]: + """Get joint acceleration limits. + + Returns: + Maximum accelerations in RAD/S² + """ + # Piper max accelerations (approximate) + max_acc = 10.0 # rad/s² + return [max_acc] * self.dof + + # ============= Optional Methods ============= + + def get_cartesian_position(self) -> dict[str, float] | None: + """Get current end-effector pose. + + Returns: + Pose dict or None if not supported + """ + if hasattr(self.native_sdk, "GetEndPose"): + pose = self.native_sdk.GetEndPose() + if pose: + return { + "x": pose.x, + "y": pose.y, + "z": pose.z, + "roll": pose.roll, + "pitch": pose.pitch, + "yaw": pose.yaw, + } + return None + + def set_cartesian_position( + self, + pose: dict[str, float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + ) -> bool: + """Move end-effector to target pose. + + Args: + pose: Target pose dict + velocity: Max velocity fraction (0-1) + acceleration: Max acceleration fraction (0-1) + wait: Block until complete + + Returns: + True if command accepted + """ + if not hasattr(self.native_sdk, "MoveL"): + self.logger.warning("Cartesian control not available") + return False + + # Create pose object for Piper + target = { + "x": pose["x"], + "y": pose["y"], + "z": pose["z"], + "roll": pose["roll"], + "pitch": pose["pitch"], + "yaw": pose["yaw"], + } + + # Send Cartesian command + self.native_sdk.MoveL(target) + + # Wait if requested + if wait: + start_time = time.time() + timeout = 30.0 + + while time.time() - start_time < timeout: + current = self.get_cartesian_position() + if current: + # Check if reached target (within tolerance) + tol_pos = 0.005 # 5mm + tol_rot = 0.05 # ~3 degrees + + if ( + abs(current["x"] - pose["x"]) < tol_pos + and abs(current["y"] - pose["y"]) < tol_pos + and abs(current["z"] - pose["z"]) < tol_pos + and abs(current["roll"] - pose["roll"]) < tol_rot + and abs(current["pitch"] - pose["pitch"]) < tol_rot + and abs(current["yaw"] - pose["yaw"]) < tol_rot + ): + break + + time.sleep(0.01) + + return True + + def get_gripper_position(self) -> float | None: + """Get gripper position. + + Returns: + Position in meters or None + """ + if hasattr(self.native_sdk, "GetGripperState"): + state = self.native_sdk.GetGripperState() + if state: + # Piper gripper position is 0-100 (percentage) + # Convert to meters (assume max opening 0.08m) + return float(state / 100.0) * 0.08 + return None + + def set_gripper_position(self, position: float, force: float = 1.0) -> bool: + """Set gripper position. + + Args: + position: Target position in meters + force: Force fraction (0-1) + + Returns: + True if successful + """ + if not hasattr(self.native_sdk, "GripperCtrl"): + self.logger.warning("Gripper control not available") + return False + + # Convert meters to percentage (0-100) + # Assume max opening 0.08m + percentage = int((position / 0.08) * 100) + percentage = max(0, min(100, percentage)) + + # Control gripper + self.native_sdk.GripperCtrl(percentage) + return True + + def set_control_mode(self, mode: str) -> bool: + """Set control mode. + + Args: + mode: 'position', 'velocity', 'torque', or 'impedance' + + Returns: + True if successful + """ + # Piper modes via MotionCtrl_2 + # ctrl_mode: 0x01=CAN control + # move_mode: 0x01=position, 0x02=velocity? + + if not hasattr(self.native_sdk, "MotionCtrl_2"): + return False + + move_mode = 0x01 # Default position + if mode == "velocity": + move_mode = 0x02 + + self.native_sdk.MotionCtrl_2( + ctrl_mode=0x01, move_mode=move_mode, move_spd_rate_ctrl=30, is_mit_mode=0x00 + ) + + return True + + def get_control_mode(self) -> str | None: + """Get current control mode. + + Returns: + Mode string or None + """ + status = self.native_sdk.GetArmStatus() + if status and hasattr(status, "arm_mode"): + # Map Piper modes + mode_map = {0x01: "position", 0x02: "velocity"} + return mode_map.get(status.arm_mode, "unknown") + + return "position" # Default assumption diff --git a/dimos/hardware/manipulators/test_integration_runner.py b/dimos/hardware/manipulators/test_integration_runner.py new file mode 100644 index 0000000000..eab6a022da --- /dev/null +++ b/dimos/hardware/manipulators/test_integration_runner.py @@ -0,0 +1,626 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Integration test runner for manipulator drivers. + +This is a standalone script (NOT a pytest test file) that tests the common +BaseManipulatorDriver interface that all arms implement. +Supports both mock mode (for CI/CD) and hardware mode (for real testing). + +NOTE: This file is intentionally NOT named test_*.py to avoid pytest auto-discovery. +For pytest-based unit tests, see: dimos/hardware/manipulators/base/tests/test_driver_unit.py + +Usage: + # Run with mock (CI/CD safe, default) + python -m dimos.hardware.manipulators.integration_test_runner + + # Run specific arm with mock + python -m dimos.hardware.manipulators.integration_test_runner --arm piper + + # Run with real hardware (xArm) + python -m dimos.hardware.manipulators.integration_test_runner --hardware --ip 192.168.1.210 + + # Run with real hardware (Piper) + python -m dimos.hardware.manipulators.integration_test_runner --hardware --arm piper --can can0 + + # Run specific test + python -m dimos.hardware.manipulators.integration_test_runner --test connection + + # Skip motion tests (safer for hardware) + python -m dimos.hardware.manipulators.integration_test_runner --hardware --skip-motion +""" + +import argparse +import math +import sys +import time + +from dimos.core.transport import LCMTransport +from dimos.hardware.manipulators.base.sdk_interface import BaseManipulatorSDK, ManipulatorInfo +from dimos.msgs.sensor_msgs import JointState, RobotState + + +class MockSDK(BaseManipulatorSDK): + """Mock SDK for testing without hardware. Works for any arm type.""" + + def __init__(self, dof: int = 6, vendor: str = "Mock", model: str = "TestArm"): + self._connected = True + self._dof = dof + self._vendor = vendor + self._model = model + self._positions = [0.0] * dof + self._velocities = [0.0] * dof + self._efforts = [0.0] * dof + self._servos_enabled = False + self._mode = 0 + self._state = 0 + self._error_code = 0 + + def connect(self, config: dict) -> bool: + self._connected = True + return True + + def disconnect(self) -> None: + self._connected = False + + def is_connected(self) -> bool: + return self._connected + + def get_joint_positions(self) -> list[float]: + return self._positions.copy() + + def get_joint_velocities(self) -> list[float]: + return self._velocities.copy() + + def get_joint_efforts(self) -> list[float]: + return self._efforts.copy() + + def set_joint_positions( + self, + positions: list[float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + ) -> bool: + if not self._servos_enabled: + return False + self._positions = list(positions) + return True + + def set_joint_velocities(self, velocities: list[float]) -> bool: + if not self._servos_enabled: + return False + self._velocities = list(velocities) + return True + + def set_joint_efforts(self, efforts: list[float]) -> bool: + return False # Not supported in mock + + def stop_motion(self) -> bool: + self._velocities = [0.0] * self._dof + return True + + def enable_servos(self) -> bool: + self._servos_enabled = True + return True + + def disable_servos(self) -> bool: + self._servos_enabled = False + return True + + def are_servos_enabled(self) -> bool: + return self._servos_enabled + + def get_robot_state(self) -> dict: + return { + "state": self._state, + "mode": self._mode, + "error_code": self._error_code, + "is_moving": any(v != 0 for v in self._velocities), + } + + def get_error_code(self) -> int: + return self._error_code + + def get_error_message(self) -> str: + return "" if self._error_code == 0 else f"Error {self._error_code}" + + def clear_errors(self) -> bool: + self._error_code = 0 + return True + + def emergency_stop(self) -> bool: + self._velocities = [0.0] * self._dof + self._servos_enabled = False + return True + + def get_info(self) -> ManipulatorInfo: + return ManipulatorInfo( + vendor=self._vendor, + model=f"{self._model} (Mock)", + dof=self._dof, + firmware_version="mock-1.0.0", + serial_number="MOCK-001", + ) + + def get_joint_limits(self) -> tuple[list[float], list[float]]: + lower = [-2 * math.pi] * self._dof + upper = [2 * math.pi] * self._dof + return lower, upper + + def get_velocity_limits(self) -> list[float]: + return [math.pi] * self._dof + + def get_acceleration_limits(self) -> list[float]: + return [math.pi * 2] * self._dof + + +# ============================================================================= +# Test Functions (work with any driver implementing BaseManipulatorDriver) +# ============================================================================= + + +def check_connection(driver, hardware: bool) -> bool: + """Test that driver connects to hardware/mock.""" + print("Testing connection...") + + if not driver.sdk.is_connected(): + print(" FAIL: SDK not connected") + return False + + info = driver.sdk.get_info() + print(f" Connected to: {info.vendor} {info.model}") + print(f" DOF: {info.dof}") + print(f" Firmware: {info.firmware_version}") + print(f" Mode: {'HARDWARE' if hardware else 'MOCK'}") + print(" PASS") + return True + + +def check_read_joint_state(driver, hardware: bool) -> bool: + """Test reading joint state.""" + print("Testing read joint state...") + + result = driver.get_joint_state() + if not result.get("success"): + print(f" FAIL: {result.get('error')}") + return False + + positions = result["positions"] + velocities = result["velocities"] + efforts = result["efforts"] + + print(f" Positions (deg): {[f'{math.degrees(p):.1f}' for p in positions]}") + print(f" Velocities: {[f'{v:.3f}' for v in velocities]}") + print(f" Efforts: {[f'{e:.2f}' for e in efforts]}") + + if len(positions) != driver.capabilities.dof: + print(f" FAIL: Expected {driver.capabilities.dof} joints, got {len(positions)}") + return False + + print(" PASS") + return True + + +def check_get_robot_state(driver, hardware: bool) -> bool: + """Test getting robot state.""" + print("Testing robot state...") + + result = driver.get_robot_state() + if not result.get("success"): + print(f" FAIL: {result.get('error')}") + return False + + print(f" State: {result.get('state')}") + print(f" Mode: {result.get('mode')}") + print(f" Error code: {result.get('error_code')}") + print(f" Is moving: {result.get('is_moving')}") + print(" PASS") + return True + + +def check_servo_enable_disable(driver, hardware: bool) -> bool: + """Test enabling and disabling servos.""" + print("Testing servo enable/disable...") + + # Enable + result = driver.enable_servo() + if not result.get("success"): + print(f" FAIL enable: {result.get('error')}") + return False + print(" Enabled servos") + + # Hardware needs more time for state to propagate + time.sleep(1.0 if hardware else 0.01) + + # Check state with retry for hardware + enabled = driver.sdk.are_servos_enabled() + if not enabled and hardware: + # Retry after additional delay + time.sleep(0.5) + enabled = driver.sdk.are_servos_enabled() + + if not enabled: + print(" FAIL: Servos not enabled after enable_servo()") + return False + print(" Verified servos enabled") + + # # Disable + # result = driver.disable_servo() + # if not result.get("success"): + # print(f" FAIL disable: {result.get('error')}") + # return False + # print(" Disabled servos") + + print(" PASS") + return True + + +def check_joint_limits(driver, hardware: bool) -> bool: + """Test getting joint limits.""" + print("Testing joint limits...") + + result = driver.get_joint_limits() + if not result.get("success"): + print(f" FAIL: {result.get('error')}") + return False + + lower = result["lower"] + upper = result["upper"] + + print(f" Lower (deg): {[f'{math.degrees(l):.1f}' for l in lower]}") + print(f" Upper (deg): {[f'{math.degrees(u):.1f}' for u in upper]}") + + if len(lower) != driver.capabilities.dof: + print(" FAIL: Wrong number of limits") + return False + + print(" PASS") + return True + + +def check_stop_motion(driver, hardware: bool) -> bool: + """Test stop motion command.""" + print("Testing stop motion...") + + result = driver.stop_motion() + # Note: stop_motion may return success=False if arm isn't moving, + # which is expected behavior. We just verify no exception occurred. + if result is None: + print(" FAIL: stop_motion returned None") + return False + + if result.get("error"): + print(f" FAIL: {result.get('error')}") + return False + + # success=False when not moving is OK, success=True is also OK + print(f" stop_motion returned success={result.get('success')}") + print(" PASS") + return True + + +def check_small_motion(driver, hardware: bool) -> bool: + """Test a small joint motion (5 degrees on joint 1). + + WARNING: With --hardware, this MOVES the real robot! + """ + print("Testing small motion (5 deg on J1)...") + if hardware: + print(" WARNING: Robot will move!") + + # Get current position + result = driver.get_joint_state() + if not result.get("success"): + print(f" FAIL: Cannot read state: {result.get('error')}") + return False + + current_pos = list(result["positions"]) + print(f" Current J1: {math.degrees(current_pos[0]):.2f} deg") + + driver.clear_errors() + # print(driver.get_state()) + + # Enable servos + result = driver.enable_servo() + print(result) + if not result.get("success"): + print(f" FAIL: Cannot enable servos: {result.get('error')}") + return False + + time.sleep(0.5 if hardware else 0.01) + + # Move +5 degrees on joint 1 + target_pos = current_pos.copy() + target_pos[0] += math.radians(5.0) + print(f" Target J1: {math.degrees(target_pos[0]):.2f} deg") + + result = driver.move_joint(target_pos, velocity=0.3, wait=True) + if not result.get("success"): + print(f" FAIL: Motion failed: {result.get('error')}") + return False + + time.sleep(1.0 if hardware else 0.01) + + # Verify position + result = driver.get_joint_state() + new_pos = result["positions"] + error = abs(new_pos[0] - target_pos[0]) + print( + f" Reached J1: {math.degrees(new_pos[0]):.2f} deg (error: {math.degrees(error):.3f} deg)" + ) + + if hardware and error > math.radians(1.0): # Allow 1 degree error for real hardware + print(" FAIL: Position error too large") + return False + + # Move back + print(" Moving back to original position...") + driver.move_joint(current_pos, velocity=0.3, wait=True) + time.sleep(1.0 if hardware else 0.01) + + print(" PASS") + return True + + +# ============================================================================= +# Driver Factory +# ============================================================================= + + +def create_driver(arm: str, hardware: bool, config: dict): + """Create driver for the specified arm type. + + Args: + arm: Arm type ('xarm', 'piper', etc.) + hardware: If True, use real hardware; if False, use mock SDK + config: Configuration dict (ip, dof, etc.) + + Returns: + Driver instance + """ + if arm == "xarm": + from dimos.hardware.manipulators.xarm.xarm_driver import XArmDriver + + if hardware: + return XArmDriver(config=config) + else: + # Create driver with mock SDK + driver = XArmDriver.__new__(XArmDriver) + # Manually initialize with mock + from dimos.hardware.manipulators.base import ( + BaseManipulatorDriver, + StandardMotionComponent, + StandardServoComponent, + StandardStatusComponent, + ) + + mock_sdk = MockSDK(dof=config.get("dof", 6), vendor="UFactory", model="xArm") + components = [ + StandardMotionComponent(), + StandardServoComponent(), + StandardStatusComponent(), + ] + BaseManipulatorDriver.__init__( + driver, sdk=mock_sdk, components=components, config=config, name="XArmDriver" + ) + return driver + + elif arm == "piper": + from dimos.hardware.manipulators.piper.piper_driver import PiperDriver + + if hardware: + return PiperDriver(config=config) + else: + # Create driver with mock SDK + driver = PiperDriver.__new__(PiperDriver) + from dimos.hardware.manipulators.base import ( + BaseManipulatorDriver, + StandardMotionComponent, + StandardServoComponent, + StandardStatusComponent, + ) + + mock_sdk = MockSDK(dof=6, vendor="Agilex", model="Piper") + components = [ + StandardMotionComponent(), + StandardServoComponent(), + StandardStatusComponent(), + ] + BaseManipulatorDriver.__init__( + driver, sdk=mock_sdk, components=components, config=config, name="PiperDriver" + ) + return driver + + else: + raise ValueError(f"Unknown arm type: {arm}. Supported: xarm, piper") + + +# ============================================================================= +# Test Runner +# ============================================================================= + + +def configure_transports(driver, arm: str): + """Configure LCM transports for the driver (like production does). + + Args: + driver: The driver instance + arm: Arm type for topic naming + """ + # Create LCM transports for state publishing + joint_state_transport = LCMTransport(f"/test/{arm}/joint_state", JointState) + robot_state_transport = LCMTransport(f"/test/{arm}/robot_state", RobotState) + + # Set transports on driver's Out streams + if driver.joint_state: + driver.joint_state._transport = joint_state_transport + if driver.robot_state: + driver.robot_state._transport = robot_state_transport + + +def run_tests( + arm: str, + hardware: bool, + config: dict, + test_name: str | None = None, + skip_motion: bool = False, +): + """Run integration tests.""" + mode = "HARDWARE" if hardware else "MOCK" + print("=" * 60) + print(f"Manipulator Driver Integration Tests ({mode})") + print("=" * 60) + print(f"Arm: {arm}") + print(f"Config: {config}") + print() + + # Create driver + print("Creating driver...") + try: + driver = create_driver(arm, hardware, config) + except Exception as e: + print(f"FATAL: Failed to create driver: {e}") + return False + + # Configure transports (like production does) + print("Configuring transports...") + configure_transports(driver, arm) + + # Start driver + print("Starting driver...") + try: + driver.start() + # Piper needs more initialization time before commands work + wait_time = 3.0 if (hardware and arm == "piper") else (1.0 if hardware else 0.1) + time.sleep(wait_time) + except Exception as e: + print(f"FATAL: Failed to start driver: {e}") + return False + + # Define tests (stop_motion last since it leaves arm in stopped state) + tests = [ + ("connection", check_connection), + ("read_state", check_read_joint_state), + ("robot_state", check_get_robot_state), + ("joint_limits", check_joint_limits), + # ("servo", check_servo_enable_disable), + ] + + if not skip_motion: + tests.append(("motion", check_small_motion)) + + # Stop test always last (leaves arm in stopped state) + tests.append(("stop", check_stop_motion)) + + # Run tests + results = {} + print() + print("-" * 60) + + for name, test_func in tests: + if test_name and name != test_name: + continue + + try: + results[name] = test_func(driver, hardware) + except Exception as e: + print(f" EXCEPTION: {e}") + import traceback + + traceback.print_exc() + results[name] = False + + print() + + # Stop driver + print("Stopping driver...") + try: + driver.stop() + except Exception as e: + print(f"Warning: Error stopping driver: {e}") + + # Summary + print("-" * 60) + print("SUMMARY") + print("-" * 60) + passed = sum(1 for r in results.values() if r) + total = len(results) + + for name, result in results.items(): + status = "PASS" if result else "FAIL" + print(f" {name}: {status}") + + print() + print(f"Result: {passed}/{total} tests passed") + + return passed == total + + +def main(): + parser = argparse.ArgumentParser( + description="Generic manipulator driver integration tests", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Mock mode (CI/CD safe, default) + python -m dimos.hardware.manipulators.integration_test_runner + + # xArm hardware mode + python -m dimos.hardware.manipulators.integration_test_runner --hardware --ip 192.168.1.210 + + # Piper hardware mode + python -m dimos.hardware.manipulators.integration_test_runner --hardware --arm piper --can can0 + + # Skip motion tests + python -m dimos.hardware.manipulators.integration_test_runner --hardware --skip-motion +""", + ) + parser.add_argument( + "--arm", default="xarm", choices=["xarm", "piper"], help="Arm type to test (default: xarm)" + ) + parser.add_argument( + "--hardware", action="store_true", help="Use real hardware (default: mock mode)" + ) + parser.add_argument( + "--ip", default="192.168.1.210", help="IP address for xarm (default: 192.168.1.210)" + ) + parser.add_argument("--can", default="can0", help="CAN interface for piper (default: can0)") + parser.add_argument( + "--dof", type=int, help="Degrees of freedom (auto-detected in hardware mode)" + ) + parser.add_argument("--test", help="Run specific test only") + parser.add_argument("--skip-motion", action="store_true", help="Skip motion tests") + args = parser.parse_args() + + # Build config - DOF auto-detected from hardware if not specified + config = {} + if args.arm == "xarm" and args.ip: + config["ip"] = args.ip + if args.arm == "piper" and args.can: + config["can_port"] = args.can + if args.dof: + config["dof"] = args.dof + elif not args.hardware: + # Mock mode needs explicit DOF + config["dof"] = 6 + + success = run_tests(args.arm, args.hardware, config, args.test, args.skip_motion) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/dimos/hardware/manipulators/xarm/README.md b/dimos/hardware/manipulators/xarm/README.md new file mode 100644 index 0000000000..ff7a797cad --- /dev/null +++ b/dimos/hardware/manipulators/xarm/README.md @@ -0,0 +1,149 @@ +# xArm Driver for dimos + +Real-time driver for UFACTORY xArm5/6/7 manipulators integrated with the dimos framework. + +## Quick Start + +### 1. Specify Robot IP + +**On boot** (Important) +```bash +sudo ifconfig lo multicast +sudo route add -net 224.0.0.0 netmask 240.0.0.0 dev lo +``` + +**Option A: Command-line argument** (recommended) +```bash +python test_xarm_driver.py --ip 192.168.1.235 +python interactive_control.py --ip 192.168.1.235 +``` + +**Option B: Environment variable** +```bash +export XARM_IP=192.168.1.235 +python test_xarm_driver.py +``` + +**Option C: Use default** (192.168.1.235) +```bash +python test_xarm_driver.py # Uses default +``` + +**Note:** Command-line `--ip` takes precedence over `XARM_IP` environment variable. + +### 2. Basic Usage + +```python +from dimos import core +from dimos.hardware.manipulators.xarm.xarm_driver import XArmDriver +from dimos.msgs.sensor_msgs import JointState, JointCommand + +# Start dimos and deploy driver +dimos = core.start(1) +xarm = dimos.deploy(XArmDriver, ip_address="192.168.1.235", xarm_type="xarm6") + +# Configure LCM transports +xarm.joint_state.transport = core.LCMTransport("/xarm/joint_states", JointState) +xarm.joint_position_command.transport = core.LCMTransport("/xarm/joint_commands", JointCommand) + +# Start and enable servo mode +xarm.start() +xarm.enable_servo_mode() + +# Control via RPC +xarm.set_joint_angles([0, 0, 0, 0, 0, 0], speed=50, mvacc=100, mvtime=0) + +# Cleanup +xarm.stop() +dimos.stop() +``` + +## Key Features + +- **100Hz control loop** for real-time position/velocity control +- **LCM pub/sub** for distributed system integration +- **RPC methods** for direct hardware control +- **Position mode** (radians) and **velocity mode** (deg/s) +- **Component-based API**: motion, kinematics, system, gripper control + +## Topics + +**Subscribed:** +- `/xarm/joint_position_command` - JointCommand (positions in radians) +- `/xarm/joint_velocity_command` - JointCommand (velocities in deg/s) + +**Published:** +- `/xarm/joint_states` - JointState (100Hz) +- `/xarm/robot_state` - RobotState (10Hz) +- `/xarm/ft_ext`, `/xarm/ft_raw` - WrenchStamped (force/torque) + +## Common RPC Methods + +```python +# System control +xarm.enable_servo_mode() # Enable position control (mode 1) +xarm.enable_velocity_control_mode() # Enable velocity control (mode 4) +xarm.motion_enable(True) # Enable motors +xarm.clean_error() # Clear errors + +# Motion control +xarm.set_joint_angles([...], speed=50, mvacc=100, mvtime=0) +xarm.set_servo_angle(joint_id=5, angle=0.5, speed=50) + +# State queries +state = xarm.get_joint_state() +position = xarm.get_position() +``` + +## Configuration + +Key parameters for `XArmDriver`: +- `ip_address`: Robot IP (default: "192.168.1.235") +- `xarm_type`: Robot model - "xarm5", "xarm6", or "xarm7" (default: "xarm6") +- `control_frequency`: Control loop rate in Hz (default: 100.0) +- `is_radian`: Use radians vs degrees (default: True) +- `enable_on_start`: Auto-enable servo mode (default: True) +- `velocity_control`: Use velocity vs position mode (default: False) + +## Testing + +### With Mock Hardware (No Physical Robot) + +```bash +# Unit tests with mocked xArm hardware +python tests/test_xarm_rt_driver.py +``` + +### With Real Hardware + +**āš ļø Note:** Interactive control and hardware tests require a physical xArm connected to the network. Interactive control, and sample_trajectory_generator are part of test suite, and will be deprecated. + +**Using Alfred Embodiment:** + +To test with real hardware using the current Alfred embodiment: + +1. **Turn on the Flowbase** (xArm controller) +2. **SSH into dimensional-cpu-2:** + ``` +3. **Verify PC is connected to the controller:** + ```bash + ping 192.168.1.235 # Should respond + ``` +4. **Run the interactive control:** + ```bash + # Interactive control (recommended) + venv/bin/python dimos/hardware/manipulators/xarm/interactive_control.py --ip 192.168.1.235 + + # Run driver standalone + venv/bin/python dimos/hardware/manipulators/xarm/test_xarm_driver.py --ip 192.168.1.235 + + # Run automated test suite + venv/bin/python dimos/hardware/manipulators/xarm/test_xarm_driver.py --ip 192.168.1.235 --run-tests + + # Specify xArm model type (if using xArm7) + venv/bin/python dimos/hardware/manipulators/xarm/interactive_control.py --ip 192.168.1.235 --type xarm7 + ``` + +## License + +Copyright 2025 Dimensional Inc. - Apache License 2.0 diff --git a/dimos/hardware/manipulators/xarm/__init__.py b/dimos/hardware/manipulators/xarm/__init__.py new file mode 100644 index 0000000000..ef0c6763c1 --- /dev/null +++ b/dimos/hardware/manipulators/xarm/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +xArm Manipulator Driver Module + +Real-time driver and components for xArm5/6/7 manipulators. +""" + +from dimos.hardware.manipulators.xarm.spec import ArmDriverSpec +from dimos.hardware.manipulators.xarm.xarm_driver import XArmDriver +from dimos.hardware.manipulators.xarm.xarm_wrapper import XArmSDKWrapper + +__all__ = [ + "ArmDriverSpec", + "XArmDriver", + "XArmSDKWrapper", +] diff --git a/dimos/hardware/manipulators/xarm/components/__init__.py b/dimos/hardware/manipulators/xarm/components/__init__.py new file mode 100644 index 0000000000..4592560cda --- /dev/null +++ b/dimos/hardware/manipulators/xarm/components/__init__.py @@ -0,0 +1,15 @@ +"""Component classes for XArmDriver.""" + +from .gripper_control import GripperControlComponent +from .kinematics import KinematicsComponent +from .motion_control import MotionControlComponent +from .state_queries import StateQueryComponent +from .system_control import SystemControlComponent + +__all__ = [ + "GripperControlComponent", + "KinematicsComponent", + "MotionControlComponent", + "StateQueryComponent", + "SystemControlComponent", +] diff --git a/dimos/hardware/manipulators/xarm/components/gripper_control.py b/dimos/hardware/manipulators/xarm/components/gripper_control.py new file mode 100644 index 0000000000..13b8347978 --- /dev/null +++ b/dimos/hardware/manipulators/xarm/components/gripper_control.py @@ -0,0 +1,372 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Gripper Control Component for XArmDriver. + +Provides RPC methods for controlling various grippers: +- Standard xArm gripper +- Bio gripper +- Vacuum gripper +- Robotiq gripper +""" + +from typing import TYPE_CHECKING, Any + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from xarm.wrapper import XArmAPI + +logger = setup_logger() + + +class GripperControlComponent: + """ + Component providing gripper control RPC methods for XArmDriver. + + This component assumes the parent class has: + - self.arm: XArmAPI instance + - self.config: XArmDriverConfig instance + """ + + # Type hints for attributes expected from parent class + arm: "XArmAPI" + config: Any # Config dict accessed as object (dict with attribute access) + + # ========================================================================= + # Standard xArm Gripper + # ========================================================================= + + @rpc + def set_gripper_enable(self, enable: int) -> tuple[int, str]: + """Enable/disable gripper.""" + try: + code = self.arm.set_gripper_enable(enable) + return ( + code, + f"Gripper {'enabled' if enable else 'disabled'}" + if code == 0 + else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_gripper_mode(self, mode: int) -> tuple[int, str]: + """Set gripper mode (0=location mode, 1=speed mode, 2=current mode).""" + try: + code = self.arm.set_gripper_mode(mode) + return (code, f"Gripper mode set to {mode}" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_gripper_speed(self, speed: float) -> tuple[int, str]: + """Set gripper speed (r/min).""" + try: + code = self.arm.set_gripper_speed(speed) + return (code, f"Gripper speed set to {speed}" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_gripper_position( + self, + position: float, + wait: bool = False, + speed: float | None = None, + timeout: float | None = None, + ) -> tuple[int, str]: + """ + Set gripper position. + + Args: + position: Target position (0-850) + wait: Wait for completion + speed: Optional speed override + timeout: Optional timeout for wait + """ + try: + code = self.arm.set_gripper_position(position, wait=wait, speed=speed, timeout=timeout) + return ( + code, + f"Gripper position set to {position}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def get_gripper_position(self) -> tuple[int, float | None]: + """Get current gripper position.""" + try: + code, position = self.arm.get_gripper_position() + return (code, position if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def get_gripper_err_code(self) -> tuple[int, int | None]: + """Get gripper error code.""" + try: + code, err = self.arm.get_gripper_err_code() + return (code, err if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def clean_gripper_error(self) -> tuple[int, str]: + """Clear gripper error.""" + try: + code = self.arm.clean_gripper_error() + return (code, "Gripper error cleared" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Bio Gripper + # ========================================================================= + + @rpc + def set_bio_gripper_enable(self, enable: int, wait: bool = True) -> tuple[int, str]: + """Enable/disable bio gripper.""" + try: + code = self.arm.set_bio_gripper_enable(enable, wait=wait) + return ( + code, + f"Bio gripper {'enabled' if enable else 'disabled'}" + if code == 0 + else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_bio_gripper_speed(self, speed: int) -> tuple[int, str]: + """Set bio gripper speed (1-100).""" + try: + code = self.arm.set_bio_gripper_speed(speed) + return ( + code, + f"Bio gripper speed set to {speed}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def open_bio_gripper( + self, speed: int = 0, wait: bool = True, timeout: float = 5 + ) -> tuple[int, str]: + """Open bio gripper.""" + try: + code = self.arm.open_bio_gripper(speed=speed, wait=wait, timeout=timeout) + return (code, "Bio gripper opened" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def close_bio_gripper( + self, speed: int = 0, wait: bool = True, timeout: float = 5 + ) -> tuple[int, str]: + """Close bio gripper.""" + try: + code = self.arm.close_bio_gripper(speed=speed, wait=wait, timeout=timeout) + return (code, "Bio gripper closed" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def get_bio_gripper_status(self) -> tuple[int, int | None]: + """Get bio gripper status.""" + try: + code, status = self.arm.get_bio_gripper_status() + return (code, status if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def get_bio_gripper_error(self) -> tuple[int, int | None]: + """Get bio gripper error code.""" + try: + code, error = self.arm.get_bio_gripper_error() + return (code, error if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def clean_bio_gripper_error(self) -> tuple[int, str]: + """Clear bio gripper error.""" + try: + code = self.arm.clean_bio_gripper_error() + return (code, "Bio gripper error cleared" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Vacuum Gripper + # ========================================================================= + + @rpc + def set_vacuum_gripper(self, on: int) -> tuple[int, str]: + """Turn vacuum gripper on/off (0=off, 1=on).""" + try: + code = self.arm.set_vacuum_gripper(on) + return ( + code, + f"Vacuum gripper {'on' if on else 'off'}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def get_vacuum_gripper(self) -> tuple[int, int | None]: + """Get vacuum gripper state.""" + try: + code, state = self.arm.get_vacuum_gripper() + return (code, state if code == 0 else None) + except Exception: + return (-1, None) + + # ========================================================================= + # Robotiq Gripper + # ========================================================================= + + @rpc + def robotiq_reset(self) -> tuple[int, str]: + """Reset Robotiq gripper.""" + try: + code = self.arm.robotiq_reset() + return (code, "Robotiq gripper reset" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def robotiq_set_activate(self, wait: bool = True, timeout: float = 3) -> tuple[int, str]: + """Activate Robotiq gripper.""" + try: + code = self.arm.robotiq_set_activate(wait=wait, timeout=timeout) + return (code, "Robotiq gripper activated" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def robotiq_set_position( + self, + position: int, + speed: int = 0xFF, + force: int = 0xFF, + wait: bool = True, + timeout: float = 5, + ) -> tuple[int, str]: + """ + Set Robotiq gripper position. + + Args: + position: Target position (0-255, 0=open, 255=closed) + speed: Gripper speed (0-255) + force: Gripper force (0-255) + wait: Wait for completion + timeout: Timeout for wait + """ + try: + code = self.arm.robotiq_set_position( + position, speed=speed, force=force, wait=wait, timeout=timeout + ) + return ( + code, + f"Robotiq position set to {position}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def robotiq_open( + self, speed: int = 0xFF, force: int = 0xFF, wait: bool = True, timeout: float = 5 + ) -> tuple[int, str]: + """Open Robotiq gripper.""" + try: + code = self.arm.robotiq_open(speed=speed, force=force, wait=wait, timeout=timeout) + return (code, "Robotiq gripper opened" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def robotiq_close( + self, speed: int = 0xFF, force: int = 0xFF, wait: bool = True, timeout: float = 5 + ) -> tuple[int, str]: + """Close Robotiq gripper.""" + try: + code = self.arm.robotiq_close(speed=speed, force=force, wait=wait, timeout=timeout) + return (code, "Robotiq gripper closed" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def robotiq_get_status(self) -> tuple[int, dict[str, Any] | None]: + """Get Robotiq gripper status.""" + try: + ret = self.arm.robotiq_get_status() + if isinstance(ret, tuple) and len(ret) >= 2: + code = ret[0] + if code == 0: + # Return status as dict if successful + status = { + "gOBJ": ret[1] if len(ret) > 1 else None, # Object detection status + "gSTA": ret[2] if len(ret) > 2 else None, # Gripper status + "gGTO": ret[3] if len(ret) > 3 else None, # Go to requested position + "gACT": ret[4] if len(ret) > 4 else None, # Activation status + "kFLT": ret[5] if len(ret) > 5 else None, # Fault status + "gFLT": ret[6] if len(ret) > 6 else None, # Fault status + "gPR": ret[7] if len(ret) > 7 else None, # Requested position echo + "gPO": ret[8] if len(ret) > 8 else None, # Actual position + "gCU": ret[9] if len(ret) > 9 else None, # Current + } + return (code, status) + return (code, None) + return (-1, None) + except Exception as e: + logger.error(f"robotiq_get_status failed: {e}") + return (-1, None) + + # ========================================================================= + # Lite6 Gripper + # ========================================================================= + + @rpc + def open_lite6_gripper(self) -> tuple[int, str]: + """Open Lite6 gripper.""" + try: + code = self.arm.open_lite6_gripper() + return (code, "Lite6 gripper opened" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def close_lite6_gripper(self) -> tuple[int, str]: + """Close Lite6 gripper.""" + try: + code = self.arm.close_lite6_gripper() + return (code, "Lite6 gripper closed" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def stop_lite6_gripper(self) -> tuple[int, str]: + """Stop Lite6 gripper.""" + try: + code = self.arm.stop_lite6_gripper() + return (code, "Lite6 gripper stopped" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) diff --git a/dimos/hardware/manipulators/xarm/components/kinematics.py b/dimos/hardware/manipulators/xarm/components/kinematics.py new file mode 100644 index 0000000000..c29007a426 --- /dev/null +++ b/dimos/hardware/manipulators/xarm/components/kinematics.py @@ -0,0 +1,85 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Kinematics Component for XArmDriver. + +Provides RPC methods for kinematic calculations including: +- Forward kinematics +- Inverse kinematics +""" + +from typing import TYPE_CHECKING, Any + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from xarm.wrapper import XArmAPI + +logger = setup_logger() + + +class KinematicsComponent: + """ + Component providing kinematics RPC methods for XArmDriver. + + This component assumes the parent class has: + - self.arm: XArmAPI instance + - self.config: XArmDriverConfig instance + """ + + # Type hints for attributes expected from parent class + arm: "XArmAPI" + config: Any # Config dict accessed as object (dict with attribute access) + + @rpc + def get_inverse_kinematics(self, pose: list[float]) -> tuple[int, list[float] | None]: + """ + Compute inverse kinematics. + + Args: + pose: [x, y, z, roll, pitch, yaw] + + Returns: + Tuple of (code, joint_angles) + """ + try: + code, angles = self.arm.get_inverse_kinematics( + pose, input_is_radian=self.config.is_radian, return_is_radian=self.config.is_radian + ) + return (code, list(angles) if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def get_forward_kinematics(self, angles: list[float]) -> tuple[int, list[float] | None]: + """ + Compute forward kinematics. + + Args: + angles: Joint angles + + Returns: + Tuple of (code, pose) + """ + try: + code, pose = self.arm.get_forward_kinematics( + angles, + input_is_radian=self.config.is_radian, + return_is_radian=self.config.is_radian, + ) + return (code, list(pose) if code == 0 else None) + except Exception: + return (-1, None) diff --git a/dimos/hardware/manipulators/xarm/components/motion_control.py b/dimos/hardware/manipulators/xarm/components/motion_control.py new file mode 100644 index 0000000000..64aaa861e0 --- /dev/null +++ b/dimos/hardware/manipulators/xarm/components/motion_control.py @@ -0,0 +1,147 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Motion Control Component for XArmDriver. + +Provides RPC methods for motion control operations including: +- Joint position control +- Joint velocity control +- Cartesian position control +- Home positioning +""" + +import math +import threading +from typing import TYPE_CHECKING, Any + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from xarm.wrapper import XArmAPI + +logger = setup_logger() + + +class MotionControlComponent: + """ + Component providing motion control RPC methods for XArmDriver. + + This component assumes the parent class has: + - self.arm: XArmAPI instance + - self.config: XArmDriverConfig instance + - self._joint_cmd_lock: threading.Lock + - self._joint_cmd_: Optional[list[float]] + """ + + # Type hints for attributes expected from parent class + arm: "XArmAPI" + config: Any # Config dict accessed as object (dict with attribute access) + _joint_cmd_lock: threading.Lock + _joint_cmd_: list[float] | None + + @rpc + def set_joint_angles(self, angles: list[float]) -> tuple[int, str]: + """ + Set joint angles (RPC method). + + Args: + angles: List of joint angles (in radians if is_radian=True) + + Returns: + Tuple of (code, message) + """ + try: + code = self.arm.set_servo_angle_j(angles=angles, is_radian=self.config.is_radian) + msg = "Success" if code == 0 else f"Error code: {code}" + return (code, msg) + except Exception as e: + logger.error(f"set_joint_angles failed: {e}") + return (-1, str(e)) + + @rpc + def set_joint_velocities(self, velocities: list[float]) -> tuple[int, str]: + """ + Set joint velocities (RPC method). + Note: Requires velocity control mode. + + Args: + velocities: List of joint velocities (rad/s) + + Returns: + Tuple of (code, message) + """ + try: + # For velocity control, you would use vc_set_joint_velocity + # This requires mode 4 (joint velocity control) + code = self.arm.vc_set_joint_velocity( + speeds=velocities, is_radian=self.config.is_radian + ) + msg = "Success" if code == 0 else f"Error code: {code}" + return (code, msg) + except Exception as e: + logger.error(f"set_joint_velocities failed: {e}") + return (-1, str(e)) + + @rpc + def set_position(self, position: list[float], wait: bool = False) -> tuple[int, str]: + """ + Set TCP position [x, y, z, roll, pitch, yaw]. + + Args: + position: Target position + wait: Wait for motion to complete + + Returns: + Tuple of (code, message) + """ + try: + code = self.arm.set_position(*position, is_radian=self.config.is_radian, wait=wait) + return (code, "Success" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def move_gohome(self, wait: bool = False) -> tuple[int, str]: + """Move to home position.""" + try: + code = self.arm.move_gohome(wait=wait, is_radian=self.config.is_radian) + return (code, "Moving home" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_joint_command(self, positions: list[float]) -> tuple[int, str]: + """ + Manually set the joint command (for testing). + This updates the shared joint_cmd that the control loop reads. + + Args: + positions: List of joint positions in radians + + Returns: + Tuple of (code, message) + """ + try: + if len(positions) != self.config.num_joints: + return (-1, f"Expected {self.config.num_joints} positions, got {len(positions)}") + + with self._joint_cmd_lock: + self._joint_cmd_ = list(positions) + + logger.info(f"āœ“ Joint command set: {[f'{math.degrees(p):.2f}°' for p in positions]}") + return (0, "Joint command updated") + except Exception as e: + return (-1, str(e)) diff --git a/dimos/hardware/manipulators/xarm/components/state_queries.py b/dimos/hardware/manipulators/xarm/components/state_queries.py new file mode 100644 index 0000000000..5615763cc4 --- /dev/null +++ b/dimos/hardware/manipulators/xarm/components/state_queries.py @@ -0,0 +1,185 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +State Query Component for XArmDriver. + +Provides RPC methods for querying robot state including: +- Joint state +- Robot state +- Cartesian position +- Firmware version +""" + +import threading +from typing import TYPE_CHECKING, Any + +from dimos.core import rpc +from dimos.msgs.sensor_msgs import JointState, RobotState +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from xarm.wrapper import XArmAPI + +logger = setup_logger() + + +class StateQueryComponent: + """ + Component providing state query RPC methods for XArmDriver. + + This component assumes the parent class has: + - self.arm: XArmAPI instance + - self.config: XArmDriverConfig instance + - self._joint_state_lock: threading.Lock + - self._joint_states_: Optional[JointState] + - self._robot_state_: Optional[RobotState] + """ + + # Type hints for attributes expected from parent class + arm: "XArmAPI" + config: Any # Config dict accessed as object (dict with attribute access) + _joint_state_lock: threading.Lock + _joint_states_: JointState | None + _robot_state_: RobotState | None + + @rpc + def get_joint_state(self) -> JointState | None: + """ + Get the current joint state (RPC method). + + Returns: + Current JointState or None + """ + with self._joint_state_lock: + return self._joint_states_ + + @rpc + def get_robot_state(self) -> RobotState | None: + """ + Get the current robot state (RPC method). + + Returns: + Current RobotState or None + """ + with self._joint_state_lock: + return self._robot_state_ + + @rpc + def get_position(self) -> tuple[int, list[float] | None]: + """ + Get TCP position [x, y, z, roll, pitch, yaw]. + + Returns: + Tuple of (code, position) + """ + try: + code, position = self.arm.get_position(is_radian=self.config.is_radian) + return (code, list(position) if code == 0 else None) + except Exception as e: + logger.error(f"get_position failed: {e}") + return (-1, None) + + @rpc + def get_version(self) -> tuple[int, str | None]: + """Get firmware version.""" + try: + code, version = self.arm.get_version() + return (code, version if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def get_servo_angle(self) -> tuple[int, list[float] | None]: + """Get joint angles.""" + try: + code, angles = self.arm.get_servo_angle(is_radian=self.config.is_radian) + return (code, list(angles) if code == 0 else None) + except Exception as e: + logger.error(f"get_servo_angle failed: {e}") + return (-1, None) + + @rpc + def get_position_aa(self) -> tuple[int, list[float] | None]: + """Get TCP position in axis-angle format.""" + try: + code, position = self.arm.get_position_aa(is_radian=self.config.is_radian) + return (code, list(position) if code == 0 else None) + except Exception as e: + logger.error(f"get_position_aa failed: {e}") + return (-1, None) + + # ========================================================================= + # Robot State Queries + # ========================================================================= + + @rpc + def get_state(self) -> tuple[int, int | None]: + """Get robot state (0=ready, 3=pause, 4=stop).""" + try: + code, state = self.arm.get_state() + return (code, state if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def get_cmdnum(self) -> tuple[int, int | None]: + """Get command queue length.""" + try: + code, cmdnum = self.arm.get_cmdnum() + return (code, cmdnum if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def get_err_warn_code(self) -> tuple[int, list[int] | None]: + """Get error and warning codes.""" + try: + err_warn = [0, 0] + code = self.arm.get_err_warn_code(err_warn) + return (code, err_warn if code == 0 else None) + except Exception: + return (-1, None) + + # ========================================================================= + # Force/Torque Sensor Queries + # ========================================================================= + + @rpc + def get_ft_sensor_data(self) -> tuple[int, list[float] | None]: + """Get force/torque sensor data [fx, fy, fz, tx, ty, tz].""" + try: + code, ft_data = self.arm.get_ft_sensor_data() + return (code, list(ft_data) if code == 0 else None) + except Exception as e: + logger.error(f"get_ft_sensor_data failed: {e}") + return (-1, None) + + @rpc + def get_ft_sensor_error(self) -> tuple[int, int | None]: + """Get FT sensor error code.""" + try: + code, error = self.arm.get_ft_sensor_error() + return (code, error if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def get_ft_sensor_mode(self) -> tuple[int, int | None]: + """Get FT sensor application mode.""" + try: + code, mode = self.arm.get_ft_sensor_app_get() + return (code, mode if code == 0 else None) + except Exception: + return (-1, None) diff --git a/dimos/hardware/manipulators/xarm/components/system_control.py b/dimos/hardware/manipulators/xarm/components/system_control.py new file mode 100644 index 0000000000..a04e9a94a0 --- /dev/null +++ b/dimos/hardware/manipulators/xarm/components/system_control.py @@ -0,0 +1,555 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +System Control Component for XArmDriver. + +Provides RPC methods for system-level control operations including: +- Mode control (servo, velocity) +- State management +- Error handling +- Emergency stop +""" + +from typing import TYPE_CHECKING, Any, Protocol + +from dimos.core import rpc +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from xarm.wrapper import XArmAPI + + class XArmConfig(Protocol): + """Protocol for XArm configuration.""" + + is_radian: bool + velocity_control: bool + + +logger = setup_logger() + + +class SystemControlComponent: + """ + Component providing system control RPC methods for XArmDriver. + + This component assumes the parent class has: + - self.arm: XArmAPI instance + - self.config: XArmDriverConfig instance + """ + + # Type hints for attributes expected from parent class + arm: "XArmAPI" + config: Any # Should be XArmConfig but accessed as dict + + @rpc + def enable_servo_mode(self) -> tuple[int, str]: + """ + Enable servo mode (mode 1). + Required for set_servo_angle_j to work. + + Returns: + Tuple of (code, message) + """ + try: + code = self.arm.set_mode(1) + if code == 0: + logger.info("Servo mode enabled") + return (code, "Servo mode enabled") + else: + logger.warning(f"Failed to enable servo mode: code={code}") + return (code, f"Error code: {code}") + except Exception as e: + logger.error(f"enable_servo_mode failed: {e}") + return (-1, str(e)) + + @rpc + def disable_servo_mode(self) -> tuple[int, str]: + """ + Disable servo mode (set to position mode). + + Returns: + Tuple of (code, message) + """ + try: + code = self.arm.set_mode(0) + if code == 0: + logger.info("Servo mode disabled (position mode)") + return (code, "Position mode enabled") + else: + logger.warning(f"Failed to disable servo mode: code={code}") + return (code, f"Error code: {code}") + except Exception as e: + logger.error(f"disable_servo_mode failed: {e}") + return (-1, str(e)) + + @rpc + def enable_velocity_control_mode(self) -> tuple[int, str]: + """ + Enable velocity control mode (mode 4). + Required for vc_set_joint_velocity to work. + + Returns: + Tuple of (code, message) + """ + try: + # IMPORTANT: Set config flag BEFORE changing robot mode + # This prevents control loop from sending wrong command type during transition + self.config.velocity_control = True + + # Step 1: Set mode to 4 (velocity control) + code = self.arm.set_mode(4) + if code != 0: + logger.warning(f"Failed to set mode to 4: code={code}") + self.config.velocity_control = False # Revert on failure + return (code, f"Failed to set mode: code={code}") + + # Step 2: Set state to 0 (ready/sport mode) - this activates the mode! + code = self.arm.set_state(0) + if code == 0: + logger.info("Velocity control mode enabled (mode=4, state=0)") + return (code, "Velocity control mode enabled") + else: + logger.warning(f"Failed to set state to 0: code={code}") + self.config.velocity_control = False # Revert on failure + return (code, f"Failed to set state: code={code}") + except Exception as e: + logger.error(f"enable_velocity_control_mode failed: {e}") + self.config.velocity_control = False # Revert on exception + return (-1, str(e)) + + @rpc + def disable_velocity_control_mode(self) -> tuple[int, str]: + """ + Disable velocity control mode and return to position control (mode 1). + + Returns: + Tuple of (code, message) + """ + try: + # IMPORTANT: Set config flag BEFORE changing robot mode + # This prevents control loop from sending velocity commands after mode change + self.config.velocity_control = False + + # Step 1: Clear any errors that may have occurred + self.arm.clean_error() + self.arm.clean_warn() + + # Step 2: Set mode to 1 (servo/position control) + code = self.arm.set_mode(1) + if code != 0: + logger.warning(f"Failed to set mode to 1: code={code}") + self.config.velocity_control = True # Revert on failure + return (code, f"Failed to set mode: code={code}") + + # Step 3: Set state to 0 (ready) - CRITICAL for accepting new commands + code = self.arm.set_state(0) + if code == 0: + logger.info("Position control mode enabled (state=0, mode=1)") + return (code, "Position control mode enabled") + else: + logger.warning(f"Failed to set state to 0: code={code}") + self.config.velocity_control = True # Revert on failure + return (code, f"Failed to set state: code={code}") + except Exception as e: + logger.error(f"disable_velocity_control_mode failed: {e}") + self.config.velocity_control = True # Revert on exception + return (-1, str(e)) + + @rpc + def motion_enable(self, enable: bool = True) -> tuple[int, str]: + """Enable or disable arm motion.""" + try: + code = self.arm.motion_enable(enable=enable) + msg = f"Motion {'enabled' if enable else 'disabled'}" + return (code, msg if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_state(self, state: int) -> tuple[int, str]: + """ + Set robot state. + + Args: + state: 0=ready, 3=pause, 4=stop + """ + try: + code = self.arm.set_state(state=state) + return (code, "Success" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def clean_error(self) -> tuple[int, str]: + """Clear error codes.""" + try: + code = self.arm.clean_error() + return (code, "Errors cleared" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def clean_warn(self) -> tuple[int, str]: + """Clear warning codes.""" + try: + code = self.arm.clean_warn() + return (code, "Warnings cleared" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def emergency_stop(self) -> tuple[int, str]: + """Emergency stop the arm.""" + try: + code = self.arm.emergency_stop() + return (code, "Emergency stop" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Configuration & Persistence + # ========================================================================= + + @rpc + def clean_conf(self) -> tuple[int, str]: + """Clean configuration.""" + try: + code = self.arm.clean_conf() + return (code, "Configuration cleaned" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def save_conf(self) -> tuple[int, str]: + """Save current configuration to robot.""" + try: + code = self.arm.save_conf() + return (code, "Configuration saved" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def reload_dynamics(self) -> tuple[int, str]: + """Reload dynamics parameters.""" + try: + code = self.arm.reload_dynamics() + return (code, "Dynamics reloaded" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Mode & State Control + # ========================================================================= + + @rpc + def set_mode(self, mode: int) -> tuple[int, str]: + """ + Set control mode. + + Args: + mode: 0=position, 1=servo, 4=velocity, etc. + """ + try: + code = self.arm.set_mode(mode) + return (code, f"Mode set to {mode}" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Collision & Safety + # ========================================================================= + + @rpc + def set_collision_sensitivity(self, sensitivity: int) -> tuple[int, str]: + """Set collision sensitivity (0-5, 0=least sensitive).""" + try: + code = self.arm.set_collision_sensitivity(sensitivity) + return ( + code, + f"Collision sensitivity set to {sensitivity}" + if code == 0 + else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_teach_sensitivity(self, sensitivity: int) -> tuple[int, str]: + """Set teach sensitivity (1-5).""" + try: + code = self.arm.set_teach_sensitivity(sensitivity) + return ( + code, + f"Teach sensitivity set to {sensitivity}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_collision_rebound(self, enable: int) -> tuple[int, str]: + """Enable/disable collision rebound (0=disable, 1=enable).""" + try: + code = self.arm.set_collision_rebound(enable) + return ( + code, + f"Collision rebound {'enabled' if enable else 'disabled'}" + if code == 0 + else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_self_collision_detection(self, enable: int) -> tuple[int, str]: + """Enable/disable self collision detection.""" + try: + code = self.arm.set_self_collision_detection(enable) + return ( + code, + f"Self collision detection {'enabled' if enable else 'disabled'}" + if code == 0 + else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Reduced Mode & Boundaries + # ========================================================================= + + @rpc + def set_reduced_mode(self, enable: int) -> tuple[int, str]: + """Enable/disable reduced mode.""" + try: + code = self.arm.set_reduced_mode(enable) + return ( + code, + f"Reduced mode {'enabled' if enable else 'disabled'}" + if code == 0 + else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_reduced_max_tcp_speed(self, speed: float) -> tuple[int, str]: + """Set maximum TCP speed in reduced mode.""" + try: + code = self.arm.set_reduced_max_tcp_speed(speed) + return ( + code, + f"Reduced max TCP speed set to {speed}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_reduced_max_joint_speed(self, speed: float) -> tuple[int, str]: + """Set maximum joint speed in reduced mode.""" + try: + code = self.arm.set_reduced_max_joint_speed(speed) + return ( + code, + f"Reduced max joint speed set to {speed}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_fence_mode(self, enable: int) -> tuple[int, str]: + """Enable/disable fence mode.""" + try: + code = self.arm.set_fence_mode(enable) + return ( + code, + f"Fence mode {'enabled' if enable else 'disabled'}" + if code == 0 + else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # TCP & Dynamics Configuration + # ========================================================================= + + @rpc + def set_tcp_offset(self, offset: list[float]) -> tuple[int, str]: + """Set TCP offset [x, y, z, roll, pitch, yaw].""" + try: + code = self.arm.set_tcp_offset(offset) + return (code, "TCP offset set" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_tcp_load(self, weight: float, center_of_gravity: list[float]) -> tuple[int, str]: + """Set TCP load (payload).""" + try: + code = self.arm.set_tcp_load(weight, center_of_gravity) + return (code, f"TCP load set: {weight}kg" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_gravity_direction(self, direction: list[float]) -> tuple[int, str]: + """Set gravity direction vector.""" + try: + code = self.arm.set_gravity_direction(direction) + return (code, "Gravity direction set" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_world_offset(self, offset: list[float]) -> tuple[int, str]: + """Set world coordinate offset.""" + try: + code = self.arm.set_world_offset(offset) + return (code, "World offset set" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Motion Parameters + # ========================================================================= + + @rpc + def set_tcp_jerk(self, jerk: float) -> tuple[int, str]: + """Set TCP jerk (mm/s³).""" + try: + code = self.arm.set_tcp_jerk(jerk) + return (code, f"TCP jerk set to {jerk}" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_tcp_maxacc(self, acc: float) -> tuple[int, str]: + """Set TCP maximum acceleration (mm/s²).""" + try: + code = self.arm.set_tcp_maxacc(acc) + return ( + code, + f"TCP max acceleration set to {acc}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_joint_jerk(self, jerk: float) -> tuple[int, str]: + """Set joint jerk (rad/s³ or °/s³).""" + try: + code = self.arm.set_joint_jerk(jerk, is_radian=self.config.is_radian) + return (code, f"Joint jerk set to {jerk}" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + @rpc + def set_joint_maxacc(self, acc: float) -> tuple[int, str]: + """Set joint maximum acceleration (rad/s² or °/s²).""" + try: + code = self.arm.set_joint_maxacc(acc, is_radian=self.config.is_radian) + return ( + code, + f"Joint max acceleration set to {acc}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) + + @rpc + def set_pause_time(self, seconds: float) -> tuple[int, str]: + """Set pause time for motion commands.""" + try: + code = self.arm.set_pause_time(seconds) + return (code, f"Pause time set to {seconds}s" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Digital I/O (Tool GPIO) + # ========================================================================= + + @rpc + def get_tgpio_digital(self, io_num: int) -> tuple[int, int | None]: + """Get tool GPIO digital input value.""" + try: + code, value = self.arm.get_tgpio_digital(io_num) + return (code, value if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def set_tgpio_digital(self, io_num: int, value: int) -> tuple[int, str]: + """Set tool GPIO digital output value (0 or 1).""" + try: + code = self.arm.set_tgpio_digital(io_num, value) + return (code, f"TGPIO {io_num} set to {value}" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Digital I/O (Controller GPIO) + # ========================================================================= + + @rpc + def get_cgpio_digital(self, io_num: int) -> tuple[int, int | None]: + """Get controller GPIO digital input value.""" + try: + code, value = self.arm.get_cgpio_digital(io_num) + return (code, value if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def set_cgpio_digital(self, io_num: int, value: int) -> tuple[int, str]: + """Set controller GPIO digital output value (0 or 1).""" + try: + code = self.arm.set_cgpio_digital(io_num, value) + return (code, f"CGPIO {io_num} set to {value}" if code == 0 else f"Error code: {code}") + except Exception as e: + return (-1, str(e)) + + # ========================================================================= + # Analog I/O + # ========================================================================= + + @rpc + def get_tgpio_analog(self, io_num: int) -> tuple[int, float | None]: + """Get tool GPIO analog input value.""" + try: + code, value = self.arm.get_tgpio_analog(io_num) + return (code, value if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def get_cgpio_analog(self, io_num: int) -> tuple[int, float | None]: + """Get controller GPIO analog input value.""" + try: + code, value = self.arm.get_cgpio_analog(io_num) + return (code, value if code == 0 else None) + except Exception: + return (-1, None) + + @rpc + def set_cgpio_analog(self, io_num: int, value: float) -> tuple[int, str]: + """Set controller GPIO analog output value.""" + try: + code = self.arm.set_cgpio_analog(io_num, value) + return ( + code, + f"CGPIO analog {io_num} set to {value}" if code == 0 else f"Error code: {code}", + ) + except Exception as e: + return (-1, str(e)) diff --git a/dimos/hardware/manipulators/xarm/spec.py b/dimos/hardware/manipulators/xarm/spec.py new file mode 100644 index 0000000000..625f036a0b --- /dev/null +++ b/dimos/hardware/manipulators/xarm/spec.py @@ -0,0 +1,63 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Protocol + +from dimos.core import In, Out +from dimos.msgs.geometry_msgs import WrenchStamped +from dimos.msgs.sensor_msgs import JointCommand, JointState + + +@dataclass +class RobotState: + """Custom message containing full robot state (deprecated - use RobotStateMsg).""" + + state: int = 0 # Robot state (0: ready, 3: paused, 4: stopped, etc.) + mode: int = 0 # Control mode (0: position, 1: servo, 4: joint velocity, 5: cartesian velocity) + error_code: int = 0 # Error code + warn_code: int = 0 # Warning code + cmdnum: int = 0 # Command queue length + mt_brake: int = 0 # Motor brake state + mt_able: int = 0 # Motor enable state + + +class ArmDriverSpec(Protocol): + """Protocol specification for xArm manipulator driver. + + Compatible with xArm5, xArm6, and xArm7 models. + """ + + # Input topics (commands) + joint_position_command: In[JointCommand] # Desired joint positions (radians) + joint_velocity_command: In[JointCommand] # Desired joint velocities (rad/s) + + # Output topics + joint_state: Out[JointState] # Current joint positions, velocities, and efforts + robot_state: Out[RobotState] # Full robot state (errors, modes, etc.) + ft_ext: Out[WrenchStamped] # External force/torque (compensated) + ft_raw: Out[WrenchStamped] # Raw force/torque sensor data + + # RPC Methods + def set_joint_angles(self, angles: list[float]) -> tuple[int, str]: ... + + def set_joint_velocities(self, velocities: list[float]) -> tuple[int, str]: ... + + def get_joint_state(self) -> JointState: ... + + def get_robot_state(self) -> RobotState: ... + + def enable_servo_mode(self) -> tuple[int, str]: ... + + def disable_servo_mode(self) -> tuple[int, str]: ... diff --git a/dimos/hardware/manipulators/xarm/xarm_blueprints.py b/dimos/hardware/manipulators/xarm/xarm_blueprints.py new file mode 100644 index 0000000000..4e84c9c991 --- /dev/null +++ b/dimos/hardware/manipulators/xarm/xarm_blueprints.py @@ -0,0 +1,260 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Blueprints for xArm manipulator control using component-based architecture. + +This module provides declarative blueprints for configuring xArm with the new +generalized component-based driver architecture. + +Usage: + # Run via CLI: + dimos run xarm-servo # Driver only + dimos run xarm-trajectory # Driver + Joint trajectory controller + dimos run xarm-cartesian # Driver + Cartesian motion controller + + # Or programmatically: + from dimos.hardware.manipulators.xarm.xarm_blueprints import xarm_trajectory + coordinator = xarm_trajectory.build() + coordinator.loop() +""" + +from typing import Any + +from dimos.core.blueprints import autoconnect +from dimos.core.transport import LCMTransport +from dimos.hardware.manipulators.xarm.xarm_driver import xarm_driver as xarm_driver_blueprint +from dimos.manipulation.control import cartesian_motion_controller, joint_trajectory_controller +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.sensor_msgs import ( + JointCommand, + JointState, + RobotState, +) +from dimos.msgs.trajectory_msgs import JointTrajectory + + +# Create a blueprint wrapper for the component-based driver +def xarm_driver(**config: Any) -> Any: + """Create a blueprint for XArmDriver. + + Args: + **config: Configuration parameters passed to XArmDriver + - ip: IP address of XArm controller (default: "192.168.1.210") + - dof: Degrees of freedom - 5, 6, or 7 (default: 6) + - has_gripper: Whether gripper is attached (default: False) + - has_force_torque: Whether F/T sensor is attached (default: False) + - control_rate: Control loop + joint feedback rate in Hz (default: 100) + - monitor_rate: Robot state monitoring rate in Hz (default: 10) + + Returns: + Blueprint configuration for XArmDriver + """ + # Set defaults + config.setdefault("ip", "192.168.1.210") + config.setdefault("dof", 6) + config.setdefault("has_gripper", False) + config.setdefault("has_force_torque", False) + config.setdefault("control_rate", 100) + config.setdefault("monitor_rate", 10) + + # Return the xarm_driver blueprint with the config + return xarm_driver_blueprint(**config) + + +# ============================================================================= +# xArm6 Servo Control Blueprint +# ============================================================================= +# XArmDriver configured for servo control mode using component-based architecture. +# Publishes joint states and robot state, listens for joint commands. +# ============================================================================= + +xarm_servo = xarm_driver( + ip="192.168.1.210", + dof=6, # XArm6 + has_gripper=False, + has_force_torque=False, + control_rate=100, + monitor_rate=10, +).transports( + { + # Joint state feedback (position, velocity, effort) + ("joint_state", JointState): LCMTransport("/xarm/joint_states", JointState), + # Robot state feedback (mode, state, errors) + ("robot_state", RobotState): LCMTransport("/xarm/robot_state", RobotState), + # Position commands input + ("joint_position_command", JointCommand): LCMTransport( + "/xarm/joint_position_command", JointCommand + ), + # Velocity commands input + ("joint_velocity_command", JointCommand): LCMTransport( + "/xarm/joint_velocity_command", JointCommand + ), + } +) + +# ============================================================================= +# xArm7 Servo Control Blueprint +# ============================================================================= + +xarm7_servo = xarm_driver( + ip="192.168.1.210", + dof=7, # XArm7 + has_gripper=False, + has_force_torque=False, + control_rate=100, + monitor_rate=10, +).transports( + { + ("joint_state", JointState): LCMTransport("/xarm/joint_states", JointState), + ("robot_state", RobotState): LCMTransport("/xarm/robot_state", RobotState), + ("joint_position_command", JointCommand): LCMTransport( + "/xarm/joint_position_command", JointCommand + ), + ("joint_velocity_command", JointCommand): LCMTransport( + "/xarm/joint_velocity_command", JointCommand + ), + } +) + +# ============================================================================= +# xArm5 Servo Control Blueprint +# ============================================================================= + +xarm5_servo = xarm_driver( + ip="192.168.1.210", + dof=5, # XArm5 + has_gripper=False, + has_force_torque=False, + control_rate=100, + monitor_rate=10, +).transports( + { + ("joint_state", JointState): LCMTransport("/xarm/joint_states", JointState), + ("robot_state", RobotState): LCMTransport("/xarm/robot_state", RobotState), + ("joint_position_command", JointCommand): LCMTransport( + "/xarm/joint_position_command", JointCommand + ), + ("joint_velocity_command", JointCommand): LCMTransport( + "/xarm/joint_velocity_command", JointCommand + ), + } +) + +# ============================================================================= +# xArm Trajectory Control Blueprint (Driver + Trajectory Controller) +# ============================================================================= +# Combines XArmDriver with JointTrajectoryController for trajectory execution. +# The controller receives JointTrajectory messages and executes them at 100Hz. +# ============================================================================= + +xarm_trajectory = autoconnect( + xarm_driver( + ip="192.168.1.210", + dof=6, # XArm6 + has_gripper=False, + has_force_torque=False, + control_rate=500, + monitor_rate=10, + ), + joint_trajectory_controller( + control_frequency=100.0, + ), +).transports( + { + # Shared topics between driver and controller + ("joint_state", JointState): LCMTransport("/xarm/joint_states", JointState), + ("robot_state", RobotState): LCMTransport("/xarm/robot_state", RobotState), + ("joint_position_command", JointCommand): LCMTransport( + "/xarm/joint_position_command", JointCommand + ), + # Trajectory input topic + ("trajectory", JointTrajectory): LCMTransport("/trajectory", JointTrajectory), + } +) + +# ============================================================================= +# xArm7 Trajectory Control Blueprint +# ============================================================================= + +xarm7_trajectory = autoconnect( + xarm_driver( + ip="192.168.1.210", + dof=7, # XArm7 + has_gripper=False, + has_force_torque=False, + control_rate=100, + monitor_rate=10, + ), + joint_trajectory_controller( + control_frequency=100.0, + ), +).transports( + { + ("joint_state", JointState): LCMTransport("/xarm/joint_states", JointState), + ("robot_state", RobotState): LCMTransport("/xarm/robot_state", RobotState), + ("joint_position_command", JointCommand): LCMTransport( + "/xarm/joint_position_command", JointCommand + ), + ("trajectory", JointTrajectory): LCMTransport("/trajectory", JointTrajectory), + } +) + +# ============================================================================= +# xArm Cartesian Control Blueprint (Driver + Controller) +# ============================================================================= +# Combines XArmDriver with CartesianMotionController for Cartesian space control. +# The controller receives target_pose and converts to joint commands via IK. +# ============================================================================= + +xarm_cartesian = autoconnect( + xarm_driver( + ip="192.168.1.210", + dof=6, # XArm6 + has_gripper=False, + has_force_torque=False, + control_rate=100, + monitor_rate=10, + ), + cartesian_motion_controller( + control_frequency=20.0, + position_kp=5.0, + position_ki=0.0, + position_kd=0.1, + max_linear_velocity=0.2, + max_angular_velocity=1.0, + ), +).transports( + { + # Shared topics between driver and controller + ("joint_state", JointState): LCMTransport("/xarm/joint_states", JointState), + ("robot_state", RobotState): LCMTransport("/xarm/robot_state", RobotState), + ("joint_position_command", JointCommand): LCMTransport( + "/xarm/joint_position_command", JointCommand + ), + # Controller-specific topics + ("target_pose", PoseStamped): LCMTransport("/target_pose", PoseStamped), + ("current_pose", PoseStamped): LCMTransport("/xarm/current_pose", PoseStamped), + } +) + + +__all__ = [ + "xarm5_servo", + "xarm7_servo", + "xarm7_trajectory", + "xarm_cartesian", + "xarm_servo", + "xarm_trajectory", +] diff --git a/dimos/hardware/manipulators/xarm/xarm_driver.py b/dimos/hardware/manipulators/xarm/xarm_driver.py new file mode 100644 index 0000000000..f6d950938c --- /dev/null +++ b/dimos/hardware/manipulators/xarm/xarm_driver.py @@ -0,0 +1,174 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""XArm driver using the generalized component-based architecture.""" + +import logging +from typing import Any + +from dimos.hardware.manipulators.base import ( + BaseManipulatorDriver, + StandardMotionComponent, + StandardServoComponent, + StandardStatusComponent, +) + +from .xarm_wrapper import XArmSDKWrapper + +logger = logging.getLogger(__name__) + + +class XArmDriver(BaseManipulatorDriver): + """XArm driver using component-based architecture. + + This driver supports XArm5, XArm6, and XArm7 models. + All the complex logic is handled by the base class and standard components. + This file just assembles the pieces. + """ + + def __init__(self, **kwargs: Any) -> None: + """Initialize the XArm driver. + + Args: + **kwargs: Arguments for Module initialization. + Driver configuration can be passed via 'config' keyword arg: + - ip: IP address of the XArm controller + - dof: Degrees of freedom (5, 6, or 7) + - has_gripper: Whether gripper is attached + - has_force_torque: Whether F/T sensor is attached + """ + # Extract driver-specific config from kwargs + config: dict[str, Any] = kwargs.pop("config", {}) + + # Extract driver-specific params that might be passed directly + driver_params = [ + "ip", + "dof", + "has_gripper", + "has_force_torque", + "control_rate", + "monitor_rate", + ] + for param in driver_params: + if param in kwargs: + config[param] = kwargs.pop(param) + + logger.info(f"Initializing XArmDriver with config: {config}") + + # Create SDK wrapper + sdk = XArmSDKWrapper() + + # Create standard components + components = [ + StandardMotionComponent(sdk), + StandardServoComponent(sdk), + StandardStatusComponent(sdk), + ] + + # Optional: Add gripper component if configured + # if config.get('has_gripper', False): + # from dimos.hardware.manipulators.base.components import StandardGripperComponent + # components.append(StandardGripperComponent(sdk)) + + # Optional: Add force/torque component if configured + # if config.get('has_force_torque', False): + # from dimos.hardware.manipulators.base.components import StandardForceTorqueComponent + # components.append(StandardForceTorqueComponent(sdk)) + + # Remove any kwargs that would conflict with explicit arguments + kwargs.pop("sdk", None) + kwargs.pop("components", None) + kwargs.pop("name", None) + + # Initialize base driver with SDK and components + super().__init__(sdk=sdk, components=components, config=config, name="XArmDriver", **kwargs) + + logger.info("XArmDriver initialized successfully") + + +# Blueprint configuration for the driver +def get_blueprint() -> dict[str, Any]: + """Get the blueprint configuration for the XArm driver. + + Returns: + Dictionary with blueprint configuration + """ + return { + "name": "XArmDriver", + "class": XArmDriver, + "config": { + "ip": "192.168.1.210", # Default IP + "dof": 7, # Default to 7-DOF + "has_gripper": False, + "has_force_torque": False, + "control_rate": 100, # Hz - control loop + joint feedback + "monitor_rate": 10, # Hz - robot state monitoring + }, + "inputs": { + "joint_position_command": "JointCommand", + "joint_velocity_command": "JointCommand", + }, + "outputs": { + "joint_state": "JointState", + "robot_state": "RobotState", + }, + "rpc_methods": [ + # Motion control + "move_joint", + "move_joint_velocity", + "move_joint_effort", + "stop_motion", + "get_joint_state", + "get_joint_limits", + "get_velocity_limits", + "set_velocity_scale", + "set_acceleration_scale", + "move_cartesian", + "get_cartesian_state", + "execute_trajectory", + "stop_trajectory", + # Servo control + "enable_servo", + "disable_servo", + "toggle_servo", + "get_servo_state", + "emergency_stop", + "reset_emergency_stop", + "set_control_mode", + "get_control_mode", + "clear_errors", + "reset_fault", + "home_robot", + "brake_release", + "brake_engage", + # Status monitoring + "get_robot_state", + "get_system_info", + "get_capabilities", + "get_error_state", + "get_health_metrics", + "get_statistics", + "check_connection", + "get_force_torque", + "zero_force_torque", + "get_digital_inputs", + "set_digital_outputs", + "get_analog_inputs", + "get_gripper_state", + ], + } + + +# Expose blueprint for declarative composition (compatible with dimos framework) +xarm_driver = XArmDriver.blueprint diff --git a/dimos/hardware/manipulators/xarm/xarm_wrapper.py b/dimos/hardware/manipulators/xarm/xarm_wrapper.py new file mode 100644 index 0000000000..a743c0e3c7 --- /dev/null +++ b/dimos/hardware/manipulators/xarm/xarm_wrapper.py @@ -0,0 +1,564 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""XArm SDK wrapper implementation.""" + +import logging +import math +from typing import Any + +from ..base.sdk_interface import BaseManipulatorSDK, ManipulatorInfo + + +class XArmSDKWrapper(BaseManipulatorSDK): + """SDK wrapper for XArm manipulators. + + This wrapper translates XArm's native SDK (which uses degrees and mm) + to our standard interface (radians and meters). + """ + + def __init__(self) -> None: + """Initialize the XArm SDK wrapper.""" + self.logger = logging.getLogger(self.__class__.__name__) + self.native_sdk: Any = None + self.dof = 7 # Default, will be updated on connect + self._connected = False + + # ============= Connection Management ============= + + def connect(self, config: dict[str, Any]) -> bool: + """Connect to XArm controller. + + Args: + config: Configuration with 'ip' and optionally 'dof' (5, 6, or 7) + + Returns: + True if connection successful + """ + try: + from xarm import XArmAPI + + ip = config.get("ip", "192.168.1.100") + self.dof = config.get("dof", 7) + + self.logger.info(f"Connecting to XArm at {ip} (DOF: {self.dof})...") + + # Create XArm API instance + # XArm SDK uses degrees by default, we'll convert to radians + self.native_sdk = XArmAPI(ip, is_radian=False) + + # Check connection + if self.native_sdk.connected: + # Initialize XArm + self.native_sdk.motion_enable(True) + self.native_sdk.set_mode(1) # Servo mode for high-frequency control + self.native_sdk.set_state(0) # Ready state + + self._connected = True + self.logger.info( + f"Successfully connected to XArm (version: {self.native_sdk.version})" + ) + return True + else: + self.logger.error("Failed to connect to XArm") + return False + + except ImportError: + self.logger.error("XArm SDK not installed. Please install: pip install xArm-Python-SDK") + return False + except Exception as e: + self.logger.error(f"Connection failed: {e}") + return False + + def disconnect(self) -> None: + """Disconnect from XArm controller.""" + if self.native_sdk: + try: + self.native_sdk.disconnect() + self._connected = False + self.logger.info("Disconnected from XArm") + except: + pass + finally: + self.native_sdk = None + + def is_connected(self) -> bool: + """Check if connected to XArm. + + Returns: + True if connected + """ + return self._connected and self.native_sdk and self.native_sdk.connected + + # ============= Joint State Query ============= + + def get_joint_positions(self) -> list[float]: + """Get current joint positions. + + Returns: + Joint positions in RADIANS + """ + code, angles = self.native_sdk.get_servo_angle() + if code != 0: + raise RuntimeError(f"XArm error getting positions: {code}") + + # Convert degrees to radians + positions = [math.radians(angle) for angle in angles[: self.dof]] + return positions + + def get_joint_velocities(self) -> list[float]: + """Get current joint velocities. + + Returns: + Joint velocities in RAD/S + """ + # XArm doesn't directly provide velocities in older versions + # Try to get from realtime data if available + if hasattr(self.native_sdk, "get_joint_speeds"): + code, speeds = self.native_sdk.get_joint_speeds() + if code == 0: + # Convert deg/s to rad/s + return [math.radians(speed) for speed in speeds[: self.dof]] + + # Return zeros if not available + return [0.0] * self.dof + + def get_joint_efforts(self) -> list[float]: + """Get current joint efforts/torques. + + Returns: + Joint efforts in Nm + """ + # Try to get joint torques + if hasattr(self.native_sdk, "get_joint_torques"): + code, torques = self.native_sdk.get_joint_torques() + if code == 0: + return list(torques[: self.dof]) + + # Return zeros if not available + return [0.0] * self.dof + + # ============= Joint Motion Control ============= + + def set_joint_positions( + self, + positions: list[float], + _velocity: float = 1.0, + _acceleration: float = 1.0, + _wait: bool = False, + ) -> bool: + """Move joints to target positions using servo mode. + + Args: + positions: Target positions in RADIANS + _velocity: UNUSED in servo mode (kept for interface compatibility) + _acceleration: UNUSED in servo mode (kept for interface compatibility) + _wait: UNUSED in servo mode (kept for interface compatibility) + + Returns: + True if command accepted + """ + # Convert radians to degrees + degrees = [math.degrees(pos) for pos in positions] + + # Use set_servo_angle_j for high-frequency servo control (100Hz+) + # This sends immediate position commands without trajectory planning + # Requires mode 1 (servo mode) and executes only the last instruction + code = self.native_sdk.set_servo_angle_j(degrees, speed=100, mvacc=500, wait=False) + + return bool(code == 0) + + def set_joint_velocities(self, velocities: list[float]) -> bool: + """Set joint velocity targets. + + Args: + velocities: Target velocities in RAD/S + + Returns: + True if command accepted + """ + # Check if velocity control is supported + if not hasattr(self.native_sdk, "vc_set_joint_velocity"): + self.logger.warning("Velocity control not supported in this XArm version") + return False + + # Convert rad/s to deg/s + deg_velocities = [math.degrees(vel) for vel in velocities] + + # Set to velocity control mode if needed + if self.native_sdk.mode != 4: + self.native_sdk.set_mode(4) # Joint velocity mode + + # Send velocity command + code = self.native_sdk.vc_set_joint_velocity(deg_velocities) + return bool(code == 0) + + def set_joint_efforts(self, efforts: list[float]) -> bool: + """Set joint effort/torque targets. + + Args: + efforts: Target efforts in Nm + + Returns: + True if command accepted + """ + # Check if torque control is supported + if not hasattr(self.native_sdk, "set_joint_torque"): + self.logger.warning("Torque control not supported in this XArm version") + return False + + # Send torque command + code = self.native_sdk.set_joint_torque(efforts) + return bool(code == 0) + + def stop_motion(self) -> bool: + """Stop all ongoing motion. + + Returns: + True if stop successful + """ + # XArm emergency stop + code = self.native_sdk.emergency_stop() + + # Re-enable after stop + if code == 0: + self.native_sdk.set_state(0) # Clear stop state + self.native_sdk.motion_enable(True) + + return bool(code == 0) + + # ============= Servo Control ============= + + def enable_servos(self) -> bool: + """Enable motor control. + + Returns: + True if servos enabled + """ + code1 = self.native_sdk.motion_enable(True) + code2 = self.native_sdk.set_state(0) # Ready state + code3 = self.native_sdk.set_mode(1) # Servo mode + return bool(code1 == 0 and code2 == 0 and code3 == 0) + + def disable_servos(self) -> bool: + """Disable motor control. + + Returns: + True if servos disabled + """ + code = self.native_sdk.motion_enable(False) + return bool(code == 0) + + def are_servos_enabled(self) -> bool: + """Check if servos are enabled. + + Returns: + True if enabled + """ + # Check motor state + return bool(self.native_sdk.mode == 1 and self.native_sdk.mode != 4) + + # ============= System State ============= + + def get_robot_state(self) -> dict[str, Any]: + """Get current robot state. + + Returns: + State dictionary + """ + return { + "state": self.native_sdk.state, # 0=ready, 1=pause, 2=stop, 3=running, 4=error + "mode": self.native_sdk.mode, # 0=position, 1=servo, 4=joint_vel, 5=cart_vel + "error_code": self.native_sdk.error_code, + "warn_code": self.native_sdk.warn_code, + "is_moving": self.native_sdk.state == 3, + "cmd_num": self.native_sdk.cmd_num, + } + + def get_error_code(self) -> int: + """Get current error code. + + Returns: + Error code (0 = no error) + """ + return int(self.native_sdk.error_code) + + def get_error_message(self) -> str: + """Get human-readable error message. + + Returns: + Error message string + """ + if self.native_sdk.error_code == 0: + return "" + + # XArm error codes (partial list) + error_map = { + 1: "Emergency stop button pressed", + 2: "Joint limit exceeded", + 3: "Command reply timeout", + 4: "Power supply error", + 5: "Motor overheated", + 6: "Motor driver error", + 7: "Other error", + 10: "Servo error", + 11: "Joint collision", + 12: "Tool IO error", + 13: "Tool communication error", + 14: "Kinematic error", + 15: "Self collision", + 16: "Joint overheated", + 17: "Planning error", + 19: "Force control error", + 20: "Joint current overlimit", + 21: "TCP command overlimit", + 22: "Overspeed", + } + + return error_map.get( + self.native_sdk.error_code, f"Unknown error {self.native_sdk.error_code}" + ) + + def clear_errors(self) -> bool: + """Clear error states. + + Returns: + True if errors cleared + """ + code = self.native_sdk.clean_error() + if code == 0: + # Reset to ready state + self.native_sdk.set_state(0) + return bool(code == 0) + + def emergency_stop(self) -> bool: + """Execute emergency stop. + + Returns: + True if e-stop executed + """ + code = self.native_sdk.emergency_stop() + return bool(code == 0) + + # ============= Information ============= + + def get_info(self) -> ManipulatorInfo: + """Get manipulator information. + + Returns: + ManipulatorInfo object + """ + return ManipulatorInfo( + vendor="UFACTORY", + model=f"xArm{self.dof}", + dof=self.dof, + firmware_version=self.native_sdk.version if self.native_sdk else None, + serial_number=self.native_sdk.get_servo_version()[1][0] if self.native_sdk else None, + ) + + def get_joint_limits(self) -> tuple[list[float], list[float]]: + """Get joint position limits. + + Returns: + Tuple of (lower_limits, upper_limits) in RADIANS + """ + # XArm joint limits in degrees (approximate, varies by model) + if self.dof == 7: + lower_deg = [-360, -118, -360, -233, -360, -97, -360] + upper_deg = [360, 118, 360, 11, 360, 180, 360] + elif self.dof == 6: + lower_deg = [-360, -118, -225, -11, -360, -97] + upper_deg = [360, 118, 11, 225, 360, 180] + else: # 5 DOF + lower_deg = [-360, -118, -225, -97, -360] + upper_deg = [360, 118, 11, 180, 360] + + # Convert to radians + lower_rad = [math.radians(d) for d in lower_deg[: self.dof]] + upper_rad = [math.radians(d) for d in upper_deg[: self.dof]] + + return (lower_rad, upper_rad) + + def get_velocity_limits(self) -> list[float]: + """Get joint velocity limits. + + Returns: + Maximum velocities in RAD/S + """ + # XArm max velocities in deg/s (default) + max_vel_deg = 180.0 + + # Convert to rad/s + max_vel_rad = math.radians(max_vel_deg) + return [max_vel_rad] * self.dof + + def get_acceleration_limits(self) -> list[float]: + """Get joint acceleration limits. + + Returns: + Maximum accelerations in RAD/S² + """ + # XArm max acceleration in deg/s² (default) + max_acc_deg = 1145.0 + + # Convert to rad/s² + max_acc_rad = math.radians(max_acc_deg) + return [max_acc_rad] * self.dof + + # ============= Optional Methods ============= + + def get_cartesian_position(self) -> dict[str, float] | None: + """Get current end-effector pose. + + Returns: + Pose dict or None if not supported + """ + code, pose = self.native_sdk.get_position() + if code != 0: + return None + + # XArm returns [x, y, z (mm), roll, pitch, yaw (degrees)] + return { + "x": pose[0] / 1000.0, # mm to meters + "y": pose[1] / 1000.0, + "z": pose[2] / 1000.0, + "roll": math.radians(pose[3]), + "pitch": math.radians(pose[4]), + "yaw": math.radians(pose[5]), + } + + def set_cartesian_position( + self, + pose: dict[str, float], + velocity: float = 1.0, + acceleration: float = 1.0, + wait: bool = False, + ) -> bool: + """Move end-effector to target pose. + + Args: + pose: Target pose dict + velocity: Max velocity fraction (0-1) + acceleration: Max acceleration fraction (0-1) + wait: Block until complete + + Returns: + True if command accepted + """ + # Convert to XArm format + xarm_pose = [ + pose["x"] * 1000.0, # meters to mm + pose["y"] * 1000.0, + pose["z"] * 1000.0, + math.degrees(pose["roll"]), + math.degrees(pose["pitch"]), + math.degrees(pose["yaw"]), + ] + + # XArm max Cartesian speed (default 500 mm/s) + max_speed = 500.0 + speed = max_speed * velocity + + # XArm max Cartesian acceleration (default 2000 mm/s²) + max_acc = 2000.0 + acc = max_acc * acceleration + + code = self.native_sdk.set_position(xarm_pose, radius=-1, speed=speed, mvacc=acc, wait=wait) + + return bool(code == 0) + + def get_force_torque(self) -> list[float] | None: + """Get F/T sensor reading. + + Returns: + [fx, fy, fz, tx, ty, tz] or None + """ + if hasattr(self.native_sdk, "get_ft_sensor_data"): + code, ft_data = self.native_sdk.get_ft_sensor_data() + if code == 0: + return list(ft_data) + return None + + def zero_force_torque(self) -> bool: + """Zero the F/T sensor. + + Returns: + True if successful + """ + if hasattr(self.native_sdk, "set_ft_sensor_zero"): + code = self.native_sdk.set_ft_sensor_zero() + return bool(code == 0) + return False + + def get_gripper_position(self) -> float | None: + """Get gripper position. + + Returns: + Position in meters or None + """ + if hasattr(self.native_sdk, "get_gripper_position"): + code, pos = self.native_sdk.get_gripper_position() + if code == 0: + # Convert mm to meters + return float(pos / 1000.0) + return None + + def set_gripper_position(self, position: float, force: float = 1.0) -> bool: + """Set gripper position. + + Args: + position: Target position in meters + force: Force fraction (0-1) + + Returns: + True if successful + """ + if hasattr(self.native_sdk, "set_gripper_position"): + # Convert meters to mm + pos_mm = position * 1000.0 + code = self.native_sdk.set_gripper_position(pos_mm, wait=False) + return bool(code == 0) + return False + + def set_control_mode(self, mode: str) -> bool: + """Set control mode. + + Args: + mode: 'position', 'velocity', 'torque', or 'impedance' + + Returns: + True if successful + """ + mode_map = { + "position": 0, + "velocity": 4, # Joint velocity mode + "servo": 1, # Servo mode (for torque control) + "impedance": 0, # Not directly supported, use position + } + + if mode not in mode_map: + return False + + code = self.native_sdk.set_mode(mode_map[mode]) + return bool(code == 0) + + def get_control_mode(self) -> str | None: + """Get current control mode. + + Returns: + Mode string or None + """ + mode_map = {0: "position", 1: "servo", 4: "velocity", 5: "cartesian_velocity"} + + return mode_map.get(self.native_sdk.mode, "unknown") diff --git a/dimos/hardware/piper_arm.py b/dimos/hardware/piper_arm.py deleted file mode 100644 index d27d1df394..0000000000 --- a/dimos/hardware/piper_arm.py +++ /dev/null @@ -1,525 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# dimos/hardware/piper_arm.py - -import select -import sys -import termios -import threading -import time -import tty - -from dimos_lcm.geometry_msgs import Pose, Twist, Vector3 -import kinpy as kp -import numpy as np -from piper_sdk import * # from the official Piper SDK -import pytest -from reactivex.disposable import Disposable -from scipy.spatial.transform import Rotation as R - -import dimos.core as core -from dimos.core import In, Module, rpc -import dimos.protocol.service.lcmservice as lcmservice -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import euler_to_quaternion, quaternion_to_euler - -logger = setup_logger(__file__) - - -class PiperArm: - def __init__(self, arm_name: str = "arm") -> None: - self.arm = C_PiperInterface_V2() - self.arm.ConnectPort() - self.resetArm() - time.sleep(0.5) - self.resetArm() - time.sleep(0.5) - self.enable() - self.enable_gripper() # Enable gripper after arm is enabled - self.gotoZero() - time.sleep(1) - self.init_vel_controller() - - def enable(self) -> None: - while not self.arm.EnablePiper(): - pass - time.sleep(0.01) - logger.info("Arm enabled") - # self.arm.ModeCtrl( - # ctrl_mode=0x01, # CAN command mode - # move_mode=0x01, # ā€œMove-Jā€, but ignored in MIT - # move_spd_rate_ctrl=100, # doesn’t matter in MIT - # is_mit_mode=0xAD # <-- the magic flag - # ) - self.arm.MotionCtrl_2(0x01, 0x01, 80, 0xAD) - - def gotoZero(self) -> None: - factor = 1000 - position = [57.0, 0.0, 215.0, 0, 90.0, 0, 0] - X = round(position[0] * factor) - Y = round(position[1] * factor) - Z = round(position[2] * factor) - RX = round(position[3] * factor) - RY = round(position[4] * factor) - RZ = round(position[5] * factor) - round(position[6] * factor) - logger.debug(f"Going to zero position: X={X}, Y={Y}, Z={Z}, RX={RX}, RY={RY}, RZ={RZ}") - self.arm.MotionCtrl_2(0x01, 0x00, 100, 0x00) - self.arm.EndPoseCtrl(X, Y, Z, RX, RY, RZ) - self.arm.GripperCtrl(0, 1000, 0x01, 0) - - def gotoObserve(self) -> None: - factor = 1000 - position = [57.0, 0.0, 280.0, 0, 120.0, 0, 0] - X = round(position[0] * factor) - Y = round(position[1] * factor) - Z = round(position[2] * factor) - RX = round(position[3] * factor) - RY = round(position[4] * factor) - RZ = round(position[5] * factor) - round(position[6] * factor) - logger.debug(f"Going to zero position: X={X}, Y={Y}, Z={Z}, RX={RX}, RY={RY}, RZ={RZ}") - self.arm.MotionCtrl_2(0x01, 0x00, 100, 0x00) - self.arm.EndPoseCtrl(X, Y, Z, RX, RY, RZ) - - def softStop(self) -> None: - self.gotoZero() - time.sleep(1) - self.arm.MotionCtrl_2( - 0x01, - 0x00, - 100, - ) - self.arm.MotionCtrl_1(0x01, 0, 0) - time.sleep(3) - - def cmd_ee_pose_values(self, x, y, z, r, p, y_, line_mode: bool = False) -> None: - """Command end-effector to target pose in space (position + Euler angles)""" - factor = 1000 - pose = [ - x * factor * factor, - y * factor * factor, - z * factor * factor, - r * factor, - p * factor, - y_ * factor, - ] - self.arm.MotionCtrl_2(0x01, 0x02 if line_mode else 0x00, 100, 0x00) - self.arm.EndPoseCtrl( - int(pose[0]), int(pose[1]), int(pose[2]), int(pose[3]), int(pose[4]), int(pose[5]) - ) - - def cmd_ee_pose(self, pose: Pose, line_mode: bool = False) -> None: - """Command end-effector to target pose using Pose message""" - # Convert quaternion to euler angles - euler = quaternion_to_euler(pose.orientation, degrees=True) - - # Command the pose - self.cmd_ee_pose_values( - pose.position.x, - pose.position.y, - pose.position.z, - euler.x, - euler.y, - euler.z, - line_mode, - ) - - def get_ee_pose(self): - """Return the current end-effector pose as Pose message with position in meters and quaternion orientation""" - pose = self.arm.GetArmEndPoseMsgs() - factor = 1000.0 - # Extract individual pose values and convert to base units - # Position values are divided by 1000 to convert from SDK units to meters - # Rotation values are divided by 1000 to convert from SDK units to radians - x = pose.end_pose.X_axis / factor / factor # Convert mm to m - y = pose.end_pose.Y_axis / factor / factor # Convert mm to m - z = pose.end_pose.Z_axis / factor / factor # Convert mm to m - rx = pose.end_pose.RX_axis / factor - ry = pose.end_pose.RY_axis / factor - rz = pose.end_pose.RZ_axis / factor - - # Create position vector (already in meters) - position = Vector3(x, y, z) - - orientation = euler_to_quaternion(Vector3(rx, ry, rz), degrees=True) - - return Pose(position, orientation) - - def cmd_gripper_ctrl(self, position, effort: float = 0.25) -> None: - """Command end-effector gripper""" - factor = 1000 - position = position * factor * factor # meters - effort = effort * factor # N/m - - self.arm.GripperCtrl(abs(round(position)), abs(round(effort)), 0x01, 0) - logger.debug(f"Commanding gripper position: {position}mm") - - def enable_gripper(self) -> None: - """Enable the gripper using the initialization sequence""" - logger.info("Enabling gripper...") - while not self.arm.EnablePiper(): - time.sleep(0.01) - self.arm.GripperCtrl(0, 1000, 0x02, 0) - self.arm.GripperCtrl(0, 1000, 0x01, 0) - logger.info("Gripper enabled") - - def release_gripper(self) -> None: - """Release gripper by opening to 100mm (10cm)""" - logger.info("Releasing gripper (opening to 100mm)") - self.cmd_gripper_ctrl(0.1) # 0.1m = 100mm = 10cm - - def get_gripper_feedback(self) -> tuple[float, float]: - """ - Get current gripper feedback. - - Returns: - Tuple of (angle_degrees, effort) where: - - angle_degrees: Current gripper angle in degrees - - effort: Current gripper effort (0.0 to 1.0 range) - """ - gripper_msg = self.arm.GetArmGripperMsgs() - angle_degrees = ( - gripper_msg.gripper_state.grippers_angle / 1000.0 - ) # Convert from SDK units to degrees - effort = gripper_msg.gripper_state.grippers_effort / 1000.0 # Convert from SDK units to N/m - return angle_degrees, effort - - def close_gripper(self, commanded_effort: float = 0.5) -> None: - """ - Close the gripper. - - Args: - commanded_effort: Effort to use when closing gripper (default 0.25 N/m) - """ - # Command gripper to close (0.0 position) - self.cmd_gripper_ctrl(0.0, effort=commanded_effort) - logger.info("Closing gripper") - - def gripper_object_detected(self, commanded_effort: float = 0.25) -> bool: - """ - Check if an object is detected in the gripper based on effort feedback. - - Args: - commanded_effort: The effort that was used when closing gripper (default 0.25 N/m) - - Returns: - True if object is detected in gripper, False otherwise - """ - # Get gripper feedback - _angle_degrees, actual_effort = self.get_gripper_feedback() - - # Check if object is grasped (effort > 80% of commanded effort) - effort_threshold = 0.8 * commanded_effort - object_present = abs(actual_effort) > effort_threshold - - if object_present: - logger.info(f"Object detected in gripper (effort: {actual_effort:.3f} N/m)") - else: - logger.info(f"No object detected (effort: {actual_effort:.3f} N/m)") - - return object_present - - def resetArm(self) -> None: - self.arm.MotionCtrl_1(0x02, 0, 0) - self.arm.MotionCtrl_2(0, 0, 0, 0x00) - logger.info("Resetting arm") - - def init_vel_controller(self) -> None: - self.chain = kp.build_serial_chain_from_urdf( - open("dimos/hardware/piper_description.urdf"), "gripper_base" - ) - self.J = self.chain.jacobian(np.zeros(6)) - self.J_pinv = np.linalg.pinv(self.J) - self.dt = 0.01 - - def cmd_vel(self, x_dot, y_dot, z_dot, R_dot, P_dot, Y_dot) -> None: - joint_state = self.arm.GetArmJointMsgs().joint_state - # print(f"[PiperArm] Current Joints (direct): {joint_state}", type(joint_state)) - joint_angles = np.array( - [ - joint_state.joint_1, - joint_state.joint_2, - joint_state.joint_3, - joint_state.joint_4, - joint_state.joint_5, - joint_state.joint_6, - ] - ) - # print(f"[PiperArm] Current Joints: {joint_angles}", type(joint_angles)) - factor = 57295.7795 # 1000*180/3.1415926 - joint_angles = joint_angles / factor # convert to radians - - q = np.array( - [ - joint_angles[0], - joint_angles[1], - joint_angles[2], - joint_angles[3], - joint_angles[4], - joint_angles[5], - ] - ) - J = self.chain.jacobian(q) - self.J_pinv = np.linalg.pinv(J) - dq = self.J_pinv @ np.array([x_dot, y_dot, z_dot, R_dot, P_dot, Y_dot]) * self.dt - newq = q + dq - - newq = newq * factor - - self.arm.MotionCtrl_2(0x01, 0x01, 100, 0xAD) - self.arm.JointCtrl( - round(newq[0]), - round(newq[1]), - round(newq[2]), - round(newq[3]), - round(newq[4]), - round(newq[5]), - ) - time.sleep(self.dt) - # print(f"[PiperArm] Moving to Joints to : {newq}") - - def cmd_vel_ee(self, x_dot, y_dot, z_dot, RX_dot, PY_dot, YZ_dot) -> None: - factor = 1000 - x_dot = x_dot * factor - y_dot = y_dot * factor - z_dot = z_dot * factor - RX_dot = RX_dot * factor - PY_dot = PY_dot * factor - YZ_dot = YZ_dot * factor - - current_pose_msg = self.get_ee_pose() - - # Convert quaternion to euler angles - quat = [ - current_pose_msg.orientation.x, - current_pose_msg.orientation.y, - current_pose_msg.orientation.z, - current_pose_msg.orientation.w, - ] - rotation = R.from_quat(quat) - euler = rotation.as_euler("xyz") # Returns [rx, ry, rz] in radians - - # Create current pose array [x, y, z, rx, ry, rz] - current_pose = np.array( - [ - current_pose_msg.position.x, - current_pose_msg.position.y, - current_pose_msg.position.z, - euler[0], - euler[1], - euler[2], - ] - ) - - # Apply velocity increment - current_pose = ( - current_pose + np.array([x_dot, y_dot, z_dot, RX_dot, PY_dot, YZ_dot]) * self.dt - ) - - self.cmd_ee_pose_values( - current_pose[0], - current_pose[1], - current_pose[2], - current_pose[3], - current_pose[4], - current_pose[5], - ) - time.sleep(self.dt) - - def disable(self) -> None: - self.softStop() - - while self.arm.DisablePiper(): - pass - time.sleep(0.01) - self.arm.DisconnectPort() - - -class VelocityController(Module): - cmd_vel: In[Twist] = None - - def __init__(self, arm, period: float = 0.01, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - self.arm = arm - self.period = period - self.latest_cmd = None - self.last_cmd_time = None - self._thread = None - - @rpc - def start(self) -> None: - super().start() - - unsub = self.cmd_vel.subscribe(self.handle_cmd_vel) - self._disposables.add(Disposable(unsub)) - - def control_loop() -> None: - while True: - # Check for timeout (1 second) - if self.last_cmd_time and (time.time() - self.last_cmd_time) > 1.0: - logger.warning( - "No velocity command received for 1 second, stopping control loop" - ) - break - - cmd_vel = self.latest_cmd - - joint_state = self.arm.GetArmJointMsgs().joint_state - # print(f"[PiperArm] Current Joints (direct): {joint_state}", type(joint_state)) - joint_angles = np.array( - [ - joint_state.joint_1, - joint_state.joint_2, - joint_state.joint_3, - joint_state.joint_4, - joint_state.joint_5, - joint_state.joint_6, - ] - ) - factor = 57295.7795 # 1000*180/3.1415926 - joint_angles = joint_angles / factor # convert to radians - q = np.array( - [ - joint_angles[0], - joint_angles[1], - joint_angles[2], - joint_angles[3], - joint_angles[4], - joint_angles[5], - ] - ) - - J = self.chain.jacobian(q) - self.J_pinv = np.linalg.pinv(J) - dq = ( - self.J_pinv - @ np.array( - [ - cmd_vel.linear.X, - cmd_vel.linear.y, - cmd_vel.linear.z, - cmd_vel.angular.x, - cmd_vel.angular.y, - cmd_vel.angular.z, - ] - ) - * self.dt - ) - newq = q + dq - - newq = newq * factor # convert radians to scaled degree units for joint control - - self.arm.MotionCtrl_2(0x01, 0x01, 100, 0xAD) - self.arm.JointCtrl( - round(newq[0]), - round(newq[1]), - round(newq[2]), - round(newq[3]), - round(newq[4]), - round(newq[5]), - ) - time.sleep(self.period) - - self._thread = threading.Thread(target=control_loop, daemon=True) - self._thread.start() - - @rpc - def stop(self) -> None: - if self._thread: - # TODO: trigger the thread to stop - self._thread.join(2) - super().stop() - - def handle_cmd_vel(self, cmd_vel: Twist) -> None: - self.latest_cmd = cmd_vel - self.last_cmd_time = time.time() - - -@pytest.mark.tool -def run_velocity_controller() -> None: - lcmservice.autoconf() - dimos = core.start(2) - - velocity_controller = dimos.deploy(VelocityController, arm=arm, period=0.01) - velocity_controller.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - - velocity_controller.start() - - logger.info("Velocity controller started") - while True: - time.sleep(1) - - # velocity_controller.stop() - - -if __name__ == "__main__": - arm = PiperArm() - - def get_key(timeout: float = 0.1): - """Non-blocking key reader for arrow keys.""" - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) - try: - tty.setraw(fd) - rlist, _, _ = select.select([fd], [], [], timeout) - if rlist: - ch1 = sys.stdin.read(1) - if ch1 == "\x1b": # Arrow keys start with ESC - ch2 = sys.stdin.read(1) - if ch2 == "[": - ch3 = sys.stdin.read(1) - return ch1 + ch2 + ch3 - else: - return ch1 - return None - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - - def teleop_linear_vel(arm) -> None: - print("Use arrow keys to control linear velocity (x/y/z). Press 'q' to quit.") - print("Up/Down: +x/-x, Left/Right: +y/-y, 'w'/'s': +z/-z") - x_dot, y_dot, z_dot = 0.0, 0.0, 0.0 - while True: - key = get_key(timeout=0.1) - if key == "\x1b[A": # Up arrow - x_dot += 0.01 - elif key == "\x1b[B": # Down arrow - x_dot -= 0.01 - elif key == "\x1b[C": # Right arrow - y_dot += 0.01 - elif key == "\x1b[D": # Left arrow - y_dot -= 0.01 - elif key == "w": - z_dot += 0.01 - elif key == "s": - z_dot -= 0.01 - elif key == "q": - logger.info("Exiting teleop") - arm.disable() - break - - # Optionally, clamp velocities to reasonable limits - x_dot = max(min(x_dot, 0.5), -0.5) - y_dot = max(min(y_dot, 0.5), -0.5) - z_dot = max(min(z_dot, 0.5), -0.5) - - # Only linear velocities, angular set to zero - arm.cmd_vel_ee(x_dot, y_dot, z_dot, 0, 0, 0) - logger.debug( - f"Current linear velocity: x={x_dot:.3f} m/s, y={y_dot:.3f} m/s, z={z_dot:.3f} m/s" - ) - - run_velocity_controller() diff --git a/dimos/hardware/sensor.py b/dimos/hardware/sensor.py deleted file mode 100644 index aa39f25ec6..0000000000 --- a/dimos/hardware/sensor.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - - -class AbstractSensor(ABC): - def __init__(self, sensor_type=None) -> None: - self.sensor_type = sensor_type - - @abstractmethod - def get_sensor_type(self): - """Return the type of sensor.""" - pass - - @abstractmethod - def calculate_intrinsics(self): - """Calculate the sensor's intrinsics.""" - pass - - @abstractmethod - def get_intrinsics(self): - """Return the sensor's intrinsics.""" - pass diff --git a/dimos/hardware/gstreamer_camera.py b/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera.py similarity index 86% rename from dimos/hardware/gstreamer_camera.py rename to dimos/hardware/sensors/camera/gstreamer/gstreamer_camera.py index 38ede23ee1..949330881a 100644 --- a/dimos/hardware/gstreamer_camera.py +++ b/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from dataclasses import dataclass import logging import sys import threading @@ -21,7 +22,7 @@ import numpy as np -from dimos.core import Module, Out, rpc +from dimos.core import Module, ModuleConfig, Out, rpc from dimos.msgs.sensor_msgs import Image, ImageFormat from dimos.utils.logging_config import setup_logger @@ -29,27 +30,34 @@ if "/usr/lib/python3/dist-packages" not in sys.path: sys.path.insert(0, "/usr/lib/python3/dist-packages") -import gi +import gi # type: ignore[import-not-found, import-untyped] gi.require_version("Gst", "1.0") gi.require_version("GstApp", "1.0") -from gi.repository import GLib, Gst +from gi.repository import GLib, Gst # type: ignore[import-not-found, import-untyped] -logger = setup_logger("dimos.hardware.gstreamer_camera", level=logging.INFO) +logger = setup_logger(level=logging.INFO) Gst.init(None) +@dataclass +class Config(ModuleConfig): + frame_id: str = "camera" + + class GstreamerCameraModule(Module): """Module that captures frames from a remote camera using GStreamer TCP with absolute timestamps.""" - video: Out[Image] = None + default_config = Config + config: Config - def __init__( + video: Out[Image] + + def __init__( # type: ignore[no-untyped-def] self, host: str = "localhost", port: int = 5000, - frame_id: str = "camera", timestamp_offset: float = 0.0, reconnect_interval: float = 5.0, *args, @@ -66,7 +74,6 @@ def __init__( """ self.host = host self.port = port - self.frame_id = frame_id self.timestamp_offset = timestamp_offset self.reconnect_interval = reconnect_interval @@ -79,8 +86,7 @@ def __init__( self.frame_count = 0 self.last_log_time = time.time() self.reconnect_timer_id = None - - Module.__init__(self, *args, **kwargs) + super().__init__(**kwargs) @rpc def start(self) -> None: @@ -120,8 +126,8 @@ def _connect(self) -> None: return try: - self._create_pipeline() - self._start_pipeline() + self._create_pipeline() # type: ignore[no-untyped-call] + self._start_pipeline() # type: ignore[no-untyped-call] self.running = True logger.info(f"GStreamer TCP camera module connected to {self.host}:{self.port}") except Exception as e: @@ -165,7 +171,7 @@ def _handle_disconnect(self) -> None: logger.warning(f"Disconnected from {self.host}:{self.port}") self._schedule_reconnect() - def _create_pipeline(self): + def _create_pipeline(self): # type: ignore[no-untyped-def] # TCP client source with Matroska demuxer to extract absolute timestamps pipeline_str = f""" tcpclientsrc host={self.host} port={self.port} ! @@ -179,39 +185,39 @@ def _create_pipeline(self): try: self.pipeline = Gst.parse_launch(pipeline_str) - self.appsink = self.pipeline.get_by_name("sink") - self.appsink.connect("new-sample", self._on_new_sample) + self.appsink = self.pipeline.get_by_name("sink") # type: ignore[attr-defined] + self.appsink.connect("new-sample", self._on_new_sample) # type: ignore[attr-defined] except Exception as e: logger.error(f"Failed to create GStreamer pipeline: {e}") raise - def _start_pipeline(self): + def _start_pipeline(self): # type: ignore[no-untyped-def] """Start the GStreamer pipeline and main loop.""" self.main_loop = GLib.MainLoop() # Start the pipeline - ret = self.pipeline.set_state(Gst.State.PLAYING) + ret = self.pipeline.set_state(Gst.State.PLAYING) # type: ignore[attr-defined] if ret == Gst.StateChangeReturn.FAILURE: logger.error("Unable to set the pipeline to playing state") raise RuntimeError("Failed to start GStreamer pipeline") # Run the main loop in a separate thread - self.main_loop_thread = threading.Thread(target=self._run_main_loop) - self.main_loop_thread.daemon = True - self.main_loop_thread.start() + self.main_loop_thread = threading.Thread(target=self._run_main_loop) # type: ignore[assignment] + self.main_loop_thread.daemon = True # type: ignore[attr-defined] + self.main_loop_thread.start() # type: ignore[attr-defined] # Set up bus message handling - bus = self.pipeline.get_bus() + bus = self.pipeline.get_bus() # type: ignore[attr-defined] bus.add_signal_watch() bus.connect("message", self._on_bus_message) def _run_main_loop(self) -> None: try: - self.main_loop.run() + self.main_loop.run() # type: ignore[attr-defined] except Exception as e: logger.error(f"Main loop error: {e}") - def _on_bus_message(self, bus, message) -> None: + def _on_bus_message(self, bus, message) -> None: # type: ignore[no-untyped-def] t = message.type if t == Gst.MessageType.EOS: @@ -230,7 +236,7 @@ def _on_bus_message(self, bus, message) -> None: if new_state == Gst.State.PLAYING: logger.info("Pipeline is now playing - connected to TCP server") - def _on_new_sample(self, appsink): + def _on_new_sample(self, appsink): # type: ignore[no-untyped-def] """Handle new video samples from the appsink.""" sample = appsink.emit("pull-sample") if sample is None: diff --git a/dimos/hardware/gstreamer_camera_test_script.py b/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py similarity index 92% rename from dimos/hardware/gstreamer_camera_test_script.py rename to dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py index f815579c0d..cc0e3424a5 100755 --- a/dimos/hardware/gstreamer_camera_test_script.py +++ b/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import time from dimos import core -from dimos.hardware.gstreamer_camera import GstreamerCameraModule +from dimos.hardware.sensors.camera.gstreamer.gstreamer_camera import GstreamerCameraModule from dimos.msgs.sensor_msgs import Image from dimos.protocol import pubsub @@ -58,7 +58,7 @@ def main() -> None: logging.getLogger().setLevel(logging.DEBUG) # Initialize LCM - pubsub.lcm.autoconf() + pubsub.lcm.autoconf() # type: ignore[attr-defined] # Start dimos logger.info("Starting dimos...") @@ -66,7 +66,7 @@ def main() -> None: # Deploy the GStreamer camera module logger.info(f"Deploying GStreamer TCP camera module (connecting to {args.host}:{args.port})...") - camera = dimos.deploy( + camera = dimos.deploy( # type: ignore[attr-defined] GstreamerCameraModule, host=args.host, port=args.port, @@ -82,7 +82,7 @@ def main() -> None: last_log_time = [time.time()] first_timestamp = [None] - def on_frame(msg) -> None: + def on_frame(msg) -> None: # type: ignore[no-untyped-def] frame_count[0] += 1 current_time = time.time() diff --git a/dimos/hardware/gstreamer_sender.py b/dimos/hardware/sensors/camera/gstreamer/gstreamer_sender.py similarity index 83% rename from dimos/hardware/gstreamer_sender.py rename to dimos/hardware/sensors/camera/gstreamer/gstreamer_sender.py index ce7c1d6145..4aee200419 100755 --- a/dimos/hardware/gstreamer_sender.py +++ b/dimos/hardware/sensors/camera/gstreamer/gstreamer_sender.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,11 +24,11 @@ if "/usr/lib/python3/dist-packages" not in sys.path: sys.path.insert(0, "/usr/lib/python3/dist-packages") -import gi +import gi # type: ignore[import-not-found, import-untyped] gi.require_version("Gst", "1.0") gi.require_version("GstVideo", "1.0") -from gi.repository import GLib, Gst +from gi.repository import GLib, Gst # type: ignore[import-not-found, import-untyped] # Initialize GStreamer Gst.init(None) @@ -85,7 +85,7 @@ def __init__( self.start_time = None self.frame_count = 0 - def create_pipeline(self): + def create_pipeline(self): # type: ignore[no-untyped-def] """Create the GStreamer pipeline with TCP server sink.""" # Create pipeline @@ -93,8 +93,8 @@ def create_pipeline(self): # Create elements self.videosrc = Gst.ElementFactory.make("v4l2src", "source") - self.videosrc.set_property("device", self.device) - self.videosrc.set_property("do-timestamp", True) + self.videosrc.set_property("device", self.device) # type: ignore[attr-defined] + self.videosrc.set_property("do-timestamp", True) # type: ignore[attr-defined] logger.info(f"Using camera device: {self.device}") # Create caps filter for video format @@ -120,17 +120,17 @@ def create_pipeline(self): # H264 encoder self.encoder = Gst.ElementFactory.make("x264enc", "encoder") - self.encoder.set_property("tune", "zerolatency") - self.encoder.set_property("bitrate", self.bitrate) - self.encoder.set_property("key-int-max", 30) + self.encoder.set_property("tune", "zerolatency") # type: ignore[attr-defined] + self.encoder.set_property("bitrate", self.bitrate) # type: ignore[attr-defined] + self.encoder.set_property("key-int-max", 30) # type: ignore[attr-defined] # H264 parser h264parse = Gst.ElementFactory.make("h264parse", "parser") # Use matroskamux which preserves timestamps better self.mux = Gst.ElementFactory.make("matroskamux", "mux") - self.mux.set_property("streamable", True) - self.mux.set_property("writing-app", "gstreamer-tcp-sender") + self.mux.set_property("streamable", True) # type: ignore[attr-defined] + self.mux.set_property("writing-app", "gstreamer-tcp-sender") # type: ignore[attr-defined] # TCP server sink tcpserversink = Gst.ElementFactory.make("tcpserversink", "sink") @@ -139,18 +139,18 @@ def create_pipeline(self): tcpserversink.set_property("sync", False) # Add elements to pipeline - self.pipeline.add(self.videosrc) - self.pipeline.add(capsfilter) - self.pipeline.add(videoconvert) + self.pipeline.add(self.videosrc) # type: ignore[attr-defined] + self.pipeline.add(capsfilter) # type: ignore[attr-defined] + self.pipeline.add(videoconvert) # type: ignore[attr-defined] if videocrop: - self.pipeline.add(videocrop) - self.pipeline.add(self.encoder) - self.pipeline.add(h264parse) - self.pipeline.add(self.mux) - self.pipeline.add(tcpserversink) + self.pipeline.add(videocrop) # type: ignore[attr-defined] + self.pipeline.add(self.encoder) # type: ignore[attr-defined] + self.pipeline.add(h264parse) # type: ignore[attr-defined] + self.pipeline.add(self.mux) # type: ignore[attr-defined] + self.pipeline.add(tcpserversink) # type: ignore[attr-defined] # Link elements - if not self.videosrc.link(capsfilter): + if not self.videosrc.link(capsfilter): # type: ignore[attr-defined] raise RuntimeError("Failed to link source to capsfilter") if not capsfilter.link(videoconvert): raise RuntimeError("Failed to link capsfilter to videoconvert") @@ -165,11 +165,11 @@ def create_pipeline(self): if not videoconvert.link(self.encoder): raise RuntimeError("Failed to link videoconvert to encoder") - if not self.encoder.link(h264parse): + if not self.encoder.link(h264parse): # type: ignore[attr-defined] raise RuntimeError("Failed to link encoder to h264parse") if not h264parse.link(self.mux): raise RuntimeError("Failed to link h264parse to mux") - if not self.mux.link(tcpserversink): + if not self.mux.link(tcpserversink): # type: ignore[attr-defined] raise RuntimeError("Failed to link mux to tcpserversink") # Add probe to inject absolute timestamps @@ -182,11 +182,11 @@ def create_pipeline(self): probe_pad.add_probe(Gst.PadProbeType.BUFFER, self._inject_absolute_timestamp, None) # Set up bus message handling - bus = self.pipeline.get_bus() + bus = self.pipeline.get_bus() # type: ignore[attr-defined] bus.add_signal_watch() bus.connect("message", self._on_bus_message) - def _inject_absolute_timestamp(self, pad, info, user_data): + def _inject_absolute_timestamp(self, pad, info, user_data): # type: ignore[no-untyped-def] buffer = info.get_buffer() if buffer: absolute_time = time.time() @@ -200,7 +200,7 @@ def _inject_absolute_timestamp(self, pad, info, user_data): self.frame_count += 1 return Gst.PadProbeReturn.OK - def _on_bus_message(self, bus, message) -> None: + def _on_bus_message(self, bus, message) -> None: # type: ignore[no-untyped-def] t = message.type if t == Gst.MessageType.EOS: @@ -220,22 +220,22 @@ def _on_bus_message(self, bus, message) -> None: f"Pipeline state changed: {old_state.value_nick} -> {new_state.value_nick}" ) - def start(self): + def start(self): # type: ignore[no-untyped-def] if self.running: logger.warning("Sender is already running") return logger.info("Creating TCP pipeline with absolute timestamps...") - self.create_pipeline() + self.create_pipeline() # type: ignore[no-untyped-call] logger.info("Starting pipeline...") - ret = self.pipeline.set_state(Gst.State.PLAYING) + ret = self.pipeline.set_state(Gst.State.PLAYING) # type: ignore[attr-defined] if ret == Gst.StateChangeReturn.FAILURE: logger.error("Failed to start pipeline") raise RuntimeError("Failed to start GStreamer pipeline") self.running = True - self.start_time = time.time() + self.start_time = time.time() # type: ignore[assignment] self.frame_count = 0 logger.info("TCP video sender started:") @@ -255,7 +255,7 @@ def start(self): self.main_loop = GLib.MainLoop() try: - self.main_loop.run() + self.main_loop.run() # type: ignore[attr-defined] except KeyboardInterrupt: logger.info("Interrupted by user") finally: @@ -340,7 +340,7 @@ def main() -> None: ) # Handle signals gracefully - def signal_handler(sig, frame) -> None: + def signal_handler(sig, frame) -> None: # type: ignore[no-untyped-def] logger.info(f"Received signal {sig}, shutting down...") sender.stop() sys.exit(0) @@ -349,7 +349,7 @@ def signal_handler(sig, frame) -> None: signal.signal(signal.SIGTERM, signal_handler) try: - sender.start() + sender.start() # type: ignore[no-untyped-call] except Exception as e: logger.error(f"Failed to start sender: {e}") sys.exit(1) diff --git a/dimos/hardware/sensors/camera/gstreamer/readme.md b/dimos/hardware/sensors/camera/gstreamer/readme.md new file mode 100644 index 0000000000..29198aea24 --- /dev/null +++ b/dimos/hardware/sensors/camera/gstreamer/readme.md @@ -0,0 +1 @@ +This gstreamer stuff is obsoleted but could be adopted as an alternative hardware for camera module if needed diff --git a/dimos/hardware/sensors/camera/module.py b/dimos/hardware/sensors/camera/module.py new file mode 100644 index 0000000000..10c541723a --- /dev/null +++ b/dimos/hardware/sensors/camera/module.py @@ -0,0 +1,117 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable, Generator +from dataclasses import dataclass, field +import time +from typing import Any + +import reactivex as rx +from reactivex import operators as ops + +from dimos.agents import Output, Reducer, Stream, skill +from dimos.core import Module, ModuleConfig, Out, rpc +from dimos.hardware.sensors.camera.spec import CameraHardware +from dimos.hardware.sensors.camera.webcam import Webcam +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo +from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier +from dimos.spec import perception +from dimos.utils.reactive import iter_observable + + +def default_transform() -> Transform: + return Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ) + + +@dataclass +class CameraModuleConfig(ModuleConfig): + frame_id: str = "camera_link" + transform: Transform | None = field(default_factory=default_transform) + hardware: Callable[[], CameraHardware[Any]] | CameraHardware[Any] = Webcam + frequency: float = 0.0 # Hz, 0 means no limit + + +class CameraModule(Module[CameraModuleConfig], perception.Camera): + color_image: Out[Image] + camera_info: Out[CameraInfo] + + hardware: CameraHardware[Any] + + config: CameraModuleConfig + default_config = CameraModuleConfig + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + @rpc + def start(self) -> None: + if callable(self.config.hardware): + self.hardware = self.config.hardware() + else: + self.hardware = self.config.hardware + + stream = self.hardware.image_stream() + + if self.config.frequency > 0: + stream = stream.pipe(sharpness_barrier(self.config.frequency)) + + self._disposables.add( + stream.subscribe(self.color_image.publish), + ) + + self._disposables.add( + rx.interval(1.0).subscribe(lambda _: self.publish_metadata()), + ) + + def publish_metadata(self) -> None: + camera_info = self.hardware.camera_info.with_ts(time.time()) + self.camera_info.publish(camera_info) + + if not self.config.transform: + return + + camera_link = self.config.transform + camera_link.ts = camera_info.ts + + camera_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), + frame_id="camera_link", + child_frame_id="camera_optical", + ts=camera_link.ts, + ) + + self.tf.publish(camera_link, camera_optical) + + # actually skills should support on_demand passive skills so we don't emit this periodically + # but just provide the latest frame on demand + @skill(stream=Stream.passive, output=Output.image, reducer=Reducer.latest) # type: ignore[arg-type] + def video_stream(self) -> Generator[Image, None, None]: + yield from iter_observable(self.hardware.image_stream().pipe(ops.sample(1.0))) + + def stop(self) -> None: + if self.hardware and hasattr(self.hardware, "stop"): + self.hardware.stop() + super().stop() + + +camera_module = CameraModule.blueprint + +__all__ = ["CameraModule", "camera_module"] diff --git a/dimos/hardware/sensors/camera/realsense/__init__.py b/dimos/hardware/sensors/camera/realsense/__init__.py new file mode 100644 index 0000000000..c3e63d77d8 --- /dev/null +++ b/dimos/hardware/sensors/camera/realsense/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.hardware.sensors.camera.realsense.camera import ( + RealSenseCamera, + RealSenseCameraConfig, + realsense_camera, +) + +__all__ = ["RealSenseCamera", "RealSenseCameraConfig", "realsense_camera"] diff --git a/dimos/hardware/sensors/camera/realsense/camera.py b/dimos/hardware/sensors/camera/realsense/camera.py new file mode 100644 index 0000000000..1149bb8d64 --- /dev/null +++ b/dimos/hardware/sensors/camera/realsense/camera.py @@ -0,0 +1,470 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit +from dataclasses import dataclass, field +import threading +import time + +import cv2 +import numpy as np +import pyrealsense2 as rs # type: ignore[import-not-found] +import reactivex as rx +from scipy.spatial.transform import Rotation # type: ignore[import-untyped] + +from dimos.core import Module, ModuleConfig, Out, rpc +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.transport import LCMTransport +from dimos.hardware.sensors.camera.spec import ( + OPTICAL_ROTATION, + StereoCamera, + StereoCameraConfig, +) +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import CameraInfo +from dimos.msgs.sensor_msgs.Image import Image, ImageFormat +from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 +from dimos.robot.foxglove_bridge import FoxgloveBridge +from dimos.utils.reactive import backpressure + + +def default_base_transform() -> Transform: + """Default identity transform for camera mounting.""" + return Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + ) + + +@dataclass +class RealSenseCameraConfig(ModuleConfig, StereoCameraConfig): + width: int = 848 + height: int = 480 + fps: int = 15 + camera_name: str = "camera" + base_frame_id: str = "base_link" + base_transform: Transform | None = field(default_factory=default_base_transform) + align_depth_to_color: bool = True + enable_depth: bool = True + enable_pointcloud: bool = False + pointcloud_fps: float = 5.0 + camera_info_fps: float = 1.0 + serial_number: str | None = None + + +class RealSenseCamera(StereoCamera, Module): + color_image: Out[Image] + depth_image: Out[Image] + pointcloud: Out[PointCloud2] + camera_info: Out[CameraInfo] + depth_camera_info: Out[CameraInfo] + + config: RealSenseCameraConfig + default_config = RealSenseCameraConfig + + @property + def _camera_link(self) -> str: + return f"{self.config.camera_name}_link" + + @property + def _color_frame(self) -> str: + return f"{self.config.camera_name}_color_frame" + + @property + def _color_optical_frame(self) -> str: + return f"{self.config.camera_name}_color_optical_frame" + + @property + def _depth_frame(self) -> str: + return f"{self.config.camera_name}_depth_frame" + + @property + def _depth_optical_frame(self) -> str: + return f"{self.config.camera_name}_depth_optical_frame" + + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(*args, **kwargs) + self._pipeline: rs.pipeline | None = None + self._profile: rs.pipeline_profile | None = None + self._align: rs.align | None = None + self._running = False + self._thread: threading.Thread | None = None + self._color_camera_info: CameraInfo | None = None + self._depth_camera_info: CameraInfo | None = None + self._depth_scale: float = 0.001 + self._color_to_depth_extrinsics: rs.extrinsics | None = None + # Pointcloud generation state + self._latest_color_img: Image | None = None + self._latest_depth_img: Image | None = None + self._pointcloud_lock = threading.Lock() + + @rpc + def start(self) -> None: + self._pipeline = rs.pipeline() + config = rs.config() + + if self.config.serial_number: + config.enable_device(self.config.serial_number) + + config.enable_stream( + rs.stream.color, + self.config.width, + self.config.height, + rs.format.bgr8, + self.config.fps, + ) + + if self.config.enable_depth: + config.enable_stream( + rs.stream.depth, + self.config.width, + self.config.height, + rs.format.z16, + self.config.fps, + ) + + self._profile = self._pipeline.start(config) + + if self.config.enable_depth: + depth_sensor = self._profile.get_device().first_depth_sensor() + self._depth_scale = depth_sensor.get_depth_scale() + + if self.config.align_depth_to_color and self.config.enable_depth: + self._align = rs.align(rs.stream.color) + + self._build_camera_info() + self._get_extrinsics() + + self._running = True + self._thread = threading.Thread(target=self._capture_loop, daemon=True) + self._thread.start() + + if self.config.enable_pointcloud and self.config.enable_depth: + interval_sec = 1.0 / self.config.pointcloud_fps + self._disposables.add( + backpressure(rx.interval(interval_sec)).subscribe( + on_next=lambda _: self._generate_pointcloud(), + on_error=lambda e: print(f"Pointcloud error: {e}"), + ) + ) + + interval_sec = 1.0 / self.config.camera_info_fps + self._disposables.add( + rx.interval(interval_sec).subscribe( + on_next=lambda _: self._publish_camera_info(), + on_error=lambda e: print(f"CameraInfo error: {e}"), + ) + ) + + def _publish_camera_info(self) -> None: + ts = time.time() + if self._color_camera_info: + self._color_camera_info.ts = ts + self.camera_info.publish(self._color_camera_info) + if self._depth_camera_info: + self._depth_camera_info.ts = ts + self.depth_camera_info.publish(self._depth_camera_info) + + def _build_camera_info(self) -> None: + if self._profile is None: + return + + # Color camera info + color_stream = self._profile.get_stream(rs.stream.color).as_video_stream_profile() + color_intrinsics = color_stream.get_intrinsics() + self._color_camera_info = self._intrinsics_to_camera_info( + color_intrinsics, self._color_optical_frame + ) + + # Depth camera info + if self.config.enable_depth: + if self.config.align_depth_to_color: + # When aligned to color, depth uses color intrinsics and frame + self._depth_camera_info = self._intrinsics_to_camera_info( + color_intrinsics, self._color_optical_frame + ) + else: + depth_stream = self._profile.get_stream(rs.stream.depth).as_video_stream_profile() + depth_intrinsics = depth_stream.get_intrinsics() + self._depth_camera_info = self._intrinsics_to_camera_info( + depth_intrinsics, self._depth_optical_frame + ) + + def _intrinsics_to_camera_info(self, intrinsics: rs.intrinsics, frame_id: str) -> CameraInfo: + fx, fy = intrinsics.fx, intrinsics.fy + cx, cy = intrinsics.ppx, intrinsics.ppy + + K = [fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0] + P = [fx, 0.0, cx, 0.0, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0] + D = list(intrinsics.coeffs) if intrinsics.coeffs else [] + + distortion_model = { + rs.distortion.none: "", + rs.distortion.modified_brown_conrady: "plumb_bob", + rs.distortion.inverse_brown_conrady: "plumb_bob", + rs.distortion.ftheta: "equidistant", + rs.distortion.brown_conrady: "plumb_bob", + rs.distortion.kannala_brandt4: "equidistant", + }.get(intrinsics.model, "") + + return CameraInfo( + height=intrinsics.height, + width=intrinsics.width, + distortion_model=distortion_model, + D=D, + K=K, + P=P, + frame_id=frame_id, + ) + + def _get_extrinsics(self) -> None: + if self._profile is None or not self.config.enable_depth: + return + + depth_stream = self._profile.get_stream(rs.stream.depth) + color_stream = self._profile.get_stream(rs.stream.color) + self._color_to_depth_extrinsics = color_stream.get_extrinsics_to(depth_stream) + + def _extrinsics_to_transform( + self, + extrinsics: rs.extrinsics, + frame_id: str, + child_frame_id: str, + ts: float, + ) -> Transform: + rotation_matrix = np.array(extrinsics.rotation).reshape(3, 3) + quat = Rotation.from_matrix(rotation_matrix).as_quat() # [x, y, z, w] + return Transform( + translation=Vector3(*extrinsics.translation), + rotation=Quaternion(quat[0], quat[1], quat[2], quat[3]), + frame_id=frame_id, + child_frame_id=child_frame_id, + ts=ts, + ) + + def _capture_loop(self) -> None: + while self._running and self._pipeline is not None: + try: + frames = self._pipeline.wait_for_frames(timeout_ms=1000) + except (RuntimeError, AttributeError): + # Pipeline stopped or None - exit loop + break + + ts = time.time() + + if self._align is not None: + frames = self._align.process(frames) + + color_frame = frames.get_color_frame() + depth_frame = frames.get_depth_frame() if self.config.enable_depth else None + + # Process color + color_img = None + if color_frame: + color_data = np.asanyarray(color_frame.get_data()) + color_data = cv2.cvtColor(color_data, cv2.COLOR_BGR2RGB) + color_img = Image( + data=color_data, + format=ImageFormat.RGB, + frame_id=self._color_optical_frame, + ts=ts, + ) + self.color_image.publish(color_img) + + # Process depth + depth_img = None + if depth_frame: + depth_data = np.asanyarray(depth_frame.get_data()) + # When aligned, depth is in color optical frame + depth_frame_id = ( + self._color_optical_frame + if self.config.align_depth_to_color + else self._depth_optical_frame + ) + depth_img = Image( + data=depth_data, + format=ImageFormat.DEPTH16, + frame_id=depth_frame_id, + ts=ts, + ) + self.depth_image.publish(depth_img) + + # Store latest images for pointcloud generation + if self.config.enable_pointcloud and color_img is not None and depth_img is not None: + with self._pointcloud_lock: + self._latest_color_img = color_img + self._latest_depth_img = depth_img + + # Publish TF + self._publish_tf(ts) + + def _publish_tf(self, ts: float) -> None: + transforms = [] + + # base_link -> camera_link (user-provided mounting transform) + if self.config.base_transform is not None: + base_to_camera = Transform( + translation=self.config.base_transform.translation, + rotation=self.config.base_transform.rotation, + frame_id=self.config.base_frame_id, + child_frame_id=self._camera_link, + ts=ts, + ) + transforms.append(base_to_camera) + + # camera_link -> camera_depth_frame (identity, depth is at camera_link origin) + camera_link_to_depth = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id=self._camera_link, + child_frame_id=self._depth_frame, + ts=ts, + ) + transforms.append(camera_link_to_depth) + + # camera_depth_frame -> camera_depth_optical_frame + depth_to_depth_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=OPTICAL_ROTATION, + frame_id=self._depth_frame, + child_frame_id=self._depth_optical_frame, + ts=ts, + ) + transforms.append(depth_to_depth_optical) + + color_tf = self._extrinsics_to_transform( + self._color_to_depth_extrinsics, + self._camera_link, + self._color_frame, + ts, + ) + # Invert the transform since extrinsics are color->depth + color_tf = color_tf.inverse() + color_tf.frame_id = self._camera_link + color_tf.child_frame_id = self._color_frame + color_tf.ts = ts + transforms.append(color_tf) + + # camera_color_frame -> camera_color_optical_frame + color_to_color_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=OPTICAL_ROTATION, + frame_id=self._color_frame, + child_frame_id=self._color_optical_frame, + ts=ts, + ) + transforms.append(color_to_color_optical) + + self.tf.publish(*transforms) + + def _generate_pointcloud(self) -> None: + """Generate and publish pointcloud from latest images (called by rx.interval).""" + with self._pointcloud_lock: + color_img = self._latest_color_img + depth_img = self._latest_depth_img + + if color_img is None or depth_img is None or self._color_camera_info is None: + return + + try: + pcd = PointCloud2.from_rgbd( + color_image=color_img, + depth_image=depth_img, + camera_info=self._color_camera_info, + depth_scale=self._depth_scale, + ) + pcd = pcd.voxel_downsample(0.005) + self.pointcloud.publish(pcd) + except Exception as e: + print(f"Pointcloud generation error: {e}") + + @rpc + def stop(self) -> None: + self._running = False + + # Stop pipeline first to unblock wait_for_frames() + if self._pipeline: + try: + self._pipeline.stop() + except Exception: + pass # Pipeline might already be stopped + self._pipeline = None + + # Now join the thread (should exit quickly since pipeline is stopped) + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=2.0) + if self._thread.is_alive(): + # Force thread termination by clearing reference + self._thread = None + + self._profile = None + self._align = None + self._color_to_depth_extrinsics = None + self._latest_color_img = None + self._latest_depth_img = None + super().stop() + + @rpc + def get_color_camera_info(self) -> CameraInfo | None: + return self._color_camera_info + + @rpc + def get_depth_camera_info(self) -> CameraInfo | None: + return self._depth_camera_info + + @rpc + def get_depth_scale(self) -> float: + return self._depth_scale + + +def main() -> None: + dimos = ModuleCoordinator(n=2) + dimos.start() + + camera = dimos.deploy(RealSenseCamera, enable_pointcloud=True, pointcloud_fps=5.0) # type: ignore[type-var] + foxglove_bridge = FoxgloveBridge() + foxglove_bridge.start() + + camera.color_image.transport = LCMTransport("/camera/color", Image) + camera.depth_image.transport = LCMTransport("/camera/depth", Image) + camera.pointcloud.transport = LCMTransport("/camera/pointcloud", PointCloud2) + camera.camera_info.transport = LCMTransport("/camera/color_info", CameraInfo) + camera.depth_camera_info.transport = LCMTransport("/camera/depth_info", CameraInfo) + + def cleanup() -> None: + try: + dimos.stop() + except Exception: + pass + + atexit.register(cleanup) + dimos.start_all_modules() + + try: + while True: + time.sleep(0.1) + except (KeyboardInterrupt, SystemExit): + pass + finally: + atexit.unregister(cleanup) + cleanup() + + +if __name__ == "__main__": + main() + + +realsense_camera = RealSenseCamera.blueprint + +__all__ = ["RealSenseCamera", "RealSenseCameraConfig", "realsense_camera"] diff --git a/dimos/hardware/sensors/camera/spec.py b/dimos/hardware/sensors/camera/spec.py new file mode 100644 index 0000000000..f7e81f472a --- /dev/null +++ b/dimos/hardware/sensors/camera/spec.py @@ -0,0 +1,103 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from typing import Generic, Protocol, TypeVar + +from reactivex.observable import Observable + +from dimos.msgs.geometry_msgs import Quaternion, Transform +from dimos.msgs.sensor_msgs import CameraInfo +from dimos.msgs.sensor_msgs.Image import Image +from dimos.protocol.service import Configurable # type: ignore[attr-defined] + +OPTICAL_ROTATION = Quaternion(-0.5, 0.5, -0.5, 0.5) + + +class CameraConfig(Protocol): + frame_id_prefix: str | None + + +CameraConfigT = TypeVar("CameraConfigT", bound=CameraConfig) + + +class CameraHardware(ABC, Configurable[CameraConfigT], Generic[CameraConfigT]): + @abstractmethod + def image_stream(self) -> Observable[Image]: + pass + + @property + @abstractmethod + def camera_info(self) -> CameraInfo: + pass + + +class StereoCameraConfig(Protocol): + """Protocol for stereo camera configuration.""" + + width: int + height: int + fps: int + camera_name: str + base_frame_id: str + base_transform: Transform | None + align_depth_to_color: bool + enable_depth: bool + enable_pointcloud: bool + pointcloud_fps: float + camera_info_fps: float + + +class StereoCamera(ABC): + """Abstract class for stereo camera modules (RealSense, ZED, etc.).""" + + @abstractmethod + def get_color_camera_info(self) -> CameraInfo | None: + """Get color camera intrinsics.""" + pass + + @abstractmethod + def get_depth_camera_info(self) -> CameraInfo | None: + """Get depth camera intrinsics.""" + pass + + @abstractmethod + def get_depth_scale(self) -> float: + """Get the depth scale factor (meters per unit).""" + pass + + @property + @abstractmethod + def _camera_link(self) -> str: + pass + + @property + @abstractmethod + def _color_frame(self) -> str: + pass + + @property + @abstractmethod + def _color_optical_frame(self) -> str: + pass + + @property + @abstractmethod + def _depth_frame(self) -> str: + pass + + @property + @abstractmethod + def _depth_optical_frame(self) -> str: + pass diff --git a/dimos/hardware/sensors/camera/test_webcam.py b/dimos/hardware/sensors/camera/test_webcam.py new file mode 100644 index 0000000000..0d1a1d0040 --- /dev/null +++ b/dimos/hardware/sensors/camera/test_webcam.py @@ -0,0 +1,60 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import pytest + +from dimos import core +from dimos.hardware.sensors.camera import zed +from dimos.hardware.sensors.camera.module import CameraModule +from dimos.hardware.sensors.camera.webcam import Webcam +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import CameraInfo, Image + + +@pytest.fixture +def dimos(): + dimos_instance = core.start(1) + yield dimos_instance + dimos_instance.stop() + + +@pytest.mark.tool +def test_streaming_single(dimos) -> None: + camera = dimos.deploy( + CameraModule, + transform=Transform( + translation=Vector3(0.05, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="sensor", + child_frame_id="camera_link", + ), + hardware=lambda: Webcam( + camera_index=0, + frequency=0.0, # full speed but set something to test sharpness barrier + camera_info=zed.CameraInfo.SingleWebcam, + ), + ) + + camera.color_image.transport = core.LCMTransport("/color_image", Image) + camera.camera_info.transport = core.LCMTransport("/camera_info", CameraInfo) + camera.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + camera.stop() + dimos.stop() diff --git a/dimos/hardware/camera/webcam.py b/dimos/hardware/sensors/camera/webcam.py similarity index 86% rename from dimos/hardware/camera/webcam.py rename to dimos/hardware/sensors/camera/webcam.py index 0f68989002..d0735f4597 100644 --- a/dimos/hardware/camera/webcam.py +++ b/dimos/hardware/sensors/camera/webcam.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ from reactivex import create from reactivex.observable import Observable -from dimos.hardware.camera.spec import CameraConfig, CameraHardware +from dimos.hardware.sensors.camera.spec import CameraConfig, CameraHardware from dimos.msgs.sensor_msgs import Image from dimos.msgs.sensor_msgs.Image import ImageFormat from dimos.utils.reactive import backpressure @@ -43,7 +43,7 @@ class WebcamConfig(CameraConfig): class Webcam(CameraHardware[WebcamConfig]): default_config = WebcamConfig - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) self._capture = None self._capture_thread = None @@ -54,13 +54,13 @@ def __init__(self, *args, **kwargs) -> None: def image_stream(self) -> Observable[Image]: """Create an observable that starts/stops camera on subscription""" - def subscribe(observer, scheduler=None): + def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] # Store the observer so emit() can use it self._observer = observer # Start the camera when someone subscribes try: - self.start() + self.start() # type: ignore[no-untyped-call] except Exception as e: observer.on_error(e) return @@ -74,23 +74,23 @@ def dispose() -> None: return backpressure(create(subscribe)) - def start(self): + def start(self): # type: ignore[no-untyped-def] if self._capture_thread and self._capture_thread.is_alive(): return # Open the video capture - self._capture = cv2.VideoCapture(self.config.camera_index) - if not self._capture.isOpened(): + self._capture = cv2.VideoCapture(self.config.camera_index) # type: ignore[assignment] + if not self._capture.isOpened(): # type: ignore[attr-defined] raise RuntimeError(f"Failed to open camera {self.config.camera_index}") # Set camera properties - self._capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.config.frame_width) - self._capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config.frame_height) + self._capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.config.frame_width) # type: ignore[attr-defined] + self._capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config.frame_height) # type: ignore[attr-defined] # Clear stop event and start the capture thread self._stop_event.clear() - self._capture_thread = threading.Thread(target=self._capture_loop, daemon=True) - self._capture_thread.start() + self._capture_thread = threading.Thread(target=self._capture_loop, daemon=True) # type: ignore[assignment] + self._capture_thread.start() # type: ignore[attr-defined] def stop(self) -> None: """Stop capturing frames""" @@ -106,7 +106,7 @@ def stop(self) -> None: self._capture.release() self._capture = None - def _frame(self, frame: str): + def _frame(self, frame: str): # type: ignore[no-untyped-def] if not self.config.frame_id_prefix: return frame else: @@ -114,7 +114,7 @@ def _frame(self, frame: str): def capture_frame(self) -> Image: # Read frame - ret, frame = self._capture.read() + ret, frame = self._capture.read() # type: ignore[attr-defined] if not ret: raise RuntimeError(f"Failed to read frame from camera {self.config.camera_index}") diff --git a/dimos/hardware/sensors/camera/zed/__init__.py b/dimos/hardware/sensors/camera/zed/__init__.py new file mode 100644 index 0000000000..dd23096c1d --- /dev/null +++ b/dimos/hardware/sensors/camera/zed/__init__.py @@ -0,0 +1,57 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ZED camera hardware interfaces.""" + +from pathlib import Path + +from dimos.msgs.sensor_msgs.CameraInfo import CalibrationProvider + +# Check if ZED SDK is available +try: + import pyzed.sl as sl + + HAS_ZED_SDK = True +except ImportError: + HAS_ZED_SDK = False + +# Only import ZED classes if SDK is available +if HAS_ZED_SDK: + from dimos.hardware.sensors.camera.zed.camera import ZEDCamera, ZEDModule, zed_camera +else: + # Provide stub classes when SDK is not available + class ZEDCamera: # type: ignore[no-redef] + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + raise ImportError( + "ZED SDK not installed. Please install pyzed package to use ZED camera functionality." + ) + + class ZEDModule: # type: ignore[no-redef] + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + raise ImportError( + "ZED SDK not installed. Please install pyzed package to use ZED camera functionality." + ) + + +# Set up camera calibration provider (always available) +CALIBRATION_DIR = Path(__file__).parent +CameraInfo = CalibrationProvider(CALIBRATION_DIR) + +__all__ = [ + "HAS_ZED_SDK", + "CameraInfo", + "ZEDCamera", + "ZEDModule", + "zed_camera", +] diff --git a/dimos/hardware/sensors/camera/zed/camera.py b/dimos/hardware/sensors/camera/zed/camera.py new file mode 100644 index 0000000000..4fc46e701d --- /dev/null +++ b/dimos/hardware/sensors/camera/zed/camera.py @@ -0,0 +1,530 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import atexit +from dataclasses import dataclass, field +import threading +import time + +import cv2 +import pyzed.sl as sl +import reactivex as rx + +from dimos.core import Module, ModuleConfig, Out, rpc +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.transport import LCMTransport +from dimos.hardware.sensors.camera.spec import ( + OPTICAL_ROTATION, + StereoCamera, + StereoCameraConfig, +) +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import CameraInfo +from dimos.msgs.sensor_msgs.Image import Image, ImageFormat +from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 +from dimos.robot.foxglove_bridge import FoxgloveBridge +from dimos.utils.reactive import backpressure + + +def default_base_transform() -> Transform: + """Default identity transform for camera mounting.""" + return Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + ) + + +@dataclass +class ZEDCameraConfig(ModuleConfig, StereoCameraConfig): + width: int = 1280 + height: int = 720 + fps: int = 15 + camera_name: str = "camera" + base_frame_id: str = "base_link" + base_transform: Transform | None = field(default_factory=default_base_transform) + align_depth_to_color: bool = True + enable_depth: bool = True + enable_pointcloud: bool = False + pointcloud_fps: float = 5.0 + camera_info_fps: float = 1.0 + camera_id: int = 0 + serial_number: int | str | None = None + resolution: str | None = None + depth_mode: str | sl.DEPTH_MODE = "NEURAL" + enable_fill_mode: bool = False + enable_tracking: bool = True + enable_imu_fusion: bool = True + enable_pose_smoothing: bool = True + enable_area_memory: bool = False + set_floor_as_origin: bool = True + world_frame: str = "world" + + +class ZEDCamera(StereoCamera, Module): + color_image: Out[Image] + depth_image: Out[Image] + pointcloud: Out[PointCloud2] + camera_info: Out[CameraInfo] + depth_camera_info: Out[CameraInfo] + + config: ZEDCameraConfig + default_config = ZEDCameraConfig + + @property + def _camera_link(self) -> str: + return f"{self.config.camera_name}_link" + + @property + def _color_frame(self) -> str: + return f"{self.config.camera_name}_color_frame" + + @property + def _color_optical_frame(self) -> str: + return f"{self.config.camera_name}_color_optical_frame" + + @property + def _depth_frame(self) -> str: + return f"{self.config.camera_name}_depth_frame" + + @property + def _depth_optical_frame(self) -> str: + return f"{self.config.camera_name}_depth_optical_frame" + + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(*args, **kwargs) + self._zed: sl.Camera | None = None + self._init_params: sl.InitParameters | None = None + self._runtime_params: sl.RuntimeParameters | None = None + self._running = False + self._thread: threading.Thread | None = None + self._color_camera_info: CameraInfo | None = None + self._depth_camera_info: CameraInfo | None = None + self._depth_scale: float = 1.0 + self._camera_link_to_color_extrinsics: sl.Transform + self._latest_color_img: Image | None = None + self._latest_depth_img: Image | None = None + self._pointcloud_lock = threading.Lock() + self._image_left: sl.Mat | None = None + self._depth_map: sl.Mat | None = None + self._pose: sl.Pose | None = None + self._tracking_enabled = False + self._stream_width = self.config.width + self._stream_height = self.config.height + self._camera_info: sl.CameraInformation | None = None + + def _publish_camera_info(self) -> None: + ts = time.time() + if self._color_camera_info: + self._color_camera_info.ts = ts + self.camera_info.publish(self._color_camera_info) + if self._depth_camera_info: + self._depth_camera_info.ts = ts + self.depth_camera_info.publish(self._depth_camera_info) + + @rpc + def start(self) -> None: + self._zed = sl.Camera() + self._init_params = sl.InitParameters() + if self.config.resolution: + self._init_params.camera_resolution = getattr(sl.RESOLUTION, self.config.resolution) + else: + self._init_params.camera_resolution = sl.RESOLUTION.HD720 + self._init_params.camera_fps = self.config.fps + if isinstance(self.config.depth_mode, sl.DEPTH_MODE): + self._init_params.depth_mode = self.config.depth_mode + else: + self._init_params.depth_mode = getattr(sl.DEPTH_MODE, self.config.depth_mode) + self._init_params.coordinate_system = sl.COORDINATE_SYSTEM.RIGHT_HANDED_Z_UP_X_FWD + self._init_params.coordinate_units = sl.UNIT.METER + if self.config.serial_number is not None: + self._init_params.set_from_serial_number(int(self.config.serial_number)) + else: + self._init_params.set_from_camera_id(self.config.camera_id) + + err = self._zed.open(self._init_params) + if err != sl.ERROR_CODE.SUCCESS: + self._zed = None + raise RuntimeError(f"Failed to open ZED camera: {err}") + + self._runtime_params = sl.RuntimeParameters() + self._runtime_params.enable_fill_mode = self.config.enable_fill_mode + self._image_left = sl.Mat() + self._depth_map = sl.Mat() + self._pose = sl.Pose() + + self._camera_info = self._zed.get_camera_information() + if self._camera_info is not None: + self._stream_width = self._camera_info.camera_configuration.resolution.width + self._stream_height = self._camera_info.camera_configuration.resolution.height + + self._build_camera_info() + self._get_extrinsics() + + if self.config.enable_tracking: + self._enable_tracking() + + interval_sec = 1.0 / self.config.camera_info_fps + self._disposables.add( + rx.interval(interval_sec).subscribe( + on_next=lambda _: self._publish_camera_info(), + on_error=lambda e: print(f"CameraInfo error: {e}"), + ) + ) + + self._running = True + self._thread = threading.Thread(target=self._capture_loop, daemon=True) + self._thread.start() + + if self.config.enable_pointcloud and self.config.enable_depth: + interval_sec = 1.0 / self.config.pointcloud_fps + self._disposables.add( + backpressure(rx.interval(interval_sec)).subscribe( + on_next=lambda _: self._generate_pointcloud(), + on_error=lambda e: print(f"Pointcloud error: {e}"), + ) + ) + + def _build_camera_info(self) -> None: + if self._camera_info is None: + return + calib = self._camera_info.camera_configuration.calibration_parameters + left_cam = calib.left_cam + + self._color_camera_info = self._intrinsics_to_camera_info( + left_cam, self._color_optical_frame + ) + + if self.config.enable_depth: + depth_frame = ( + self._color_optical_frame + if self.config.align_depth_to_color + else self._depth_optical_frame + ) + self._depth_camera_info = self._intrinsics_to_camera_info(left_cam, depth_frame) + + def _intrinsics_to_camera_info( + self, intrinsics: sl.CameraParameters, frame_id: str + ) -> CameraInfo: + fx, fy = intrinsics.fx, intrinsics.fy + cx, cy = intrinsics.cx, intrinsics.cy + + K = [fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0] + P = [fx, 0.0, cx, 0.0, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0] + D = list(intrinsics.disto) + + return CameraInfo( + height=self._stream_height, + width=self._stream_width, + distortion_model="plumb_bob", + D=D, + K=K, + P=P, + frame_id=frame_id, + ) + + def _get_extrinsics(self) -> None: + if self._camera_info is None: + return + sensors_config = self._camera_info.sensors_configuration + # camera_imu_transform gives the transform from IMU (body center) to left camera + self._camera_link_to_color_extrinsics = sensors_config.camera_imu_transform + + def _extrinsics_to_transform( + self, + extrinsics: sl.Transform, + frame_id: str, + child_frame_id: str, + ts: float, + ) -> Transform: + translation = extrinsics.get_translation().get() + quat = extrinsics.get_orientation().get() # [x, y, z, w] + return Transform( + translation=Vector3(*translation), + rotation=Quaternion(quat[0], quat[1], quat[2], quat[3]), + frame_id=frame_id, + child_frame_id=child_frame_id, + ts=ts, + ) + + def _enable_tracking(self) -> None: + if self._zed is None: + return + tracking_params = sl.PositionalTrackingParameters() + tracking_params.enable_area_memory = self.config.enable_area_memory + tracking_params.enable_pose_smoothing = self.config.enable_pose_smoothing + tracking_params.enable_imu_fusion = self.config.enable_imu_fusion + tracking_params.set_floor_as_origin = self.config.set_floor_as_origin + err = self._zed.enable_positional_tracking(tracking_params) + if err != sl.ERROR_CODE.SUCCESS: + print(f"Failed to enable positional tracking: {err}") + self._tracking_enabled = False + return + self._tracking_enabled = True + + def _capture_loop(self) -> None: + while self._running and self._zed is not None: + try: + err = self._zed.grab(self._runtime_params) + except Exception: + break + + if err != sl.ERROR_CODE.SUCCESS: + if not self._running: + break + time.sleep(0.001) + continue + + ts = time.time() + + color_img = None + if self._image_left is not None: + self._zed.retrieve_image(self._image_left, sl.VIEW.LEFT) + color_data = self._image_left.get_data() + if color_data.ndim == 3 and color_data.shape[2] == 4: + color_data = color_data[:, :, :3] + color_data = cv2.cvtColor(color_data, cv2.COLOR_BGR2RGB) + color_img = Image( + data=color_data, + format=ImageFormat.RGB, + frame_id=self._color_optical_frame, + ts=ts, + ) + self.color_image.publish(color_img) + + depth_img = None + if self.config.enable_depth and self._depth_map is not None: + self._zed.retrieve_measure(self._depth_map, sl.MEASURE.DEPTH) + depth_data = self._depth_map.get_data() + if depth_data.ndim == 3: + depth_data = depth_data[:, :, 0] + depth_frame_id = ( + self._color_optical_frame + if self.config.align_depth_to_color + else self._depth_optical_frame + ) + depth_img = Image( + data=depth_data, + format=ImageFormat.DEPTH, + frame_id=depth_frame_id, + ts=ts, + ) + self.depth_image.publish(depth_img) + + if self.config.enable_pointcloud and color_img is not None and depth_img is not None: + with self._pointcloud_lock: + self._latest_color_img = color_img + self._latest_depth_img = depth_img + + self._publish_tf(ts) + + def _tracking_transform(self, ts: float) -> Transform | None: + if not self._tracking_enabled or self._zed is None or self._pose is None: + return None + state = self._zed.get_position(self._pose, sl.REFERENCE_FRAME.WORLD) + if state != sl.POSITIONAL_TRACKING_STATE.OK: + return None + + translation = self._pose.get_translation().get().tolist() + rotation = self._pose.get_orientation().get().tolist() + world_to_camera = Transform( + translation=Vector3(*translation), + rotation=Quaternion(*rotation), + frame_id=self.config.world_frame, + child_frame_id=self._camera_link, + ts=ts, + ) + if self.config.base_transform is None: + return world_to_camera + + base_to_camera = Transform( + translation=self.config.base_transform.translation, + rotation=self.config.base_transform.rotation, + frame_id=self.config.base_frame_id, + child_frame_id=self._camera_link, + ts=ts, + ) + camera_to_base = base_to_camera.inverse() + world_to_base = world_to_camera + camera_to_base + world_to_base.frame_id = self.config.world_frame + world_to_base.child_frame_id = self.config.base_frame_id + world_to_base.ts = ts + return world_to_base + + def _publish_tf(self, ts: float) -> None: + transforms = [] + + if self.config.base_transform is not None: + base_to_camera = Transform( + translation=self.config.base_transform.translation, + rotation=self.config.base_transform.rotation, + frame_id=self.config.base_frame_id, + child_frame_id=self._camera_link, + ts=ts, + ) + transforms.append(base_to_camera) + + # camera_imu_transform is IMU -> left_camera (coordinate transform), + # we need to invert to get the pose of left camera in camera_link frame + camera_link_to_depth = self._extrinsics_to_transform( + self._camera_link_to_color_extrinsics, + self._camera_link, + self._depth_frame, + ts, + ).inverse() + camera_link_to_depth.frame_id = self._camera_link + camera_link_to_depth.child_frame_id = self._depth_frame + transforms.append(camera_link_to_depth) + + depth_to_depth_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=OPTICAL_ROTATION, + frame_id=self._depth_frame, + child_frame_id=self._depth_optical_frame, + ts=ts, + ) + transforms.append(depth_to_depth_optical) + + color_tf = self._extrinsics_to_transform( + self._camera_link_to_color_extrinsics, + self._camera_link, + self._color_frame, + ts, + ).inverse() + color_tf.frame_id = self._camera_link + color_tf.child_frame_id = self._color_frame + transforms.append(color_tf) + + color_to_color_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=OPTICAL_ROTATION, + frame_id=self._color_frame, + child_frame_id=self._color_optical_frame, + ts=ts, + ) + transforms.append(color_to_color_optical) + + tracking_tf = self._tracking_transform(ts) + if tracking_tf is not None: + transforms.append(tracking_tf) + + self.tf.publish(*transforms) + + def _generate_pointcloud(self) -> None: + with self._pointcloud_lock: + color_img = self._latest_color_img + depth_img = self._latest_depth_img + + if color_img is None or depth_img is None or self._color_camera_info is None: + return + + try: + pcd = PointCloud2.from_rgbd( + color_image=color_img, + depth_image=depth_img, + camera_info=self._color_camera_info, + depth_scale=self._depth_scale, + ) + pcd = pcd.voxel_downsample(0.005) + self.pointcloud.publish(pcd) + except Exception as e: + print(f"Pointcloud generation error: {e}") + + @rpc + def stop(self) -> None: + self._running = False + + if self._zed: + if self._tracking_enabled: + try: + self._zed.disable_positional_tracking() + except Exception: + pass + try: + self._zed.close() + except Exception: + pass + self._zed = None + + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=2.0) + if self._thread.is_alive(): + self._thread = None + + self._color_camera_info = None + self._depth_camera_info = None + self._latest_color_img = None + self._latest_depth_img = None + self._image_left = None + self._depth_map = None + self._pose = None + self._camera_info = None + self._tracking_enabled = False + super().stop() + + @rpc + def get_color_camera_info(self) -> CameraInfo | None: + return self._color_camera_info + + @rpc + def get_depth_camera_info(self) -> CameraInfo | None: + return self._depth_camera_info + + @rpc + def get_depth_scale(self) -> float: + return self._depth_scale + + +def main() -> None: + dimos = ModuleCoordinator(n=2) + dimos.start() + + camera = dimos.deploy(ZEDCamera, enable_pointcloud=True, pointcloud_fps=5.0) # type: ignore[type-var] + foxglove_bridge = FoxgloveBridge() + foxglove_bridge.start() + + camera.color_image.transport = LCMTransport("/camera/color", Image) + camera.depth_image.transport = LCMTransport("/camera/depth", Image) + camera.pointcloud.transport = LCMTransport("/camera/pointcloud", PointCloud2) + camera.camera_info.transport = LCMTransport("/camera/color_info", CameraInfo) + camera.depth_camera_info.transport = LCMTransport("/camera/depth_info", CameraInfo) + + def cleanup() -> None: + try: + dimos.stop() + except Exception: + pass + + atexit.register(cleanup) + dimos.start_all_modules() + + try: + while True: + time.sleep(0.1) + except (KeyboardInterrupt, SystemExit): + pass + finally: + atexit.unregister(cleanup) + cleanup() + + +if __name__ == "__main__": + main() + + +ZEDModule = ZEDCamera +zed_camera = ZEDCamera.blueprint + +__all__ = ["ZEDCamera", "ZEDCameraConfig", "ZEDModule", "zed_camera"] diff --git a/dimos/hardware/camera/zed/single_webcam.yaml b/dimos/hardware/sensors/camera/zed/single_webcam.yaml similarity index 100% rename from dimos/hardware/camera/zed/single_webcam.yaml rename to dimos/hardware/sensors/camera/zed/single_webcam.yaml diff --git a/dimos/hardware/camera/zed/test_zed.py b/dimos/hardware/sensors/camera/zed/test_zed.py similarity index 94% rename from dimos/hardware/camera/zed/test_zed.py rename to dimos/hardware/sensors/camera/zed/test_zed.py index 33810d3c2a..2d912553c6 100644 --- a/dimos/hardware/camera/zed/test_zed.py +++ b/dimos/hardware/sensors/camera/zed/test_zed.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ def test_zed_import_and_calibration_access() -> None: """Test that zed module can be imported and calibrations accessed.""" # Import zed module from camera - from dimos.hardware.camera import zed + from dimos.hardware.sensors.camera import zed # Test that CameraInfo is accessible assert hasattr(zed, "CameraInfo") diff --git a/dimos/hardware/fake_zed_module.py b/dimos/hardware/sensors/fake_zed_module.py similarity index 87% rename from dimos/hardware/fake_zed_module.py rename to dimos/hardware/sensors/fake_zed_module.py index c4c46c33b3..e8fc51bf31 100644 --- a/dimos/hardware/fake_zed_module.py +++ b/dimos/hardware/sensors/fake_zed_module.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,13 +17,14 @@ FakeZEDModule - Replays recorded ZED data for testing without hardware. """ +from dataclasses import dataclass import functools import logging from dimos_lcm.sensor_msgs import CameraInfo import numpy as np -from dimos.core import Module, Out, rpc +from dimos.core import Module, ModuleConfig, Out, rpc from dimos.msgs.geometry_msgs import PoseStamped from dimos.msgs.sensor_msgs import Image, ImageFormat from dimos.msgs.std_msgs import Header @@ -31,32 +32,38 @@ from dimos.utils.logging_config import setup_logger from dimos.utils.testing import TimedSensorReplay -logger = setup_logger(__name__, level=logging.INFO) +logger = setup_logger(level=logging.INFO) -class FakeZEDModule(Module): +@dataclass +class FakeZEDModuleConfig(ModuleConfig): + frame_id: str = "zed_camera" + + +class FakeZEDModule(Module[FakeZEDModuleConfig]): """ Fake ZED module that replays recorded data instead of real camera. """ # Define LCM outputs (same as ZEDModule) - color_image: Out[Image] = None - depth_image: Out[Image] = None - camera_info: Out[CameraInfo] = None - pose: Out[PoseStamped] = None + color_image: Out[Image] + depth_image: Out[Image] + camera_info: Out[CameraInfo] + pose: Out[PoseStamped] + + default_config = FakeZEDModuleConfig + config: FakeZEDModuleConfig - def __init__(self, recording_path: str, frame_id: str = "zed_camera", **kwargs) -> None: + def __init__(self, recording_path: str, **kwargs: object) -> None: """ Initialize FakeZEDModule with recording path. Args: recording_path: Path to recorded data directory - frame_id: TF frame ID for messages """ super().__init__(**kwargs) self.recording_path = recording_path - self.frame_id = frame_id self._running = False # Initialize TF publisher @@ -65,11 +72,11 @@ def __init__(self, recording_path: str, frame_id: str = "zed_camera", **kwargs) logger.info(f"FakeZEDModule initialized with recording: {self.recording_path}") @functools.cache - def _get_color_stream(self): + def _get_color_stream(self): # type: ignore[no-untyped-def] """Get cached color image stream.""" logger.info(f"Loading color image stream from {self.recording_path}/color") - def image_autocast(x): + def image_autocast(x): # type: ignore[no-untyped-def] """Convert raw numpy array to Image.""" if isinstance(x, np.ndarray): return Image(data=x, format=ImageFormat.RGB) @@ -81,11 +88,11 @@ def image_autocast(x): return color_replay.stream() @functools.cache - def _get_depth_stream(self): + def _get_depth_stream(self): # type: ignore[no-untyped-def] """Get cached depth image stream.""" logger.info(f"Loading depth image stream from {self.recording_path}/depth") - def depth_autocast(x): + def depth_autocast(x): # type: ignore[no-untyped-def] """Convert raw numpy array to depth Image.""" if isinstance(x, np.ndarray): # Depth images are float32 @@ -98,11 +105,11 @@ def depth_autocast(x): return depth_replay.stream() @functools.cache - def _get_pose_stream(self): + def _get_pose_stream(self): # type: ignore[no-untyped-def] """Get cached pose stream.""" logger.info(f"Loading pose stream from {self.recording_path}/pose") - def pose_autocast(x): + def pose_autocast(x): # type: ignore[no-untyped-def] """Convert raw pose dict to PoseStamped.""" if isinstance(x, dict): import time @@ -120,11 +127,11 @@ def pose_autocast(x): return pose_replay.stream() @functools.cache - def _get_camera_info_stream(self): + def _get_camera_info_stream(self): # type: ignore[no-untyped-def] """Get cached camera info stream.""" logger.info(f"Loading camera info stream from {self.recording_path}/camera_info") - def camera_info_autocast(x): + def camera_info_autocast(x): # type: ignore[no-untyped-def] """Convert raw camera info dict to CameraInfo message.""" if isinstance(x, dict): # Extract calibration parameters @@ -262,7 +269,7 @@ def stop(self) -> None: super().stop() - def _publish_pose(self, msg) -> None: + def _publish_pose(self, msg) -> None: # type: ignore[no-untyped-def] """Publish pose and TF transform.""" if msg: self.pose.publish(msg) diff --git a/dimos/hardware/ufactory.py b/dimos/hardware/ufactory.py deleted file mode 100644 index 57caf2e3bd..0000000000 --- a/dimos/hardware/ufactory.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.hardware.end_effector import EndEffector - - -class UFactoryEndEffector(EndEffector): - def __init__(self, model=None, **kwargs) -> None: - super().__init__(**kwargs) - self.model = model - - def get_model(self): - return self.model - - -class UFactory7DOFArm: - def __init__(self, arm_length=None) -> None: - self.arm_length = arm_length - - def get_arm_length(self): - return self.arm_length diff --git a/dimos/manipulation/control/__init__.py b/dimos/manipulation/control/__init__.py new file mode 100644 index 0000000000..ec85660eb3 --- /dev/null +++ b/dimos/manipulation/control/__init__.py @@ -0,0 +1,48 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manipulation Control Modules + +Hardware-agnostic controllers for robotic manipulation tasks. + +Submodules: +- servo_control: Real-time servo-level controllers (Cartesian motion control) +- trajectory_controller: Trajectory planning and execution +""" + +# Re-export from servo_control for backwards compatibility +from dimos.manipulation.control.servo_control import ( + CartesianMotionController, + CartesianMotionControllerConfig, + cartesian_motion_controller, +) + +# Re-export from trajectory_controller +from dimos.manipulation.control.trajectory_controller import ( + JointTrajectoryController, + JointTrajectoryControllerConfig, + joint_trajectory_controller, +) + +__all__ = [ + # Servo control + "CartesianMotionController", + "CartesianMotionControllerConfig", + # Trajectory control + "JointTrajectoryController", + "JointTrajectoryControllerConfig", + "cartesian_motion_controller", + "joint_trajectory_controller", +] diff --git a/dimos/manipulation/control/servo_control/README.md b/dimos/manipulation/control/servo_control/README.md new file mode 100644 index 0000000000..fb11fdb2a4 --- /dev/null +++ b/dimos/manipulation/control/servo_control/README.md @@ -0,0 +1,477 @@ +# Cartesian Motion Controller + +Hardware-agnostic Cartesian space motion controller for robotic manipulators. + +## Overview + +The `CartesianMotionController` provides closed-loop Cartesian pose tracking by: +1. **Subscribing** to target poses (PoseStamped) +2. **Computing** Cartesian error (position + orientation) +3. **Generating** velocity commands using PID control +4. **Converting** to joint space via IK +5. **Publishing** joint commands to the hardware driver + +## Architecture + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ TargetSetter (Interactive CLI) │ +│ - User inputs target positions │ +│ - Preserves orientation when left blank │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ PoseStamped (/target_pose) + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ CartesianMotionController │ +│ - Computes FK (current pose) │ +│ - Computes Cartesian error │ +│ - PID control → Cartesian velocity │ +│ - Integrates velocity → next desired pose │ +│ - Computes IK → target joint angles │ +│ - Publishes current pose for feedback │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ JointCommand │ PoseStamped + │ │ (current_pose) + ā–¼ ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” (back to TargetSetter +│ Hardware Driver (xArm, etc.) │ for orientation preservation) +│ - 100Hz control loop │ +│ - Sends commands to robot │ +│ - Publishes JointState │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ JointState + │ (feedback) + ā–¼ + (back to controller) +``` + +## Key Features + +### āœ“ Hardware Agnostic +- Works with **any** arm driver implementing `ArmDriverSpec` protocol +- Only requires `get_inverse_kinematics()` and `get_forward_kinematics()` RPC methods +- Supports xArm, Piper, UR, Franka, or custom arms + +### āœ“ PID-Based Control +- Separate PIDs for position (X, Y, Z) and orientation (roll, pitch, yaw) +- Configurable gains and velocity limits +- Smooth, stable motion with damping + +### āœ“ Safety Features +- Configurable position/orientation error limits +- Automatic emergency stop on excessive errors +- Command timeout detection +- Convergence monitoring + +### āœ“ Flexible Input +- RPC method: `set_target_pose(position, orientation, frame_id)` +- Topic subscription: `target_pose` (PoseStamped messages) +- Supports both Euler angles and quaternions + +## Usage + +### Basic Example + +```python +from dimos.hardware.manipulators.xarm import XArmDriver, XArmDriverConfig +from dimos.manipulation.control import CartesianMotionController, CartesianMotionControllerConfig + +# 1. Create hardware driver +arm_driver = XArmDriver(config=XArmDriverConfig(ip_address="192.168.1.235")) + +# 2. Create Cartesian controller (hardware-agnostic!) +controller = CartesianMotionController( + arm_driver=arm_driver, + config=CartesianMotionControllerConfig( + control_frequency=20.0, + position_kp=1.0, + max_linear_velocity=0.15, # m/s + ) +) + +# 3. Set up topic connections (shared memory) +from dimos.core.transport import pSHMTransport + +transport_joint_state = pSHMTransport("joint_state") +transport_joint_cmd = pSHMTransport("joint_cmd") + +arm_driver.joint_state.connection = transport_joint_state +controller.joint_state.connection = transport_joint_state +controller.joint_position_command.connection = transport_joint_cmd +arm_driver.joint_position_command.connection = transport_joint_cmd + +# 4. Start modules +arm_driver.start() +controller.start() + +# 5. Send Cartesian goal (move 10cm in X) +controller.set_target_pose( + position=[0.3, 0.0, 0.5], # xyz in meters + orientation=[0, 0, 0], # roll, pitch, yaw in radians + frame_id="world" +) + +# 6. Wait for convergence +while not controller.is_converged(): + time.sleep(0.1) + +print("Target reached!") +``` + +### Using Quaternions + +```python +from dimos.msgs.geometry_msgs import Quaternion + +# Create quaternion (identity rotation) +quat = Quaternion(x=0, y=0, z=0, w=1) + +controller.set_target_pose( + position=[0.4, 0.1, 0.6], + orientation=[quat.x, quat.y, quat.z, quat.w], # 4-element list +) +``` + +### Using PoseStamped Messages + +```python +from dimos.msgs.geometry_msgs import PoseStamped + +# Create target pose +target = PoseStamped( + frame_id="world", + position=[0.3, 0.2, 0.5], + orientation=[0, 0, 0, 1] # quaternion +) + +# Option 1: Via RPC +controller.set_target_pose( + position=list(target.position), + orientation=list(target.orientation) +) + +# Option 2: Via topic (if connected) +controller.target_pose.publish(target) +``` + +### Using the TargetSetter Tool + +The `TargetSetter` is an interactive CLI tool that makes it easy to manually send target poses to the controller. It provides a user-friendly interface for testing and teleoperation. + +**Key Features:** +- **Interactive terminal UI** - prompts for x, y, z coordinates +- **Orientation preservation** - automatically uses current orientation when left blank +- **Live feedback** - subscribes to controller's current pose +- **Simple workflow** - just enter coordinates and press Enter + +**Setup:** + +```python +# Terminal 1: Start the controller (as shown in Basic Example above) +arm_driver = XArmDriver(config=XArmDriverConfig(ip_address="192.168.1.235")) +controller = CartesianMotionController(arm_driver=arm_driver) + +# Set up LCM transports for target_pose and current_pose +from dimos.core import LCMTransport +controller.target_pose.connection = LCMTransport("/target_pose", PoseStamped) +controller.current_pose.connection = LCMTransport("/xarm/current_pose", PoseStamped) + +arm_driver.start() +controller.start() + +# Terminal 2: Run the target setter +python -m dimos.manipulation.control.target_setter +``` + +**Usage Example:** + +``` +================================================================================ +Interactive Target Setter +================================================================================ +Mode: WORLD FRAME (absolute coordinates) + +Enter target coordinates (Ctrl+C to quit) +================================================================================ + +-------------------------------------------------------------------------------- + +Enter target position (in meters): + x (m): 0.3 + y (m): 0.0 + z (m): 0.5 + +Enter orientation (in degrees, leave blank to preserve current orientation): + roll (°): + pitch (°): + yaw (°): + +āœ“ Published target (preserving current orientation): + Position: x=0.3000m, y=0.0000m, z=0.5000m + Orientation: roll=0.0°, pitch=0.0°, yaw=0.0° +``` + +**How It Works:** + +1. **TargetSetter** subscribes to `/xarm/current_pose` from the controller +2. User enters target position (x, y, z) in meters +3. User can optionally enter orientation (roll, pitch, yaw) in degrees +4. If orientation is left blank (0, 0, 0), TargetSetter uses the current orientation from the controller +5. TargetSetter publishes the target pose to `/target_pose` topic +6. **CartesianMotionController** receives the target and tracks it + +**Benefits:** + +- **No orientation math** - just move positions without worrying about quaternions +- **Safe testing** - manually verify each move before sending +- **Quick iteration** - test different positions interactively +- **Educational** - see the controller respond in real-time + +## Configuration + +```python +@dataclass +class CartesianMotionControllerConfig: + # Control loop + control_frequency: float = 20.0 # Hz (recommend 10-50Hz) + command_timeout: float = 1.0 # seconds + + # PID gains (position) + position_kp: float = 1.0 # m/s per meter of error + position_ki: float = 0.0 # Integral gain + position_kd: float = 0.1 # Derivative gain (damping) + + # PID gains (orientation) + orientation_kp: float = 2.0 # rad/s per radian of error + orientation_ki: float = 0.0 + orientation_kd: float = 0.2 + + # Safety limits + max_linear_velocity: float = 0.2 # m/s + max_angular_velocity: float = 1.0 # rad/s + max_position_error: float = 0.5 # m (emergency stop threshold) + max_orientation_error: float = 1.57 # rad (~90°) + + # Convergence + position_tolerance: float = 0.001 # m (1mm) + orientation_tolerance: float = 0.01 # rad (~0.57°) + + # Control mode + velocity_control_mode: bool = True # Use velocity-based control +``` + +## Hardware Abstraction + +The controller uses the **Protocol pattern** for hardware abstraction: + +```python +# spec.py +class ArmDriverSpec(Protocol): + # Required RPC methods + def get_inverse_kinematics(self, pose: list[float]) -> tuple[int, list[float] | None]: ... + def get_forward_kinematics(self, angles: list[float]) -> tuple[int, list[float] | None]: ... + + # Required topics + joint_state: Out[JointState] + robot_state: Out[RobotState] + joint_position_command: In[JointCommand] +``` + +**Any driver implementing this protocol works with the controller!** + +### Adding a New Arm + +1. Implement `ArmDriverSpec` protocol: + ```python + class MyArmDriver(Module): + @rpc + def get_inverse_kinematics(self, pose: list[float]) -> tuple[int, list[float] | None]: + # Your IK implementation + return (0, joint_angles) + + @rpc + def get_forward_kinematics(self, angles: list[float]) -> tuple[int, list[float] | None]: + # Your FK implementation + return (0, tcp_pose) + ``` + +2. Use with controller: + ```python + my_driver = MyArmDriver() + controller = CartesianMotionController(arm_driver=my_driver) + ``` + +**That's it! No changes to the controller needed.** + +## RPC Methods + +### Control Methods + +```python +@rpc +def set_target_pose( + position: list[float], # [x, y, z] in meters + orientation: list[float], # [qx, qy, qz, qw] or [roll, pitch, yaw] + frame_id: str = "world" +) -> None +``` + +```python +@rpc +def clear_target() -> None +``` + +### Query Methods + +```python +@rpc +def get_current_pose() -> Optional[Pose] +``` + +```python +@rpc +def is_converged() -> bool +``` + +## Topics + +### Inputs (Subscriptions) + +| Topic | Type | Description | +|-------|------|-------------| +| `joint_state` | `JointState` | Current joint positions/velocities (from driver) | +| `robot_state` | `RobotState` | Robot status (from driver) | +| `target_pose` | `PoseStamped` | Desired TCP pose (from planner) | + +### Outputs (Publications) + +| Topic | Type | Description | +|-------|------|-------------| +| `joint_position_command` | `JointCommand` | Target joint angles (to driver) | +| `cartesian_velocity` | `Twist` | Debug: Cartesian velocity commands | +| `current_pose` | `PoseStamped` | Current TCP pose (for TargetSetter and other tools) | + +## Control Algorithm + +``` +1. Read current joint state from driver +2. Compute FK: joint angles → TCP pose +3. Compute error: e = target_pose - current_pose +4. PID control: velocity = PID(e, dt) +5. Integrate: next_pose = current_pose + velocity * dt +6. Compute IK: next_pose → target_joints +7. Publish target_joints to driver +``` + +### Why This Works + +- **Outer loop (Cartesian)**: Runs at 10-50Hz, computes IK +- **Inner loop (Joint)**: Driver runs at 100Hz, executes smoothly +- **Decoupling**: Separates high-level planning from low-level control + +## Tuning Guide + +### Conservative (Safe) +```python +config = CartesianMotionControllerConfig( + control_frequency=10.0, + position_kp=0.5, + max_linear_velocity=0.1, # Slow! +) +``` + +### Moderate (Recommended) +```python +config = CartesianMotionControllerConfig( + control_frequency=20.0, + position_kp=1.0, + position_kd=0.1, + max_linear_velocity=0.15, +) +``` + +### Aggressive (Fast) +```python +config = CartesianMotionControllerConfig( + control_frequency=50.0, + position_kp=2.0, + position_kd=0.2, + max_linear_velocity=0.3, +) +``` + +### Tips + +- **Increase Kp**: Faster response, but may oscillate +- **Increase Kd**: More damping, smoother motion +- **Increase Ki**: Eliminates steady-state error (usually not needed) +- **Lower frequency**: Less CPU load, smoother +- **Higher frequency**: Faster response, more accurate + +## Extending + +### Next Steps (Phase 2+) + +1. **Trajectory Following**: Add waypoint tracking + ```python + controller.follow_trajectory(waypoints: list[Pose], duration: float) + ``` + +2. **Collision Avoidance**: Integrate with planning + ```python + controller.set_collision_checker(checker: CollisionChecker) + ``` + +3. **Impedance Control**: Add force/torque feedback + ```python + controller.set_impedance(stiffness: float, damping: float) + ``` + +4. **Visual Servoing**: Integrate with perception + ```python + controller.track_object(object_id: int) + ``` + +## Troubleshooting + +### Controller not moving +- Check `arm_driver` is started and publishing `joint_state` +- Verify topic connections are set up +- Check robot is in correct mode (servo mode for xArm) + +### Oscillation / Instability +- Reduce `position_kp` or `orientation_kp` +- Increase `position_kd` or `orientation_kd` +- Lower `control_frequency` + +### IK failures +- Target pose may be unreachable +- Check joint limits +- Verify pose is within workspace +- Check singularity avoidance + +### Not converging +- Increase `position_tolerance` / `orientation_tolerance` +- Check for workspace limits +- Increase `max_linear_velocity` + +## Files + +``` +dimos/manipulation/control/ +ā”œā”€ā”€ __init__.py # Module exports +ā”œā”€ā”€ cartesian_motion_controller.py # Main controller +ā”œā”€ā”€ target_setter.py # Interactive target pose publisher +ā”œā”€ā”€ example_cartesian_control.py # Usage example +└── README.md # This file +``` + +## Related Modules + +- [xarm_driver.py](../../hardware/manipulators/xarm/xarm_driver.py) - Hardware driver for xArm +- [spec.py](../../hardware/manipulators/xarm/spec.py) - Protocol specification +- [simple_controller.py](../../utils/simple_controller.py) - PID implementation + +## License + +Copyright 2025 Dimensional Inc. - Apache 2.0 License diff --git a/dimos/manipulation/control/servo_control/__init__.py b/dimos/manipulation/control/servo_control/__init__.py new file mode 100644 index 0000000000..5418a7e24b --- /dev/null +++ b/dimos/manipulation/control/servo_control/__init__.py @@ -0,0 +1,32 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Servo Control Modules + +Real-time servo-level controllers for robotic manipulation. +Includes Cartesian motion control with PID-based tracking. +""" + +from dimos.manipulation.control.servo_control.cartesian_motion_controller import ( + CartesianMotionController, + CartesianMotionControllerConfig, + cartesian_motion_controller, +) + +__all__ = [ + "CartesianMotionController", + "CartesianMotionControllerConfig", + "cartesian_motion_controller", +] diff --git a/dimos/manipulation/control/servo_control/cartesian_motion_controller.py b/dimos/manipulation/control/servo_control/cartesian_motion_controller.py new file mode 100644 index 0000000000..cfbdb77cbf --- /dev/null +++ b/dimos/manipulation/control/servo_control/cartesian_motion_controller.py @@ -0,0 +1,721 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Cartesian Motion Controller + +Hardware-agnostic Cartesian space motion controller for robotic manipulators. +Converts Cartesian pose goals to joint commands using IK/FK from the arm driver. + +Architecture: +- Subscribes to joint_state and robot_state from hardware driver +- Subscribes to target_pose (PoseStamped) from high-level planners +- Publishes joint_position_command to hardware driver +- Uses PID control for smooth Cartesian tracking +- Supports velocity-based and position-based control modes +""" + +from dataclasses import dataclass +import math +import threading +import time +from typing import Any + +from dimos.core import In, Module, Out, rpc +from dimos.core.module import ModuleConfig +from dimos.hardware.manipulators.xarm.spec import ArmDriverSpec +from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Twist, Vector3 +from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState +from dimos.utils.logging_config import setup_logger +from dimos.utils.simple_controller import PIDController + +logger = setup_logger() + + +@dataclass +class CartesianMotionControllerConfig(ModuleConfig): + """Configuration for Cartesian motion controller.""" + + # Control loop parameters + control_frequency: float = 20.0 # Hz - Cartesian control loop rate + command_timeout: float = 30.0 # seconds - timeout for stale targets (RPC mode needs longer) + + # PID gains for position control (m/s per meter of error) + position_kp: float = 5.0 # Proportional gain + position_ki: float = 0.1 # Integral gain + position_kd: float = 0.1 # Derivative gain + + # PID gains for orientation control (rad/s per radian of error) + orientation_kp: float = 2.0 # Proportional gain + orientation_ki: float = 0.0 # Integral gain + orientation_kd: float = 0.2 # Derivative gain + + # Safety limits + max_linear_velocity: float = 0.2 # m/s - maximum TCP linear velocity + max_angular_velocity: float = 1.0 # rad/s - maximum TCP angular velocity + max_position_error: float = 0.7 # m - max allowed position error before emergency stop + max_orientation_error: float = 6.28 # rad (~360°) - allow any orientation + + # Convergence thresholds + position_tolerance: float = 0.001 # m - position considered "reached" + orientation_tolerance: float = 0.01 # rad (~0.57°) - orientation considered "reached" + + # Control mode + velocity_control_mode: bool = True # Use velocity control (True) or position steps (False) + + # Frame configuration + control_frame: str = "world" # Frame for target poses (world, base_link, etc.) + + +class CartesianMotionController(Module): + """ + Hardware-agnostic Cartesian motion controller. + + This controller provides Cartesian space motion control for manipulators by: + 1. Receiving target poses (PoseStamped) + 2. Computing Cartesian error (position + orientation) + 3. Generating Cartesian velocity commands (Twist) + 4. Computing IK to convert to joint space + 5. Publishing joint commands to the driver + + The controller is hardware-agnostic: it works with any arm driver that + implements the ArmDriverSpec protocol (provides IK/FK RPC methods). + """ + + default_config = CartesianMotionControllerConfig + config: CartesianMotionControllerConfig # Type hint for proper attribute access + + # RPC methods to request from other modules (resolved at blueprint build time) + rpc_calls = [ + "XArmDriver.get_forward_kinematics", + "XArmDriver.get_inverse_kinematics", + ] + + # Input topics (initialized by Module base class) + joint_state: In[JointState] = None # type: ignore[assignment] + robot_state: In[RobotState] = None # type: ignore[assignment] + target_pose: In[PoseStamped] = None # type: ignore[assignment] + + # Output topics (initialized by Module base class) + joint_position_command: Out[JointCommand] = None # type: ignore[assignment] + cartesian_velocity: Out[Twist] = None # type: ignore[assignment] + current_pose: Out[PoseStamped] = None # type: ignore[assignment] + + def __init__(self, arm_driver: ArmDriverSpec | None = None, *args: Any, **kwargs: Any) -> None: + """ + Initialize the Cartesian motion controller. + + Args: + arm_driver: (Optional) Hardware driver implementing ArmDriverSpec protocol. + When using blueprints, this is resolved automatically via rpc_calls. + """ + super().__init__(*args, **kwargs) + + # Hardware driver reference - set via arm_driver param (legacy) or RPC wiring (blueprint) + self._arm_driver_legacy = arm_driver + + # State tracking + self._latest_joint_state: JointState | None = None + self._latest_robot_state: RobotState | None = None + self._target_pose_: PoseStamped | None = None + self._last_target_time: float = 0.0 + + # Current TCP pose (computed via FK) + self._current_tcp_pose: Pose | None = None + + # Thread management + self._control_thread: threading.Thread | None = None + self._stop_event = threading.Event() + + # State locks + self._state_lock = threading.Lock() + self._target_lock = threading.Lock() + + # PID controllers for Cartesian space + self._pid_x = PIDController( + kp=self.config.position_kp, + ki=self.config.position_ki, + kd=self.config.position_kd, + output_limits=(-self.config.max_linear_velocity, self.config.max_linear_velocity), + ) + self._pid_y = PIDController( + kp=self.config.position_kp, + ki=self.config.position_ki, + kd=self.config.position_kd, + output_limits=(-self.config.max_linear_velocity, self.config.max_linear_velocity), + ) + self._pid_z = PIDController( + kp=self.config.position_kp, + ki=self.config.position_ki, + kd=self.config.position_kd, + output_limits=(-self.config.max_linear_velocity, self.config.max_linear_velocity), + ) + + # Orientation PIDs (using axis-angle representation) + self._pid_roll = PIDController( + kp=self.config.orientation_kp, + ki=self.config.orientation_ki, + kd=self.config.orientation_kd, + output_limits=(-self.config.max_angular_velocity, self.config.max_angular_velocity), + ) + self._pid_pitch = PIDController( + kp=self.config.orientation_kp, + ki=self.config.orientation_ki, + kd=self.config.orientation_kd, + output_limits=(-self.config.max_angular_velocity, self.config.max_angular_velocity), + ) + self._pid_yaw = PIDController( + kp=self.config.orientation_kp, + ki=self.config.orientation_ki, + kd=self.config.orientation_kd, + output_limits=(-self.config.max_angular_velocity, self.config.max_angular_velocity), + ) + + # Control status + self._is_tracking: bool = False + self._last_convergence_check: float = 0.0 + + logger.info( + f"CartesianMotionController initialized at {self.config.control_frequency}Hz " + f"(velocity_mode={self.config.velocity_control_mode})" + ) + + def _call_fk(self, joint_positions: list[float]) -> tuple[int, list[float] | None]: + """Call FK - uses blueprint RPC wiring or legacy arm_driver reference.""" + try: + result: tuple[int, list[float] | None] = self.get_rpc_calls( + "XArmDriver.get_forward_kinematics" + )(joint_positions) + return result + except (ValueError, KeyError): + if self._arm_driver_legacy: + result_fk: tuple[int, list[float] | None] = ( + self._arm_driver_legacy.get_forward_kinematics(joint_positions) # type: ignore[attr-defined] + ) + return result_fk + raise RuntimeError("No arm driver available - use blueprint or pass arm_driver param") + + def _call_ik(self, pose: list[float]) -> tuple[int, list[float] | None]: + """Call IK - uses blueprint RPC wiring or legacy arm_driver reference.""" + try: + result: tuple[int, list[float] | None] = self.get_rpc_calls( + "XArmDriver.get_inverse_kinematics" + )(pose) + return result + except (ValueError, KeyError): + if self._arm_driver_legacy: + result_ik: tuple[int, list[float] | None] = ( + self._arm_driver_legacy.get_inverse_kinematics(pose) # type: ignore[attr-defined] + ) + return result_ik + raise RuntimeError("No arm driver available - use blueprint or pass arm_driver param") + + @rpc + def start(self) -> None: + """Start the Cartesian motion controller.""" + super().start() + + # Subscribe to input topics + # Note: Accessing .connection property triggers transport resolution from connected streams + try: + if self.joint_state.connection is not None or self.joint_state._transport is not None: + self.joint_state.subscribe(self._on_joint_state) + logger.info("Subscribed to joint_state") + except Exception as e: + logger.warning(f"Failed to subscribe to joint_state: {e}") + + try: + if self.robot_state.connection is not None or self.robot_state._transport is not None: + self.robot_state.subscribe(self._on_robot_state) + logger.info("Subscribed to robot_state") + except Exception as e: + logger.warning(f"Failed to subscribe to robot_state: {e}") + + try: + if self.target_pose.connection is not None or self.target_pose._transport is not None: + self.target_pose.subscribe(self._on_target_pose) + logger.info("Subscribed to target_pose") + except Exception: + logger.debug("target_pose not connected (expected - uses RPC)") + + # Start control loop thread + self._stop_event.clear() + self._control_thread = threading.Thread( + target=self._control_loop, daemon=True, name="cartesian_control_thread" + ) + self._control_thread.start() + + logger.info("CartesianMotionController started") + + @rpc + def stop(self) -> None: + """Stop the Cartesian motion controller.""" + logger.info("Stopping CartesianMotionController...") + + # Signal thread to stop + self._stop_event.set() + + # Wait for control thread + if self._control_thread and self._control_thread.is_alive(): + self._control_thread.join(timeout=2.0) + + super().stop() + logger.info("CartesianMotionController stopped") + + # ========================================================================= + # RPC Methods - High-level control + # ========================================================================= + + @rpc + def set_target_pose( + self, position: list[float], orientation: list[float], frame_id: str = "world" + ) -> None: + """ + Set a target Cartesian pose for the controller to track. + + Args: + position: [x, y, z] in meters + orientation: [qx, qy, qz, qw] quaternion OR [roll, pitch, yaw] euler angles + frame_id: Reference frame for the pose + """ + # Detect if orientation is euler (3 elements) or quaternion (4 elements) + if len(orientation) == 3: + # Convert euler to quaternion using Pose's built-in conversion + euler_angles = Vector3(orientation[0], orientation[1], orientation[2]) + quat = Quaternion.from_euler(euler_angles) + orientation = [quat.x, quat.y, quat.z, quat.w] + + target = PoseStamped( + ts=time.time(), frame_id=frame_id, position=position, orientation=orientation + ) + + with self._target_lock: + self._target_pose_ = target + self._last_target_time = time.time() + self._is_tracking = True + + logger.info( + f"New target set: pos=[{position[0]:.6f}, {position[1]:.6f}, {position[2]:.6f}] m, " + f"frame={frame_id}" + ) + + @rpc + def clear_target(self) -> None: + """Clear the current target (stop tracking).""" + with self._target_lock: + self._target_pose_ = None + self._is_tracking = False + logger.info("Target cleared, tracking stopped") + + @rpc + def get_current_pose(self) -> Pose | None: + """ + Get the current TCP pose (computed via FK). + + Returns: + Current Pose or None if not available + """ + return self._current_tcp_pose + + @rpc + def is_converged(self) -> bool: + """ + Check if the controller has converged to the target. + + Returns: + True if within tolerance, False otherwise + """ + with self._target_lock: + target_pose = self._target_pose_ + + current_pose = self._current_tcp_pose + + if not target_pose or not current_pose: + return False + + pos_error, ori_error = self._compute_pose_error(current_pose, target_pose) + return ( + pos_error < self.config.position_tolerance + and ori_error < self.config.orientation_tolerance + ) + + # ========================================================================= + # Private Methods - Callbacks + # ========================================================================= + + def _on_joint_state(self, msg: JointState) -> None: + """Callback when new joint state is received.""" + logger.debug(f"Received joint_state: {len(msg.position)} joints") + with self._state_lock: + self._latest_joint_state = msg + + def _on_robot_state(self, msg: RobotState) -> None: + """Callback when new robot state is received.""" + with self._state_lock: + self._latest_robot_state = msg + + def _on_target_pose(self, msg: PoseStamped) -> None: + """Callback when new target pose is received.""" + with self._target_lock: + self._target_pose_ = msg + self._last_target_time = time.time() + self._is_tracking = True + logger.debug(f"New target received: {msg}") + + # ========================================================================= + # Private Methods - Control Loop + # ========================================================================= + + def _control_loop(self) -> None: + """ + Main control loop running at control_frequency Hz. + + Algorithm: + 1. Read current joint state + 2. Compute FK to get current TCP pose + 3. Compute Cartesian error to target + 4. Generate Cartesian velocity command (PID) + 5. Integrate velocity to get next desired pose + 6. Compute IK to get target joint angles + 7. Publish joint command + """ + period = 1.0 / self.config.control_frequency + next_time = time.time() + + logger.info(f"Cartesian control loop started at {self.config.control_frequency}Hz") + + while not self._stop_event.is_set(): + # Sleep at start of loop to maintain frequency even when using continue + sleep_time = next_time - time.time() + if sleep_time > 0: + if self._stop_event.wait(timeout=sleep_time): + break + else: + # Loop overrun - reset timing + next_time = time.time() + + next_time += period + + try: + current_time = time.time() + dt = period # Use fixed timestep for consistent control + + # Read shared state + with self._state_lock: + joint_state = self._latest_joint_state + + with self._target_lock: + target_pose = self._target_pose_ + last_target_time = self._last_target_time + is_tracking = self._is_tracking + + # Check if we have valid state + if joint_state is None or len(joint_state.position) == 0: + continue + + # Compute current TCP pose via FK + code, current_pose_list = self._call_fk(list(joint_state.position)) + + if code != 0 or current_pose_list is None: + logger.warning(f"FK failed with code: {code}") + continue + + # Convert FK result to Pose (xArm returns [x, y, z, roll, pitch, yaw] in mm) + if len(current_pose_list) == 6: + # Convert position from mm to m for internal use + position_m = [ + current_pose_list[0] / 1000.0, + current_pose_list[1] / 1000.0, + current_pose_list[2] / 1000.0, + ] + euler_angles = Vector3( + current_pose_list[3], current_pose_list[4], current_pose_list[5] + ) + quat = Quaternion.from_euler(euler_angles) + self._current_tcp_pose = Pose( + position=position_m, + orientation=[quat.x, quat.y, quat.z, quat.w], + ) + + # Publish current pose for target setters to use + current_pose_stamped = PoseStamped( + ts=current_time, + frame_id="world", + position=position_m, + orientation=[quat.x, quat.y, quat.z, quat.w], + ) + self.current_pose.publish(current_pose_stamped) + else: + logger.warning(f"Unexpected FK result format: {current_pose_list}") + continue + + # Check for target timeout + if is_tracking and (current_time - last_target_time) > self.config.command_timeout: + logger.warning("Target pose timeout - clearing target") + with self._target_lock: + self._target_pose_ = None + self._is_tracking = False + continue + + # If not tracking, skip control + if not is_tracking or target_pose is None: + logger.debug( + f"Not tracking: is_tracking={is_tracking}, target_pose={target_pose is not None}" + ) + continue + + # Check if we have current pose + if self._current_tcp_pose is None: + logger.warning("No current TCP pose available, skipping control") + continue + + # Compute Cartesian error + pos_error_mag, ori_error_mag = self._compute_pose_error( + self._current_tcp_pose, target_pose + ) + + # Log error periodically (every 1 second) + if not hasattr(self, "_last_error_log_time"): + self._last_error_log_time = 0.0 + if current_time - self._last_error_log_time > 1.0: + logger.info( + f"Curr=[{self._current_tcp_pose.x:.3f},{self._current_tcp_pose.y:.3f},{self._current_tcp_pose.z:.3f}]m Tgt=[{target_pose.x:.3f},{target_pose.y:.3f},{target_pose.z:.3f}]m Err={pos_error_mag * 1000:.1f}mm" + ) + self._last_error_log_time = current_time + + # Safety check: excessive error + if pos_error_mag > self.config.max_position_error: + logger.error( + f"Position error too large: {pos_error_mag:.3f}m > " + f"{self.config.max_position_error}m - STOPPING" + ) + with self._target_lock: + self._target_pose_ = None + self._is_tracking = False + continue + + if ori_error_mag > self.config.max_orientation_error: + logger.error( + f"Orientation error too large: {ori_error_mag:.3f}rad > " + f"{self.config.max_orientation_error}rad - STOPPING" + ) + with self._target_lock: + self._target_pose_ = None + self._is_tracking = False + continue + + # Check convergence periodically + if current_time - self._last_convergence_check > 1.0: + if ( + pos_error_mag < self.config.position_tolerance + and ori_error_mag < self.config.orientation_tolerance + ): + logger.info( + f"Converged! pos_err={pos_error_mag * 1000:.2f}mm, " + f"ori_err={math.degrees(ori_error_mag):.2f}°" + ) + self._last_convergence_check = current_time + + # Generate Cartesian velocity command + cartesian_twist = self._compute_cartesian_velocity( + self._current_tcp_pose, target_pose, dt + ) + + # Publish debug twist + if self.cartesian_velocity._transport or hasattr( + self.cartesian_velocity, "connection" + ): + try: + self.cartesian_velocity.publish(cartesian_twist) + except Exception: + pass + + # Integrate velocity to get next desired pose + next_pose = self._integrate_velocity(self._current_tcp_pose, cartesian_twist, dt) + + # Compute IK to get target joint angles + # Convert Pose to xArm format: [x, y, z, roll, pitch, yaw] + # Note: xArm IK expects position in mm, so convert from m to mm + next_pose_list = [ + next_pose.x * 1000.0, # m to mm + next_pose.y * 1000.0, # m to mm + next_pose.z * 1000.0, # m to mm + next_pose.roll, + next_pose.pitch, + next_pose.yaw, + ] + + logger.debug( + f"Calling IK for pose (mm): [{next_pose_list[0]:.1f}, {next_pose_list[1]:.1f}, {next_pose_list[2]:.1f}]" + ) + code, target_joints = self._call_ik(next_pose_list) + + if code != 0 or target_joints is None: + logger.warning(f"IK failed with code: {code}, target_joints={target_joints}") + continue + + logger.debug(f"IK successful: {len(target_joints)} joints") + + # Dynamically get joint count from actual joint_state (works for xarm5/6/7) + # IK may return extra values (e.g., gripper), so truncate to match actual DOF + num_arm_joints = len(joint_state.position) + if len(target_joints) > num_arm_joints: + if not hasattr(self, "_ik_truncation_logged"): + logger.info( + f"IK returns {len(target_joints)} joints, using first {num_arm_joints} to match arm DOF" + ) + self._ik_truncation_logged = True + target_joints = target_joints[:num_arm_joints] + elif len(target_joints) < num_arm_joints: + logger.warning( + f"IK returns {len(target_joints)} joints but arm has {num_arm_joints} - joint count mismatch!" + ) + + # Publish joint command + joint_cmd = JointCommand( + timestamp=current_time, + positions=list(target_joints), + ) + + # Always try to publish - the Out stream will handle transport availability + try: + self.joint_position_command.publish(joint_cmd) + logger.debug( + f"āœ“ Pub cmd: [{target_joints[0]:.6f}, {target_joints[1]:.6f}, {target_joints[2]:.6f}, ...]" + ) + except Exception as e: + logger.error(f"āœ— Failed to publish joint command: {e}") + + except Exception as e: + logger.error(f"Error in control loop: {e}") + import traceback + + traceback.print_exc() + + logger.info("Cartesian control loop stopped") + + def _compute_pose_error(self, current_pose: Pose, target_pose: Pose) -> tuple[float, float]: + """ + Compute position and orientation error between current and target pose. + + Args: + current_pose: Current TCP pose + target_pose: Desired TCP pose + + Returns: + Tuple of (position_error_magnitude, orientation_error_magnitude) + """ + # Position error (Euclidean distance) + pos_error = Vector3( + target_pose.x - current_pose.x, + target_pose.y - current_pose.y, + target_pose.z - current_pose.z, + ) + pos_error_mag = math.sqrt(pos_error.x**2 + pos_error.y**2 + pos_error.z**2) + + # Orientation error (angle between quaternions) + # q_error = q_current^-1 * q_target + q_current_inv = current_pose.orientation.conjugate() + q_error = q_current_inv * target_pose.orientation + + # Extract angle from axis-angle representation + # For quaternion [x, y, z, w], angle = 2 * acos(w) + ori_error_mag = 2 * math.acos(min(1.0, abs(q_error.w))) + + return pos_error_mag, ori_error_mag + + def _compute_cartesian_velocity( + self, current_pose: Pose, target_pose: Pose, dt: float + ) -> Twist: + """ + Compute Cartesian velocity command using PID control. + + Args: + current_pose: Current TCP pose + target_pose: Desired TCP pose + dt: Time step + + Returns: + Twist message with linear and angular velocities + """ + # Position error + error_x = target_pose.x - current_pose.x + error_y = target_pose.y - current_pose.y + error_z = target_pose.z - current_pose.z + + # Compute linear velocities via PID + vel_x = self._pid_x.update(error_x, dt) # type: ignore[no-untyped-call] + vel_y = self._pid_y.update(error_y, dt) # type: ignore[no-untyped-call] + vel_z = self._pid_z.update(error_z, dt) # type: ignore[no-untyped-call] + + # Orientation error (convert to euler for simpler PID) + # This is an approximation; axis-angle would be more accurate + error_roll = self._normalize_angle(target_pose.roll - current_pose.roll) + error_pitch = self._normalize_angle(target_pose.pitch - current_pose.pitch) + error_yaw = self._normalize_angle(target_pose.yaw - current_pose.yaw) + + # Compute angular velocities via PID + omega_x = self._pid_roll.update(error_roll, dt) # type: ignore[no-untyped-call] + omega_y = self._pid_pitch.update(error_pitch, dt) # type: ignore[no-untyped-call] + omega_z = self._pid_yaw.update(error_yaw, dt) # type: ignore[no-untyped-call] + + return Twist( + linear=Vector3(vel_x, vel_y, vel_z), angular=Vector3(omega_x, omega_y, omega_z) + ) + + def _integrate_velocity(self, current_pose: Pose, velocity: Twist, dt: float) -> Pose: + """ + Integrate Cartesian velocity to compute next desired pose. + + Args: + current_pose: Current TCP pose + velocity: Desired Cartesian velocity (Twist) + dt: Time step + + Returns: + Next desired pose + """ + # Integrate position (simple Euler integration) + next_position = Vector3( + current_pose.x + velocity.linear.x * dt, + current_pose.y + velocity.linear.y * dt, + current_pose.z + velocity.linear.z * dt, + ) + + # Integrate orientation (simple euler integration - good for small dt) + next_roll = current_pose.roll + velocity.angular.x * dt + next_pitch = current_pose.pitch + velocity.angular.y * dt + next_yaw = current_pose.yaw + velocity.angular.z * dt + + euler_angles = Vector3(next_roll, next_pitch, next_yaw) + next_orientation = Quaternion.from_euler(euler_angles) + + return Pose( + position=next_position, + orientation=[ + next_orientation.x, + next_orientation.y, + next_orientation.z, + next_orientation.w, + ], + ) + + @staticmethod + def _normalize_angle(angle: float) -> float: + """Normalize angle to [-pi, pi].""" + return math.atan2(math.sin(angle), math.cos(angle)) + + +# Expose blueprint for declarative composition +cartesian_motion_controller = CartesianMotionController.blueprint diff --git a/dimos/manipulation/control/servo_control/example_cartesian_control.py b/dimos/manipulation/control/servo_control/example_cartesian_control.py new file mode 100644 index 0000000000..eeff04e424 --- /dev/null +++ b/dimos/manipulation/control/servo_control/example_cartesian_control.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Example: Topic-Based Cartesian Motion Control with xArm + +Demonstrates topic-based Cartesian space motion control. The controller +subscribes to /target_pose and automatically moves to received targets. + +This example shows: +1. Deploy xArm driver with LCM transports +2. Deploy CartesianMotionController with LCM transports +3. Configure controller to subscribe to /target_pose topic +4. Keep system running to process incoming targets + +Use target_setter.py to publish target poses to /target_pose topic. + +Pattern matches: interactive_control.py + sample_trajectory_generator.py +""" + +import signal +import time + +from dimos import core +from dimos.hardware.manipulators.xarm import XArmDriver +from dimos.manipulation.control import CartesianMotionController +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState + +# Global flag for graceful shutdown +shutdown_requested = False + + +def signal_handler(sig, frame): # type: ignore[no-untyped-def] + """Handle Ctrl+C for graceful shutdown.""" + global shutdown_requested + print("\n\nShutdown requested...") + shutdown_requested = True + + +def main(): # type: ignore[no-untyped-def] + """ + Deploy and run topic-based Cartesian motion control system. + + The system subscribes to /target_pose and automatically moves + the robot to received target poses. + """ + + # Register signal handler for graceful shutdown + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # ========================================================================= + # Step 1: Start dimos cluster + # ========================================================================= + print("=" * 80) + print("Topic-Based Cartesian Motion Control") + print("=" * 80) + print("\nStarting dimos cluster...") + dimos = core.start(1) # Start with 1 worker + + try: + # ========================================================================= + # Step 2: Deploy xArm driver + # ========================================================================= + print("\nDeploying xArm driver...") + arm_driver = dimos.deploy( # type: ignore[attr-defined] + XArmDriver, + ip_address="192.168.1.210", + xarm_type="xarm6", + report_type="dev", + enable_on_start=True, + ) + + # Set up driver transports + arm_driver.joint_state.transport = core.LCMTransport("/xarm/joint_states", JointState) + arm_driver.robot_state.transport = core.LCMTransport("/xarm/robot_state", RobotState) + arm_driver.joint_position_command.transport = core.LCMTransport( + "/xarm/joint_position_command", JointCommand + ) + arm_driver.joint_velocity_command.transport = core.LCMTransport( + "/xarm/joint_velocity_command", JointCommand + ) + + print("Starting xArm driver...") + arm_driver.start() + + # ========================================================================= + # Step 3: Deploy Cartesian motion controller + # ========================================================================= + print("\nDeploying Cartesian motion controller...") + controller = dimos.deploy( # type: ignore[attr-defined] + CartesianMotionController, + arm_driver=arm_driver, + control_frequency=20.0, + position_kp=1.0, + position_kd=0.1, + orientation_kp=2.0, + orientation_kd=0.2, + max_linear_velocity=0.15, + max_angular_velocity=0.8, + position_tolerance=0.002, + orientation_tolerance=0.02, + velocity_control_mode=True, + ) + + # Set up controller transports + controller.joint_state.transport = core.LCMTransport("/xarm/joint_states", JointState) + controller.robot_state.transport = core.LCMTransport("/xarm/robot_state", RobotState) + controller.joint_position_command.transport = core.LCMTransport( + "/xarm/joint_position_command", JointCommand + ) + + # IMPORTANT: Configure controller to subscribe to /target_pose topic + controller.target_pose.transport = core.LCMTransport("/target_pose", PoseStamped) + + # Publish current pose for target setters to use + controller.current_pose.transport = core.LCMTransport("/xarm/current_pose", PoseStamped) + + print("Starting controller...") + controller.start() + + # ========================================================================= + # Step 4: Keep system running + # ========================================================================= + print("\n" + "=" * 80) + print("āœ“ System ready!") + print("=" * 80) + print("\nController is now listening to /target_pose topic") + print("Use target_setter.py to publish target poses") + print("\nPress Ctrl+C to shutdown") + print("=" * 80 + "\n") + + # Keep running until shutdown requested + while not shutdown_requested: + time.sleep(0.5) + + # ========================================================================= + # Step 5: Clean shutdown + # ========================================================================= + print("\nShutting down...") + print("Stopping controller...") + controller.stop() + print("Stopping driver...") + arm_driver.stop() + print("āœ“ Shutdown complete") + + finally: + # Always stop dimos cluster + print("Stopping dimos cluster...") + dimos.stop() # type: ignore[attr-defined] + + +if __name__ == "__main__": + """ + Topic-Based Cartesian Control for xArm. + + Usage: + # Terminal 1: Start the controller (this script) + python3 example_cartesian_control.py + + # Terminal 2: Publish target poses + python3 target_setter.py --world 0.4 0.0 0.5 # Absolute world coordinates + python3 target_setter.py --relative 0.05 0 0 # Relative movement (50mm in X) + + The controller subscribes to /target_pose topic and automatically moves + the robot to received target poses. + + Requirements: + - xArm robot connected at 192.168.2.235 + - Robot will be automatically enabled in servo mode + - Proper network configuration + """ + try: + main() # type: ignore[no-untyped-call] + except KeyboardInterrupt: + print("\n\nInterrupted by user") + except Exception as e: + print(f"\nError: {e}") + import traceback + + traceback.print_exc() diff --git a/dimos/manipulation/control/target_setter.py b/dimos/manipulation/control/target_setter.py new file mode 100644 index 0000000000..1a937d12bb --- /dev/null +++ b/dimos/manipulation/control/target_setter.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Interactive Target Pose Publisher for Cartesian Motion Control. + +Interactive terminal UI for publishing absolute target poses to /target_pose topic. +Pure publisher - OUT channel only, no subscriptions or driver connections. +""" + +import math +import sys +import time + +from dimos import core +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 + + +class TargetSetter: + """ + Publishes target poses to /target_pose topic. + + Subscribes to /xarm/current_pose to get current TCP pose for: + - Preserving orientation when left blank + - Supporting relative mode movements + """ + + def __init__(self) -> None: + """Initialize the target setter.""" + # Create LCM transport for publishing targets + self.target_pub: core.LCMTransport[PoseStamped] = core.LCMTransport( + "/target_pose", PoseStamped + ) + + # Subscribe to current pose from controller + self.current_pose_sub: core.LCMTransport[PoseStamped] = core.LCMTransport( + "/xarm/current_pose", PoseStamped + ) + self.latest_current_pose: PoseStamped | None = None + + print("TargetSetter initialized") + print(" Publishing to: /target_pose") + print(" Subscribing to: /xarm/current_pose") + + def start(self) -> bool: + """Start subscribing to current pose.""" + self.current_pose_sub.subscribe(self._on_current_pose) + print(" Waiting for current pose...") + # Wait for initial pose + for _ in range(50): # 5 second timeout + if self.latest_current_pose is not None: + print(" āœ“ Current pose received") + return True + time.sleep(0.1) + print(" ⚠ Warning: No current pose received (timeout)") + return False + + def _on_current_pose(self, msg: PoseStamped) -> None: + """Callback for current pose updates.""" + self.latest_current_pose = msg + + def publish_pose( + self, x: float, y: float, z: float, roll: float = 0.0, pitch: float = 0.0, yaw: float = 0.0 + ) -> None: + """ + Publish target pose (absolute world frame coordinates). + + Args: + x, y, z: Position in meters + roll, pitch, yaw: Orientation in radians (0, 0, 0 = preserve current) + """ + # Check if orientation is identity (0, 0, 0) - preserve current orientation + is_identity = abs(roll) < 1e-6 and abs(pitch) < 1e-6 and abs(yaw) < 1e-6 + + if is_identity and self.latest_current_pose is not None: + # Use current orientation + q = self.latest_current_pose.orientation + orientation = [q.x, q.y, q.z, q.w] + print("\nāœ“ Published target (preserving current orientation):") + else: + # Convert Euler to Quaternion + euler = Vector3(roll, pitch, yaw) + quat = Quaternion.from_euler(euler) + orientation = [quat.x, quat.y, quat.z, quat.w] + print("\nāœ“ Published target:") + + pose = PoseStamped( + ts=time.time(), + frame_id="world", + position=[x, y, z], + orientation=orientation, + ) + + self.target_pub.broadcast(None, pose) + + print(f" Position: x={x:.4f}m, y={y:.4f}m, z={z:.4f}m") + print( + f" Orientation: roll={math.degrees(roll):.1f}°, " + f"pitch={math.degrees(pitch):.1f}°, yaw={math.degrees(yaw):.1f}°" + ) + + +def interactive_mode(setter: TargetSetter) -> None: + """ + Interactive mode: repeatedly prompt for target poses. + + Args: + setter: TargetSetter instance + """ + print("\n" + "=" * 80) + print("Interactive Target Setter") + print("=" * 80) + print("Mode: WORLD FRAME (absolute coordinates)") + print("\nFormat: x y z [roll pitch yaw]") + print(" - 3 values: position only (keep current orientation)") + print(" - 6 values: position + orientation (degrees)") + print("Example: 0.4 0.0 0.2 (position only)") + print("Example: 0.4 0.0 0.2 0 180 0 (with orientation)") + print("Ctrl+C to quit") + print("=" * 80) + + try: + while True: + try: + # Print current pose before asking for input + if setter.latest_current_pose is not None: + p = setter.latest_current_pose + # Convert quaternion to euler for display + quat = Quaternion(p.orientation) + euler = quat.to_euler() + print( + f"Current: {p.x:.3f} {p.y:.3f} {p.z:.3f} {math.degrees(euler.x):.1f} {math.degrees(euler.y):.1f} {math.degrees(euler.z):.1f}" + ) + + line = input("> ").strip() + + if not line: + continue + + parts = line.split() + + if len(parts) == 3: + # Position only - keep current orientation + x, y, z = [float(p) for p in parts] + setter.publish_pose(x, y, z) + + elif len(parts) == 6: + # Full pose (orientation in degrees) + x, y, z = [float(p) for p in parts[:3]] + roll = math.radians(float(parts[3])) + pitch = math.radians(float(parts[4])) + yaw = math.radians(float(parts[5])) + setter.publish_pose(x, y, z, roll, pitch, yaw) + + else: + print("⚠ Expected 3 (x y z) or 6 (x y z roll pitch yaw) values") + continue + + except ValueError as e: + print(f"⚠ Invalid input: {e}") + continue + + except KeyboardInterrupt: + print("\n\nExiting interactive mode...") + + +def print_banner() -> None: + """Print welcome banner.""" + print("\n" + "=" * 80) + print("xArm Target Pose Publisher") + print("=" * 80) + print("\nPublishes absolute target poses to /target_pose topic.") + print("Subscribes to /xarm/current_pose for orientation preservation.") + print("=" * 80) + + +def main() -> int: + """Main entry point.""" + print_banner() + + # Create setter and start subscribing to current pose + setter = TargetSetter() + if not setter.start(): + print("\n⚠ Warning: Could not get current pose - controller may not be running") + print("Make sure example_cartesian_control.py is running in another terminal!") + response = input("Continue anyway? [y/N]: ").strip().lower() + if response != "y": + return 0 + + try: + # Run interactive mode + interactive_mode(setter) + except KeyboardInterrupt: + print("\n\nInterrupted by user") + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\n\nInterrupted by user") + sys.exit(0) + except Exception as e: + print(f"\nError: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/dimos/manipulation/control/trajectory_controller/__init__.py b/dimos/manipulation/control/trajectory_controller/__init__.py new file mode 100644 index 0000000000..fb4360d4cc --- /dev/null +++ b/dimos/manipulation/control/trajectory_controller/__init__.py @@ -0,0 +1,31 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Trajectory Controller Module + +Joint-space trajectory execution for robotic manipulators. +""" + +from dimos.manipulation.control.trajectory_controller.joint_trajectory_controller import ( + JointTrajectoryController, + JointTrajectoryControllerConfig, + joint_trajectory_controller, +) + +__all__ = [ + "JointTrajectoryController", + "JointTrajectoryControllerConfig", + "joint_trajectory_controller", +] diff --git a/dimos/manipulation/control/trajectory_controller/example_trajectory_control.py b/dimos/manipulation/control/trajectory_controller/example_trajectory_control.py new file mode 100644 index 0000000000..100e095a45 --- /dev/null +++ b/dimos/manipulation/control/trajectory_controller/example_trajectory_control.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Example: Joint Trajectory Control with xArm + +Demonstrates joint-space trajectory execution. The controller +executes trajectories by sampling at 100Hz and sending joint commands. + +This example shows: +1. Deploy xArm driver with LCM transports +2. Deploy JointTrajectoryController with LCM transports +3. Execute trajectories via RPC or topic +4. Monitor execution status + +Use trajectory_setter.py to interactively create and execute trajectories. +""" + +import signal +import time + +from dimos import core +from dimos.hardware.manipulators.xarm import XArmDriver +from dimos.manipulation.control import JointTrajectoryController +from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState +from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryState + +# Global flag for graceful shutdown +shutdown_requested = False + + +def signal_handler(sig, frame): # type: ignore[no-untyped-def] + """Handle Ctrl+C for graceful shutdown.""" + global shutdown_requested + print("\n\nShutdown requested...") + shutdown_requested = True + + +def main(): # type: ignore[no-untyped-def] + """ + Deploy and run joint trajectory control system. + + The system executes joint trajectories at 100Hz by sampling + and forwarding joint positions to the arm driver. + """ + + # Register signal handler for graceful shutdown + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # ========================================================================= + # Step 1: Start dimos cluster + # ========================================================================= + print("=" * 80) + print("Joint Trajectory Control") + print("=" * 80) + print("\nStarting dimos cluster...") + dimos = core.start(1) # Start with 1 worker + + try: + # ========================================================================= + # Step 2: Deploy xArm driver + # ========================================================================= + print("\nDeploying xArm driver...") + arm_driver = dimos.deploy( # type: ignore[attr-defined] + XArmDriver, + ip_address="192.168.1.210", + xarm_type="xarm6", + report_type="dev", + enable_on_start=True, + ) + + # Set up driver transports + arm_driver.joint_state.transport = core.LCMTransport("/xarm/joint_states", JointState) + arm_driver.robot_state.transport = core.LCMTransport("/xarm/robot_state", RobotState) + arm_driver.joint_position_command.transport = core.LCMTransport( + "/xarm/joint_position_command", JointCommand + ) + + print("Starting xArm driver...") + arm_driver.start() + + # ========================================================================= + # Step 3: Deploy Joint Trajectory Controller + # ========================================================================= + print("\nDeploying Joint Trajectory Controller...") + controller = dimos.deploy( # type: ignore[attr-defined] + JointTrajectoryController, + control_frequency=100.0, # 100Hz execution + ) + + # Set up controller transports + controller.joint_state.transport = core.LCMTransport("/xarm/joint_states", JointState) + controller.robot_state.transport = core.LCMTransport("/xarm/robot_state", RobotState) + controller.joint_position_command.transport = core.LCMTransport( + "/xarm/joint_position_command", JointCommand + ) + + # Subscribe to trajectory topic (from trajectory_setter.py) + controller.trajectory.transport = core.LCMTransport("/trajectory", JointTrajectory) + + print("Starting controller...") + controller.start() + + # Wait for joint state + print("\nWaiting for joint state...") + time.sleep(1.0) + + # ========================================================================= + # Step 4: Keep system running + # ========================================================================= + print("\n" + "=" * 80) + print("System ready!") + print("=" * 80) + print("\nJoint Trajectory Controller is running at 100Hz") + print("Listening on /trajectory topic") + print("\nUse trajectory_setter.py in another terminal to publish trajectories") + print("\nPress Ctrl+C to shutdown") + print("=" * 80 + "\n") + + # Keep running until shutdown requested + while not shutdown_requested: + # Print status periodically + status = controller.get_status() + if status.state == TrajectoryState.EXECUTING: + print( + f"\rExecuting: {status.progress:.1%} | " + f"elapsed={status.time_elapsed:.2f}s | " + f"remaining={status.time_remaining:.2f}s", + end="", + ) + time.sleep(0.5) + + # ========================================================================= + # Step 5: Clean shutdown + # ========================================================================= + print("\n\nShutting down...") + print("Stopping controller...") + controller.stop() + print("Stopping driver...") + arm_driver.stop() + print("Shutdown complete") + + finally: + # Always stop dimos cluster + print("Stopping dimos cluster...") + dimos.stop() # type: ignore[attr-defined] + + +if __name__ == "__main__": + """ + Joint Trajectory Control for xArm. + + Usage: + # Terminal 1: Start the controller (this script) + python3 example_trajectory_control.py + + # Terminal 2: Create and execute trajectories + python3 trajectory_setter.py + + The controller executes joint trajectories at 100Hz by sampling + and forwarding joint positions to the arm driver. + + Requirements: + - xArm robot connected at 192.168.1.210 + - Robot will be automatically enabled in servo mode + - Proper network configuration + """ + try: + main() # type: ignore[no-untyped-call] + except KeyboardInterrupt: + print("\n\nInterrupted by user") + except Exception as e: + print(f"\nError: {e}") + import traceback + + traceback.print_exc() diff --git a/dimos/manipulation/control/trajectory_controller/joint_trajectory_controller.py b/dimos/manipulation/control/trajectory_controller/joint_trajectory_controller.py new file mode 100644 index 0000000000..6ecdff1714 --- /dev/null +++ b/dimos/manipulation/control/trajectory_controller/joint_trajectory_controller.py @@ -0,0 +1,368 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Joint Trajectory Controller + +A simple joint-space trajectory executor. Does NOT: +- Use Cartesian space +- Compute error +- Apply PID +- Call IK + +Just samples a trajectory at time t and sends joint positions to the driver. + +Behavior: +- execute_trajectory(): Preempts any active trajectory, starts new one immediately +- cancel(): Stops at current position +- reset(): Required to recover from FAULT state +""" + +from dataclasses import dataclass +import threading +import time +from typing import Any + +from dimos.core import In, Module, Out, rpc +from dimos.core.module import ModuleConfig +from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState +from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryState, TrajectoryStatus +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +@dataclass +class JointTrajectoryControllerConfig(ModuleConfig): + """Configuration for joint trajectory controller.""" + + control_frequency: float = 100.0 # Hz - trajectory execution rate + + +class JointTrajectoryController(Module): + """ + Joint-space trajectory executor. + + Executes joint trajectories at 100Hz by sampling and forwarding + joint positions to the arm driver. Uses ROS action-server-like + state machine for execution control. + + State Machine: + IDLE ──execute()──► EXECUTING ──done──► COMPLETED + ā–² │ │ + │ cancel() reset() + │ ā–¼ │ + └─────reset()───── ABORTED ā—„ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + error + ā–¼ + FAULT ──reset()──► IDLE + """ + + default_config = JointTrajectoryControllerConfig + config: JointTrajectoryControllerConfig # Type hint for proper attribute access + + # Input topics + joint_state: In[JointState] = None # type: ignore[assignment] # Feedback from arm driver + robot_state: In[RobotState] = None # type: ignore[assignment] # Robot status from arm driver + trajectory: In[JointTrajectory] = None # type: ignore[assignment] # Trajectory to execute (topic-based) + + # Output topics + joint_position_command: Out[JointCommand] = None # type: ignore[assignment] # To arm driver + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + # State machine + self._state = TrajectoryState.IDLE + self._lock = threading.Lock() + + # Active trajectory + self._trajectory: JointTrajectory | None = None + self._start_time: float = 0.0 + + # Latest feedback + self._latest_joint_state: JointState | None = None + self._latest_robot_state: RobotState | None = None + + # Error tracking + self._error_message: str = "" + + # Execution thread + self._exec_thread: threading.Thread | None = None + self._stop_event = threading.Event() + + logger.info(f"JointTrajectoryController initialized at {self.config.control_frequency}Hz") + + @rpc + def start(self) -> None: + """Start the trajectory controller.""" + super().start() + + # Subscribe to feedback topics + try: + if self.joint_state.connection is not None or self.joint_state._transport is not None: + self.joint_state.subscribe(self._on_joint_state) + logger.info("Subscribed to joint_state") + except Exception as e: + logger.warning(f"Failed to subscribe to joint_state: {e}") + + try: + if self.robot_state.connection is not None or self.robot_state._transport is not None: + self.robot_state.subscribe(self._on_robot_state) + logger.info("Subscribed to robot_state") + except Exception as e: + logger.warning(f"Failed to subscribe to robot_state: {e}") + + # Subscribe to trajectory topic + try: + if self.trajectory.connection is not None or self.trajectory._transport is not None: + self.trajectory.subscribe(self._on_trajectory) + logger.info("Subscribed to trajectory topic") + except Exception: + logger.debug("trajectory topic not connected (expected - can use RPC instead)") + + # Start execution thread + self._stop_event.clear() + self._exec_thread = threading.Thread( + target=self._execution_loop, daemon=True, name="trajectory_exec_thread" + ) + self._exec_thread.start() + + logger.info("JointTrajectoryController started") + + @rpc + def stop(self) -> None: + """Stop the trajectory controller.""" + logger.info("Stopping JointTrajectoryController...") + + self._stop_event.set() + + if self._exec_thread and self._exec_thread.is_alive(): + self._exec_thread.join(timeout=2.0) + + super().stop() + logger.info("JointTrajectoryController stopped") + + # ========================================================================= + # RPC Methods - Action-server-like interface + # ========================================================================= + + @rpc + def execute_trajectory(self, trajectory: JointTrajectory) -> bool: + """ + Set and start executing a new trajectory immediately. + If currently executing, preempts and starts new trajectory. + + Args: + trajectory: JointTrajectory to execute + + Returns: + True if accepted, False if in FAULT state or trajectory invalid + """ + with self._lock: + # Cannot execute if in FAULT state + if self._state == TrajectoryState.FAULT: + logger.warning( + "Cannot execute trajectory: controller in FAULT state (call reset())" + ) + return False + + # Validate trajectory + if trajectory is None or trajectory.duration <= 0: + logger.warning("Invalid trajectory: None or zero duration") + return False + + if not trajectory.points: + logger.warning("Invalid trajectory: no points") + return False + + # Preempt any active trajectory + if self._state == TrajectoryState.EXECUTING: + logger.info("Preempting active trajectory") + + # Start new trajectory + self._trajectory = trajectory + self._start_time = time.time() + self._state = TrajectoryState.EXECUTING + self._error_message = "" + + logger.info( + f"Executing trajectory: {len(trajectory.points)} points, " + f"duration={trajectory.duration:.3f}s" + ) + return True + + @rpc + def cancel(self) -> bool: + """ + Cancel the currently executing trajectory. + Robot stops at current position. + + Returns: + True if cancelled, False if no active trajectory + """ + with self._lock: + if self._state != TrajectoryState.EXECUTING: + logger.debug("No active trajectory to cancel") + return False + + self._state = TrajectoryState.ABORTED + logger.info("Trajectory cancelled") + return True + + @rpc + def reset(self) -> bool: + """ + Reset from FAULT, COMPLETED, or ABORTED state back to IDLE. + Required before executing new trajectories after a fault. + + Returns: + True if reset successful, False if currently EXECUTING + """ + with self._lock: + if self._state == TrajectoryState.EXECUTING: + logger.warning("Cannot reset while executing (call cancel() first)") + return False + + self._state = TrajectoryState.IDLE + self._trajectory = None + self._error_message = "" + logger.info("Controller reset to IDLE") + return True + + @rpc + def get_status(self) -> TrajectoryStatus: + """ + Get the current status of the trajectory execution. + + Returns: + TrajectoryStatus with state, progress, and error info + """ + with self._lock: + time_elapsed = 0.0 + time_remaining = 0.0 + progress = 0.0 + + if self._trajectory is not None and self._state == TrajectoryState.EXECUTING: + time_elapsed = time.time() - self._start_time + time_remaining = max(0.0, self._trajectory.duration - time_elapsed) + progress = ( + min(1.0, time_elapsed / self._trajectory.duration) + if self._trajectory.duration > 0 + else 1.0 + ) + + return TrajectoryStatus( + state=self._state, + progress=progress, + time_elapsed=time_elapsed, + time_remaining=time_remaining, + error=self._error_message, + ) + + # ========================================================================= + # Callbacks + # ========================================================================= + + def _on_joint_state(self, msg: JointState) -> None: + """Callback for joint state feedback.""" + self._latest_joint_state = msg + + def _on_robot_state(self, msg: RobotState) -> None: + """Callback for robot state feedback.""" + self._latest_robot_state = msg + + def _on_trajectory(self, msg: JointTrajectory) -> None: + """Callback when trajectory is received via topic.""" + logger.info( + f"Received trajectory via topic: {len(msg.points)} points, duration={msg.duration:.3f}s" + ) + self.execute_trajectory(msg) + + # ========================================================================= + # Execution Loop + # ========================================================================= + + def _execution_loop(self) -> None: + """ + Main execution loop running at control_frequency Hz. + + When EXECUTING: + 1. Compute elapsed time + 2. Sample trajectory at t + 3. Publish joint command + 4. Check if done + """ + period = 1.0 / self.config.control_frequency + logger.info(f"Execution loop started at {self.config.control_frequency}Hz") + + while not self._stop_event.is_set(): + try: + with self._lock: + # Only process if executing + if self._state != TrajectoryState.EXECUTING: + # Release lock and sleep + pass + else: + # Compute elapsed time + t = time.time() - self._start_time + + # Check if trajectory complete + if self._trajectory is None: + self._state = TrajectoryState.FAULT + logger.error("Trajectory is None during execution") + elif t >= self._trajectory.duration: + self._state = TrajectoryState.COMPLETED + logger.info( + f"Trajectory completed: duration={self._trajectory.duration:.3f}s" + ) + else: + # Sample trajectory + q_ref, _qd_ref = self._trajectory.sample(t) + + # Create and publish command (outside lock would be better but simpler here) + cmd = JointCommand(positions=q_ref, timestamp=time.time()) + + # Publish - must release lock first for thread safety + trajectory_active = True + + if trajectory_active if "trajectory_active" in dir() else False: + try: + self.joint_position_command.publish(cmd) + except Exception as e: + logger.error(f"Failed to publish joint command: {e}") + with self._lock: + self._state = TrajectoryState.FAULT + self._error_message = f"Publish failed: {e}" + + # Reset flag + trajectory_active = False + + # Maintain loop frequency + time.sleep(period) + + except Exception as e: + logger.error(f"Error in execution loop: {e}") + with self._lock: + if self._state == TrajectoryState.EXECUTING: + self._state = TrajectoryState.FAULT + self._error_message = str(e) + time.sleep(period) + + logger.info("Execution loop stopped") + + +# Expose blueprint for declarative composition +joint_trajectory_controller = JointTrajectoryController.blueprint diff --git a/dimos/manipulation/control/trajectory_controller/spec.py b/dimos/manipulation/control/trajectory_controller/spec.py new file mode 100644 index 0000000000..3da272a5b9 --- /dev/null +++ b/dimos/manipulation/control/trajectory_controller/spec.py @@ -0,0 +1,101 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Joint Trajectory Controller Specification + +A simple joint-space trajectory executor. Does NOT: +- Use Cartesian space +- Compute error +- Apply PID +- Call IK + +Just samples a trajectory at time t and sends joint positions to the driver. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol + +if TYPE_CHECKING: + from dimos.core import In, Out + from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState + from dimos.msgs.trajectory_msgs import JointTrajectory as JointTrajectoryMsg, TrajectoryState + +# Input topics +joint_state: In[JointState] | None = None # Feedback from arm driver +robot_state: In[RobotState] | None = None # Robot status from arm driver +trajectory: In[JointTrajectoryMsg] | None = None # Desired trajectory + +# Output topics +joint_position_command: Out[JointCommand] | None = None # To arm driver + + +def execute_trajectory() -> bool: + """ + Set and start executing a new trajectory immediately. + Returns True if accepted, False if controller busy or traj invalid. + """ + raise NotImplementedError("Protocol method") + + +def cancel() -> bool: + """ + Cancel the currently executing trajectory. + Returns True if cancelled, False if no active trajectory. + """ + raise NotImplementedError("Protocol method") + + +def get_status() -> TrajectoryStatusProtocol: + """ + Get the current status of the trajectory execution. + Returns a TrajectoryStatus message with details. + "state": "IDLE" | "EXECUTING" | "COMPLETED" | "ABORTED" | "FAULT", + "progress": float in [0,1], + "active_traj_id": Optional[str], + "error": Optional[str], + """ + raise NotImplementedError("Protocol method") + ... + + +class JointTrajectoryProtocol(Protocol): + """Protocol for a joint trajectory object.""" + + duration: float # Total duration in seconds + + def sample(self, t: float) -> tuple[list[float], list[float]]: + """ + Sample the trajectory at time t. + + Args: + t: Time in seconds (0 <= t <= duration) + + Returns: + Tuple of (q_ref, qd_ref): + - q_ref: Joint positions (radians) + - qd_ref: Joint velocities (rad/s) + """ + ... + + +class TrajectoryStatusProtocol(Protocol): + """Status of trajectory execution.""" + + state: TrajectoryState # Current state + progress: float # Progress 0.0 to 1.0 + time_elapsed: float # Seconds since trajectory start + time_remaining: float # Estimated seconds remaining + error: str | None # Error message if FAULT state diff --git a/dimos/manipulation/control/trajectory_setter.py b/dimos/manipulation/control/trajectory_setter.py new file mode 100644 index 0000000000..5b8b2ff234 --- /dev/null +++ b/dimos/manipulation/control/trajectory_setter.py @@ -0,0 +1,465 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Interactive Trajectory Publisher for Joint Trajectory Control. + +Interactive terminal UI for creating joint trajectories using the +JointTrajectoryGenerator with trapezoidal velocity profiles. + +Workflow: +1. Add waypoints (joint positions only, no timing) +2. Generator applies trapezoidal velocity profile +3. Preview the generated trajectory +4. Publish to /trajectory topic + +Use with example_trajectory_control.py running in another terminal. +""" + +import math +import sys +import time + +from dimos import core +from dimos.manipulation.planning import JointTrajectoryGenerator +from dimos.msgs.sensor_msgs import JointState +from dimos.msgs.trajectory_msgs import JointTrajectory + + +class TrajectorySetter: + """ + Creates and publishes JointTrajectory using trapezoidal velocity profiles. + + Uses JointTrajectoryGenerator to compute proper timing and velocities + from a list of waypoints. Subscribes to arm-specific joint_states to get + current joint positions. + + Supports multiple arm types: + - xarm (xarm5/6/7) + - piper + - Any future arm that publishes joint_states + """ + + def __init__(self, arm_type: str = "xarm"): + """ + Initialize the trajectory setter. + + Args: + arm_type: Type of arm ("xarm", "piper", etc.) + """ + self.arm_type = arm_type.lower() + + # Publisher for trajectories + self.trajectory_pub: core.LCMTransport[JointTrajectory] = core.LCMTransport( + "/trajectory", JointTrajectory + ) + + # Subscribe to arm-specific joint state topic + joint_state_topic = f"/{self.arm_type}/joint_states" + self.joint_state_sub: core.LCMTransport[JointState] = core.LCMTransport( + joint_state_topic, JointState + ) + self.latest_joint_state: JointState | None = None + + # Will be set dynamically from joint_state + self.num_joints: int | None = None + self.generator: JointTrajectoryGenerator | None = None + + print(f"TrajectorySetter initialized for {self.arm_type.upper()}") + print(" Publishing to: /trajectory") + print(f" Subscribing to: {joint_state_topic}") + + def start(self) -> bool: + """Start subscribing to joint state.""" + self.joint_state_sub.subscribe(self._on_joint_state) + print(" Waiting for joint state...") + + for _ in range(50): # 5 second timeout + if self.latest_joint_state is not None: + # Dynamically determine joint count from actual joint_state + self.num_joints = len(self.latest_joint_state.position) + print(f" āœ“ Joint state received ({self.num_joints} joints)") + + # Now create generator with correct joint count + self.generator = JointTrajectoryGenerator( + num_joints=self.num_joints, + max_velocity=1.0, # rad/s + max_acceleration=2.0, # rad/s^2 + points_per_segment=50, + ) + print(f" Max velocity: {self.generator.max_velocity[0]:.2f} rad/s") + print(f" Max acceleration: {self.generator.max_acceleration[0]:.2f} rad/s^2") + return True + time.sleep(0.1) + + print(" ⚠ Warning: No joint state received (timeout)") + return False + + def _on_joint_state(self, msg: JointState) -> None: + """Callback for joint state updates.""" + self.latest_joint_state = msg + + def get_current_joints(self) -> list[float] | None: + """Get current joint positions in radians (first num_joints only).""" + if self.latest_joint_state is None: + return None + # Only take first num_joints (exclude gripper if present) + return list(self.latest_joint_state.position[: self.num_joints]) + + def generate_trajectory(self, waypoints: list[list[float]]) -> JointTrajectory: + """ + Generate a trajectory from waypoints using trapezoidal velocity profile. + + Args: + waypoints: List of joint positions [j1, j2, ..., j6] in radians + + Returns: + JointTrajectory with proper timing and velocities + """ + if self.generator is None: + raise RuntimeError("Generator not initialized - joint state not received yet") + return self.generator.generate(waypoints) + + def publish_trajectory(self, trajectory: JointTrajectory) -> None: + """ + Publish a JointTrajectory to the /trajectory topic. + + Args: + trajectory: Generated trajectory to publish + """ + self.trajectory_pub.broadcast(None, trajectory) + print( + f"\nPublished trajectory: {len(trajectory.points)} points, " + f"duration={trajectory.duration:.2f}s" + ) + + +def parse_joint_input(line: str, num_joints: int) -> list[float] | None: + """ + Parse joint positions from user input. + + Accepts degrees by default, or radians with 'r' suffix. + """ + parts = line.strip().split() + if len(parts) != num_joints: + return None + + positions = [] + for part in parts: + try: + if part.endswith("r"): + positions.append(float(part[:-1])) + else: + positions.append(math.radians(float(part))) + except ValueError: + return None + + return positions + + +def preview_waypoints(waypoints: list[list[float]], num_joints: int) -> None: + """Show waypoints list.""" + if not waypoints: + print("No waypoints") + return + + # Dynamically generate header based on joint count + joint_headers = " ".join([f"{'J' + str(i + 1):>7}" for i in range(num_joints)]) + line_width = 6 + 3 + num_joints * 8 + 10 + + print(f"\nWaypoints ({len(waypoints)}):") + print("-" * line_width) + print(f" # | {joint_headers} (degrees)") + print("-" * line_width) + for i, joints in enumerate(waypoints): + deg = [f"{math.degrees(j):7.1f}" for j in joints] + print(f" {i + 1:2} | {' '.join(deg)}") + print("-" * line_width) + + +def preview_trajectory(trajectory: JointTrajectory, num_joints: int) -> None: + """Show generated trajectory preview.""" + # Dynamically generate header based on joint count + joint_headers = " ".join([f"{'J' + str(i + 1):>7}" for i in range(num_joints)]) + line_width = 9 + 3 + num_joints * 8 + 10 + + print("\n" + "=" * line_width) + print("GENERATED TRAJECTORY") + print("=" * line_width) + print(f"Duration: {trajectory.duration:.3f}s") + print(f"Points: {len(trajectory.points)}") + print("-" * line_width) + print(f"{'Time':>6} | {joint_headers} (degrees)") + print("-" * line_width) + + # Sample at regular intervals + num_samples = min(15, max(len(trajectory.points) // 10, 5)) + for i in range(num_samples + 1): + t = (i / num_samples) * trajectory.duration + q_ref, _ = trajectory.sample(t) + q_deg = [f"{math.degrees(q):7.1f}" for q in q_ref] + print(f"{t:6.2f} | {' '.join(q_deg)}") + + print("-" * line_width) + + # Show velocity profile info + if trajectory.points: + max_vels = [0.0] * len(trajectory.points[0].velocities) + for pt in trajectory.points: + for j, v in enumerate(pt.velocities): + max_vels[j] = max(max_vels[j], abs(v)) + vel_deg = [f"{math.degrees(v):5.1f}" for v in max_vels] + print(f"Peak velocities (deg/s): [{', '.join(vel_deg)}]") + print("=" * line_width) + + +def interactive_mode(setter: TrajectorySetter) -> None: + """Interactive mode for creating trajectories.""" + if setter.num_joints is None: + print("Error: No joint state received. Cannot start interactive mode.") + return + + # Generate dynamic joint list for help text + joint_args = " ".join([f"" for i in range(setter.num_joints)]) + + print("\n" + "=" * 80) + print("Interactive Trajectory Setter") + print("=" * 80) + print(f"\nArm: {setter.num_joints} joints") + print("\nCommands:") + print(f" add {joint_args} - Add waypoint (degrees)") + print(" here - Add current position as waypoint") + print(" current - Show current joints") + print(" list - List waypoints") + print(" delete - Delete waypoint n") + print(" preview - Generate and preview trajectory") + print(" run - Generate and publish trajectory") + print(" clear - Clear waypoints") + print(" vel - Set max velocity (rad/s)") + print(" accel - Set max acceleration (rad/s^2)") + print(" limits - Show current limits") + print(" quit - Exit") + print("=" * 80) + + waypoints: list[list[float]] = [] + generated_trajectory: JointTrajectory | None = None + + try: + while True: + prompt = f"[{len(waypoints)} wp] > " + line = input(prompt).strip() + + if not line: + continue + + parts = line.split() + cmd = parts[0].lower() + + # ADD waypoint + if cmd == "add" and len(parts) >= setter.num_joints + 1: + joints = parse_joint_input( + " ".join(parts[1 : setter.num_joints + 1]), setter.num_joints + ) + if joints: + waypoints.append(joints) + generated_trajectory = None # Invalidate cached trajectory + deg = [f"{math.degrees(j):.1f}" for j in joints] + print(f"Added waypoint {len(waypoints)}: [{', '.join(deg)}] deg") + else: + print(f"Invalid joint values (need {setter.num_joints} values in degrees)") + + # HERE - add current position + elif cmd == "here": + joints = setter.get_current_joints() + if joints: + waypoints.append(joints) + generated_trajectory = None + deg = [f"{math.degrees(j):.1f}" for j in joints] + print(f"Added waypoint {len(waypoints)}: [{', '.join(deg)}] deg") + else: + print("No joint state available") + + # CURRENT + elif cmd == "current": + joints = setter.get_current_joints() + if joints: + deg = [f"{math.degrees(j):.1f}" for j in joints] + print(f"Current: [{', '.join(deg)}] deg") + else: + print("No joint state available") + + # LIST + elif cmd == "list": + preview_waypoints(waypoints, setter.num_joints) + + # DELETE + elif cmd == "delete" and len(parts) >= 2: + try: + idx = int(parts[1]) - 1 + if 0 <= idx < len(waypoints): + waypoints.pop(idx) + generated_trajectory = None + print(f"Deleted waypoint {idx + 1}") + else: + print(f"Invalid index (1-{len(waypoints)})") + except ValueError: + print("Invalid index") + + # PREVIEW + elif cmd == "preview": + if len(waypoints) < 2: + print("Need at least 2 waypoints") + else: + print("\nGenerating trajectory...") + try: + generated_trajectory = setter.generate_trajectory(waypoints) + preview_trajectory(generated_trajectory, setter.num_joints) + except Exception as e: + print(f"Error generating trajectory: {e}") + + # RUN + elif cmd == "run": + if len(waypoints) < 2: + print("Need at least 2 waypoints") + continue + + # Generate if not already generated + if generated_trajectory is None: + print("\nGenerating trajectory...") + try: + generated_trajectory = setter.generate_trajectory(waypoints) + except Exception as e: + print(f"Error generating trajectory: {e}") + continue + + preview_trajectory(generated_trajectory, setter.num_joints) + confirm = input("\nPublish to robot? [y/N]: ").strip().lower() + if confirm == "y": + setter.publish_trajectory(generated_trajectory) + + # CLEAR + elif cmd == "clear": + waypoints.clear() + generated_trajectory = None + print("Cleared") + + # VEL - set max velocity + elif cmd == "vel" and len(parts) >= 2: + if setter.generator is None: + print("Generator not initialized") + continue + try: + vel = float(parts[1]) + if vel <= 0: + print("Velocity must be positive") + else: + setter.generator.set_limits(vel, setter.generator.max_acceleration) + generated_trajectory = None + print( + f"Max velocity set to {vel:.2f} rad/s ({math.degrees(vel):.1f} deg/s)" + ) + except ValueError: + print("Invalid velocity") + + # ACCEL - set max acceleration + elif cmd == "accel" and len(parts) >= 2: + if setter.generator is None: + print("Generator not initialized") + continue + try: + accel = float(parts[1]) + if accel <= 0: + print("Acceleration must be positive") + else: + setter.generator.set_limits(setter.generator.max_velocity, accel) + generated_trajectory = None + print(f"Max acceleration set to {accel:.2f} rad/s^2") + except ValueError: + print("Invalid acceleration") + + # LIMITS - show current limits + elif cmd == "limits": + if setter.generator is None: + print("Generator not initialized") + continue + v = setter.generator.max_velocity[0] + a = setter.generator.max_acceleration[0] + print(f"Max velocity: {v:.2f} rad/s ({math.degrees(v):.1f} deg/s)") + print(f"Max acceleration: {a:.2f} rad/s^2 ({math.degrees(a):.1f} deg/s^2)") + + # QUIT + elif cmd in ("quit", "exit", "q"): + break + + else: + print(f"Unknown command: {cmd}") + + except KeyboardInterrupt: + print("\n\nExiting...") + + +def main() -> int: + """Main entry point.""" + import argparse + + parser = argparse.ArgumentParser(description="Interactive Trajectory Setter for robot arms") + parser.add_argument( + "--arm", + type=str, + default="xarm", + choices=["xarm", "piper"], + help="Type of arm to control (default: xarm)", + ) + parser.add_argument( + "--custom-arm", + type=str, + help="Custom arm type (will subscribe to //joint_states)", + ) + args = parser.parse_args() + + arm_type = args.custom_arm if args.custom_arm else args.arm + + print("\n" + "=" * 80) + print("Trajectory Setter") + print("=" * 80) + print(f"\nArm Type: {arm_type.upper()}") + print("Generates joint trajectories using trapezoidal velocity profiles.") + print("Run example_trajectory_control.py in another terminal first!") + print("=" * 80) + + setter = TrajectorySetter(arm_type=arm_type) + if not setter.start(): + print(f"\nWarning: Could not get joint state from /{arm_type}/joint_states") + print("Controller may not be running or arm type may be incorrect.") + response = input("Continue anyway? [y/N]: ").strip().lower() + if response != "y": + return 0 + + interactive_mode(setter) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\n\nInterrupted by user") + sys.exit(0) + except Exception as e: + print(f"\nError: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/dimos/manipulation/manip_aio_pipeline.py b/dimos/manipulation/manip_aio_pipeline.py index 164c7b1774..fe3598ab1e 100644 --- a/dimos/manipulation/manip_aio_pipeline.py +++ b/dimos/manipulation/manip_aio_pipeline.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,14 +28,16 @@ import websockets from dimos.perception.common.utils import colorize_depth -from dimos.perception.detection2d.detic_2d_det import Detic2DDetector +from dimos.perception.detection2d.detic_2d_det import ( # type: ignore[import-not-found, import-untyped] + Detic2DDetector, +) from dimos.perception.grasp_generation.utils import draw_grasps_on_image from dimos.perception.object_detection_stream import ObjectDetectionStream from dimos.perception.pointcloud.pointcloud_filtering import PointcloudFiltering from dimos.perception.pointcloud.utils import create_point_cloud_overlay_visualization from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.manip_aio_pipeline") +logger = setup_logger() class ManipulationPipeline: @@ -79,19 +81,19 @@ def __init__( self.grasp_loop_thread = None # Storage for grasp results and filtered objects - self.latest_grasps: list[dict] = [] # Simplified: just a list of grasps + self.latest_grasps: list[dict] = [] # type: ignore[type-arg] # Simplified: just a list of grasps self.grasps_consumed = False - self.latest_filtered_objects = [] + self.latest_filtered_objects = [] # type: ignore[var-annotated] self.latest_rgb_for_grasps = None # Store RGB image for grasp overlay self.grasp_lock = threading.Lock() # Track pending requests - simplified to single task - self.grasp_task: asyncio.Task | None = None + self.grasp_task: asyncio.Task | None = None # type: ignore[type-arg] # Reactive subjects for streaming filtered objects and grasps - self.filtered_objects_subject = rx.subject.Subject() - self.grasps_subject = rx.subject.Subject() - self.grasp_overlay_subject = rx.subject.Subject() # Add grasp overlay subject + self.filtered_objects_subject = rx.subject.Subject() # type: ignore[var-annotated] + self.grasps_subject = rx.subject.Subject() # type: ignore[var-annotated] + self.grasp_overlay_subject = rx.subject.Subject() # type: ignore[var-annotated] # Add grasp overlay subject # Initialize grasp client if enabled if self.enable_grasp_generation and self.grasp_server_url: @@ -109,7 +111,7 @@ def __init__( logger.info(f"Initialized ManipulationPipeline with confidence={min_confidence}") - def create_streams(self, zed_stream: rx.Observable) -> dict[str, rx.Observable]: + def create_streams(self, zed_stream: rx.Observable) -> dict[str, rx.Observable]: # type: ignore[type-arg] """ Create streams using exact old main logic. """ @@ -118,7 +120,7 @@ def create_streams(self, zed_stream: rx.Observable) -> dict[str, rx.Observable]: # RGB stream for object detection (from old main) video_stream = zed_frame_stream.pipe( - ops.map(lambda x: x.get("rgb") if x is not None else None), + ops.map(lambda x: x.get("rgb") if x is not None else None), # type: ignore[attr-defined] ops.filter(lambda x: x is not None), ops.share(), ) @@ -138,7 +140,7 @@ def create_streams(self, zed_stream: rx.Observable) -> dict[str, rx.Observable]: frame_lock = threading.Lock() # Subscribe to combined ZED frames (from old main) - def on_zed_frame(zed_data) -> None: + def on_zed_frame(zed_data) -> None: # type: ignore[no-untyped-def] nonlocal latest_rgb, latest_depth if zed_data is not None: with frame_lock: @@ -146,7 +148,7 @@ def on_zed_frame(zed_data) -> None: latest_depth = zed_data.get("depth") # Depth stream for point cloud filtering (from old main) - def get_depth_or_overlay(zed_data): + def get_depth_or_overlay(zed_data): # type: ignore[no-untyped-def] if zed_data is None: return None @@ -165,7 +167,7 @@ def get_depth_or_overlay(zed_data): ) # Process object detection results with point cloud filtering (from old main) - def on_detection_next(result) -> None: + def on_detection_next(result) -> None: # type: ignore[no-untyped-def] nonlocal latest_point_cloud_overlay if result.get("objects"): # Get latest RGB and depth frames @@ -190,9 +192,9 @@ def on_detection_next(result) -> None: # Create point cloud overlay visualization overlay_viz = create_point_cloud_overlay_visualization( - base_image=base_image, - objects=filtered_objects, - intrinsics=self.camera_intrinsics, + base_image=base_image, # type: ignore[arg-type] + objects=filtered_objects, # type: ignore[arg-type] + intrinsics=self.camera_intrinsics, # type: ignore[arg-type] ) # Store the overlay for the stream @@ -205,7 +207,7 @@ def on_detection_next(result) -> None: with frame_lock: self.latest_rgb_for_grasps = rgb.copy() - task = self.request_scene_grasps(filtered_objects) + task = self.request_scene_grasps(filtered_objects) # type: ignore[arg-type] if task: # Check for results after a delay def check_grasps_later() -> None: @@ -213,7 +215,7 @@ def check_grasps_later() -> None: # Wait for task to complete if hasattr(self, "grasp_task") and self.grasp_task: try: - self.grasp_task.result( + self.grasp_task.result( # type: ignore[call-arg] timeout=3.0 ) # Get result with timeout except Exception as e: @@ -226,7 +228,7 @@ def check_grasps_later() -> None: if grasps and hasattr(self, "latest_rgb_for_grasps"): # Create grasp overlay on the saved RGB image try: - bgr_image = cv2.cvtColor( + bgr_image = cv2.cvtColor( # type: ignore[call-overload] self.latest_rgb_for_grasps, cv2.COLOR_RGB2BGR ) result_bgr = draw_grasps_on_image( @@ -256,7 +258,7 @@ def check_grasps_later() -> None: with frame_lock: latest_point_cloud_overlay = None - def on_error(error) -> None: + def on_error(error) -> None: # type: ignore[no-untyped-def] logger.error(f"Error in stream: {error}") def on_completed() -> None: @@ -273,13 +275,13 @@ def start_subscriptions() -> None: time.sleep(2) # Give subscriptions time to start # Subscribe to object detection stream (from old main) - object_detector.get_stream().subscribe( + object_detector.get_stream().subscribe( # type: ignore[no-untyped-call] on_next=on_detection_next, on_error=on_error, on_completed=on_completed ) # Create visualization stream for web interface (from old main) - viz_stream = object_detector.get_stream().pipe( - ops.map(lambda x: x["viz_frame"] if x is not None else None), + viz_stream = object_detector.get_stream().pipe( # type: ignore[no-untyped-call] + ops.map(lambda x: x["viz_frame"] if x is not None else None), # type: ignore[index] ops.filter(lambda x: x is not None), ) @@ -295,7 +297,7 @@ def start_subscriptions() -> None: return { "detection_viz": viz_stream, "pointcloud_viz": depth_stream, - "objects": object_detector.get_stream().pipe(ops.map(lambda x: x.get("objects", []))), + "objects": object_detector.get_stream().pipe(ops.map(lambda x: x.get("objects", []))), # type: ignore[attr-defined, no-untyped-call] "filtered_objects": filtered_objects_stream, "grasps": grasps_stream, "grasp_overlay": grasp_overlay_stream, @@ -305,20 +307,22 @@ def _start_grasp_loop(self) -> None: """Start asyncio event loop in a background thread for WebSocket communication.""" def run_loop() -> None: - self.grasp_loop = asyncio.new_event_loop() + self.grasp_loop = asyncio.new_event_loop() # type: ignore[assignment] asyncio.set_event_loop(self.grasp_loop) - self.grasp_loop.run_forever() + self.grasp_loop.run_forever() # type: ignore[attr-defined] - self.grasp_loop_thread = threading.Thread(target=run_loop, daemon=True) - self.grasp_loop_thread.start() + self.grasp_loop_thread = threading.Thread(target=run_loop, daemon=True) # type: ignore[assignment] + self.grasp_loop_thread.start() # type: ignore[attr-defined] # Wait for loop to start while self.grasp_loop is None: time.sleep(0.01) async def _send_grasp_request( - self, points: np.ndarray, colors: np.ndarray | None - ) -> list[dict] | None: + self, + points: np.ndarray, # type: ignore[type-arg] + colors: np.ndarray | None, # type: ignore[type-arg] + ) -> list[dict] | None: # type: ignore[type-arg] """Send grasp request to Dimensional Grasp server.""" try: # Comprehensive client-side validation to prevent server errors @@ -371,7 +375,7 @@ async def _send_grasp_request( # Clamp color values to valid range [0, 1] colors = np.clip(colors, 0.0, 1.0) - async with websockets.connect(self.grasp_server_url) as websocket: + async with websockets.connect(self.grasp_server_url) as websocket: # type: ignore[arg-type] request = { "points": points.tolist(), "colors": colors.tolist(), # Always send colors array @@ -417,7 +421,7 @@ async def _send_grasp_request( return None - def request_scene_grasps(self, objects: list[dict]) -> asyncio.Task | None: + def request_scene_grasps(self, objects: list[dict]) -> asyncio.Task | None: # type: ignore[type-arg] """Request grasps for entire scene by combining all object point clouds.""" if not self.grasp_loop or not objects: return None @@ -496,7 +500,7 @@ def request_scene_grasps(self, objects: list[dict]) -> asyncio.Task | None: logger.warning("Failed to create grasp task") return None - def get_latest_grasps(self, timeout: float = 5.0) -> list[dict] | None: + def get_latest_grasps(self, timeout: float = 5.0) -> list[dict] | None: # type: ignore[type-arg] """Get latest grasp results, waiting for new ones if current ones have been consumed.""" # Mark current grasps as consumed and get a reference with self.grasp_lock: @@ -523,7 +527,7 @@ def clear_grasps(self) -> None: with self.grasp_lock: self.latest_grasps = [] - def _prepare_colors(self, colors: np.ndarray | None) -> np.ndarray | None: + def _prepare_colors(self, colors: np.ndarray | None) -> np.ndarray | None: # type: ignore[type-arg] """Prepare colors array, converting from various formats if needed.""" if colors is None: return None @@ -533,7 +537,7 @@ def _prepare_colors(self, colors: np.ndarray | None) -> np.ndarray | None: return colors - def _convert_grasp_format(self, grasps: list[dict]) -> list[dict]: + def _convert_grasp_format(self, grasps: list[dict]) -> list[dict]: # type: ignore[type-arg] """Convert Grasp format to our visualization format.""" converted = [] @@ -557,7 +561,7 @@ def _convert_grasp_format(self, grasps: list[dict]) -> list[dict]: return converted - def _rotation_matrix_to_euler(self, rotation_matrix: np.ndarray) -> dict[str, float]: + def _rotation_matrix_to_euler(self, rotation_matrix: np.ndarray) -> dict[str, float]: # type: ignore[type-arg] """Convert rotation matrix to Euler angles (in radians).""" sy = np.sqrt(rotation_matrix[0, 0] ** 2 + rotation_matrix[1, 0] ** 2) diff --git a/dimos/manipulation/manip_aio_processer.py b/dimos/manipulation/manip_aio_processer.py index e0bfc73256..71ed42bff3 100644 --- a/dimos/manipulation/manip_aio_processer.py +++ b/dimos/manipulation/manip_aio_processer.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,7 +27,9 @@ combine_object_data, detection_results_to_object_data, ) -from dimos.perception.detection2d.detic_2d_det import Detic2DDetector +from dimos.perception.detection2d.detic_2d_det import ( # type: ignore[import-not-found, import-untyped] + Detic2DDetector, +) from dimos.perception.grasp_generation.grasp_generation import HostedGraspGenerator from dimos.perception.grasp_generation.utils import create_grasp_overlay from dimos.perception.pointcloud.pointcloud_filtering import PointcloudFiltering @@ -39,7 +41,7 @@ from dimos.perception.segmentation.sam_2d_seg import Sam2DSegmenter from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.manip_aio_processor") +logger = setup_logger() class ManipulationProcessor: @@ -106,7 +108,7 @@ def __init__( self.grasp_generator = None if self.enable_grasp_generation: try: - self.grasp_generator = HostedGraspGenerator(server_url=grasp_server_url) + self.grasp_generator = HostedGraspGenerator(server_url=grasp_server_url) # type: ignore[arg-type] logger.info("Hosted grasp generator initialized successfully") except Exception as e: logger.error(f"Failed to initialize hosted grasp generator: {e}") @@ -119,7 +121,10 @@ def __init__( ) def process_frame( - self, rgb_image: np.ndarray, depth_image: np.ndarray, generate_grasps: bool | None = None + self, + rgb_image: np.ndarray, # type: ignore[type-arg] + depth_image: np.ndarray, # type: ignore[type-arg] + generate_grasps: bool | None = None, ) -> dict[str, Any]: """ Process a single RGB-D frame through the complete pipeline. @@ -164,7 +169,7 @@ def process_frame( segmentation_results = self.run_segmentation(rgb_image) results["segmentation2d_objects"] = segmentation_results.get("objects", []) results["segmentation_viz"] = segmentation_results.get("viz_frame") - segmentation_time = time.time() - step_start + segmentation_time = time.time() - step_start # type: ignore[assignment] # Step 3: Point Cloud Processing pointcloud_time = 0 @@ -178,7 +183,7 @@ def process_frame( detected_objects = self.run_pointcloud_filtering( rgb_image, depth_image, detection2d_objects ) - pointcloud_time += time.time() - step_start + pointcloud_time += time.time() - step_start # type: ignore[assignment] # Process segmentation objects if available segmentation_filtered_objects = [] @@ -187,11 +192,13 @@ def process_frame( segmentation_filtered_objects = self.run_pointcloud_filtering( rgb_image, depth_image, segmentation2d_objects ) - pointcloud_time += time.time() - step_start + pointcloud_time += time.time() - step_start # type: ignore[assignment] # Combine all objects using intelligent duplicate removal all_objects = combine_object_data( - detected_objects, segmentation_filtered_objects, overlap_threshold=0.8 + detected_objects, # type: ignore[arg-type] + segmentation_filtered_objects, # type: ignore[arg-type] + overlap_threshold=0.8, ) # Get full point cloud @@ -201,7 +208,7 @@ def process_frame( misc_start = time.time() misc_clusters, misc_voxel_grid = extract_and_cluster_misc_points( full_pcd, - all_objects, + all_objects, # type: ignore[arg-type] eps=0.03, min_points=100, enable_filtering=True, @@ -226,9 +233,9 @@ def process_frame( # Create visualizations results["pointcloud_viz"] = ( create_point_cloud_overlay_visualization( - base_image=base_image, - objects=all_objects, - intrinsics=self.camera_intrinsics, + base_image=base_image, # type: ignore[arg-type] + objects=all_objects, # type: ignore[arg-type] + intrinsics=self.camera_intrinsics, # type: ignore[arg-type] ) if all_objects else base_image @@ -236,9 +243,9 @@ def process_frame( results["detected_pointcloud_viz"] = ( create_point_cloud_overlay_visualization( - base_image=base_image, + base_image=base_image, # type: ignore[arg-type] objects=detected_objects, - intrinsics=self.camera_intrinsics, + intrinsics=self.camera_intrinsics, # type: ignore[arg-type] ) if detected_objects else base_image @@ -251,7 +258,7 @@ def process_frame( for i in range(len(misc_clusters)) ] results["misc_pointcloud_viz"] = overlay_point_clouds_on_image( - base_image=base_image, + base_image=base_image, # type: ignore[arg-type] point_clouds=misc_clusters, camera_intrinsics=self.camera_intrinsics, colors=cluster_colors, @@ -267,7 +274,7 @@ def process_frame( ) if should_generate_grasps and all_objects and full_pcd: - grasps = self.run_grasp_generation(all_objects, full_pcd) + grasps = self.run_grasp_generation(all_objects, full_pcd) # type: ignore[arg-type] results["grasps"] = grasps if grasps: results["grasp_overlay"] = create_grasp_overlay( @@ -295,7 +302,7 @@ def process_frame( return results - def run_object_detection(self, rgb_image: np.ndarray) -> dict[str, Any]: + def run_object_detection(self, rgb_image: np.ndarray) -> dict[str, Any]: # type: ignore[type-arg] """Run object detection on RGB image.""" try: # Convert RGB to BGR for Detic detector @@ -329,19 +336,24 @@ def run_object_detection(self, rgb_image: np.ndarray) -> dict[str, Any]: return {"objects": [], "viz_frame": rgb_image.copy()} def run_pointcloud_filtering( - self, rgb_image: np.ndarray, depth_image: np.ndarray, objects: list[dict] - ) -> list[dict]: + self, + rgb_image: np.ndarray, # type: ignore[type-arg] + depth_image: np.ndarray, # type: ignore[type-arg] + objects: list[dict], # type: ignore[type-arg] + ) -> list[dict]: # type: ignore[type-arg] """Run point cloud filtering on detected objects.""" try: filtered_objects = self.pointcloud_filter.process_images( - rgb_image, depth_image, objects + rgb_image, + depth_image, + objects, # type: ignore[arg-type] ) - return filtered_objects if filtered_objects else [] + return filtered_objects if filtered_objects else [] # type: ignore[return-value] except Exception as e: logger.error(f"Point cloud filtering failed: {e}") return [] - def run_segmentation(self, rgb_image: np.ndarray) -> dict[str, Any]: + def run_segmentation(self, rgb_image: np.ndarray) -> dict[str, Any]: # type: ignore[type-arg] """Run semantic segmentation on RGB image.""" if not self.segmenter: return {"objects": [], "viz_frame": rgb_image.copy()} @@ -351,7 +363,7 @@ def run_segmentation(self, rgb_image: np.ndarray) -> dict[str, Any]: bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) # Get segmentation results - masks, bboxes, track_ids, probs, names = self.segmenter.process_image(bgr_image) + masks, bboxes, track_ids, probs, names = self.segmenter.process_image(bgr_image) # type: ignore[no-untyped-call] # Convert to ObjectData format using utility function objects = detection_results_to_object_data( @@ -380,7 +392,7 @@ def run_segmentation(self, rgb_image: np.ndarray) -> dict[str, Any]: logger.error(f"Segmentation failed: {e}") return {"objects": [], "viz_frame": rgb_image.copy()} - def run_grasp_generation(self, filtered_objects: list[dict], full_pcd) -> list[dict] | None: + def run_grasp_generation(self, filtered_objects: list[dict], full_pcd) -> list[dict] | None: # type: ignore[no-untyped-def, type-arg] """Run grasp generation using the configured generator.""" if not self.grasp_generator: logger.warning("Grasp generation requested but no generator available") @@ -388,7 +400,7 @@ def run_grasp_generation(self, filtered_objects: list[dict], full_pcd) -> list[d try: # Generate grasps using the configured generator - grasps = self.grasp_generator.generate_grasps_from_objects(filtered_objects, full_pcd) + grasps = self.grasp_generator.generate_grasps_from_objects(filtered_objects, full_pcd) # type: ignore[arg-type] # Return parsed results directly (list of grasp dictionaries) return grasps diff --git a/dimos/manipulation/manipulation_history.py b/dimos/manipulation/manipulation_history.py index a77900ba30..8d9b281d76 100644 --- a/dimos/manipulation/manipulation_history.py +++ b/dimos/manipulation/manipulation_history.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ ) from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.types.manipulation_history") +logger = setup_logger() @dataclass @@ -256,7 +256,7 @@ def create_task_entry( self.add_entry(entry) return entry - def search(self, **kwargs) -> list[ManipulationHistoryEntry]: + def search(self, **kwargs) -> list[ManipulationHistoryEntry]: # type: ignore[no-untyped-def] """Flexible search method that can search by any field in ManipulationHistoryEntry using dot notation. This method supports dot notation to access nested fields. String values automatically use @@ -294,7 +294,7 @@ def search(self, **kwargs) -> list[ManipulationHistoryEntry]: return results - def _check_field_match(self, entry, field_path, value) -> bool: + def _check_field_match(self, entry, field_path, value) -> bool: # type: ignore[no-untyped-def] """Check if a field matches the value, with special handling for strings, collections and comparisons. For string values, we automatically use substring matching (contains). @@ -315,19 +315,19 @@ def _check_field_match(self, entry, field_path, value) -> bool: True if the field matches the value, False otherwise """ try: - field_value = self._get_value_by_path(entry, field_path) + field_value = self._get_value_by_path(entry, field_path) # type: ignore[no-untyped-call] # Handle comparison operators for timestamps and numbers if isinstance(value, tuple) and len(value) == 2: op, compare_value = value if op == ">": - return field_value > compare_value + return field_value > compare_value # type: ignore[no-any-return] elif op == "<": - return field_value < compare_value + return field_value < compare_value # type: ignore[no-any-return] elif op == ">=": - return field_value >= compare_value + return field_value >= compare_value # type: ignore[no-any-return] elif op == "<=": - return field_value <= compare_value + return field_value <= compare_value # type: ignore[no-any-return] # Handle lists (from collection searches) if isinstance(field_value, list): @@ -346,12 +346,12 @@ def _check_field_match(self, entry, field_path, value) -> bool: return value in field_value # All other types use exact matching else: - return field_value == value + return field_value == value # type: ignore[no-any-return] except (AttributeError, KeyError): return False - def _get_value_by_path(self, obj, path): + def _get_value_by_path(self, obj, path): # type: ignore[no-untyped-def] """Get a value from an object using a dot-separated path. This method handles three special cases: @@ -385,7 +385,7 @@ def _get_value_by_path(self, obj, path): if not remaining_path: # If * is the last part, return all values return list(items) elif isinstance(current, list): - items = current + items = current # type: ignore[assignment] if not remaining_path: # If * is the last part, return all items return items else: # Not a collection @@ -398,7 +398,7 @@ def _get_value_by_path(self, obj, path): for item in items: try: # Recursively get values from each item - value = self._get_value_by_path(item, remaining_path) + value = self._get_value_by_path(item, remaining_path) # type: ignore[no-untyped-call] if isinstance(value, list): # Flatten nested lists results.extend(value) else: diff --git a/dimos/manipulation/manipulation_interface.py b/dimos/manipulation/manipulation_interface.py index ae63eb79ed..edeb99c0f0 100644 --- a/dimos/manipulation/manipulation_interface.py +++ b/dimos/manipulation/manipulation_interface.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ if TYPE_CHECKING: from reactivex.disposable import Disposable -logger = setup_logger("dimos.robot.manipulation_interface") +logger = setup_logger() class ManipulationInterface: @@ -53,7 +53,7 @@ def __init__( self, output_dir: str, new_memory: bool = False, - perception_stream: ObjectDetectionStream = None, + perception_stream: ObjectDetectionStream = None, # type: ignore[assignment] ) -> None: """ Initialize a new ManipulationInterface instance. @@ -136,7 +136,7 @@ def add_manipulation_task( """ # Add task to history - self.manipulation_history.add_entry( + self.manipulation_history.add_entry( # type: ignore[call-arg] task=task, result=None, notes=None, manipulation_response=manipulation_response ) @@ -150,7 +150,7 @@ def get_manipulation_task(self, task_id: str) -> ManipulationTask | None: Returns: The task object or None if not found """ - return self.history.get_manipulation_task(task_id) + return self.history.get_manipulation_task(task_id) # type: ignore[attr-defined, no-any-return] def get_all_manipulation_tasks(self) -> list[ManipulationTask]: """ @@ -159,7 +159,7 @@ def get_all_manipulation_tasks(self) -> list[ManipulationTask]: Returns: List of all manipulation tasks """ - return self.history.get_all_manipulation_tasks() + return self.history.get_all_manipulation_tasks() # type: ignore[attr-defined, no-any-return] def update_task_status( self, task_id: str, status: str, result: dict[str, Any] | None = None @@ -175,7 +175,7 @@ def update_task_status( Returns: The updated task or None if task not found """ - return self.history.update_task_status(task_id, status, result) + return self.history.update_task_status(task_id, status, result) # type: ignore[attr-defined, no-any-return] # === Perception stream methods === @@ -185,13 +185,13 @@ def _setup_perception_subscription(self) -> None: """ if self.perception_stream: # Subscribe to the stream and update latest_objects - self.stream_subscription = self.perception_stream.get_stream().subscribe( + self.stream_subscription = self.perception_stream.get_stream().subscribe( # type: ignore[no-untyped-call] on_next=self._update_latest_objects, on_error=lambda e: logger.error(f"Error in perception stream: {e}"), ) logger.info("Subscribed to perception stream") - def _update_latest_objects(self, data) -> None: + def _update_latest_objects(self, data) -> None: # type: ignore[no-untyped-def] """ Update the latest detected objects. @@ -237,7 +237,7 @@ def get_objects_by_label(self, label: str) -> list[ObjectData]: """ return [obj for obj in self.latest_objects if obj["label"] == label] - def set_perception_stream(self, perception_stream) -> None: + def set_perception_stream(self, perception_stream) -> None: # type: ignore[no-untyped-def] """ Set or update the perception stream. diff --git a/dimos/manipulation/planning/__init__.py b/dimos/manipulation/planning/__init__.py new file mode 100644 index 0000000000..d197980a96 --- /dev/null +++ b/dimos/manipulation/planning/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manipulation Planning Module + +Trajectory generation and motion planning for robotic manipulators. +""" + +from dimos.manipulation.planning.trajectory_generator.joint_trajectory_generator import ( + JointTrajectoryGenerator, +) + +__all__ = ["JointTrajectoryGenerator"] diff --git a/dimos/manipulation/planning/trajectory_generator/__init__.py b/dimos/manipulation/planning/trajectory_generator/__init__.py new file mode 100644 index 0000000000..a7449cf45f --- /dev/null +++ b/dimos/manipulation/planning/trajectory_generator/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Trajectory Generator Module + +Generates time-parameterized trajectories from waypoints. +""" + +from dimos.manipulation.planning.trajectory_generator.joint_trajectory_generator import ( + JointTrajectoryGenerator, +) + +__all__ = ["JointTrajectoryGenerator"] diff --git a/dimos/manipulation/planning/trajectory_generator/joint_trajectory_generator.py b/dimos/manipulation/planning/trajectory_generator/joint_trajectory_generator.py new file mode 100644 index 0000000000..6b732d133c --- /dev/null +++ b/dimos/manipulation/planning/trajectory_generator/joint_trajectory_generator.py @@ -0,0 +1,453 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Joint Trajectory Generator + +Generates time-parameterized joint trajectories from waypoints using +trapezoidal velocity profiles. + +Trapezoidal Profile: + velocity + ^ + | ____________________ + | / \ + | / \ + | / \ + |/ \ + +------------------------------> time + accel cruise decel +""" + +import math + +from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryPoint + + +class JointTrajectoryGenerator: + """ + Generates joint trajectories with trapezoidal velocity profiles. + + For each segment between waypoints: + 1. Determines the limiting joint (one that takes longest) + 2. Applies trapezoidal velocity profile based on limits + 3. Scales other joints to complete in the same time + 4. Generates trajectory points with proper timing + + Usage: + generator = JointTrajectoryGenerator(num_joints=6) + generator.set_limits(max_velocity=1.0, max_acceleration=2.0) + trajectory = generator.generate(waypoints) + """ + + def __init__( + self, + num_joints: int = 6, + max_velocity: list[float] | float = 1.0, + max_acceleration: list[float] | float = 2.0, + points_per_segment: int = 50, + ) -> None: + """ + Initialize trajectory generator. + + Args: + num_joints: Number of joints + max_velocity: rad/s (single value applies to all joints, or per-joint list) + max_acceleration: rad/s^2 (single value or per-joint list) + points_per_segment: Number of intermediate points per waypoint segment + """ + self.num_joints = num_joints + self.points_per_segment = points_per_segment + + # Initialize limits + self.max_velocity: list[float] = [] + self.max_acceleration: list[float] = [] + self.set_limits(max_velocity, max_acceleration) + + def set_limits( + self, + max_velocity: list[float] | float, + max_acceleration: list[float] | float, + ) -> None: + """ + Set velocity and acceleration limits. + + Args: + max_velocity: rad/s (single value applies to all joints, or per-joint) + max_acceleration: rad/s^2 (single value or per-joint) + """ + if isinstance(max_velocity, (int, float)): + self.max_velocity = [float(max_velocity)] * self.num_joints + else: + self.max_velocity = list(max_velocity) + + if isinstance(max_acceleration, (int, float)): + self.max_acceleration = [float(max_acceleration)] * self.num_joints + else: + self.max_acceleration = list(max_acceleration) + + def generate(self, waypoints: list[list[float]]) -> JointTrajectory: + """ + Generate a trajectory through waypoints with trapezoidal velocity profile. + + Args: + waypoints: List of joint positions [q1, q2, ..., qn] in radians + First waypoint is start, last is goal + + Returns: + JointTrajectory with time-parameterized points + """ + if not waypoints or len(waypoints) < 2: + raise ValueError("Need at least 2 waypoints") + + all_points: list[TrajectoryPoint] = [] + current_time = 0.0 + + # Add first waypoint + all_points.append( + TrajectoryPoint( + time_from_start=0.0, + positions=list(waypoints[0]), + velocities=[0.0] * self.num_joints, + ) + ) + + # Process each segment + for i in range(len(waypoints) - 1): + start = waypoints[i] + end = waypoints[i + 1] + + # Generate segment with trapezoidal profile + segment_points, segment_duration = self._generate_segment(start, end, current_time) + + # Add points (skip first as it duplicates previous endpoint) + all_points.extend(segment_points[1:]) + current_time += segment_duration + + return JointTrajectory(points=all_points) + + def _generate_segment( + self, + start: list[float], + end: list[float], + start_time: float, + ) -> tuple[list[TrajectoryPoint], float]: + """ + Generate trajectory points for a single segment using trapezoidal profile. + + Args: + start: Starting joint positions + end: Ending joint positions + start_time: Time offset for this segment + + Returns: + Tuple of (list of TrajectoryPoints, segment duration) + """ + # Calculate displacement for each joint + displacements = [end[j] - start[j] for j in range(self.num_joints)] + + # Find the limiting joint (one that takes longest) + segment_duration = 0.0 + for j in range(self.num_joints): + t = self._compute_trapezoidal_time( + abs(displacements[j]), + self.max_velocity[j], + self.max_acceleration[j], + ) + segment_duration = max(segment_duration, t) + + # Ensure minimum duration + segment_duration = max(segment_duration, 0.01) + + # Generate points along the segment + points: list[TrajectoryPoint] = [] + + for i in range(self.points_per_segment + 1): + # Normalized time [0, 1] + s = i / self.points_per_segment + t = start_time + s * segment_duration + + # Compute position and velocity for each joint + positions = [] + velocities = [] + + for j in range(self.num_joints): + # Compute scaled limits for this joint to fit in segment_duration + v_scaled, a_scaled = self._compute_scaled_limits( + abs(displacements[j]), + segment_duration, + self.max_velocity[j], + self.max_acceleration[j], + ) + + pos, vel = self._trapezoidal_interpolate( + s, + start[j], + end[j], + segment_duration, + v_scaled, + a_scaled, + ) + positions.append(pos) + velocities.append(vel) + + points.append( + TrajectoryPoint( + time_from_start=t, + positions=positions, + velocities=velocities, + ) + ) + + return points, segment_duration + + def _compute_trapezoidal_time( + self, + distance: float, + v_max: float, + a_max: float, + ) -> float: + """ + Compute time to travel a distance with trapezoidal velocity profile. + + Two cases: + 1. Triangle profile: Can't reach v_max (short distance) + 2. Trapezoidal profile: Reaches v_max with cruise phase + + Args: + distance: Absolute distance to travel + v_max: Maximum velocity + a_max: Maximum acceleration + + Returns: + Time to complete the motion + """ + if distance < 1e-9: + return 0.0 + + # Time to accelerate to v_max + t_accel = v_max / a_max + + # Distance covered during accel + decel (both at a_max) + d_accel = 0.5 * a_max * t_accel**2 + d_total_ramp = 2 * d_accel # accel + decel + + if distance <= d_total_ramp: + # Triangle profile - can't reach v_max + # d = 2 * (0.5 * a * t^2) = a * t^2 + # t = sqrt(d / a) + t_ramp = math.sqrt(distance / a_max) + return 2 * t_ramp + else: + # Trapezoidal profile - has cruise phase + d_cruise = distance - d_total_ramp + t_cruise = d_cruise / v_max + return 2 * t_accel + t_cruise + + def _compute_scaled_limits( + self, + distance: float, + duration: float, + v_max: float, + a_max: float, + ) -> tuple[float, float]: + """ + Compute scaled velocity and acceleration to travel distance in given duration. + + This scales down the profile so the joint travels its distance in the + same time as the limiting joint. + + Args: + distance: Absolute distance to travel + duration: Required duration (from limiting joint) + v_max: Maximum velocity limit + a_max: Maximum acceleration limit + + Returns: + Tuple of (scaled_velocity, scaled_acceleration) + """ + if distance < 1e-9 or duration < 1e-9: + return v_max, a_max + + # Compute optimal time for this joint + t_opt = self._compute_trapezoidal_time(distance, v_max, a_max) + + if t_opt >= duration - 1e-9: + # This is the limiting joint or close to it + return v_max, a_max + + # Need to scale down to fit in longer duration + # Use simple scaling: scale both v and a by the same factor + # This preserves the profile shape + scale = t_opt / duration + + # For a symmetric trapezoidal/triangular profile: + # If we scale time by k, we need to scale velocity by 1/k + # But we also need to ensure we travel the same distance + + # Simpler approach: compute the average velocity needed + distance / duration + + # For trapezoidal profile, v_avg = v_peak * (1 - t_accel/duration) + # For simplicity, use a heuristic: scale velocity so trajectory fits + + # Check if we can use a triangle profile + # Triangle: d = 0.5 * v_peak * T, so v_peak = 2 * d / T + v_peak_triangle = 2 * distance / duration + a_for_triangle = 4 * distance / (duration * duration) + + if v_peak_triangle <= v_max and a_for_triangle <= a_max: + # Use triangle profile with these params + return v_peak_triangle, a_for_triangle + + # Use trapezoidal with reduced velocity + # Solve: distance = v * t_cruise + v^2/a + # where t_cruise = duration - 2*v/a + # This is complex, so use iterative scaling + v_scaled = v_max * scale + a_scaled = a_max * scale * scale # acceleration scales with square of time scale + + # Verify and adjust + t_check = self._compute_trapezoidal_time(distance, v_scaled, a_scaled) + if abs(t_check - duration) > 0.01 * duration: + # Fallback: use triangle profile scaled to fit + v_scaled = 2 * distance / duration + a_scaled = 4 * distance / (duration * duration) + + return min(v_scaled, v_max), min(a_scaled, a_max) + + def _trapezoidal_interpolate( + self, + s: float, + start: float, + end: float, + duration: float, + v_max: float, + a_max: float, + ) -> tuple[float, float]: + """ + Interpolate position and velocity using trapezoidal profile. + + Args: + s: Normalized time [0, 1] + start: Start position + end: End position + duration: Total segment duration + v_max: Max velocity for this joint (scaled) + a_max: Max acceleration for this joint (scaled) + + Returns: + Tuple of (position, velocity) + """ + distance = abs(end - start) + direction = 1.0 if end >= start else -1.0 + + if distance < 1e-9 or duration < 1e-9: + return end, 0.0 + + # Handle endpoint exactly + if s >= 1.0 - 1e-9: + return end, 0.0 + if s <= 1e-9: + return start, 0.0 + + # Current time + t = s * duration + + # Compute profile parameters for this joint + t_accel = v_max / a_max if a_max > 1e-9 else duration / 2 + d_accel = 0.5 * a_max * t_accel**2 + d_total_ramp = 2 * d_accel + + if distance <= d_total_ramp + 1e-9: + # Triangle profile + t_peak = duration / 2 + v_peak = 2 * distance / duration + a_eff = v_peak / t_peak if t_peak > 1e-9 else a_max + + if t <= t_peak: + # Accelerating + pos_offset = 0.5 * a_eff * t * t + vel = direction * a_eff * t + else: + # Decelerating + dt = t - t_peak + pos_offset = distance / 2 + v_peak * dt - 0.5 * a_eff * dt * dt + vel = direction * max(0.0, v_peak - a_eff * dt) + else: + # Trapezoidal profile + d_cruise = distance - d_total_ramp + t_cruise = d_cruise / v_max if v_max > 1e-9 else 0 + + if t <= t_accel: + # Accelerating phase + pos_offset = 0.5 * a_max * t * t + vel = direction * a_max * t + elif t <= t_accel + t_cruise: + # Cruise phase + dt = t - t_accel + pos_offset = d_accel + v_max * dt + vel = direction * v_max + else: + # Decelerating phase + dt = t - t_accel - t_cruise + pos_offset = d_accel + d_cruise + v_max * dt - 0.5 * a_max * dt * dt + vel = direction * max(0.0, v_max - a_max * dt) + + position = start + direction * pos_offset + + # Clamp to ensure we don't overshoot + if direction > 0: + position = min(position, end) + else: + position = max(position, end) + + return position, vel + + def preview(self, trajectory: JointTrajectory) -> str: + """ + Generate a text preview of the trajectory. + + Args: + trajectory: Generated trajectory to preview + + Returns: + Formatted string showing trajectory details + """ + lines = [ + "Trajectory Preview", + "=" * 60, + f"Duration: {trajectory.duration:.3f}s", + f"Points: {len(trajectory.points)}", + "", + "Waypoints (time -> positions):", + "-" * 60, + ] + + # Show key points (first, last, and evenly spaced) + indices = [0] + step = max(1, len(trajectory.points) // 5) + indices.extend(range(step, len(trajectory.points) - 1, step)) + indices.append(len(trajectory.points) - 1) + indices = sorted(set(indices)) + + for i in indices: + pt = trajectory.points[i] + pos_str = ", ".join(f"{p:+.3f}" for p in pt.positions) + vel_str = ", ".join(f"{v:+.3f}" for v in pt.velocities) + lines.append(f" t={pt.time_from_start:6.3f}s: pos=[{pos_str}]") + lines.append(f" vel=[{vel_str}]") + + lines.append("-" * 60) + return "\n".join(lines) diff --git a/dimos/manipulation/planning/trajectory_generator/spec.py b/dimos/manipulation/planning/trajectory_generator/spec.py new file mode 100644 index 0000000000..5357679f28 --- /dev/null +++ b/dimos/manipulation/planning/trajectory_generator/spec.py @@ -0,0 +1,76 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Joint Trajectory Generator Specification + +Generates time-parameterized joint trajectories from waypoints using +trapezoidal velocity profiles. Does NOT execute - just generates. + +Input: List of joint positions (waypoints) without timing +Output: JointTrajectory with proper time parameterization + +Trapezoidal Profile: + velocity + ^ + | ____________________ + | / \ + | / \ + | / \ + |/ \ + +------------------------------> time + accel cruise decel +""" + +from typing import Protocol + +from dimos.msgs.trajectory_msgs import JointTrajectory + + +class JointTrajectoryGeneratorSpec(Protocol): + """Protocol for joint trajectory generator. + + Generates time-parameterized trajectories from waypoints. + """ + + # Configuration + max_velocity: list[float] # rad/s per joint + max_acceleration: list[float] # rad/s^2 per joint + + def generate(self, waypoints: list[list[float]]) -> JointTrajectory: + """ + Generate a trajectory through waypoints with trapezoidal velocity profile. + + Args: + waypoints: List of joint positions [q1, q2, ..., qn] in radians + First waypoint is start, last is goal + + Returns: + JointTrajectory with time-parameterized points + """ + ... + + def set_limits( + self, + max_velocity: list[float] | float, + max_acceleration: list[float] | float, + ) -> None: + """ + Set velocity and acceleration limits. + + Args: + max_velocity: rad/s (single value applies to all joints, or per-joint) + max_acceleration: rad/s^2 (single value or per-joint) + """ + ... diff --git a/dimos/manipulation/test_manipulation_history.py b/dimos/manipulation/test_manipulation_history.py index 141c9365aa..ec4e503bed 100644 --- a/dimos/manipulation/test_manipulation_history.py +++ b/dimos/manipulation/test_manipulation_history.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/manipulation/visual_servoing/detection3d.py b/dimos/manipulation/visual_servoing/detection3d.py index f7371f531a..fca085df8c 100644 --- a/dimos/manipulation/visual_servoing/detection3d.py +++ b/dimos/manipulation/visual_servoing/detection3d.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ from dimos.perception.segmentation.sam_2d_seg import Sam2DSegmenter from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.manipulation.visual_servoing.detection3d") +logger = setup_logger() class Detection3DProcessor: @@ -91,7 +91,10 @@ def __init__( ) def process_frame( - self, rgb_image: np.ndarray, depth_image: np.ndarray, transform: np.ndarray | None = None + self, + rgb_image: np.ndarray, # type: ignore[type-arg] + depth_image: np.ndarray, # type: ignore[type-arg] + transform: np.ndarray | None = None, # type: ignore[type-arg] ) -> tuple[Detection3DArray, Detection2DArray]: """ Process a single RGB-D frame to extract 3D object detections. @@ -109,7 +112,7 @@ def process_frame( bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) # Run Sam segmentation with tracking - masks, bboxes, track_ids, probs, names = self.detector.process_image(bgr_image) + masks, bboxes, track_ids, probs, names = self.detector.process_image(bgr_image) # type: ignore[no-untyped-call] if not masks or len(masks) == 0: return Detection3DArray( @@ -233,11 +236,11 @@ def process_frame( def visualize_detections( self, - rgb_image: np.ndarray, + rgb_image: np.ndarray, # type: ignore[type-arg] detections_3d: list[Detection3D], detections_2d: list[Detection2D], show_coordinates: bool = True, - ) -> np.ndarray: + ) -> np.ndarray: # type: ignore[type-arg] """ Visualize detections with 3D position overlay next to bounding boxes. @@ -287,7 +290,7 @@ def get_closest_detection( return None # Sort by depth (Z coordinate) - def get_z_coord(d): + def get_z_coord(d): # type: ignore[no-untyped-def] return abs(d.bbox.center.position.z) return min(valid_detections, key=get_z_coord) diff --git a/dimos/manipulation/visual_servoing/manipulation_module.py b/dimos/manipulation/visual_servoing/manipulation_module.py index a89d43ed7b..088db9eb26 100644 --- a/dimos/manipulation/visual_servoing/manipulation_module.py +++ b/dimos/manipulation/visual_servoing/manipulation_module.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,7 +29,9 @@ from reactivex.disposable import Disposable from dimos.core import In, Module, Out, rpc -from dimos.hardware.piper_arm import PiperArm +from dimos.hardware.manipulators.piper.piper_arm import ( # type: ignore[import-not-found, import-untyped] + PiperArm, +) from dimos.manipulation.visual_servoing.detection3d import Detection3DProcessor from dimos.manipulation.visual_servoing.pbvs import PBVS from dimos.manipulation.visual_servoing.utils import ( @@ -51,7 +53,7 @@ pose_to_matrix, ) -logger = setup_logger("dimos.manipulation.visual_servoing.manipulation_module") +logger = setup_logger() class GraspStage(Enum): @@ -107,16 +109,16 @@ class ManipulationModule(Module): """ # LCM inputs - rgb_image: In[Image] = None - depth_image: In[Image] = None - camera_info: In[CameraInfo] = None + rgb_image: In[Image] + depth_image: In[Image] + camera_info: In[CameraInfo] # LCM outputs - viz_image: Out[Image] = None + viz_image: Out[Image] - def __init__( + def __init__( # type: ignore[no-untyped-def] self, - ee_to_camera_6dof: list | None = None, + ee_to_camera_6dof: list | None = None, # type: ignore[type-arg] **kwargs, ) -> None: """ @@ -173,11 +175,11 @@ def __init__( self.pose_stabilization_threshold = 0.01 self.stabilization_timeout = 25.0 self.stabilization_start_time = None - self.reached_poses = deque(maxlen=self.pose_history_size) + self.reached_poses = deque(maxlen=self.pose_history_size) # type: ignore[var-annotated] self.adjustment_count = 0 # Pose reachability tracking - self.ee_pose_history = deque(maxlen=20) # Keep history of EE poses + self.ee_pose_history = deque(maxlen=20) # type: ignore[var-annotated] # Keep history of EE poses self.stuck_pose_threshold = 0.001 # 1mm movement threshold self.stuck_pose_adjustment_degrees = 5.0 self.stuck_count = 0 @@ -266,11 +268,11 @@ def _on_depth_image(self, msg: Image) -> None: def _on_camera_info(self, msg: CameraInfo) -> None: """Handle camera info messages.""" try: - self.camera_intrinsics = [msg.K[0], msg.K[4], msg.K[2], msg.K[5]] + self.camera_intrinsics = [msg.K[0], msg.K[4], msg.K[2], msg.K[5]] # type: ignore[assignment] if self.detector is None: - self.detector = Detection3DProcessor(self.camera_intrinsics) - self.pbvs = PBVS() + self.detector = Detection3DProcessor(self.camera_intrinsics) # type: ignore[arg-type, assignment] + self.pbvs = PBVS() # type: ignore[assignment] logger.info("Initialized detection and PBVS processors") self.latest_camera_info = msg @@ -278,7 +280,7 @@ def _on_camera_info(self, msg: CameraInfo) -> None: logger.error(f"Error processing camera info: {e}") @rpc - def get_single_rgb_frame(self) -> np.ndarray | None: + def get_single_rgb_frame(self) -> np.ndarray | None: # type: ignore[type-arg] """ get the latest rgb frame from the camera """ @@ -406,8 +408,8 @@ def _run_pick_and_place(self) -> None: time.sleep(0.01) continue - if feedback.success is not None: - if feedback.success: + if feedback.success is not None: # type: ignore[attr-defined] + if feedback.success: # type: ignore[attr-defined] logger.info("Pick and place completed successfully!") else: logger.warning("Pick and place failed") @@ -458,7 +460,7 @@ def calculate_dynamic_grasp_pitch(self, target_pose: Pose) -> float: normalized_dist * (self.max_grasp_pitch_degrees - self.min_grasp_pitch_degrees) ) - return pitch_degrees + return pitch_degrees # type: ignore[no-any-return] def check_within_workspace(self, target_pose: Pose) -> bool: """ @@ -507,7 +509,7 @@ def _check_if_stuck(self) -> bool: Returns: Tuple of (is_stuck, max_std_dev_mm) """ - if len(self.ee_pose_history) < self.ee_pose_history.maxlen: + if len(self.ee_pose_history) < self.ee_pose_history.maxlen: # type: ignore[operator] return False # Extract positions from pose history @@ -520,7 +522,7 @@ def _check_if_stuck(self) -> bool: # Check if all standard deviations are below stuck threshold is_stuck = np.all(std_devs < self.stuck_pose_threshold) - return is_stuck + return is_stuck # type: ignore[return-value] def check_reach_and_adjust(self) -> bool: """ @@ -643,9 +645,9 @@ def execute_pre_grasp(self) -> None: return ee_pose = self.arm.get_ee_pose() - dynamic_pitch = self.calculate_dynamic_grasp_pitch(self.pbvs.current_target.bbox.center) + dynamic_pitch = self.calculate_dynamic_grasp_pitch(self.pbvs.current_target.bbox.center) # type: ignore[attr-defined] - _, _, _, has_target, target_pose = self.pbvs.compute_control( + _, _, _, has_target, target_pose = self.pbvs.compute_control( # type: ignore[attr-defined] ee_pose, self.pregrasp_distance, dynamic_pitch ) if target_pose and has_target: @@ -665,7 +667,7 @@ def execute_pre_grasp(self) -> None: self.arm.cmd_ee_pose(target_pose) self.current_executed_pose = target_pose self.waiting_for_reach = True - self.waiting_start_time = time.time() + self.waiting_start_time = time.time() # type: ignore[assignment] self.target_updated = False self.adjustment_count += 1 time.sleep(0.2) @@ -674,7 +676,7 @@ def execute_grasp(self) -> None: """Execute grasp stage: move to final grasp position.""" if self.waiting_for_reach: if self.check_reach_and_adjust() and not self.grasp_reached_time: - self.grasp_reached_time = time.time() + self.grasp_reached_time = time.time() # type: ignore[assignment] return if self.grasp_reached_time: @@ -739,7 +741,7 @@ def execute_close_and_retract(self) -> None: self.current_executed_pose = self.final_pregrasp_pose self.arm.close_gripper() self.waiting_for_reach = True - self.waiting_start_time = time.time() + self.waiting_start_time = time.time() # type: ignore[assignment] def execute_place(self) -> None: """Execute place stage: move to place position and release object.""" @@ -759,13 +761,13 @@ def execute_place(self) -> None: if place_pose: logger.info("Moving to place position") self.arm.cmd_ee_pose(place_pose, line_mode=True) - self.current_executed_pose = place_pose + self.current_executed_pose = place_pose # type: ignore[assignment] self.waiting_for_reach = True - self.waiting_start_time = time.time() + self.waiting_start_time = time.time() # type: ignore[assignment] else: logger.error("Failed to get place target pose") self.task_failed = True - self.overall_success = False + self.overall_success = False # type: ignore[assignment] def execute_retract(self) -> None: """Execute retract stage: retract from place position.""" @@ -793,11 +795,11 @@ def execute_retract(self) -> None: else: logger.error("No place pose stored for retraction") self.task_failed = True - self.overall_success = False + self.overall_success = False # type: ignore[assignment] def capture_and_process( self, - ) -> tuple[np.ndarray | None, Detection3DArray | None, Detection2DArray | None, Pose | None]: + ) -> tuple[np.ndarray | None, Detection3DArray | None, Detection2DArray | None, Pose | None]: # type: ignore[type-arg] """Capture frame from camera data and process detections.""" if self.latest_rgb is None or self.latest_depth is None or self.detector is None: return None, None, None, None @@ -852,8 +854,8 @@ def update(self) -> dict[str, Any] | None: if rgb is None: return None - self.last_detection_3d_array = detection_3d_array - self.last_detection_2d_array = detection_2d_array + self.last_detection_3d_array = detection_3d_array # type: ignore[assignment] + self.last_detection_2d_array = detection_2d_array # type: ignore[assignment] if self.target_click: x, y = self.target_click if self.pick_target(x, y): @@ -890,16 +892,16 @@ def update(self) -> dict[str, Any] | None: ) if self.task_running: - self.current_visualization = create_manipulation_visualization( + self.current_visualization = create_manipulation_visualization( # type: ignore[assignment] rgb, feedback, detection_3d_array, detection_2d_array ) if self.current_visualization is not None: self._publish_visualization(self.current_visualization) - return feedback + return feedback # type: ignore[return-value] - def _publish_visualization(self, viz_image: np.ndarray) -> None: + def _publish_visualization(self, viz_image: np.ndarray) -> None: # type: ignore[type-arg] """Publish visualization image to LCM.""" try: viz_rgb = cv2.cvtColor(viz_image, cv2.COLOR_BGR2RGB) @@ -910,14 +912,14 @@ def _publish_visualization(self, viz_image: np.ndarray) -> None: def check_target_stabilized(self) -> bool: """Check if the commanded poses have stabilized.""" - if len(self.reached_poses) < self.reached_poses.maxlen: + if len(self.reached_poses) < self.reached_poses.maxlen: # type: ignore[operator] return False positions = np.array( [[p.position.x, p.position.y, p.position.z] for p in self.reached_poses] ) std_devs = np.std(positions, axis=0) - return np.all(std_devs < self.pose_stabilization_threshold) + return np.all(std_devs < self.pose_stabilization_threshold) # type: ignore[return-value] def get_place_target_pose(self) -> Pose | None: """Get the place target pose with z-offset applied based on object height.""" diff --git a/dimos/manipulation/visual_servoing/pbvs.py b/dimos/manipulation/visual_servoing/pbvs.py index 77bf83396e..f94c233834 100644 --- a/dimos/manipulation/visual_servoing/pbvs.py +++ b/dimos/manipulation/visual_servoing/pbvs.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ from dimos_lcm.vision_msgs import Detection3D import numpy as np -from scipy.spatial.transform import Rotation as R +from scipy.spatial.transform import Rotation as R # type: ignore[import-untyped] from dimos.manipulation.visual_servoing.utils import ( create_pbvs_visualization, @@ -33,7 +33,7 @@ from dimos.msgs.vision_msgs import Detection3DArray from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.manipulation.pbvs") +logger = setup_logger() class PBVS: @@ -84,7 +84,7 @@ def __init__( target_tolerance=target_tolerance, ) else: - self.controller = None + self.controller = None # type: ignore[assignment] # Store parameters for direct mode error computation self.target_tolerance = target_tolerance @@ -100,7 +100,7 @@ def __init__( # Detection history for robust tracking self.detection_history_size = 3 - self.detection_history = deque(maxlen=self.detection_history_size) + self.detection_history = deque(maxlen=self.detection_history_size) # type: ignore[var-annotated] # For direct control mode visualization self.last_position_error = None @@ -272,11 +272,11 @@ def compute_control( # Return has_target=True since we have a target, regardless of tracking status return velocity_cmd, angular_velocity_cmd, target_reached, True, None - def create_status_overlay( + def create_status_overlay( # type: ignore[no-untyped-def] self, - image: np.ndarray, + image: np.ndarray, # type: ignore[type-arg] grasp_stage=None, - ) -> np.ndarray: + ) -> np.ndarray: # type: ignore[type-arg] """ Create PBVS status overlay on image. @@ -374,7 +374,7 @@ def compute_control( grasp_pose.position.y - ee_pose.position.y, grasp_pose.position.z - ee_pose.position.z, ) - self.last_position_error = error + self.last_position_error = error # type: ignore[assignment] # Compute velocity command with proportional control velocity_cmd = Vector3( @@ -393,7 +393,7 @@ def compute_control( float(velocity_cmd.z * scale), ) - self.last_velocity_cmd = velocity_cmd + self.last_velocity_cmd = velocity_cmd # type: ignore[assignment] # Compute angular velocity for orientation control angular_velocity_cmd = self._compute_angular_velocity(grasp_pose.orientation, ee_pose) @@ -441,7 +441,7 @@ def _compute_angular_velocity(self, target_rot: Quaternion, current_pose: Pose) pitch_error = error_axis_angle[1] yaw_error = error_axis_angle[2] - self.last_rotation_error = Vector3(roll_error, pitch_error, yaw_error) + self.last_rotation_error = Vector3(roll_error, pitch_error, yaw_error) # type: ignore[assignment] # Apply proportional control angular_velocity = Vector3( @@ -460,15 +460,15 @@ def _compute_angular_velocity(self, target_rot: Quaternion, current_pose: Pose) angular_velocity.x * scale, angular_velocity.y * scale, angular_velocity.z * scale ) - self.last_angular_velocity_cmd = angular_velocity + self.last_angular_velocity_cmd = angular_velocity # type: ignore[assignment] return angular_velocity def create_status_overlay( self, - image: np.ndarray, + image: np.ndarray, # type: ignore[type-arg] current_target: Detection3D | None = None, - ) -> np.ndarray: + ) -> np.ndarray: # type: ignore[type-arg] """ Create PBVS status overlay on image. diff --git a/dimos/manipulation/visual_servoing/utils.py b/dimos/manipulation/visual_servoing/utils.py index 06479723f6..5922739429 100644 --- a/dimos/manipulation/visual_servoing/utils.py +++ b/dimos/manipulation/visual_servoing/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -56,9 +56,9 @@ def match_detection_by_id( def transform_pose( - obj_pos: np.ndarray, - obj_orientation: np.ndarray, - transform_matrix: np.ndarray, + obj_pos: np.ndarray, # type: ignore[type-arg] + obj_orientation: np.ndarray, # type: ignore[type-arg] + transform_matrix: np.ndarray, # type: ignore[type-arg] to_optical: bool = False, to_robot: bool = False, ) -> Pose: @@ -112,11 +112,11 @@ def transform_pose( def transform_points_3d( - points_3d: np.ndarray, - transform_matrix: np.ndarray, + points_3d: np.ndarray, # type: ignore[type-arg] + transform_matrix: np.ndarray, # type: ignore[type-arg] to_optical: bool = False, to_robot: bool = False, -) -> np.ndarray: +) -> np.ndarray: # type: ignore[type-arg] """ Transform 3D points with optional frame convention conversion. Applies the same transformation pipeline as transform_pose but for multiple points. @@ -181,11 +181,11 @@ def transform_points_3d( def select_points_from_depth( - depth_image: np.ndarray, + depth_image: np.ndarray, # type: ignore[type-arg] target_point: tuple[int, int], - camera_intrinsics: list[float] | np.ndarray, + camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] radius: int = 5, -) -> np.ndarray: +) -> np.ndarray: # type: ignore[type-arg] """ Select points around a target point within a bounding box and project them to 3D. @@ -349,12 +349,12 @@ def calculate_object_similarity( dim_similarities.append(dim_similarity) # Return average similarity across all dimensions - size_similarity = np.mean(dim_similarities) if dim_similarities else 0.0 + size_similarity = np.mean(dim_similarities) if dim_similarities else 0.0 # type: ignore[assignment] # Weighted combination total_similarity = distance_weight * distance_similarity + size_weight * size_similarity - return total_similarity, distance, size_similarity + return total_similarity, distance, size_similarity # type: ignore[return-value] def find_best_object_match( @@ -440,7 +440,9 @@ def parse_zed_pose(zed_pose_data: dict[str, Any]) -> Pose | None: def estimate_object_depth( - depth_image: np.ndarray, segmentation_mask: np.ndarray | None, bbox: list[float] + depth_image: np.ndarray, # type: ignore[type-arg] + segmentation_mask: np.ndarray | None, # type: ignore[type-arg] + bbox: list[float], ) -> float: """ Estimate object depth dimension using segmentation mask and depth data. @@ -478,7 +480,7 @@ def estimate_object_depth( depth_range = depth_90 - depth_10 # Clamp to reasonable bounds with single operation - return np.clip(depth_range, 0.02, 0.5) + return np.clip(depth_range, 0.02, 0.5) # type: ignore[no-any-return] # Fast fallback using area calculation bbox_area = (x2 - x1) * (y2 - y1) @@ -495,12 +497,12 @@ def estimate_object_depth( # ============= Visualization Functions ============= -def create_manipulation_visualization( - rgb_image: np.ndarray, +def create_manipulation_visualization( # type: ignore[no-untyped-def] + rgb_image: np.ndarray, # type: ignore[type-arg] feedback, detection_3d_array=None, detection_2d_array=None, -) -> np.ndarray: +) -> np.ndarray: # type: ignore[type-arg] """ Create simple visualization for manipulation class using feedback. @@ -630,13 +632,13 @@ def create_manipulation_visualization( return viz -def create_pbvs_visualization( - image: np.ndarray, +def create_pbvs_visualization( # type: ignore[no-untyped-def] + image: np.ndarray, # type: ignore[type-arg] current_target=None, position_error=None, target_reached: bool = False, grasp_stage: str = "idle", -) -> np.ndarray: +) -> np.ndarray: # type: ignore[type-arg] """ Create simple PBVS visualization overlay. @@ -720,11 +722,11 @@ def create_pbvs_visualization( def visualize_detections_3d( - rgb_image: np.ndarray, + rgb_image: np.ndarray, # type: ignore[type-arg] detections: list[Detection3D], show_coordinates: bool = True, bboxes_2d: list[list[float]] | None = None, -) -> np.ndarray: +) -> np.ndarray: # type: ignore[type-arg] """ Visualize detections with 3D position overlay next to bounding boxes. @@ -796,4 +798,4 @@ def visualize_detections_3d( 1, ) - return viz + return viz # type: ignore[no-any-return] diff --git a/dimos/mapping/costmapper.py b/dimos/mapping/costmapper.py new file mode 100644 index 0000000000..ee7512baba --- /dev/null +++ b/dimos/mapping/costmapper.py @@ -0,0 +1,158 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import asdict, dataclass, field +import queue +import threading +import time + +from reactivex import operators as ops +import rerun as rr +import rerun.blueprint as rrb + +from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig +from dimos.core.module import ModuleConfig +from dimos.dashboard.rerun_init import connect_rerun +from dimos.mapping.pointclouds.occupancy import ( + OCCUPANCY_ALGOS, + HeightCostConfig, + OccupancyConfig, +) +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +@dataclass +class Config(ModuleConfig): + algo: str = "height_cost" + config: OccupancyConfig = field(default_factory=HeightCostConfig) + + +class CostMapper(Module): + default_config = Config + config: Config + + global_map: In[PointCloud2] + global_costmap: Out[OccupancyGrid] + + # Background Rerun logging (decouples viz from data pipeline) + _rerun_queue: queue.Queue[tuple[OccupancyGrid, float, float] | None] + _rerun_thread: threading.Thread | None = None + + @classmethod + def rerun_views(cls): # type: ignore[no-untyped-def] + """Return Rerun view blueprints for costmap visualization.""" + return [ + rrb.TimeSeriesView( + name="Costmap (ms)", + origin="/metrics/costmap", + contents=["+ /metrics/costmap/calc_ms"], + ), + ] + + def __init__(self, global_config: GlobalConfig | None = None, **kwargs: object) -> None: + super().__init__(**kwargs) + self._global_config = global_config or GlobalConfig() + self._rerun_queue = queue.Queue(maxsize=2) + + def _rerun_worker(self) -> None: + """Background thread: pull from queue and log to Rerun (non-blocking).""" + while True: + try: + item = self._rerun_queue.get(timeout=1.0) + if item is None: # Shutdown signal + break + + grid, calc_time_ms, rx_monotonic = item + + # Generate mesh + log to Rerun (blocks in background, not on data path) + try: + # 3D floor overlay (expensive mesh generation) + rr.log( + "world/nav/costmap/floor", + grid.to_rerun( + mode="mesh", + colormap=None, # Uses Foxglove-style colors (blue-purple free, black occupied) + z_offset=0.05, # 5cm above floor to avoid z-fighting + ), + ) + + # Log timing metrics + rr.log("metrics/costmap/calc_ms", rr.Scalars(calc_time_ms)) + latency_ms = (time.monotonic() - rx_monotonic) * 1000 + rr.log("metrics/costmap/latency_ms", rr.Scalars(latency_ms)) + except Exception as e: + logger.warning(f"Rerun logging error: {e}") + except queue.Empty: + continue + + @rpc + def start(self) -> None: + super().start() + + # Only start Rerun logging if Rerun backend is selected + if self._global_config.viewer_backend.startswith("rerun"): + connect_rerun(global_config=self._global_config) + + # Start background Rerun logging thread + self._rerun_thread = threading.Thread(target=self._rerun_worker, daemon=True) + self._rerun_thread.start() + logger.info("CostMapper: started async Rerun logging thread") + + def _publish_costmap(grid: OccupancyGrid, calc_time_ms: float, rx_monotonic: float) -> None: + # Publish to downstream FIRST (fast, not blocked by Rerun) + self.global_costmap.publish(grid) + + # Queue for async Rerun logging (non-blocking, drops if queue full) + if self._rerun_thread and self._rerun_thread.is_alive(): + try: + self._rerun_queue.put_nowait((grid, calc_time_ms, rx_monotonic)) + except queue.Full: + pass # Drop viz frame, data pipeline continues + + def _calculate_and_time( + msg: PointCloud2, + ) -> tuple[OccupancyGrid, float, float]: + rx_monotonic = time.monotonic() # Capture receipt time + start = time.perf_counter() + grid = self._calculate_costmap(msg) + elapsed_ms = (time.perf_counter() - start) * 1000 + return grid, elapsed_ms, rx_monotonic + + self._disposables.add( + self.global_map.observable() # type: ignore[no-untyped-call] + .pipe(ops.map(_calculate_and_time)) + .subscribe(lambda result: _publish_costmap(result[0], result[1], result[2])) + ) + + @rpc + def stop(self) -> None: + # Shutdown background Rerun thread + if self._rerun_thread and self._rerun_thread.is_alive(): + self._rerun_queue.put(None) # Shutdown signal + self._rerun_thread.join(timeout=2.0) + + super().stop() + + # @timed() # TODO: fix thread leak in timed decorator + def _calculate_costmap(self, msg: PointCloud2) -> OccupancyGrid: + fn = OCCUPANCY_ALGOS[self.config.algo] + return fn(msg, **asdict(self.config.config)) + + +cost_mapper = CostMapper.blueprint diff --git a/dimos/mapping/google_maps/conftest.py b/dimos/mapping/google_maps/conftest.py index 09a7843261..725100bcc8 100644 --- a/dimos/mapping/google_maps/conftest.py +++ b/dimos/mapping/google_maps/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/mapping/google_maps/google_maps.py b/dimos/mapping/google_maps/google_maps.py index e75de042f4..7f5ce32e99 100644 --- a/dimos/mapping/google_maps/google_maps.py +++ b/dimos/mapping/google_maps/google_maps.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ import os -import googlemaps +import googlemaps # type: ignore[import-untyped] from dimos.mapping.google_maps.types import ( Coordinates, @@ -27,7 +27,7 @@ from dimos.mapping.utils.distance import distance_in_meters from dimos.utils.logging_config import setup_logger -logger = setup_logger(__file__) +logger = setup_logger() class GoogleMaps: diff --git a/dimos/mapping/google_maps/test_google_maps.py b/dimos/mapping/google_maps/test_google_maps.py index 52e1493ec3..13f7fa8eaa 100644 --- a/dimos/mapping/google_maps/test_google_maps.py +++ b/dimos/mapping/google_maps/test_google_maps.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/mapping/google_maps/types.py b/dimos/mapping/google_maps/types.py index 67713f55ee..29f9bee6eb 100644 --- a/dimos/mapping/google_maps/types.py +++ b/dimos/mapping/google_maps/types.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/mapping/occupancy/conftest.py b/dimos/mapping/occupancy/conftest.py new file mode 100644 index 0000000000..f20dc1310b --- /dev/null +++ b/dimos/mapping/occupancy/conftest.py @@ -0,0 +1,30 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +from dimos.mapping.occupancy.gradient import gradient +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid +from dimos.utils.data import get_data + + +@pytest.fixture +def occupancy() -> OccupancyGrid: + return OccupancyGrid(np.load(get_data("occupancy_simple.npy"))) + + +@pytest.fixture +def occupancy_gradient(occupancy) -> OccupancyGrid: + return gradient(occupancy, max_distance=1.5) diff --git a/dimos/mapping/occupancy/extrude_occupancy.py b/dimos/mapping/occupancy/extrude_occupancy.py new file mode 100644 index 0000000000..799319cbf6 --- /dev/null +++ b/dimos/mapping/occupancy/extrude_occupancy.py @@ -0,0 +1,235 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import numpy as np +from numpy.typing import NDArray + +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid + +# Rectangle type: (x, y, width, height) +Rect = tuple[int, int, int, int] + + +def identify_convex_shapes(occupancy_grid: OccupancyGrid) -> list[Rect]: + """Identify occupied zones and decompose them into convex rectangles. + + This function finds all occupied cells in the occupancy grid and + decomposes them into axis-aligned rectangles suitable for MuJoCo + collision geometry. + + Args: + occupancy_grid: The input occupancy grid. + output_path: Path to save the visualization image. + + Returns: + List of rectangles as (x, y, width, height) tuples in grid coords. + """ + grid = occupancy_grid.grid + + # Create binary mask of occupied cells (treat UNKNOWN as OCCUPIED) + occupied_mask = ((grid == CostValues.OCCUPIED) | (grid == CostValues.UNKNOWN)).astype( + np.uint8 + ) * 255 + + return _decompose_to_rectangles(occupied_mask) + + +def _decompose_to_rectangles(mask: NDArray[np.uint8]) -> list[Rect]: + """Decompose a binary mask into rectangles using greedy maximal rectangles. + + Iteratively finds and removes the largest rectangle until the mask is empty. + + Args: + mask: Binary mask of the shape (255 for occupied, 0 for free). + + Returns: + List of rectangles as (x, y, width, height) tuples. + """ + rectangles: list[Rect] = [] + remaining = mask.copy() + + max_iterations = 10000 # Safety limit + + for _ in range(max_iterations): + # Find the largest rectangle in the remaining mask + rect = _find_largest_rectangle(remaining) + + if rect is None: + break + + x_start, y_start, x_end, y_end = rect + + # Add rectangle to shapes + # Store as (x, y, width, height) + # x_end and y_end are exclusive (like Python slicing) + rectangles.append((x_start, y_start, x_end - x_start, y_end - y_start)) + + # Remove this rectangle from the mask + remaining[y_start:y_end, x_start:x_end] = 0 + + return rectangles + + +def _find_largest_rectangle(mask: NDArray[np.uint8]) -> tuple[int, int, int, int] | None: + """Find the largest rectangle of 1s in a binary mask. + + Uses the histogram method for O(rows * cols) complexity. + + Args: + mask: Binary mask (non-zero = occupied). + + Returns: + (x_start, y_start, x_end, y_end) or None if no rectangle found. + Coordinates are exclusive on the end (like Python slicing). + """ + if not np.any(mask): + return None + + rows, cols = mask.shape + binary = (mask > 0).astype(np.int32) + + # Build histogram of heights for each row + heights = np.zeros((rows, cols), dtype=np.int32) + heights[0] = binary[0] + for i in range(1, rows): + heights[i] = np.where(binary[i] > 0, heights[i - 1] + 1, 0) + + best_area = 0 + best_rect: tuple[int, int, int, int] | None = None + + # For each row, find largest rectangle in histogram + for row_idx in range(rows): + hist = heights[row_idx] + rect = _largest_rect_in_histogram(hist, row_idx) + if rect is not None: + x_start, y_start, x_end, y_end = rect + area = (x_end - x_start) * (y_end - y_start) + if area > best_area: + best_area = area + best_rect = rect + + return best_rect + + +def _largest_rect_in_histogram( + hist: NDArray[np.int32], bottom_row: int +) -> tuple[int, int, int, int] | None: + """Find largest rectangle in a histogram. + + Args: + hist: Array of heights. + bottom_row: The row index this histogram ends at. + + Returns: + (x_start, y_start, x_end, y_end) or None. + """ + n = len(hist) + if n == 0: + return None + + # Stack-based algorithm for largest rectangle in histogram + stack: list[int] = [] # Stack of indices + best_area = 0 + best_rect: tuple[int, int, int, int] | None = None + + for i in range(n + 1): + h = hist[i] if i < n else 0 + + while stack and hist[stack[-1]] > h: + height = hist[stack.pop()] + width_start = stack[-1] + 1 if stack else 0 + width_end = i + area = height * (width_end - width_start) + + if area > best_area: + best_area = area + # Convert to rectangle coordinates + y_start = bottom_row - height + 1 + y_end = bottom_row + 1 + best_rect = (width_start, y_start, width_end, y_end) + + stack.append(i) + + return best_rect + + +def generate_mujoco_scene( + occupancy_grid: OccupancyGrid, +) -> str: + """Generate a MuJoCo scene XML from an occupancy grid. + + Creates a scene with a flat floor and extruded boxes for each occupied + region. All boxes are red and used for collision. + + Args: + occupancy_grid: The input occupancy grid. + + Returns: + Path to the generated XML file. + """ + extrude_height = 0.5 + + # Get rectangles from the occupancy grid + rectangles = identify_convex_shapes(occupancy_grid) + + resolution = occupancy_grid.resolution + origin_x = occupancy_grid.origin.position.x + origin_y = occupancy_grid.origin.position.y + + # Build XML + xml_lines = [ + '', + '', + ' ', + ' ', + " ", + ' ', + ' ', + ' ', + ' ', + " ", + " ", + ' ', + ' ', + ] + + # Add each rectangle as a box geom + for i, (gx, gy, gw, gh) in enumerate(rectangles): + # Convert grid coordinates to world coordinates + # Grid origin is top-left, world origin is at occupancy_grid.origin + # gx, gy are in grid cells, need to convert to meters + world_x = origin_x + (gx + gw / 2) * resolution + world_y = origin_y + (gy + gh / 2) * resolution + world_z = extrude_height / 2 # Center of the box + + # Box half-sizes + half_x = (gw * resolution) / 2 + half_y = (gh * resolution) / 2 + half_z = extrude_height / 2 + + xml_lines.append( + f' ' + ) + + xml_lines.append(" ") + xml_lines.append(' ') + xml_lines.append("\n") + + xml_content = "\n".join(xml_lines) + + return xml_content diff --git a/dimos/mapping/occupancy/gradient.py b/dimos/mapping/occupancy/gradient.py new file mode 100644 index 0000000000..880f2692da --- /dev/null +++ b/dimos/mapping/occupancy/gradient.py @@ -0,0 +1,202 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from scipy import ndimage # type: ignore[import-untyped] + +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid + + +def gradient( + occupancy_grid: OccupancyGrid, obstacle_threshold: int = 50, max_distance: float = 2.0 +) -> OccupancyGrid: + """Create a gradient OccupancyGrid for path planning. + + Creates a gradient where free space has value 0 and values increase near obstacles. + This can be used as a cost map for path planning algorithms like A*. + + Args: + obstacle_threshold: Cell values >= this are considered obstacles (default: 50) + max_distance: Maximum distance to compute gradient in meters (default: 2.0) + + Returns: + New OccupancyGrid with gradient values: + - -1: Unknown cells (preserved as-is) + - 0: Free space far from obstacles + - 1-99: Increasing cost as you approach obstacles + - 100: At obstacles + + Note: Unknown cells remain as unknown (-1) and do not receive gradient values. + """ + + # Remember which cells are unknown + unknown_mask = occupancy_grid.grid == CostValues.UNKNOWN + + # Create binary obstacle map + # Consider cells >= threshold as obstacles (1), everything else as free (0) + # Unknown cells are not considered obstacles for distance calculation + obstacle_map = (occupancy_grid.grid >= obstacle_threshold).astype(np.float32) + + # Compute distance transform (distance to nearest obstacle in cells) + # Unknown cells are treated as if they don't exist for distance calculation + distance_cells = ndimage.distance_transform_edt(1 - obstacle_map) + + # Convert to meters and clip to max distance + distance_meters = np.clip(distance_cells * occupancy_grid.resolution, 0, max_distance) + + # Invert and scale to 0-100 range + # Far from obstacles (max_distance) -> 0 + # At obstacles (0 distance) -> 100 + gradient_values = (1 - distance_meters / max_distance) * 100 + + # Ensure obstacles are exactly 100 + gradient_values[obstacle_map > 0] = CostValues.OCCUPIED + + # Convert to int8 for OccupancyGrid + gradient_data = gradient_values.astype(np.int8) + + # Preserve unknown cells as unknown (don't apply gradient to them) + gradient_data[unknown_mask] = CostValues.UNKNOWN + + # Create new OccupancyGrid with gradient + gradient_grid = OccupancyGrid( + grid=gradient_data, + resolution=occupancy_grid.resolution, + origin=occupancy_grid.origin, + frame_id=occupancy_grid.frame_id, + ts=occupancy_grid.ts, + ) + + return gradient_grid + + +def voronoi_gradient( + occupancy_grid: OccupancyGrid, obstacle_threshold: int = 50, max_distance: float = 2.0 +) -> OccupancyGrid: + """Create a Voronoi-based gradient OccupancyGrid for path planning. + + Unlike the regular gradient which can result in suboptimal paths in narrow + corridors (where the center still has high cost), this method creates a cost + map based on the Voronoi diagram of obstacles. Cells on Voronoi edges + (equidistant from multiple obstacles) have minimum cost, encouraging paths + that stay maximally far from all obstacles. + + For a corridor of width 10 cells: + - Regular gradient: center cells might be 95 (still high cost) + - Voronoi gradient: center cells are 0 (optimal path) + + The cost is interpolated based on relative position between the nearest + obstacle and the nearest Voronoi edge: + - At obstacle: cost = 100 + - At Voronoi edge: cost = 0 + - In between: cost = 99 * d_voronoi / (d_obstacle + d_voronoi) + + Args: + obstacle_threshold: Cell values >= this are considered obstacles (default: 50) + max_distance: Maximum distance in meters beyond which cost is 0 (default: 2.0) + + Returns: + New OccupancyGrid with gradient values: + - -1: Unknown cells (preserved as-is) + - 0: On Voronoi edges (equidistant from obstacles) or far from obstacles + - 1-99: Increasing cost closer to obstacles + - 100: At obstacles + """ + # Remember which cells are unknown + unknown_mask = occupancy_grid.grid == CostValues.UNKNOWN + + # Create binary obstacle map + obstacle_map = (occupancy_grid.grid >= obstacle_threshold).astype(np.float32) + + # Check if there are any obstacles + if not np.any(obstacle_map): + # No obstacles - everything is free + gradient_data = np.zeros_like(occupancy_grid.grid, dtype=np.int8) + gradient_data[unknown_mask] = CostValues.UNKNOWN + return OccupancyGrid( + grid=gradient_data, + resolution=occupancy_grid.resolution, + origin=occupancy_grid.origin, + frame_id=occupancy_grid.frame_id, + ts=occupancy_grid.ts, + ) + + # Label connected obstacle regions (clusters) + # This groups all cells of the same wall/obstacle together + obstacle_labels, num_obstacles = ndimage.label(obstacle_map) + + # If only one obstacle cluster, Voronoi edges don't make sense + # Fall back to regular gradient behavior + if num_obstacles <= 1: + return gradient(occupancy_grid, obstacle_threshold, max_distance) + + # Compute distance transform with indices to nearest obstacle + # indices[0][i,j], indices[1][i,j] = row,col of nearest obstacle to (i,j) + distance_cells, indices = ndimage.distance_transform_edt(1 - obstacle_map, return_indices=True) + + # For each cell, find which obstacle cluster it belongs to (Voronoi region) + # by looking up the label of its nearest obstacle cell + nearest_obstacle_cluster = obstacle_labels[indices[0], indices[1]] + + # Find Voronoi edges: cells where neighbors belong to different obstacle clusters + # Using max/min filters: an edge exists where max != min in the 3x3 neighborhood + footprint = np.ones((3, 3), dtype=bool) + local_max = ndimage.maximum_filter( + nearest_obstacle_cluster, footprint=footprint, mode="nearest" + ) + local_min = ndimage.minimum_filter( + nearest_obstacle_cluster, footprint=footprint, mode="nearest" + ) + voronoi_edges = local_max != local_min + + # Don't count obstacle cells as Voronoi edges + voronoi_edges &= obstacle_map == 0 + + # Compute distance to nearest Voronoi edge + if not np.any(voronoi_edges): + # No Voronoi edges found - fall back to regular gradient + return gradient(occupancy_grid, obstacle_threshold, max_distance) + + voronoi_distance = ndimage.distance_transform_edt(~voronoi_edges) + + # Calculate cost based on position between obstacle and Voronoi edge + # cost = 99 * d_voronoi / (d_obstacle + d_voronoi) + # At Voronoi edge: d_voronoi = 0, cost = 0 + # Near obstacle: d_obstacle small, d_voronoi large, cost high + total_distance = distance_cells + voronoi_distance + with np.errstate(divide="ignore", invalid="ignore"): + cost_ratio = np.where(total_distance > 0, voronoi_distance / total_distance, 0) + + gradient_values = cost_ratio * 99 + + # Ensure obstacles are exactly 100 + gradient_values[obstacle_map > 0] = CostValues.OCCUPIED + + # Apply max_distance clipping - cells beyond max_distance from obstacles get cost 0 + max_distance_cells = max_distance / occupancy_grid.resolution + gradient_values[distance_cells > max_distance_cells] = 0 + + # Convert to int8 + gradient_data = gradient_values.astype(np.int8) + + # Preserve unknown cells + gradient_data[unknown_mask] = CostValues.UNKNOWN + + return OccupancyGrid( + grid=gradient_data, + resolution=occupancy_grid.resolution, + origin=occupancy_grid.origin, + frame_id=occupancy_grid.frame_id, + ts=occupancy_grid.ts, + ) diff --git a/dimos/mapping/occupancy/inflation.py b/dimos/mapping/occupancy/inflation.py new file mode 100644 index 0000000000..a9ef628cd6 --- /dev/null +++ b/dimos/mapping/occupancy/inflation.py @@ -0,0 +1,53 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from scipy import ndimage # type: ignore[import-untyped] + +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid + + +def simple_inflate(occupancy_grid: OccupancyGrid, radius: float) -> OccupancyGrid: + """Inflate obstacles by a given radius (binary inflation). + Args: + radius: Inflation radius in meters + Returns: + New OccupancyGrid with inflated obstacles + """ + # Convert radius to grid cells + cell_radius = int(np.ceil(radius / occupancy_grid.resolution)) + + # Get grid as numpy array + grid_array = occupancy_grid.grid + + # Create circular kernel for binary inflation + y, x = np.ogrid[-cell_radius : cell_radius + 1, -cell_radius : cell_radius + 1] + kernel = (x**2 + y**2 <= cell_radius**2).astype(np.uint8) + + # Find occupied cells + occupied_mask = grid_array >= CostValues.OCCUPIED + + # Binary inflation + inflated = ndimage.binary_dilation(occupied_mask, structure=kernel) + result_grid = grid_array.copy() + result_grid[inflated] = CostValues.OCCUPIED + + # Create new OccupancyGrid with inflated data using numpy constructor + return OccupancyGrid( + grid=result_grid, + resolution=occupancy_grid.resolution, + origin=occupancy_grid.origin, + frame_id=occupancy_grid.frame_id, + ts=occupancy_grid.ts, + ) diff --git a/dimos/mapping/occupancy/operations.py b/dimos/mapping/occupancy/operations.py new file mode 100644 index 0000000000..be17670a6a --- /dev/null +++ b/dimos/mapping/occupancy/operations.py @@ -0,0 +1,88 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from scipy import ndimage # type: ignore[import-untyped] + +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid + + +def smooth_occupied( + occupancy_grid: OccupancyGrid, min_neighbor_fraction: float = 0.4 +) -> OccupancyGrid: + """Smooth occupied zones by removing unsupported protrusions. + + Removes occupied cells that don't have sufficient neighboring occupied + cells. + + Args: + occupancy_grid: Input occupancy grid + min_neighbor_fraction: Minimum fraction of 8-connected neighbors + that must be occupied for a cell to remain occupied. + Returns: + New OccupancyGrid with smoothed occupied zones + """ + grid_array = occupancy_grid.grid + occupied_mask = grid_array >= CostValues.OCCUPIED + + # Count occupied neighbors for each cell (8-connectivity). + kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], dtype=np.uint8) + neighbor_count = ndimage.convolve( + occupied_mask.astype(np.uint8), kernel, mode="constant", cval=0 + ) + + # Remove cells with too few occupied neighbors. + min_neighbors = int(np.ceil(8 * min_neighbor_fraction)) + unsupported = occupied_mask & (neighbor_count < min_neighbors) + + result_grid = grid_array.copy() + result_grid[unsupported] = CostValues.FREE + + return OccupancyGrid( + grid=result_grid, + resolution=occupancy_grid.resolution, + origin=occupancy_grid.origin, + frame_id=occupancy_grid.frame_id, + ts=occupancy_grid.ts, + ) + + +def overlay_occupied(base: OccupancyGrid, overlay: OccupancyGrid) -> OccupancyGrid: + """Overlay occupied zones from one grid onto another. + + Marks cells as occupied in the base grid wherever they are occupied + in the overlay grid. + + Args: + base: The base occupancy grid + overlay: The grid whose occupied zones will be overlaid onto base + Returns: + New OccupancyGrid with combined occupied zones + """ + if base.grid.shape != overlay.grid.shape: + raise ValueError( + f"Grid shapes must match: base {base.grid.shape} vs overlay {overlay.grid.shape}" + ) + + result_grid = base.grid.copy() + overlay_occupied_mask = overlay.grid >= CostValues.OCCUPIED + result_grid[overlay_occupied_mask] = CostValues.OCCUPIED + + return OccupancyGrid( + grid=result_grid, + resolution=base.resolution, + origin=base.origin, + frame_id=base.frame_id, + ts=base.ts, + ) diff --git a/dimos/mapping/occupancy/path_map.py b/dimos/mapping/occupancy/path_map.py new file mode 100644 index 0000000000..a99a423de8 --- /dev/null +++ b/dimos/mapping/occupancy/path_map.py @@ -0,0 +1,40 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Literal, TypeAlias + +from dimos.mapping.occupancy.gradient import voronoi_gradient +from dimos.mapping.occupancy.inflation import simple_inflate +from dimos.mapping.occupancy.operations import overlay_occupied, smooth_occupied +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid + +NavigationStrategy: TypeAlias = Literal["simple", "mixed"] + + +def make_navigation_map( + occupancy_grid: OccupancyGrid, robot_width: float, strategy: NavigationStrategy +) -> OccupancyGrid: + half_width = robot_width / 2 + gradient_distance = 1.5 + + if strategy == "simple": + costmap = simple_inflate(occupancy_grid, half_width) + elif strategy == "mixed": + costmap = smooth_occupied(occupancy_grid) + costmap = simple_inflate(costmap, half_width) + costmap = overlay_occupied(costmap, occupancy_grid) + else: + raise ValueError(f"Unknown strategy: {strategy}") + + return voronoi_gradient(costmap, max_distance=gradient_distance) diff --git a/dimos/mapping/occupancy/path_mask.py b/dimos/mapping/occupancy/path_mask.py new file mode 100644 index 0000000000..5ad3010111 --- /dev/null +++ b/dimos/mapping/occupancy/path_mask.py @@ -0,0 +1,98 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cv2 +import numpy as np +from numpy.typing import NDArray + +from dimos.msgs.nav_msgs import Path +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid + + +def make_path_mask( + occupancy_grid: OccupancyGrid, + path: Path, + robot_width: float, + pose_index: int = 0, + max_length: float = float("inf"), +) -> NDArray[np.bool_]: + """Generate a numpy mask of path cells the robot will travel through. + + Creates a boolean mask where True indicates cells that the robot will + occupy while following the path, accounting for the robot's width. + + Args: + occupancy_grid: The occupancy grid providing dimensions and resolution. + path: The path containing poses the robot will follow. + robot_width: The width of the robot in meters. + pose_index: The index in path.poses to start drawing from. Defaults to 0. + max_length: Maximum cumulative length to draw. Defaults to infinity. + + Returns: + A 2D boolean numpy array (height x width) where True indicates + cells the robot will pass through. + """ + mask = np.zeros((occupancy_grid.height, occupancy_grid.width), dtype=np.uint8) + + line_width_pixels = max(1, int(robot_width / occupancy_grid.resolution)) + + poses = path.poses + if len(poses) < pose_index + 2: + return mask.astype(np.bool_) + + # Draw lines between consecutive points + cumulative_length = 0.0 + for i in range(pose_index, len(poses) - 1): + pos1 = poses[i].position + pos2 = poses[i + 1].position + + segment_length = np.sqrt( + (pos2.x - pos1.x) ** 2 + (pos2.y - pos1.y) ** 2 + (pos2.z - pos1.z) ** 2 + ) + + if cumulative_length + segment_length > max_length: + break + + cumulative_length += segment_length + + grid_pt1 = occupancy_grid.world_to_grid(pos1) + grid_pt2 = occupancy_grid.world_to_grid(pos2) + + pt1 = (round(grid_pt1.x), round(grid_pt1.y)) + pt2 = (round(grid_pt2.x), round(grid_pt2.y)) + + cv2.line(mask, pt1, pt2, (255.0,), thickness=line_width_pixels) + + bool_mask = mask.astype(np.bool_) + + total_points = np.sum(bool_mask) + + if total_points == 0: + return bool_mask + + occupied_mask = occupancy_grid.grid >= CostValues.OCCUPIED + occupied_in_path = bool_mask & occupied_mask + occupied_count = np.sum(occupied_in_path) + + if occupied_count / total_points > 0.05: + raise ValueError( + f"More than 5% of path points are occupied: " + f"{occupied_count}/{total_points} ({100 * occupied_count / total_points:.1f}%)" + ) + + # Some of the points on the edge of the path may be occupied due to + # rounding. Remove them. + bool_mask = bool_mask & ~occupied_mask # type: ignore[assignment] + + return bool_mask diff --git a/dimos/mapping/occupancy/path_resampling.py b/dimos/mapping/occupancy/path_resampling.py new file mode 100644 index 0000000000..2090bf8f04 --- /dev/null +++ b/dimos/mapping/occupancy/path_resampling.py @@ -0,0 +1,256 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import math + +import numpy as np +from scipy.ndimage import uniform_filter1d # type: ignore[import-untyped] + +from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Vector3 +from dimos.msgs.nav_msgs import Path +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import euler_to_quaternion + +logger = setup_logger() + + +def _add_orientations_to_path(path: Path, goal_orientation: Quaternion) -> None: + """Add orientations to path poses based on direction of movement. + + Args: + path: Path with poses to add orientations to + goal_orientation: Desired orientation for the final pose + + Returns: + Path with orientations added to all poses + """ + if not path.poses or len(path.poses) < 2: + return + + # Calculate orientations for all poses except the last one + for i in range(len(path.poses) - 1): + current_pose = path.poses[i] + next_pose = path.poses[i + 1] + + # Calculate direction to next point + dx = next_pose.position.x - current_pose.position.x + dy = next_pose.position.y - current_pose.position.y + + # Calculate yaw angle + yaw = math.atan2(dy, dx) + + # Convert to quaternion (roll=0, pitch=0, yaw) + orientation = euler_to_quaternion(Vector3(0, 0, yaw)) + current_pose.orientation = orientation + + # Set last pose orientation + identity_quat = Quaternion(0, 0, 0, 1) + if goal_orientation != identity_quat: + # Use the provided goal orientation if it's not the identity + path.poses[-1].orientation = goal_orientation + elif len(path.poses) > 1: + # Use the previous pose's orientation + path.poses[-1].orientation = path.poses[-2].orientation + else: + # Single pose with identity goal orientation + path.poses[-1].orientation = identity_quat + + +# TODO: replace goal_pose with just goal_orientation +def simple_resample_path(path: Path, goal_pose: Pose, spacing: float) -> Path: + """Resample a path to have approximately uniform spacing between poses. + + Args: + path: The original Path + spacing: Desired distance between consecutive poses + + Returns: + A new Path with resampled poses + """ + if len(path) < 2 or spacing <= 0: + return path + + resampled = [] + resampled.append(path.poses[0]) + + accumulated_distance = 0.0 + + for i in range(1, len(path.poses)): + current = path.poses[i] + prev = path.poses[i - 1] + + # Calculate segment distance + dx = current.x - prev.x + dy = current.y - prev.y + segment_length = (dx**2 + dy**2) ** 0.5 + + if segment_length < 1e-10: + continue + + # Direction vector + dir_x = dx / segment_length + dir_y = dy / segment_length + + # Add points along this segment + while accumulated_distance + segment_length >= spacing: + # Distance along segment for next point + dist_along = spacing - accumulated_distance + if dist_along < 0: + break + + # Create new pose + new_x = prev.x + dir_x * dist_along + new_y = prev.y + dir_y * dist_along + new_pose = PoseStamped( + frame_id=path.frame_id, + position=[new_x, new_y, 0.0], + orientation=prev.orientation, # Keep same orientation + ) + resampled.append(new_pose) + + # Update for next iteration + accumulated_distance = 0 + segment_length -= dist_along + prev = new_pose + + accumulated_distance += segment_length + + # Add last pose if not already there + if len(path.poses) > 1: + last = path.poses[-1] + if not resampled or (resampled[-1].x != last.x or resampled[-1].y != last.y): + resampled.append(last) + + ret = Path(frame_id=path.frame_id, poses=resampled) + + _add_orientations_to_path(ret, goal_pose.orientation) + + return ret + + +def smooth_resample_path( + path: Path, goal_pose: Pose, spacing: float, smoothing_window: int = 100 +) -> Path: + """Resample a path with smoothing to reduce jagged corners and abrupt turns. + + This produces smoother paths than simple_resample_path by: + - First upsampling the path to have many points + - Applying a moving average filter to smooth the coordinates + - Resampling at the desired spacing + - Keeping start and end points fixed + + Args: + path: The original Path + goal_pose: Goal pose with desired final orientation + spacing: Desired approximate distance between consecutive poses + smoothing_window: Size of the smoothing window (larger = smoother) + + Returns: + A new Path with smoothly resampled poses + """ + + if len(path.poses) == 1: + p = path.poses[0].position + o = goal_pose.orientation + new_pose = PoseStamped( + frame_id=path.frame_id, + position=[p.x, p.y, p.z], + orientation=[o.x, o.y, o.z, o.w], + ) + return Path(frame_id=path.frame_id, poses=[new_pose]) + + if len(path) < 2 or spacing <= 0: + return path + + # Extract x, y coordinates from path + xs = np.array([p.x for p in path.poses]) + ys = np.array([p.y for p in path.poses]) + + # Remove duplicate consecutive points + diffs = np.sqrt(np.diff(xs) ** 2 + np.diff(ys) ** 2) + valid_mask = np.concatenate([[True], diffs > 1e-10]) + xs = xs[valid_mask] + ys = ys[valid_mask] + + if len(xs) < 2: + return path + + # Calculate total path length + dx = np.diff(xs) + dy = np.diff(ys) + segment_lengths = np.sqrt(dx**2 + dy**2) + total_length = np.sum(segment_lengths) + + if total_length < spacing: + return path + + # Upsample: create many points along the original path using linear interpolation + # This gives us enough points for effective smoothing + upsample_factor = 10 + num_upsampled = max(len(xs) * upsample_factor, 100) + + arc_length = np.concatenate([[0], np.cumsum(segment_lengths)]) + upsample_distances = np.linspace(0, total_length, num_upsampled) + + # Linear interpolation along arc length + xs_upsampled = np.interp(upsample_distances, arc_length, xs) + ys_upsampled = np.interp(upsample_distances, arc_length, ys) + + # Apply moving average smoothing + # Use 'nearest' mode to avoid shrinking at boundaries + window = min(smoothing_window, len(xs_upsampled) // 3) + if window >= 3: + xs_smooth = uniform_filter1d(xs_upsampled, size=window, mode="nearest") + ys_smooth = uniform_filter1d(ys_upsampled, size=window, mode="nearest") + else: + xs_smooth = xs_upsampled + ys_smooth = ys_upsampled + + # Keep start and end points exactly as original + xs_smooth[0] = xs[0] + ys_smooth[0] = ys[0] + xs_smooth[-1] = xs[-1] + ys_smooth[-1] = ys[-1] + + # Recalculate arc length on smoothed path + dx_smooth = np.diff(xs_smooth) + dy_smooth = np.diff(ys_smooth) + segment_lengths_smooth = np.sqrt(dx_smooth**2 + dy_smooth**2) + arc_length_smooth = np.concatenate([[0], np.cumsum(segment_lengths_smooth)]) + total_length_smooth = arc_length_smooth[-1] + + # Resample at desired spacing + num_samples = max(2, int(np.ceil(total_length_smooth / spacing)) + 1) + sample_distances = np.linspace(0, total_length_smooth, num_samples) + + # Interpolate to get final points + sampled_x = np.interp(sample_distances, arc_length_smooth, xs_smooth) + sampled_y = np.interp(sample_distances, arc_length_smooth, ys_smooth) + + # Create resampled poses + resampled = [] + for i in range(len(sampled_x)): + new_pose = PoseStamped( + frame_id=path.frame_id, + position=[float(sampled_x[i]), float(sampled_y[i]), 0.0], + orientation=Quaternion(0, 0, 0, 1), + ) + resampled.append(new_pose) + + ret = Path(frame_id=path.frame_id, poses=resampled) + + _add_orientations_to_path(ret, goal_pose.orientation) + + return ret diff --git a/dimos/mapping/occupancy/test_extrude_occupancy.py b/dimos/mapping/occupancy/test_extrude_occupancy.py new file mode 100644 index 0000000000..81caba7c8d --- /dev/null +++ b/dimos/mapping/occupancy/test_extrude_occupancy.py @@ -0,0 +1,25 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.mapping.occupancy.extrude_occupancy import generate_mujoco_scene +from dimos.utils.data import get_data + + +def test_generate_mujoco_scene(occupancy) -> None: + with open(get_data("expected_occupancy_scene.xml")) as f: + expected = f.read() + + actual = generate_mujoco_scene(occupancy) + + assert actual == expected diff --git a/dimos/mapping/occupancy/test_gradient.py b/dimos/mapping/occupancy/test_gradient.py new file mode 100644 index 0000000000..a097873aae --- /dev/null +++ b/dimos/mapping/occupancy/test_gradient.py @@ -0,0 +1,37 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +from dimos.mapping.occupancy.gradient import gradient, voronoi_gradient +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.msgs.sensor_msgs.Image import Image +from dimos.utils.data import get_data + + +@pytest.mark.parametrize("method", ["simple", "voronoi"]) +def test_gradient(occupancy, method) -> None: + expected = Image.from_file(get_data(f"gradient_{method}.png")) + + match method: + case "simple": + og = gradient(occupancy, max_distance=1.5) + case "voronoi": + og = voronoi_gradient(occupancy, max_distance=1.5) + case _: + raise ValueError(f"Unknown resampling method: {method}") + + actual = visualize_occupancy_grid(og, "rainbow") + np.testing.assert_array_equal(actual.data, expected.data) diff --git a/dimos/mapping/occupancy/test_inflation.py b/dimos/mapping/occupancy/test_inflation.py new file mode 100644 index 0000000000..a30ad413b1 --- /dev/null +++ b/dimos/mapping/occupancy/test_inflation.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import cv2 +import numpy as np + +from dimos.mapping.occupancy.inflation import simple_inflate +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.utils.data import get_data + + +def test_inflation(occupancy) -> None: + expected = cv2.imread(get_data("inflation_simple.png"), cv2.IMREAD_COLOR) + + og = simple_inflate(occupancy, 0.2) + + result = visualize_occupancy_grid(og, "rainbow") + np.testing.assert_array_equal(result.data, expected) diff --git a/dimos/mapping/occupancy/test_operations.py b/dimos/mapping/occupancy/test_operations.py new file mode 100644 index 0000000000..89332d0bdd --- /dev/null +++ b/dimos/mapping/occupancy/test_operations.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import cv2 +import numpy as np + +from dimos.mapping.occupancy.operations import overlay_occupied, smooth_occupied +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.utils.data import get_data + + +def test_smooth_occupied(occupancy) -> None: + expected = cv2.imread(get_data("smooth_occupied.png"), cv2.IMREAD_COLOR) + + result = visualize_occupancy_grid(smooth_occupied(occupancy), "rainbow") + + np.testing.assert_array_equal(result.data, expected) + + +def test_overlay_occupied(occupancy) -> None: + expected = cv2.imread(get_data("overlay_occupied.png"), cv2.IMREAD_COLOR) + overlay = occupancy.copy() + overlay.grid[50:100, 50:100] = 100 + + result = visualize_occupancy_grid(overlay_occupied(occupancy, overlay), "rainbow") + + np.testing.assert_array_equal(result.data, expected) diff --git a/dimos/mapping/occupancy/test_path_map.py b/dimos/mapping/occupancy/test_path_map.py new file mode 100644 index 0000000000..b3e250db9d --- /dev/null +++ b/dimos/mapping/occupancy/test_path_map.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import cv2 +import numpy as np +import pytest + +from dimos.mapping.occupancy.path_map import make_navigation_map +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.utils.data import get_data + + +@pytest.mark.parametrize("strategy", ["simple", "mixed"]) +def test_make_navigation_map(occupancy, strategy) -> None: + expected = cv2.imread(get_data(f"make_navigation_map_{strategy}.png"), cv2.IMREAD_COLOR) + robot_width = 0.4 + + og = make_navigation_map(occupancy, robot_width, strategy=strategy) + + result = visualize_occupancy_grid(og, "rainbow") + np.testing.assert_array_equal(result.data, expected) diff --git a/dimos/mapping/occupancy/test_path_mask.py b/dimos/mapping/occupancy/test_path_mask.py new file mode 100644 index 0000000000..dede997946 --- /dev/null +++ b/dimos/mapping/occupancy/test_path_mask.py @@ -0,0 +1,48 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import numpy as np +import pytest + +from dimos.mapping.occupancy.path_mask import make_path_mask +from dimos.mapping.occupancy.path_resampling import smooth_resample_path +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.msgs.geometry_msgs import Pose +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.navigation.replanning_a_star.min_cost_astar import min_cost_astar +from dimos.utils.data import get_data + + +@pytest.mark.parametrize( + "pose_index,max_length,expected_image", + [ + (0, float("inf"), "make_path_mask_full.png"), + (50, 2, "make_path_mask_two_meters.png"), + ], +) +def test_make_path_mask(occupancy_gradient, pose_index, max_length, expected_image) -> None: + start = Vector3(4.0, 2.0, 0) + goal_pose = Pose(6.15, 10.0, 0, 0, 0, 0, 1) + expected = Image.from_file(get_data(expected_image)) + path = min_cost_astar(occupancy_gradient, goal_pose.position, start, use_cpp=False) + path = smooth_resample_path(path, goal_pose, 0.1) + robot_width = 0.4 + path_mask = make_path_mask(occupancy_gradient, path, robot_width, pose_index, max_length) + actual = visualize_occupancy_grid(occupancy_gradient, "rainbow") + + actual.data[path_mask] = [0, 100, 0] + + np.testing.assert_array_equal(actual.data, expected.data) diff --git a/dimos/mapping/occupancy/test_path_resampling.py b/dimos/mapping/occupancy/test_path_resampling.py new file mode 100644 index 0000000000..c23f71cf89 --- /dev/null +++ b/dimos/mapping/occupancy/test_path_resampling.py @@ -0,0 +1,50 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +from dimos.mapping.occupancy.gradient import gradient +from dimos.mapping.occupancy.path_resampling import simple_resample_path, smooth_resample_path +from dimos.mapping.occupancy.visualize_path import visualize_path +from dimos.msgs.geometry_msgs import Pose +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid +from dimos.msgs.sensor_msgs.Image import Image +from dimos.navigation.replanning_a_star.min_cost_astar import min_cost_astar +from dimos.utils.data import get_data + + +@pytest.fixture +def costmap() -> OccupancyGrid: + return gradient(OccupancyGrid(np.load(get_data("occupancy_simple.npy"))), max_distance=1.5) + + +@pytest.mark.parametrize("method", ["simple", "smooth"]) +def test_resample_path(costmap, method) -> None: + start = Vector3(4.0, 2.0, 0) + goal_pose = Pose(6.15, 10.0, 0, 0, 0, 0, 1) + expected = Image.from_file(get_data(f"resample_path_{method}.png")) + path = min_cost_astar(costmap, goal_pose.position, start, use_cpp=False) + + match method: + case "simple": + resampled = simple_resample_path(path, goal_pose, 0.1) + case "smooth": + resampled = smooth_resample_path(path, goal_pose, 0.1) + case _: + raise ValueError(f"Unknown resampling method: {method}") + + actual = visualize_path(costmap, resampled, 0.2, 0.4) + np.testing.assert_array_equal(actual.data, expected.data) diff --git a/dimos/mapping/occupancy/test_visualizations.py b/dimos/mapping/occupancy/test_visualizations.py new file mode 100644 index 0000000000..17b2629e80 --- /dev/null +++ b/dimos/mapping/occupancy/test_visualizations.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import cv2 +import numpy as np +import pytest + +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.utils.data import get_data + + +@pytest.mark.parametrize("palette", ["rainbow", "turbo"]) +def test_visualize_occupancy_grid(occupancy_gradient, palette) -> None: + expected = cv2.imread(get_data(f"visualize_occupancy_{palette}.png"), cv2.IMREAD_COLOR) + + result = visualize_occupancy_grid(occupancy_gradient, palette) + + np.testing.assert_array_equal(result.data, expected) diff --git a/dimos/mapping/occupancy/visualizations.py b/dimos/mapping/occupancy/visualizations.py new file mode 100644 index 0000000000..33a1336874 --- /dev/null +++ b/dimos/mapping/occupancy/visualizations.py @@ -0,0 +1,160 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import lru_cache +from typing import Literal, TypeAlias + +import cv2 +import numpy as np +from numpy.typing import NDArray + +from dimos.msgs.nav_msgs import Path +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ImageFormat + +Palette: TypeAlias = Literal["rainbow", "turbo"] + + +def visualize_occupancy_grid( + occupancy_grid: OccupancyGrid, palette: Palette, path: Path | None = None +) -> Image: + match palette: + case "rainbow": + bgr_image = rainbow_image(occupancy_grid.grid) + case "turbo": + bgr_image = turbo_image(occupancy_grid.grid) + case _: + raise NotImplementedError() + + if path is not None and len(path.poses) > 0: + _draw_path(occupancy_grid, bgr_image, path) + + return Image( + data=bgr_image, + format=ImageFormat.BGR, + frame_id=occupancy_grid.frame_id, + ts=occupancy_grid.ts, + ) + + +def _draw_path(occupancy_grid: OccupancyGrid, bgr_image: NDArray[np.uint8], path: Path) -> None: + points = [] + for pose in path.poses: + grid_coord = occupancy_grid.world_to_grid([pose.x, pose.y, pose.z]) + pixel_x = int(grid_coord.x) + pixel_y = int(grid_coord.y) + + if 0 <= pixel_x < occupancy_grid.width and 0 <= pixel_y < occupancy_grid.height: + points.append((pixel_x, pixel_y)) + + if len(points) > 1: + points_array = np.array(points, dtype=np.int32) + cv2.polylines(bgr_image, [points_array], isClosed=False, color=(0, 0, 0), thickness=1) + + +def rainbow_image(grid: NDArray[np.int8]) -> NDArray[np.uint8]: + """Convert the occupancy grid to a rainbow-colored Image. + + Color scheme: + - -1 (unknown): black + - 100 (occupied): magenta + - 0-99: rainbow from blue (0) to red (99) + + Returns: + Image with rainbow visualization of the occupancy grid + """ + + # Create a copy of the grid for visualization + # Map values to 0-255 range for colormap + height, width = grid.shape + vis_grid = np.zeros((height, width), dtype=np.uint8) + + # Handle 0-99: map to colormap range + gradient_mask = (grid >= 0) & (grid < 100) + vis_grid[gradient_mask] = ((grid[gradient_mask] / 99.0) * 255).astype(np.uint8) + + # Apply JET colormap (blue to red) - returns BGR + bgr_image = cv2.applyColorMap(vis_grid, cv2.COLORMAP_JET) + + unknown_mask = grid == -1 + bgr_image[unknown_mask] = [0, 0, 0] + + occupied_mask = grid == 100 + bgr_image[occupied_mask] = [255, 0, 255] + + return bgr_image.astype(np.uint8) + + +def turbo_image(grid: NDArray[np.int8]) -> NDArray[np.uint8]: + """Convert the occupancy grid to a turbo-colored Image. + + Returns: + Image with turbo visualization of the occupancy grid + """ + color_lut = _turbo_lut() + + # Map grid values to lookup indices + # Values: -1 -> 255, 0-100 -> 0-100, clipped to valid range + lookup_indices = np.where(grid == -1, 255, np.clip(grid, 0, 100)).astype(np.uint8) + + # Create BGR image using lookup table (vectorized operation) + return color_lut[lookup_indices] + + +def _interpolate_turbo(t: float) -> tuple[int, int, int]: + """D3's interpolateTurbo colormap implementation. + + Based on Anton Mikhailov's Turbo colormap using polynomial approximations. + + Args: + t: Value in [0, 1] + + Returns: + RGB tuple (0-255 range) + """ + t = max(0.0, min(1.0, t)) + + r = 34.61 + t * (1172.33 - t * (10793.56 - t * (33300.12 - t * (38394.49 - t * 14825.05)))) + g = 23.31 + t * (557.33 + t * (1225.33 - t * (3574.96 - t * (1073.77 + t * 707.56)))) + b = 27.2 + t * (3211.1 - t * (15327.97 - t * (27814.0 - t * (22569.18 - t * 6838.66)))) + + return ( + max(0, min(255, round(r))), + max(0, min(255, round(g))), + max(0, min(255, round(b))), + ) + + +@lru_cache(maxsize=1) +def _turbo_lut() -> NDArray[np.uint8]: + # Pre-compute lookup table for all possible values (-1 to 100) + color_lut = np.zeros((256, 3), dtype=np.uint8) + + for value in range(-1, 101): + # Normalize to [0, 1] range based on domain [-1, 100] + t = (value + 1) / 101.0 + + if value == -1: + rgb = (34, 24, 28) + elif value == 100: + rgb = (0, 0, 0) + else: + rgb = _interpolate_turbo(t * 2 - 1) + + # Map -1 to index 255, 0-100 to indices 0-100 + idx = 255 if value == -1 else value + color_lut[idx] = [rgb[2], rgb[1], rgb[0]] + + return color_lut diff --git a/dimos/mapping/occupancy/visualize_path.py b/dimos/mapping/occupancy/visualize_path.py new file mode 100644 index 0000000000..1a6e4887f1 --- /dev/null +++ b/dimos/mapping/occupancy/visualize_path.py @@ -0,0 +1,89 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cv2 +import numpy as np + +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.msgs.nav_msgs import Path +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid +from dimos.msgs.sensor_msgs.Image import Image +from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ImageFormat + + +def visualize_path( + occupancy_grid: OccupancyGrid, + path: Path, + robot_width: float, + robot_length: float, + thickness: int = 1, + scale: int = 8, +) -> Image: + image = visualize_occupancy_grid(occupancy_grid, "rainbow") + bgr = image.data + + bgr = cv2.resize( + bgr, + (bgr.shape[1] * scale, bgr.shape[0] * scale), + interpolation=cv2.INTER_NEAREST, + ) + + # Convert robot dimensions from meters to grid cells, then to scaled pixels + resolution = occupancy_grid.resolution + robot_width_px = int((robot_width / resolution) * scale) + robot_length_px = int((robot_length / resolution) * scale) + + # Draw robot rectangle at each path point + for pose in path.poses: + # Convert world coordinates to grid coordinates + grid_coord = occupancy_grid.world_to_grid([pose.x, pose.y, pose.z]) + cx = int(grid_coord.x * scale) + cy = int(grid_coord.y * scale) + + # Get yaw angle from pose orientation + yaw = pose.yaw + + # Define rectangle corners centered at origin (length along x, width along y) + half_length = robot_length_px / 2 + half_width = robot_width_px / 2 + corners = np.array( + [ + [-half_length, -half_width], + [half_length, -half_width], + [half_length, half_width], + [-half_length, half_width], + ], + dtype=np.float32, + ) + + # Rotate corners by yaw angle + cos_yaw = np.cos(yaw) + sin_yaw = np.sin(yaw) + rotation_matrix = np.array([[cos_yaw, -sin_yaw], [sin_yaw, cos_yaw]]) + rotated_corners = corners @ rotation_matrix.T + + # Translate to center position + rotated_corners[:, 0] += cx + rotated_corners[:, 1] += cy + + # Draw the rotated rectangle + pts = rotated_corners.astype(np.int32).reshape((-1, 1, 2)) + cv2.polylines(bgr, [pts], isClosed=True, color=(0, 0, 0), thickness=thickness) + + return Image( + data=bgr, + format=ImageFormat.BGR, + frame_id=occupancy_grid.frame_id, + ts=occupancy_grid.ts, + ) diff --git a/dimos/mapping/osm/README.md b/dimos/mapping/osm/README.md index be3d4a3ee2..cb94c0160b 100644 --- a/dimos/mapping/osm/README.md +++ b/dimos/mapping/osm/README.md @@ -28,7 +28,7 @@ You have to update it with your current location and when you stray too far from ```python curr_map = CurrentLocationMap(QwenVlModel()) -# Set your latest position. +# Set your latest position. curr_map.update_position(LatLon(lat=..., lon=...)) # If you want to get back a GPS position of a feature (Qwen gets your current position). diff --git a/dimos/mapping/osm/current_location_map.py b/dimos/mapping/osm/current_location_map.py index 88942935af..ef0a832cd6 100644 --- a/dimos/mapping/osm/current_location_map.py +++ b/dimos/mapping/osm/current_location_map.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from PIL import Image as PILImage, ImageDraw from dimos.mapping.osm.osm import MapImage, get_osm_map from dimos.mapping.osm.query import query_for_one_position, query_for_one_position_and_context @@ -19,7 +20,7 @@ from dimos.models.vl.base import VlModel from dimos.utils.logging_config import setup_logger -logger = setup_logger(__file__) +logger = setup_logger() class CurrentLocationMap: @@ -31,7 +32,7 @@ def __init__(self, vl_model: VlModel) -> None: self._vl_model = vl_model self._position = None self._map_image = None - self._zoom_level = 19 + self._zoom_level = 15 self._n_tiles = 6 # What ratio of the width is considered the center. 1.0 means the entire map is the center. self._center_width = 0.4 @@ -40,16 +41,19 @@ def update_position(self, position: LatLon) -> None: self._position = position def query_for_one_position(self, query: str) -> LatLon | None: - return query_for_one_position(self._vl_model, self._get_current_map(), query) + return query_for_one_position(self._vl_model, self._get_current_map(), query) # type: ignore[no-untyped-call] def query_for_one_position_and_context( self, query: str, robot_position: LatLon ) -> tuple[LatLon, str] | None: return query_for_one_position_and_context( - self._vl_model, self._get_current_map(), query, robot_position + self._vl_model, + self._get_current_map(), # type: ignore[no-untyped-call] + query, + robot_position, ) - def _get_current_map(self): + def _get_current_map(self): # type: ignore[no-untyped-def] if not self._position: raise ValueError("Current position has not been set.") @@ -63,12 +67,47 @@ def _fetch_new_map(self) -> None: logger.info( f"Getting a new OSM map, position={self._position}, zoom={self._zoom_level} n_tiles={self._n_tiles}" ) - self._map_image = get_osm_map(self._position, self._zoom_level, self._n_tiles) + self._map_image = get_osm_map(self._position, self._zoom_level, self._n_tiles) # type: ignore[arg-type] - def _position_is_too_far_off_center(self) -> bool: + # Add position marker + import numpy as np + + assert self._map_image is not None + assert self._position is not None + pil_image = PILImage.fromarray(self._map_image.image.data) + draw = ImageDraw.Draw(pil_image) x, y = self._map_image.latlon_to_pixel(self._position) - width = self._map_image.image.width + radius = 20 + draw.ellipse( + [x - radius, y - radius, x + radius, y + radius], + fill=(255, 0, 0), + outline=(0, 0, 0), + width=3, + ) + + self._map_image.image.data[:] = np.array(pil_image) + + def _position_is_too_far_off_center(self) -> bool: + x, y = self._map_image.latlon_to_pixel(self._position) # type: ignore[arg-type, union-attr] + width = self._map_image.image.width # type: ignore[union-attr] size_min = width * (0.5 - self._center_width / 2) size_max = width * (0.5 + self._center_width / 2) return x < size_min or x > size_max or y < size_min or y > size_max + + def save_current_map_image(self, filepath: str = "osm_debug_map.png") -> str: + """Save the current OSM map image to a file for debugging. + + Args: + filepath: Path where to save the image + + Returns: + The filepath where the image was saved + """ + if not self._map_image: + self._get_current_map() # type: ignore[no-untyped-call] + + if self._map_image is not None: + self._map_image.image.save(filepath) + logger.info(f"Saved OSM map image to {filepath}") + return filepath diff --git a/dimos/mapping/osm/demo_osm.py b/dimos/mapping/osm/demo_osm.py index cf907378f3..3e4ba8e61b 100644 --- a/dimos/mapping/osm/demo_osm.py +++ b/dimos/mapping/osm/demo_osm.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,40 +14,19 @@ # limitations under the License. from dotenv import load_dotenv -from reactivex import interval -from dimos.agents2.agent import llm_agent -from dimos.agents2.cli.human import human_input -from dimos.agents2.skills.osm import osm_skill -from dimos.agents2.system_prompt import get_system_prompt +from dimos.agents.agent import llm_agent +from dimos.agents.cli.human import human_input +from dimos.agents.skills.demo_robot import demo_robot +from dimos.agents.skills.osm import osm_skill from dimos.core.blueprints import autoconnect -from dimos.core.module import Module -from dimos.core.stream import Out -from dimos.mapping.types import LatLon load_dotenv() -class DemoRobot(Module): - gps_location: Out[LatLon] = None - - def start(self) -> None: - super().start() - self._disposables.add(interval(1.0).subscribe(lambda _: self._publish_gps_location())) - - def stop(self) -> None: - super().stop() - - def _publish_gps_location(self) -> None: - self.gps_location.publish(LatLon(lat=37.78092426217621, lon=-122.40682866540769)) - - -demo_robot = DemoRobot.blueprint - - demo_osm = autoconnect( demo_robot(), osm_skill(), human_input(), - llm_agent(system_prompt=get_system_prompt()), + llm_agent(), ) diff --git a/dimos/mapping/osm/osm.py b/dimos/mapping/osm/osm.py index 9f967046f6..31fb044087 100644 --- a/dimos/mapping/osm/osm.py +++ b/dimos/mapping/osm/osm.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import numpy as np from PIL import Image as PILImage -import requests +import requests # type: ignore[import-untyped] from dimos.mapping.types import ImageCoord, LatLon from dimos.msgs.sensor_msgs import Image, ImageFormat diff --git a/dimos/mapping/osm/query.py b/dimos/mapping/osm/query.py index 4501525880..fd6e3694f6 100644 --- a/dimos/mapping/osm/query.py +++ b/dimos/mapping/osm/query.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ _PROLOGUE = "This is an image of an open street map I'm on." _JSON = "Please only respond with valid JSON." -logger = setup_logger(__name__) +logger = setup_logger() def query_for_one_position(vl_model: VlModel, map_image: MapImage, query: str) -> LatLon | None: diff --git a/dimos/mapping/osm/test_osm.py b/dimos/mapping/osm/test_osm.py index 0e993f3157..475e2b40fc 100644 --- a/dimos/mapping/osm/test_osm.py +++ b/dimos/mapping/osm/test_osm.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/mapping/pointclouds/accumulators/general.py b/dimos/mapping/pointclouds/accumulators/general.py new file mode 100644 index 0000000000..d0d4668dc3 --- /dev/null +++ b/dimos/mapping/pointclouds/accumulators/general.py @@ -0,0 +1,77 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from open3d.geometry import PointCloud # type: ignore[import-untyped] +from open3d.io import read_point_cloud # type: ignore[import-untyped] + +from dimos.core.global_config import GlobalConfig + + +class GeneralPointCloudAccumulator: + _point_cloud: PointCloud + _voxel_size: float + + def __init__(self, voxel_size: float, global_config: GlobalConfig) -> None: + self._point_cloud = PointCloud() + self._voxel_size = voxel_size + + if global_config.mujoco_global_map_from_pointcloud: + path = global_config.mujoco_global_map_from_pointcloud + self._point_cloud = read_point_cloud(path) + + def get_point_cloud(self) -> PointCloud: + return self._point_cloud + + def add(self, point_cloud: PointCloud) -> None: + """Voxelise *frame* and splice it into the running map.""" + new_pct = point_cloud.voxel_down_sample(voxel_size=self._voxel_size) + + # Skip for empty pointclouds. + if len(new_pct.points) == 0: + return + + self._point_cloud = _splice_cylinder(self._point_cloud, new_pct, shrink=0.5) + + +def _splice_cylinder( + map_pcd: PointCloud, + patch_pcd: PointCloud, + axis: int = 2, + shrink: float = 0.95, +) -> PointCloud: + center = patch_pcd.get_center() + patch_pts = np.asarray(patch_pcd.points) + + # Axes perpendicular to cylinder + axes = [0, 1, 2] + axes.remove(axis) + + planar_dists = np.linalg.norm(patch_pts[:, axes] - center[axes], axis=1) + radius = planar_dists.max() * shrink + + axis_min = (patch_pts[:, axis].min() - center[axis]) * shrink + center[axis] + axis_max = (patch_pts[:, axis].max() - center[axis]) * shrink + center[axis] + + map_pts = np.asarray(map_pcd.points) + planar_dists_map = np.linalg.norm(map_pts[:, axes] - center[axes], axis=1) + + victims = np.nonzero( + (planar_dists_map < radius) + & (map_pts[:, axis] >= axis_min) + & (map_pts[:, axis] <= axis_max) + )[0] + + survivors = map_pcd.select_by_index(victims, invert=True) + return survivors + patch_pcd diff --git a/dimos/mapping/pointclouds/accumulators/protocol.py b/dimos/mapping/pointclouds/accumulators/protocol.py new file mode 100644 index 0000000000..f453165816 --- /dev/null +++ b/dimos/mapping/pointclouds/accumulators/protocol.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Protocol + +from open3d.geometry import PointCloud # type: ignore[import-untyped] + + +class PointCloudAccumulator(Protocol): + def get_point_cloud(self) -> PointCloud: + """Get the accumulated pointcloud.""" + ... + + def add(self, point_cloud: PointCloud) -> None: + """Add a pointcloud to the accumulator.""" + ... diff --git a/dimos/mapping/pointclouds/demo.py b/dimos/mapping/pointclouds/demo.py new file mode 100644 index 0000000000..5251fc3406 --- /dev/null +++ b/dimos/mapping/pointclouds/demo.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cv2 +from open3d.geometry import PointCloud # type: ignore[import-untyped] +import typer + +from dimos.mapping.occupancy.gradient import gradient +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.mapping.pointclouds.occupancy import simple_occupancy +from dimos.mapping.pointclouds.util import ( + height_colorize, + read_pointcloud, + visualize, +) +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.utils.data import get_data + +app = typer.Typer() + + +def _get_sum_map() -> PointCloud: + return read_pointcloud(get_data("apartment") / "sum.ply") + + +def _get_occupancy_grid() -> OccupancyGrid: + resolution = 0.05 + min_height = 0.15 + max_height = 0.6 + occupancygrid = simple_occupancy( + PointCloud2(_get_sum_map()), + resolution=resolution, + min_height=min_height, + max_height=max_height, + ) + return occupancygrid + + +def _show_occupancy_grid(og: OccupancyGrid) -> None: + cost_map = visualize_occupancy_grid(og, "turbo").to_opencv() + cost_map = cv2.flip(cost_map, 0) + + # Resize to make the image larger (scale by 4x) + height, width = cost_map.shape[:2] + cost_map = cv2.resize(cost_map, (width * 4, height * 4), interpolation=cv2.INTER_NEAREST) + + cv2.namedWindow("Occupancy Grid", cv2.WINDOW_NORMAL) + cv2.imshow("Occupancy Grid", cost_map) + cv2.waitKey(0) + cv2.destroyAllWindows() + + +@app.command() +def view_sum() -> None: + pointcloud = _get_sum_map() + height_colorize(pointcloud) + visualize(pointcloud) + + +@app.command() +def view_map() -> None: + og = _get_occupancy_grid() + _show_occupancy_grid(og) + + +@app.command() +def view_map_inflated() -> None: + og = gradient(_get_occupancy_grid(), max_distance=1.5) + _show_occupancy_grid(og) + + +if __name__ == "__main__": + app() diff --git a/dimos/mapping/pointclouds/occupancy.py b/dimos/mapping/pointclouds/occupancy.py new file mode 100644 index 0000000000..0f6ad8c0de --- /dev/null +++ b/dimos/mapping/pointclouds/occupancy.py @@ -0,0 +1,501 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Protocol, TypeVar + +from numba import njit, prange # type: ignore[import-untyped] +import numpy as np +from scipy import ndimage # type: ignore[import-untyped] + +from dimos.msgs.geometry_msgs import Pose +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid + +if TYPE_CHECKING: + from numpy.typing import NDArray + + +@njit(cache=True) # type: ignore[untyped-decorator] +def _height_map_kernel( + points: NDArray[np.floating[Any]], + min_height_map: NDArray[np.floating[Any]], + max_height_map: NDArray[np.floating[Any]], + min_x: float, + min_y: float, + inv_res: float, + width: int, + height: int, +) -> None: + """Build min/max height maps from points (faster than np.fmax/fmin.at).""" + n = points.shape[0] + for i in range(n): + x = points[i, 0] + y = points[i, 1] + z = points[i, 2] + + gx = int((x - min_x) * inv_res + 0.5) + gy = int((y - min_y) * inv_res + 0.5) + + if 0 <= gx < width and 0 <= gy < height: + cur_min = min_height_map[gy, gx] + cur_max = max_height_map[gy, gx] + # NaN comparisons are always False, so first point sets the value + if z < cur_min or cur_min != cur_min: # cur_min != cur_min checks for NaN + min_height_map[gy, gx] = z + if z > cur_max or cur_max != cur_max: + max_height_map[gy, gx] = z + + +@njit(cache=True, parallel=True) # type: ignore[untyped-decorator] +def _simple_occupancy_kernel( + points: NDArray[np.floating[Any]], + grid: NDArray[np.signedinteger[Any]], + min_x: float, + min_y: float, + inv_res: float, + width: int, + height: int, + min_height: float, + max_height: float, +) -> None: + """Numba-accelerated kernel for simple_occupancy grid population.""" + n = points.shape[0] + # Pass 1: Mark ground as free + for i in prange(n): + x = points[i, 0] + y = points[i, 1] + z = points[i, 2] + if z < min_height: + gx = int((x - min_x) * inv_res + 0.5) + gy = int((y - min_y) * inv_res + 0.5) + if 0 <= gx < width and 0 <= gy < height: + grid[gy, gx] = 0 + + # Pass 2: Mark obstacles (overwrites ground) + for i in prange(n): + x = points[i, 0] + y = points[i, 1] + z = points[i, 2] + if min_height <= z <= max_height: + gx = int((x - min_x) * inv_res + 0.5) + gy = int((y - min_y) * inv_res + 0.5) + if 0 <= gx < width and 0 <= gy < height: + grid[gy, gx] = 100 + + +if TYPE_CHECKING: + from collections.abc import Callable + + from dimos.msgs.sensor_msgs import PointCloud2 + + +@dataclass(frozen=True) +class OccupancyConfig: + """Base config for all occupancy grid generators.""" + + resolution: float = 0.05 + frame_id: str | None = None + + +ConfigT = TypeVar("ConfigT", bound=OccupancyConfig, covariant=True) + + +class OccupancyFn(Protocol[ConfigT]): + """Protocol for pointcloud-to-occupancy conversion functions. + + Functions matching this protocol take a PointCloud2 and config kwargs, + returning an OccupancyGrid. Call with: fn(cloud, resolution=0.1, ...) + """ + + @property + def config_class(self) -> type[ConfigT]: ... + + def __call__(self, cloud: PointCloud2, **kwargs: Any) -> OccupancyGrid: ... + + +# Populated after function definitions below +OCCUPANCY_ALGOS: dict[str, Callable[..., OccupancyGrid]] = {} + + +@dataclass(frozen=True) +class HeightCostConfig(OccupancyConfig): + """Config for height-cost based occupancy (terrain slope analysis).""" + + can_pass_under: float = 0.6 + can_climb: float = 0.15 + ignore_noise: float = 0.05 + smoothing: float = 1.0 + + +def height_cost_occupancy(cloud: PointCloud2, **kwargs: Any) -> OccupancyGrid: + """Create a costmap based on terrain slope (rate of change of height). + + Costs are assigned based on the gradient magnitude of the terrain height. + Steeper slopes get higher costs, with max_step height change mapping to cost 100. + Cells without observations are marked unknown (-1). + + Args: + cloud: PointCloud2 message containing 3D points + **kwargs: HeightCostConfig fields - resolution, can_pass_under, can_climb, + ignore_noise, smoothing, frame_id + + Returns: + OccupancyGrid with costs 0-100 based on terrain slope, -1 for unknown + """ + cfg = HeightCostConfig(**kwargs) + points, _ = cloud.as_numpy() + points = points.astype(np.float64) # Upcast to avoid float32 rounding + ts = cloud.ts if hasattr(cloud, "ts") and cloud.ts is not None else 0.0 + + if len(points) == 0: + return OccupancyGrid( + width=1, + height=1, + resolution=cfg.resolution, + frame_id=cfg.frame_id or cloud.frame_id, + ) + + # Find bounds of the point cloud in X-Y plane (use all points) + min_x = np.min(points[:, 0]) + max_x = np.max(points[:, 0]) + min_y = np.min(points[:, 1]) + max_y = np.max(points[:, 1]) + + # Add padding + padding = 1.0 + min_x -= padding + max_x += padding + min_y -= padding + max_y += padding + + # Calculate grid dimensions + width = int(np.ceil((max_x - min_x) / cfg.resolution)) + height = int(np.ceil((max_y - min_y) / cfg.resolution)) + + # Create origin pose + origin = Pose() + origin.position.x = min_x + origin.position.y = min_y + origin.position.z = 0.0 + origin.orientation.w = 1.0 + + # Step 1: Build min and max height maps for each cell + # Initialize with NaN to track which cells have observations + min_height_map = np.full((height, width), np.nan, dtype=np.float32) + max_height_map = np.full((height, width), np.nan, dtype=np.float32) + + # Use numba kernel (faster than np.fmax/fmin.at) + _height_map_kernel( + points, + min_height_map, + max_height_map, + min_x, + min_y, + 1.0 / cfg.resolution, + width, + height, + ) + + # Step 2: Determine effective height for each cell + # If gap between min and max > can_pass_under, robot can pass under - use min (ground) + # Otherwise use max (solid obstacle) + height_gap = max_height_map - min_height_map + height_map = np.where(height_gap > cfg.can_pass_under, min_height_map, max_height_map) + + # Track which cells have observations + observed_mask = ~np.isnan(height_map) + + # Step 3: Apply smoothing to fill gaps while preserving unknown space + if cfg.smoothing > 0 and np.any(observed_mask): + # Use a weighted smoothing approach that only interpolates from known cells + # Create a weight map (1 for observed, 0 for unknown) + weights = observed_mask.astype(np.float32) + height_map_filled = np.where(observed_mask, height_map, 0.0) + + # Smooth both height values and weights + smoothed_heights = ndimage.gaussian_filter(height_map_filled, sigma=cfg.smoothing) + smoothed_weights = ndimage.gaussian_filter(weights, sigma=cfg.smoothing) + + # Avoid division by zero (use np.divide with where to prevent warning) + valid_smooth = smoothed_weights > 0.01 + height_map_smoothed = np.full_like(smoothed_heights, np.nan) + np.divide(smoothed_heights, smoothed_weights, out=height_map_smoothed, where=valid_smooth) + + # Keep original values where we had observations, use smoothed elsewhere + height_map = np.where(observed_mask, height_map, height_map_smoothed) + + # Update observed mask to include smoothed cells + observed_mask = ~np.isnan(height_map) + + # Step 4: Calculate rate of change (gradient magnitude) + # Use Sobel filters for gradient calculation + if np.any(observed_mask): + # Replace NaN with 0 for gradient calculation + height_for_grad = np.where(observed_mask, height_map, 0.0) + + # Calculate gradients (Sobel gives gradient in pixels, scale by resolution) + grad_x = ndimage.sobel(height_for_grad, axis=1) / (8.0 * cfg.resolution) + grad_y = ndimage.sobel(height_for_grad, axis=0) / (8.0 * cfg.resolution) + + # Gradient magnitude = height change per meter + gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2) + + # Map gradient to cost: can_climb height change over one cell maps to cost 100 + # gradient_magnitude is in m/m, so multiply by resolution to get height change per cell + height_change_per_cell = gradient_magnitude * cfg.resolution + + # Ignore height changes below noise threshold (lidar floor noise) + height_change_per_cell = np.where( + height_change_per_cell < cfg.ignore_noise, 0.0, height_change_per_cell + ) + + cost_float = (height_change_per_cell / cfg.can_climb) * 100.0 + cost_float = np.clip(cost_float, 0, 100) + + # Erode observed mask - only trust gradients where all neighbors are observed + # This prevents false high costs at boundaries with unknown regions + structure = ndimage.generate_binary_structure(2, 1) # 4-connectivity + valid_gradient_mask = ndimage.binary_erosion(observed_mask, structure=structure) + + # Convert to int8, marking cells without valid gradients as -1 + cost = np.where(valid_gradient_mask, cost_float.astype(np.int8), -1) + else: + cost = np.full((height, width), -1, dtype=np.int8) + + return OccupancyGrid( + grid=cost, + resolution=cfg.resolution, + origin=origin, + frame_id=cfg.frame_id or cloud.frame_id, + ts=ts, + ) + + +@dataclass(frozen=True) +class GeneralOccupancyConfig(OccupancyConfig): + """Config for general obstacle-based occupancy.""" + + min_height: float = 0.1 + max_height: float = 2.0 + mark_free_radius: float = 0.4 + + +# can remove, just needs pulling out of unitree type/map.py +def general_occupancy(cloud: PointCloud2, **kwargs: Any) -> OccupancyGrid: + """Create an OccupancyGrid from a PointCloud2 message. + + Args: + cloud: PointCloud2 message containing 3D points + **kwargs: GeneralOccupancyConfig fields - resolution, min_height, max_height, + frame_id, mark_free_radius + + Returns: + OccupancyGrid with occupied cells where points were projected + """ + cfg = GeneralOccupancyConfig(**kwargs) + points, _ = cloud.as_numpy() + points = points.astype(np.float64) # Upcast to avoid float32 rounding + + if len(points) == 0: + return OccupancyGrid( + width=1, + height=1, + resolution=cfg.resolution, + frame_id=cfg.frame_id or cloud.frame_id, + ) + + # Filter points by height for obstacles + obstacle_mask = (points[:, 2] >= cfg.min_height) & (points[:, 2] <= cfg.max_height) + obstacle_points = points[obstacle_mask] + + # Get points below min_height for marking as free space + ground_mask = points[:, 2] < cfg.min_height + ground_points = points[ground_mask] + + # Find bounds of the point cloud in X-Y plane (use all points) + min_x = np.min(points[:, 0]) + max_x = np.max(points[:, 0]) + min_y = np.min(points[:, 1]) + max_y = np.max(points[:, 1]) + + # Add some padding around the bounds + padding = 1.0 # 1 meter padding + min_x -= padding + max_x += padding + min_y -= padding + max_y += padding + + # Calculate grid dimensions + width = int(np.ceil((max_x - min_x) / cfg.resolution)) + height = int(np.ceil((max_y - min_y) / cfg.resolution)) + + # Create origin pose (bottom-left corner of the grid) + origin = Pose() + origin.position.x = min_x + origin.position.y = min_y + origin.position.z = 0.0 + origin.orientation.w = 1.0 # No rotation + + # Initialize grid (all unknown) + grid = np.full((height, width), -1, dtype=np.int8) + + # First, mark ground points as free space + if len(ground_points) > 0: + ground_x = ((ground_points[:, 0] - min_x) / cfg.resolution).astype(np.int32) + ground_y = ((ground_points[:, 1] - min_y) / cfg.resolution).astype(np.int32) + + # Clip indices to grid bounds + ground_x = np.clip(ground_x, 0, width - 1) + ground_y = np.clip(ground_y, 0, height - 1) + + # Mark ground cells as free + grid[ground_y, ground_x] = 0 # Free space + + # Then mark obstacle points (will override ground if at same location) + if len(obstacle_points) > 0: + obs_x = ((obstacle_points[:, 0] - min_x) / cfg.resolution).astype(np.int32) + obs_y = ((obstacle_points[:, 1] - min_y) / cfg.resolution).astype(np.int32) + + # Clip indices to grid bounds + obs_x = np.clip(obs_x, 0, width - 1) + obs_y = np.clip(obs_y, 0, height - 1) + + # Mark cells as occupied + grid[obs_y, obs_x] = 100 # Lethal obstacle + + # Apply mark_free_radius to expand free space areas + if cfg.mark_free_radius > 0: + # Expand existing free space areas by the specified radius + # This will NOT expand from obstacles, only from free space + + free_mask = grid == 0 # Current free space + free_radius_cells = int(np.ceil(cfg.mark_free_radius / cfg.resolution)) + + # Create circular kernel + y, x = np.ogrid[ + -free_radius_cells : free_radius_cells + 1, + -free_radius_cells : free_radius_cells + 1, + ] + kernel = x**2 + y**2 <= free_radius_cells**2 + + # Dilate free space areas + expanded_free = ndimage.binary_dilation(free_mask, structure=kernel, iterations=1) + + # Mark expanded areas as free, but don't override obstacles + grid[expanded_free & (grid != 100)] = 0 + + # Create and return OccupancyGrid + # Get timestamp from cloud if available + ts = cloud.ts if hasattr(cloud, "ts") and cloud.ts is not None else 0.0 + + return OccupancyGrid( + grid=grid, + resolution=cfg.resolution, + origin=origin, + frame_id=cfg.frame_id or cloud.frame_id, + ts=ts, + ) + + +@dataclass(frozen=True) +class SimpleOccupancyConfig(OccupancyConfig): + """Config for simple occupancy with morphological closing.""" + + min_height: float = 0.1 + max_height: float = 2.0 + closing_iterations: int = 1 + closing_connectivity: int = 2 + can_pass_under: float = 0.6 + can_climb: float = 0.15 + ignore_noise: float = 0.05 + smoothing: float = 1.0 + + +def simple_occupancy(cloud: PointCloud2, **kwargs: Any) -> OccupancyGrid: + """Create a simple occupancy grid with morphological closing. + + Args: + cloud: PointCloud2 message containing 3D points + **kwargs: SimpleOccupancyConfig fields - resolution, min_height, max_height, + frame_id, closing_iterations, closing_connectivity + + Returns: + OccupancyGrid with occupied/free cells + """ + cfg = SimpleOccupancyConfig(**kwargs) + points, _ = cloud.as_numpy() + points = points.astype(np.float64) # Upcast to avoid float32 rounding + + if len(points) == 0: + return OccupancyGrid( + width=1, + height=1, + resolution=cfg.resolution, + frame_id=cfg.frame_id or cloud.frame_id, + ) + + # Find bounds of the point cloud in X-Y plane + min_x = float(np.min(points[:, 0])) - 1.0 + max_x = float(np.max(points[:, 0])) + 1.0 + min_y = float(np.min(points[:, 1])) - 1.0 + max_y = float(np.max(points[:, 1])) + 1.0 + + # Calculate grid dimensions + width = int(np.ceil((max_x - min_x) / cfg.resolution)) + height = int(np.ceil((max_y - min_y) / cfg.resolution)) + + # Create origin pose (bottom-left corner of the grid) + origin = Pose() + origin.position.x = min_x + origin.position.y = min_y + origin.position.z = 0.0 + origin.orientation.w = 1.0 + + # Initialize grid (all unknown) + grid = np.full((height, width), -1, dtype=np.int8) + + # Use numba kernel for fast grid population + _simple_occupancy_kernel( + points, + grid, + min_x, + min_y, + 1.0 / cfg.resolution, + width, + height, + cfg.min_height, + cfg.max_height, + ) + + ts = cloud.ts if hasattr(cloud, "ts") and cloud.ts is not None else 0.0 + + return OccupancyGrid( + grid=grid, + resolution=cfg.resolution, + origin=origin, + frame_id=cfg.frame_id or cloud.frame_id, + ts=ts, + ) + + +# Populate algorithm registry +OCCUPANCY_ALGOS.update( + { + "height_cost": height_cost_occupancy, + "general": general_occupancy, + "simple": simple_occupancy, + } +) diff --git a/dimos/mapping/pointclouds/test_occupancy.py b/dimos/mapping/pointclouds/test_occupancy.py new file mode 100644 index 0000000000..2e301c772d --- /dev/null +++ b/dimos/mapping/pointclouds/test_occupancy.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cv2 +import numpy as np +from open3d.geometry import PointCloud +import pytest + +from dimos.core import LCMTransport +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.mapping.pointclouds.occupancy import ( + height_cost_occupancy, + simple_occupancy, +) +from dimos.mapping.pointclouds.util import read_pointcloud +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.msgs.sensor_msgs.Image import Image +from dimos.utils.data import get_data +from dimos.utils.testing.moment import OutputMoment +from dimos.utils.testing.test_moment import Go2Moment + + +@pytest.fixture +def apartment() -> PointCloud: + return read_pointcloud(get_data("apartment") / "sum.ply") + + +@pytest.fixture +def big_office() -> PointCloud: + return read_pointcloud(get_data("big_office.ply")) + + +@pytest.mark.parametrize( + "occupancy_fn,output_name", + [ + (simple_occupancy, "occupancy_simple.png"), + ], +) +def test_occupancy(apartment: PointCloud, occupancy_fn, output_name: str) -> None: + expected_image = cv2.imread(str(get_data(output_name)), cv2.IMREAD_GRAYSCALE) + cloud = PointCloud2.from_numpy(np.asarray(apartment.points), frame_id="map") + + occupancy_grid = occupancy_fn(cloud) + + # Convert grid from -1..100 to 0..101 for PNG + computed_image = (occupancy_grid.grid + 1).astype(np.uint8) + + np.testing.assert_array_equal(computed_image, expected_image) + + +@pytest.mark.parametrize( + "occupancy_fn,output_name", + [ + (height_cost_occupancy, "big_office_height_cost_occupancy.png"), + (simple_occupancy, "big_office_simple_occupancy.png"), + ], +) +def test_occupancy2(big_office, occupancy_fn, output_name): + expected_image = Image.from_file(get_data(output_name)) + cloud = PointCloud2.from_numpy(np.asarray(big_office.points), frame_id="") + + occupancy_grid = occupancy_fn(cloud) + + actual = visualize_occupancy_grid(occupancy_grid, "rainbow") + actual.ts = expected_image.ts + np.testing.assert_array_equal(actual, expected_image) + + +class HeightCostMoment(Go2Moment): + costmap: OutputMoment[OccupancyGrid] = OutputMoment(LCMTransport("/costmap", OccupancyGrid)) + + +@pytest.fixture +def height_cost_moment(): + moment = HeightCostMoment() + + def get_moment(ts: float, publish: bool = True) -> HeightCostMoment: + moment.seek(ts) + if moment.lidar.value is not None: + costmap = height_cost_occupancy( + moment.lidar.value, + resolution=0.05, + can_pass_under=0.6, + can_climb=0.15, + ) + moment.costmap.set(costmap) + if publish: + moment.publish() + return moment + + yield get_moment + + moment.stop() + + +def test_height_cost_occupancy_from_lidar(height_cost_moment) -> None: + """Test height_cost_occupancy with real lidar data.""" + moment = height_cost_moment(1.0) + + costmap = moment.costmap.value + assert costmap is not None + + # Basic sanity checks + assert costmap.grid is not None + assert costmap.width > 0 + assert costmap.height > 0 + + # Costs should be in range -1 to 100 (-1 = unknown) + assert costmap.grid.min() >= -1 + assert costmap.grid.max() <= 100 + + # Check we have some unknown, some known + known_mask = costmap.grid >= 0 + assert known_mask.sum() > 0, "Expected some known cells" + assert (~known_mask).sum() > 0, "Expected some unknown cells" diff --git a/dimos/mapping/pointclouds/test_occupancy_speed.py b/dimos/mapping/pointclouds/test_occupancy_speed.py new file mode 100644 index 0000000000..c34c2865f2 --- /dev/null +++ b/dimos/mapping/pointclouds/test_occupancy_speed.py @@ -0,0 +1,58 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +import time + +import pytest + +from dimos.mapping.pointclouds.occupancy import OCCUPANCY_ALGOS +from dimos.mapping.voxels import VoxelGridMapper +from dimos.utils.cli.plot import bar +from dimos.utils.data import _get_data_dir, get_data +from dimos.utils.testing import TimedSensorReplay + + +@pytest.mark.tool +def test_build_map(): + mapper = VoxelGridMapper(publish_interval=-1) + + for ts, frame in TimedSensorReplay("unitree_go2_bigoffice/lidar").iterate_duration(): + print(ts, frame) + mapper.add_frame(frame) + + pickle_file = _get_data_dir() / "unitree_go2_bigoffice_map.pickle" + global_pcd = mapper.get_global_pointcloud2() + + with open(pickle_file, "wb") as f: + pickle.dump(global_pcd, f) + + mapper.stop() + + +def test_costmap_calc(): + path = get_data("unitree_go2_bigoffice_map.pickle") + pointcloud = pickle.loads(path.read_bytes()) + + names = [] + times_ms = [] + for name, algo in OCCUPANCY_ALGOS.items(): + start = time.perf_counter() + result = algo(pointcloud) + elapsed = time.perf_counter() - start + names.append(name) + times_ms.append(elapsed * 1000) + print(f"{name}: {elapsed * 1000:.1f}ms - {result}") + + bar(names, times_ms, title="Occupancy Algorithm Speed", ylabel="ms") diff --git a/dimos/mapping/pointclouds/util.py b/dimos/mapping/pointclouds/util.py new file mode 100644 index 0000000000..f85b2520eb --- /dev/null +++ b/dimos/mapping/pointclouds/util.py @@ -0,0 +1,58 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Iterable +import colorsys +from pathlib import Path + +import numpy as np +import open3d as o3d # type: ignore[import-untyped] +from open3d.geometry import PointCloud # type: ignore[import-untyped] + + +def read_pointcloud(path: Path) -> PointCloud: + return o3d.io.read_point_cloud(path) + + +def sum_pointclouds(pointclouds: Iterable[PointCloud]) -> PointCloud: + it = iter(pointclouds) + ret = next(it) + for x in it: + ret += x + return ret.remove_duplicated_points() + + +def height_colorize(pointcloud: PointCloud) -> None: + points = np.asarray(pointcloud.points) + z_values = points[:, 2] + z_min = z_values.min() + z_max = z_values.max() + + z_normalized = (z_values - z_min) / (z_max - z_min) + + # Create rainbow color map. + colors = np.array([colorsys.hsv_to_rgb(0.7 * (1 - h), 1.0, 1.0) for h in z_normalized]) + + pointcloud.colors = o3d.utility.Vector3dVector(colors) + + +def visualize(pointcloud: PointCloud) -> None: + voxel_size = 0.05 # 0.05m voxels + voxel_grid = o3d.geometry.VoxelGrid.create_from_point_cloud(pointcloud, voxel_size=voxel_size) + o3d.visualization.draw_geometries( + [voxel_grid], + window_name="Combined Point Clouds (Voxelized)", + width=1024, + height=768, + ) diff --git a/dimos/mapping/test_voxels.py b/dimos/mapping/test_voxels.py new file mode 100644 index 0000000000..8fdb1f2827 --- /dev/null +++ b/dimos/mapping/test_voxels.py @@ -0,0 +1,207 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable, Generator +import time + +import numpy as np +import pytest + +from dimos.core import LCMTransport +from dimos.mapping.voxels import VoxelGridMapper +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.utils.data import get_data +from dimos.utils.testing.moment import OutputMoment +from dimos.utils.testing.replay import TimedSensorReplay +from dimos.utils.testing.test_moment import Go2Moment + + +@pytest.fixture +def mapper() -> Generator[VoxelGridMapper, None, None]: + mapper = VoxelGridMapper() + yield mapper + mapper.stop() + + +class Go2MapperMoment(Go2Moment): + global_map: OutputMoment[PointCloud2] = OutputMoment(LCMTransport("/global_map", PointCloud2)) + + +MomentFactory = Callable[[float, bool], Go2MapperMoment] + + +@pytest.fixture +def moment() -> Generator[MomentFactory, None, None]: + instances: list[Go2MapperMoment] = [] + + def get_moment(ts: float, publish: bool = True) -> Go2MapperMoment: + m = Go2MapperMoment() + m.seek(ts) + if publish: + m.publish() + instances.append(m) + return m + + yield get_moment + for m in instances: + m.stop() + + +@pytest.fixture +def moment1(moment: MomentFactory) -> Go2MapperMoment: + return moment(10, False) + + +@pytest.fixture +def moment2(moment: MomentFactory) -> Go2MapperMoment: + return moment(85, False) + + +@pytest.mark.tool +def two_perspectives_loop(moment: MomentFactory) -> None: + while True: + moment(10, True) + time.sleep(1) + moment(85, True) + time.sleep(1) + + +def test_carving( + mapper: VoxelGridMapper, moment1: Go2MapperMoment, moment2: Go2MapperMoment +) -> None: + lidar_frame1 = moment1.lidar.value + assert lidar_frame1 is not None + lidar_frame1_transport: LCMTransport[PointCloud2] = LCMTransport("/prev_lidar", PointCloud2) + lidar_frame1_transport.publish(lidar_frame1) + lidar_frame1_transport.stop() + + lidar_frame2 = moment2.lidar.value + assert lidar_frame2 is not None + + # Debug: check XY overlap + pts1 = np.asarray(lidar_frame1.pointcloud.points) + pts2 = np.asarray(lidar_frame2.pointcloud.points) + + voxel_size = mapper.config.voxel_size + xy1 = set(map(tuple, (pts1[:, :2] / voxel_size).astype(int))) + xy2 = set(map(tuple, (pts2[:, :2] / voxel_size).astype(int))) + + overlap = xy1 & xy2 + print(f"\nFrame1 XY columns: {len(xy1)}") + print(f"Frame2 XY columns: {len(xy2)}") + print(f"Overlapping XY columns: {len(overlap)}") + + # Carving mapper (default, carve_columns=True) + mapper.add_frame(lidar_frame1) + mapper.add_frame(lidar_frame2) + + moment2.global_map.set(mapper.get_global_pointcloud2()) + moment2.publish() + + count_carving = mapper.size() + # Additive mapper (carve_columns=False) + additive_mapper = VoxelGridMapper(carve_columns=False) + additive_mapper.add_frame(lidar_frame1) + additive_mapper.add_frame(lidar_frame2) + count_additive = additive_mapper.size() + + print("\n=== Carving comparison ===") + print(f"Additive (no carving): {count_additive}") + print(f"With carving: {count_carving}") + print(f"Voxels carved: {count_additive - count_carving}") + + # Carving should result in fewer voxels + assert count_carving < count_additive, ( + f"Carving should remove some voxels. Additive: {count_additive}, Carving: {count_carving}" + ) + + additive_global_map: LCMTransport[PointCloud2] = LCMTransport( + "additive_global_map", PointCloud2 + ) + additive_global_map.publish(additive_mapper.get_global_pointcloud2()) + additive_global_map.stop() + additive_mapper.stop() + + +def test_injest_a_few(mapper: VoxelGridMapper) -> None: + data_dir = get_data("unitree_go2_office_walk2") + lidar_store = TimedSensorReplay(f"{data_dir}/lidar") + + for i in [1, 4, 8]: + frame = lidar_store.find_closest_seek(i) + assert frame is not None + print("add", frame) + mapper.add_frame(frame) + + assert len(mapper.get_global_pointcloud2()) == 30136 + + +@pytest.mark.parametrize( + "voxel_size, expected_points", + [ + (0.5, 277), + (0.1, 7290), + (0.05, 28199), + ], +) +def test_roundtrip(moment1: Go2MapperMoment, voxel_size: float, expected_points: int) -> None: + lidar_frame = moment1.lidar.value + assert lidar_frame is not None + + mapper = VoxelGridMapper(voxel_size=voxel_size) + mapper.add_frame(lidar_frame) + + global1 = mapper.get_global_pointcloud2() + assert len(global1) == expected_points + + # loseless roundtrip + if voxel_size == 0.05: + assert len(global1) == len(lidar_frame) + # TODO: we want __eq__ on PointCloud2 - should actually compare + # all points in both frames + + mapper.add_frame(global1) + # no new information, no global map change + assert len(mapper.get_global_pointcloud2()) == len(global1) + + moment1.publish() + mapper.stop() + + +def test_roundtrip_range_preserved(mapper: VoxelGridMapper) -> None: + """Test that input coordinate ranges are preserved in output.""" + data_dir = get_data("unitree_go2_office_walk2") + lidar_store = TimedSensorReplay(f"{data_dir}/lidar") + + frame = lidar_store.find_closest_seek(1.0) + assert frame is not None + input_pts = np.asarray(frame.pointcloud.points) + + mapper.add_frame(frame) + + out_pcd = mapper.get_global_pointcloud().to_legacy() + out_pts = np.asarray(out_pcd.points) + + voxel_size = mapper.config.voxel_size + tolerance = voxel_size # Allow one voxel of difference at boundaries + + # TODO: we want __eq__ on PointCloud2 - should actually compare + # all points in both frames + + for axis, name in enumerate(["X", "Y", "Z"]): + in_min, in_max = input_pts[:, axis].min(), input_pts[:, axis].max() + out_min, out_max = out_pts[:, axis].min(), out_pts[:, axis].max() + + assert abs(in_min - out_min) < tolerance, f"{name} min mismatch: in={in_min}, out={out_min}" + assert abs(in_max - out_max) < tolerance, f"{name} max mismatch: in={in_max}, out={out_max}" diff --git a/dimos/mapping/types.py b/dimos/mapping/types.py index 9c39522011..9584e8e8ba 100644 --- a/dimos/mapping/types.py +++ b/dimos/mapping/types.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/mapping/utils/distance.py b/dimos/mapping/utils/distance.py index 7e19fec9ab..6e8c48c205 100644 --- a/dimos/mapping/utils/distance.py +++ b/dimos/mapping/utils/distance.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/mapping/voxels.py b/dimos/mapping/voxels.py new file mode 100644 index 0000000000..a36dc9bc17 --- /dev/null +++ b/dimos/mapping/voxels.py @@ -0,0 +1,345 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +import queue +import threading +import time + +import numpy as np +import open3d as o3d # type: ignore[import-untyped] +import open3d.core as o3c # type: ignore[import-untyped] +from reactivex import interval, operators as ops +from reactivex.disposable import Disposable +from reactivex.subject import Subject +import rerun as rr +import rerun.blueprint as rrb + +from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig +from dimos.core.module import ModuleConfig +from dimos.dashboard.rerun_init import connect_rerun +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.utils.decorators import simple_mcache +from dimos.utils.logging_config import setup_logger +from dimos.utils.reactive import backpressure + +logger = setup_logger() + + +@dataclass +class Config(ModuleConfig): + frame_id: str = "world" + # -1 never publishes, 0 publishes on every frame, >0 publishes at interval in seconds + publish_interval: float = 0 + voxel_size: float = 0.05 + block_count: int = 2_000_000 + device: str = "CUDA:0" + carve_columns: bool = True + + +class VoxelGridMapper(Module): + default_config = Config + config: Config + + lidar: In[LidarMessage] + global_map: Out[PointCloud2] + + @classmethod + def rerun_views(cls): # type: ignore[no-untyped-def] + """Return Rerun view blueprints for voxel map visualization.""" + return [ + rrb.TimeSeriesView( + name="Voxel Pipeline (ms)", + origin="/metrics/voxel_map", + contents=[ + "+ /metrics/voxel_map/extract_ms", + "+ /metrics/voxel_map/transport_ms", + "+ /metrics/voxel_map/publish_ms", + ], + ), + rrb.TimeSeriesView( + name="Voxel Count", + origin="/metrics/voxel_map", + contents=["+ /metrics/voxel_map/voxel_count"], + ), + ] + + def __init__(self, global_config: GlobalConfig | None = None, **kwargs: object) -> None: + super().__init__(**kwargs) + self._global_config = global_config or GlobalConfig() + + dev = ( + o3c.Device(self.config.device) + if (self.config.device.startswith("CUDA") and o3c.cuda.is_available()) + else o3c.Device("CPU:0") + ) + + print(f"VoxelGridMapper using device: {dev}") + + self.vbg = o3d.t.geometry.VoxelBlockGrid( + attr_names=("dummy",), + attr_dtypes=(o3c.uint8,), + attr_channels=(o3c.SizeVector([1]),), + voxel_size=self.config.voxel_size, + block_resolution=1, + block_count=self.config.block_count, + device=dev, + ) + + self._dev = dev + self._voxel_hashmap = self.vbg.hashmap() + self._key_dtype = self._voxel_hashmap.key_tensor().dtype + self._latest_frame_ts: float = 0.0 + # Monotonic timestamp of last received frame (for accurate latency in replay) + self._latest_frame_rx_monotonic: float | None = None + + # Background Rerun logging (decouples viz from data pipeline) + self._rerun_queue: queue.Queue[PointCloud2 | None] = queue.Queue(maxsize=2) + self._rerun_thread: threading.Thread | None = None + + def _rerun_worker(self) -> None: + """Background thread: pull from queue and log to Rerun (non-blocking).""" + while True: + try: + pc = self._rerun_queue.get(timeout=1.0) + if pc is None: # Shutdown signal + break + + # Log to Rerun (blocks in background, doesn't affect data pipeline) + try: + rr.log( + "world/map", + pc.to_rerun( + mode="boxes", + size=self.config.voxel_size, + colormap="turbo", + ), + ) + except Exception as e: + logger.warning(f"Rerun logging error: {e}") + except queue.Empty: + continue + + @rpc + def start(self) -> None: + super().start() + + # Only start Rerun logging if Rerun backend is selected + if self._global_config.viewer_backend.startswith("rerun"): + connect_rerun(global_config=self._global_config) + + # Start background Rerun logging thread (decouples viz from data pipeline) + self._rerun_thread = threading.Thread(target=self._rerun_worker, daemon=True) + self._rerun_thread.start() + logger.info("VoxelGridMapper: started async Rerun logging thread") + + # Subject to trigger publishing, with backpressure to drop if busy + self._publish_trigger: Subject[None] = Subject() + self._disposables.add( + backpressure(self._publish_trigger) + .pipe(ops.map(lambda _: self.publish_global_map())) + .subscribe() + ) + + lidar_unsub = self.lidar.subscribe(self._on_frame) + self._disposables.add(Disposable(lidar_unsub)) + + # If publish_interval > 0, publish on timer; otherwise publish on each frame + if self.config.publish_interval > 0: + self._disposables.add( + interval(self.config.publish_interval).subscribe( + lambda _: self._publish_trigger.on_next(None) + ) + ) + + @rpc + def stop(self) -> None: + # Shutdown background Rerun thread + if self._rerun_thread and self._rerun_thread.is_alive(): + self._rerun_queue.put(None) # Shutdown signal + self._rerun_thread.join(timeout=2.0) + + super().stop() + + def _on_frame(self, frame: LidarMessage) -> None: + # Track receipt time with monotonic clock (works correctly in replay) + self._latest_frame_rx_monotonic = time.monotonic() + self.add_frame(frame) + if self.config.publish_interval == 0: + self._publish_trigger.on_next(None) + + def publish_global_map(self) -> None: + # Snapshot monotonic timestamp once (won't be overwritten during slow publish) + rx_monotonic = self._latest_frame_rx_monotonic + + start_total = time.perf_counter() + + # 1. Extract pointcloud from GPU hashmap + t1 = time.perf_counter() + pc = self.get_global_pointcloud2() + extract_ms = (time.perf_counter() - t1) * 1000 + + # 2. Publish to downstream (NO auto-logging - fast!) + t2 = time.perf_counter() + self.global_map.publish(pc) + publish_ms = (time.perf_counter() - t2) * 1000 + + # 3. Queue for async Rerun logging (non-blocking, drops if queue full) + try: + self._rerun_queue.put_nowait(pc) + except queue.Full: + pass # Drop viz frame, data pipeline continues + + # Log detailed timing breakdown to Rerun + total_ms = (time.perf_counter() - start_total) * 1000 + rr.log("metrics/voxel_map/publish_ms", rr.Scalars(total_ms)) + rr.log("metrics/voxel_map/extract_ms", rr.Scalars(extract_ms)) + rr.log("metrics/voxel_map/transport_ms", rr.Scalars(publish_ms)) + rr.log("metrics/voxel_map/voxel_count", rr.Scalars(float(len(pc)))) + + # Log pipeline latency (time from frame receipt to publish complete) + if rx_monotonic is not None: + latency_ms = (time.monotonic() - rx_monotonic) * 1000 + rr.log("metrics/voxel_map/latency_ms", rr.Scalars(latency_ms)) + + def size(self) -> int: + return self._voxel_hashmap.size() # type: ignore[no-any-return] + + def __len__(self) -> int: + return self.size() + + # @timed() # TODO: fix thread leak in timed decorator + def add_frame(self, frame: PointCloud2) -> None: + # Track latest frame timestamp for proper latency measurement + if hasattr(frame, "ts") and frame.ts: + self._latest_frame_ts = frame.ts + + # we are potentially moving into CUDA here + pcd = ensure_tensor_pcd(frame.pointcloud, self._dev) + + if pcd.is_empty(): + return + + pts = pcd.point["positions"].to(self._dev, o3c.float32) + vox = (pts / self.config.voxel_size).floor().to(self._key_dtype) + keys_Nx3 = vox.contiguous() + + if self.config.carve_columns: + self._carve_and_insert(keys_Nx3) + else: + self._voxel_hashmap.activate(keys_Nx3) + + self.get_global_pointcloud.invalidate_cache(self) # type: ignore[attr-defined] + self.get_global_pointcloud2.invalidate_cache(self) # type: ignore[attr-defined] + + def _carve_and_insert(self, new_keys: o3c.Tensor) -> None: + """Column carving: remove all existing voxels sharing (X,Y) with new_keys, then insert.""" + if new_keys.shape[0] == 0: + self._voxel_hashmap.activate(new_keys) + return + + # Extract (X, Y) from incoming keys + xy_keys = new_keys[:, :2].contiguous() + + # Build temp hashmap for O(1) (X,Y) membership lookup + xy_hashmap = o3c.HashMap( + init_capacity=xy_keys.shape[0], + key_dtype=self._key_dtype, + key_element_shape=o3c.SizeVector([2]), + value_dtypes=[o3c.uint8], + value_element_shapes=[o3c.SizeVector([1])], + device=self._dev, + ) + dummy_vals = o3c.Tensor.zeros((xy_keys.shape[0], 1), o3c.uint8, self._dev) + xy_hashmap.insert(xy_keys, dummy_vals) + + # Get existing keys from main hashmap + active_indices = self._voxel_hashmap.active_buf_indices() + if active_indices.shape[0] == 0: + self._voxel_hashmap.activate(new_keys) + return + + existing_keys = self._voxel_hashmap.key_tensor()[active_indices] + existing_xy = existing_keys[:, :2].contiguous() + + # Find which existing keys have (X,Y) in the incoming set + _, found_mask = xy_hashmap.find(existing_xy) + + # Erase those columns + to_erase = existing_keys[found_mask] + if to_erase.shape[0] > 0: + self._voxel_hashmap.erase(to_erase) + + # Insert new keys + self._voxel_hashmap.activate(new_keys) + + # returns PointCloud2 message (ready to send off down the pipeline) + @simple_mcache + def get_global_pointcloud2(self) -> PointCloud2: + return PointCloud2( + # we are potentially moving out of CUDA here + ensure_legacy_pcd(self.get_global_pointcloud()), + frame_id=self.frame_id, + ts=self._latest_frame_ts if self._latest_frame_ts else time.time(), + ) + + @simple_mcache + # @timed() + def get_global_pointcloud(self) -> o3d.t.geometry.PointCloud: + voxel_coords, _ = self.vbg.voxel_coordinates_and_flattened_indices() + pts = voxel_coords + (self.config.voxel_size * 0.5) + out = o3d.t.geometry.PointCloud(device=self._dev) + out.point["positions"] = pts + return out + + +def ensure_tensor_pcd( + pcd_any: o3d.t.geometry.PointCloud | o3d.geometry.PointCloud, + device: o3c.Device, +) -> o3d.t.geometry.PointCloud: + """Convert legacy / cuda.pybind point clouds into o3d.t.geometry.PointCloud on `device`.""" + + if isinstance(pcd_any, o3d.t.geometry.PointCloud): + return pcd_any.to(device) + + assert isinstance(pcd_any, o3d.geometry.PointCloud), ( + "Input must be a legacy PointCloud or a tensor PointCloud" + ) + + # Legacy CPU point cloud -> tensor + if isinstance(pcd_any, o3d.geometry.PointCloud): + return o3d.t.geometry.PointCloud.from_legacy(pcd_any, o3c.float32, device) + + pts = np.asarray(pcd_any.points, dtype=np.float32) + pcd_t = o3d.t.geometry.PointCloud(device=device) + pcd_t.point["positions"] = o3c.Tensor(pts, o3c.float32, device) + return pcd_t + + +def ensure_legacy_pcd( + pcd_any: o3d.t.geometry.PointCloud | o3d.geometry.PointCloud, +) -> o3d.geometry.PointCloud: + if isinstance(pcd_any, o3d.geometry.PointCloud): + return pcd_any + + assert isinstance(pcd_any, o3d.t.geometry.PointCloud), ( + "Input must be a legacy PointCloud or a tensor PointCloud" + ) + + return pcd_any.to_legacy() + + +voxel_mapper = VoxelGridMapper.blueprint diff --git a/dimos/models/Detic/.gitignore b/dimos/models/Detic/.gitignore deleted file mode 100644 index b794d988fb..0000000000 --- a/dimos/models/Detic/.gitignore +++ /dev/null @@ -1,62 +0,0 @@ -third_party/detectron2 -./models -configs-experimental -experiments -# output dir -index.html -data/* -slurm/ -slurm -slurm-output -slurm-output/ -output -instant_test_output -inference_test_output - - -*.png -*.diff -*.jpg -!/projects/DensePose/doc/images/*.jpg - -# compilation and distribution -__pycache__ -_ext -*.pyc -*.pyd -*.so -*.dll -*.egg-info/ -build/ -dist/ -wheels/ - -# pytorch/python/numpy formats -*.pth -*.pkl -*.ts -model_ts*.txt - -# ipython/jupyter notebooks -*.ipynb -**/.ipynb_checkpoints/ - -# Editor temporaries -*.swn -*.swo -*.swp -*~ - -# editor settings -.idea -.vscode -_darcs - -# project dirs -/detectron2/model_zoo/configs -/datasets/* -!/datasets/*.* -!/datasets/metadata -/projects/*/datasets -/models -/snippet diff --git a/dimos/models/Detic/.gitmodules b/dimos/models/Detic/.gitmodules deleted file mode 100644 index d945b4731e..0000000000 --- a/dimos/models/Detic/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "third_party/Deformable-DETR"] - path = third_party/Deformable-DETR - url = https://github.com/fundamentalvision/Deformable-DETR.git -[submodule "third_party/CenterNet2"] - path = third_party/CenterNet2 - url = https://github.com/xingyizhou/CenterNet2.git diff --git a/dimos/models/Detic/CODE_OF_CONDUCT.md b/dimos/models/Detic/CODE_OF_CONDUCT.md deleted file mode 100644 index 0f7ad8bfc1..0000000000 --- a/dimos/models/Detic/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,5 +0,0 @@ -# Code of Conduct - -Facebook has adopted a Code of Conduct that we expect project participants to adhere to. -Please read the [full text](https://code.fb.com/codeofconduct/) -so that you can understand what actions will and will not be tolerated. diff --git a/dimos/models/Detic/CONTRIBUTING.md b/dimos/models/Detic/CONTRIBUTING.md deleted file mode 100644 index 282a20270b..0000000000 --- a/dimos/models/Detic/CONTRIBUTING.md +++ /dev/null @@ -1,39 +0,0 @@ -# Contributing to Detic -We want to make contributing to this project as easy and transparent as -possible. - -## Our Development Process -Minor changes and improvements will be released on an ongoing basis. Larger changes (e.g., changesets implementing a new paper) will be released on a more periodic basis. - -## Pull Requests -We actively welcome your pull requests. - -1. Fork the repo and create your branch from `main`. -2. If you've added code that should be tested, add tests. -3. If you've changed APIs, update the documentation. -4. Ensure the test suite passes. -5. Make sure your code lints. -6. If you haven't already, complete the Contributor License Agreement ("CLA"). - -## Contributor License Agreement ("CLA") -In order to accept your pull request, we need you to submit a CLA. You only need -to do this once to work on any of Facebook's open source projects. - -Complete your CLA here: - -## Issues -We use GitHub issues to track public bugs. Please ensure your description is -clear and has sufficient instructions to be able to reproduce the issue. - -Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe -disclosure of security bugs. In those cases, please go through the process -outlined on that page and do not file a public issue. - -## Coding Style -* 4 spaces for indentation rather than tabs -* 80 character line length -* PEP8 formatting following [Black](https://black.readthedocs.io/en/stable/) - -## License -By contributing to Detic, you agree that your contributions will be licensed -under the LICENSE file in the root directory of this source tree. diff --git a/dimos/models/Detic/LICENSE b/dimos/models/Detic/LICENSE deleted file mode 100644 index cd1b070674..0000000000 --- a/dimos/models/Detic/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "[]" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/dimos/models/Detic/README.md b/dimos/models/Detic/README.md deleted file mode 100644 index 3a1285cbc9..0000000000 --- a/dimos/models/Detic/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# Detecting Twenty-thousand Classes using Image-level Supervision - -**Detic**: A **Det**ector with **i**mage **c**lasses that can use image-level labels to easily train detectors. - -

- -> [**Detecting Twenty-thousand Classes using Image-level Supervision**](http://arxiv.org/abs/2201.02605), -> Xingyi Zhou, Rohit Girdhar, Armand Joulin, Philipp Krähenbühl, Ishan Misra, -> *ECCV 2022 ([arXiv 2201.02605](http://arxiv.org/abs/2201.02605))* - - -## Features - -- Detects **any** class given class names (using [CLIP](https://github.com/openai/CLIP)). - -- We train the detector on ImageNet-21K dataset with 21K classes. - -- Cross-dataset generalization to OpenImages and Objects365 **without finetuning**. - -- State-of-the-art results on Open-vocabulary LVIS and Open-vocabulary COCO. - -- Works for DETR-style detectors. - - -## Installation - -See [installation instructions](docs/INSTALL.md). - -## Demo - -**Update April 2022**: we released more real-time models [here](docs/MODEL_ZOO.md#real-time-models). - -Replicate web demo and docker image: [![Replicate](https://replicate.com/facebookresearch/detic/badge)](https://replicate.com/facebookresearch/detic) - - -Integrated into [Huggingface Spaces šŸ¤—](https://huggingface.co/spaces) using [Gradio](https://github.com/gradio-app/gradio). Try out the web demo: [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/akhaliq/Detic) - -Run our demo using Colab (no GPU needed): [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1QtTW9-ukX2HKZGvt0QvVGqjuqEykoZKI) - -We use the default detectron2 [demo interface](https://github.com/facebookresearch/detectron2/blob/main/GETTING_STARTED.md). -For example, to run our [21K model](docs/MODEL_ZOO.md#cross-dataset-evaluation) on a [messy desk image](https://web.eecs.umich.edu/~fouhey/fun/desk/desk.jpg) (image credit [David Fouhey](https://web.eecs.umich.edu/~fouhey)) with the lvis vocabulary, run - -~~~ -mkdir models -wget https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth -O models/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth -wget https://eecs.engin.umich.edu/~fouhey/fun/desk/desk.jpg -python demo.py --config-file configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml --input desk.jpg --output out.jpg --vocabulary lvis --opts MODEL.WEIGHTS models/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth -~~~ - -If setup correctly, the output should look like: - -

- -The same model can run with other vocabularies (COCO, OpenImages, or Objects365), or a **custom vocabulary**. For example: - -~~~ -python demo.py --config-file configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml --input desk.jpg --output out2.jpg --vocabulary custom --custom_vocabulary headphone,webcam,paper,coffe --confidence-threshold 0.3 --opts MODEL.WEIGHTS models/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth -~~~ - -The output should look like: - -

- -Note that `headphone`, `paper` and `coffe` (typo intended) are **not** LVIS classes. Despite the misspelled class name, our detector can produce a reasonable detection for `coffe`. - -## Benchmark evaluation and training - -Please first [prepare datasets](datasets/README.md), then check our [MODEL ZOO](docs/MODEL_ZOO.md) to reproduce results in our paper. We highlight key results below: - -- Open-vocabulary LVIS - - | | mask mAP | mask mAP_novel | - |-----------------------|-----------|-----------------| - |Box-Supervised | 30.2 | 16.4 | - |Detic | 32.4 | 24.9 | - -- Standard LVIS - - | | Detector/ Backbone | mask mAP | mask mAP_rare | - |-----------------------|----------|-----------|-----------------| - |Box-Supervised | CenterNet2-ResNet50 | 31.5 | 25.6 | - |Detic | CenterNet2-ResNet50 | 33.2 | 29.7 | - |Box-Supervised | CenterNet2-SwinB | 40.7 | 35.9 | - |Detic | CenterNet2-SwinB | 41.7 | 41.7 | - - | | Detector/ Backbone | box mAP | box mAP_rare | - |-----------------------|----------|-----------|-----------------| - |Box-Supervised | DeformableDETR-ResNet50 | 31.7 | 21.4 | - |Detic | DeformableDETR-ResNet50 | 32.5 | 26.2 | - -- Cross-dataset generalization - - | | Backbone | Objects365 box mAP | OpenImages box mAP50 | - |-----------------------|----------|-----------|-----------------| - |Box-Supervised | SwinB | 19.1 | 46.2 | - |Detic | SwinB | 21.4 | 55.2 | - - -## License - -The majority of Detic is licensed under the [Apache 2.0 license](LICENSE), however portions of the project are available under separate license terms: SWIN-Transformer, CLIP, and TensorFlow Object Detection API are licensed under the MIT license; UniDet is licensed under the Apache 2.0 license; and the LVIS API is licensed under a [custom license](https://github.com/lvis-dataset/lvis-api/blob/master/LICENSE). If you later add other third party code, please keep this license info updated, and please let us know if that component is licensed under something other than CC-BY-NC, MIT, or CC0 - -## Ethical Considerations -Detic's wide range of detection capabilities may introduce similar challenges to many other visual recognition and open-set recognition methods. -As the user can define arbitrary detection classes, class design and semantics may impact the model output. - -## Citation - -If you find this project useful for your research, please use the following BibTeX entry. - - @inproceedings{zhou2022detecting, - title={Detecting Twenty-thousand Classes using Image-level Supervision}, - author={Zhou, Xingyi and Girdhar, Rohit and Joulin, Armand and Kr{\"a}henb{\"u}hl, Philipp and Misra, Ishan}, - booktitle={ECCV}, - year={2022} - } diff --git a/dimos/models/Detic/cog.yaml b/dimos/models/Detic/cog.yaml deleted file mode 100644 index 3c8a94941e..0000000000 --- a/dimos/models/Detic/cog.yaml +++ /dev/null @@ -1,28 +0,0 @@ -build: - gpu: true - cuda: "10.1" - python_version: "3.8" - system_packages: - - "libgl1-mesa-glx" - - "libglib2.0-0" - python_packages: - - "ipython==7.30.1" - - "numpy==1.21.4" - - "torch==1.8.1" - - "torchvision==0.9.1" - - "dataclasses==0.6" - - "opencv-python==4.5.5.62" - - "imageio==2.9.0" - - "ftfy==6.0.3" - - "regex==2021.10.8" - - "tqdm==4.62.3" - - "timm==0.4.12" - - "fasttext==0.9.2" - - "scikit-learn==1.0.2" - - "lvis==0.5.3" - - "nltk==3.6.7" - - "git+https://github.com/openai/CLIP.git" - run: - - pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu101/torch1.8/index.html - -predict: "predict.py:Predictor" diff --git a/dimos/models/Detic/configs/Base-C2_L_R5021k_640b64_4x.yaml b/dimos/models/Detic/configs/Base-C2_L_R5021k_640b64_4x.yaml deleted file mode 100644 index eb3c3c0f3b..0000000000 --- a/dimos/models/Detic/configs/Base-C2_L_R5021k_640b64_4x.yaml +++ /dev/null @@ -1,82 +0,0 @@ -MODEL: - META_ARCHITECTURE: "CustomRCNN" - MASK_ON: True - PROPOSAL_GENERATOR: - NAME: "CenterNet" - WEIGHTS: "models/resnet50_miil_21k.pkl" - BACKBONE: - NAME: build_p67_timm_fpn_backbone - TIMM: - BASE_NAME: resnet50_in21k - FPN: - IN_FEATURES: ["layer3", "layer4", "layer5"] - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.12, 57.375] - ROI_HEADS: - NAME: DeticCascadeROIHeads - IN_FEATURES: ["p3", "p4", "p5"] - IOU_THRESHOLDS: [0.6] - NUM_CLASSES: 1203 - SCORE_THRESH_TEST: 0.02 - NMS_THRESH_TEST: 0.5 - ROI_BOX_CASCADE_HEAD: - IOUS: [0.6, 0.7, 0.8] - ROI_BOX_HEAD: - NAME: "FastRCNNConvFCHead" - NUM_FC: 2 - POOLER_RESOLUTION: 7 - CLS_AGNOSTIC_BBOX_REG: True - MULT_PROPOSAL_SCORE: True - - USE_SIGMOID_CE: True - USE_FED_LOSS: True - ROI_MASK_HEAD: - NAME: "MaskRCNNConvUpsampleHead" - NUM_CONV: 4 - POOLER_RESOLUTION: 14 - CLS_AGNOSTIC_MASK: True - CENTERNET: - NUM_CLASSES: 1203 - REG_WEIGHT: 1. - NOT_NORM_REG: True - ONLY_PROPOSAL: True - WITH_AGN_HM: True - INFERENCE_TH: 0.0001 - PRE_NMS_TOPK_TRAIN: 4000 - POST_NMS_TOPK_TRAIN: 2000 - PRE_NMS_TOPK_TEST: 1000 - POST_NMS_TOPK_TEST: 256 - NMS_TH_TRAIN: 0.9 - NMS_TH_TEST: 0.9 - POS_WEIGHT: 0.5 - NEG_WEIGHT: 0.5 - IGNORE_HIGH_FP: 0.85 -DATASETS: - TRAIN: ("lvis_v1_train",) - TEST: ("lvis_v1_val",) -DATALOADER: - SAMPLER_TRAIN: "RepeatFactorTrainingSampler" - REPEAT_THRESHOLD: 0.001 - NUM_WORKERS: 8 -TEST: - DETECTIONS_PER_IMAGE: 300 -SOLVER: - LR_SCHEDULER_NAME: "WarmupCosineLR" - CHECKPOINT_PERIOD: 1000000000 - WARMUP_ITERS: 10000 - WARMUP_FACTOR: 0.0001 - USE_CUSTOM_SOLVER: True - OPTIMIZER: "ADAMW" - MAX_ITER: 90000 - IMS_PER_BATCH: 64 - BASE_LR: 0.0002 - CLIP_GRADIENTS: - ENABLED: True -INPUT: - FORMAT: RGB - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 640 -OUTPUT_DIR: "./output/Detic/auto" -EVAL_PROPOSAL_AR: False -VERSION: 2 -FP16: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Base-DeformDETR_L_R50_4x.yaml b/dimos/models/Detic/configs/Base-DeformDETR_L_R50_4x.yaml deleted file mode 100644 index a689ee5bf3..0000000000 --- a/dimos/models/Detic/configs/Base-DeformDETR_L_R50_4x.yaml +++ /dev/null @@ -1,59 +0,0 @@ -MODEL: - META_ARCHITECTURE: "DeformableDetr" - WEIGHTS: "detectron2://ImageNetPretrained/torchvision/R-50.pkl" - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.120, 57.375] - MASK_ON: False - RESNETS: - DEPTH: 50 - STRIDE_IN_1X1: False - OUT_FEATURES: ["res3", "res4", "res5"] - DETR: - CLS_WEIGHT: 2.0 - GIOU_WEIGHT: 2.0 - L1_WEIGHT: 5.0 - NUM_OBJECT_QUERIES: 300 - DIM_FEEDFORWARD: 1024 - WITH_BOX_REFINE: True - TWO_STAGE: True - NUM_CLASSES: 1203 - USE_FED_LOSS: True -DATASETS: - TRAIN: ("lvis_v1_train",) - TEST: ("lvis_v1_val",) -SOLVER: - CHECKPOINT_PERIOD: 10000000 - USE_CUSTOM_SOLVER: True - IMS_PER_BATCH: 32 - BASE_LR: 0.0002 - STEPS: (150000,) - MAX_ITER: 180000 - WARMUP_FACTOR: 1.0 - WARMUP_ITERS: 10 - WEIGHT_DECAY: 0.0001 - OPTIMIZER: "ADAMW" - BACKBONE_MULTIPLIER: 0.1 - CLIP_GRADIENTS: - ENABLED: True - CLIP_TYPE: "full_model" - CLIP_VALUE: 0.01 - NORM_TYPE: 2.0 - CUSTOM_MULTIPLIER: 0.1 - CUSTOM_MULTIPLIER_NAME: ['reference_points', 'sampling_offsets'] -INPUT: - FORMAT: "RGB" - MIN_SIZE_TRAIN: (480, 512, 544, 576, 608, 640, 672, 704, 736, 768, 800) - CROP: - ENABLED: True - TYPE: "absolute_range" - SIZE: (384, 600) - CUSTOM_AUG: "DETR" -TEST: - DETECTIONS_PER_IMAGE: 300 -DATALOADER: - FILTER_EMPTY_ANNOTATIONS: False - NUM_WORKERS: 4 - SAMPLER_TRAIN: "RepeatFactorTrainingSampler" - REPEAT_THRESHOLD: 0.001 -OUTPUT_DIR: "output/Detic/auto" -VERSION: 2 \ No newline at end of file diff --git a/dimos/models/Detic/configs/Base_OVCOCO_C4_1x.yaml b/dimos/models/Detic/configs/Base_OVCOCO_C4_1x.yaml deleted file mode 100644 index 189d03cf58..0000000000 --- a/dimos/models/Detic/configs/Base_OVCOCO_C4_1x.yaml +++ /dev/null @@ -1,31 +0,0 @@ -MODEL: - META_ARCHITECTURE: "CustomRCNN" - RPN: - PRE_NMS_TOPK_TEST: 6000 - POST_NMS_TOPK_TEST: 1000 - ROI_HEADS: - NAME: "CustomRes5ROIHeads" - WEIGHTS: "detectron2://ImageNetPretrained/MSRA/R-50.pkl" - RESNETS: - DEPTH: 50 - ROI_BOX_HEAD: - CLS_AGNOSTIC_BBOX_REG: True - USE_SIGMOID_CE: True - USE_ZEROSHOT_CLS: True - ZEROSHOT_WEIGHT_PATH: 'datasets/metadata/coco_clip_a+cname.npy' - IGNORE_ZERO_CATS: True - CAT_FREQ_PATH: 'datasets/coco/zero-shot/instances_train2017_seen_2_oriorder_cat_info.json' -DATASETS: - TRAIN: ("coco_zeroshot_train_oriorder",) - TEST: ("coco_generalized_zeroshot_val",) -SOLVER: - IMS_PER_BATCH: 16 - BASE_LR: 0.02 - STEPS: (60000, 80000) - MAX_ITER: 90000 - CHECKPOINT_PERIOD: 1000000000 -INPUT: - MIN_SIZE_TRAIN: (800,) -VERSION: 2 -OUTPUT_DIR: output/Detic-COCO/auto -FP16: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_CXT21k_640b32_4x.yaml b/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_CXT21k_640b32_4x.yaml deleted file mode 100644 index 7064a02100..0000000000 --- a/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_CXT21k_640b32_4x.yaml +++ /dev/null @@ -1,17 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - WEIGHTS: '' - TIMM: - BASE_NAME: convnext_tiny_21k - OUT_LEVELS: [2, 3, 4] - PRETRAINED: True - FPN: - IN_FEATURES: ["layer2", "layer3", "layer4"] -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 -DATASETS: - TRAIN: ("lvis_v1_train+coco",) \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_R18_640b32_4x.yaml b/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_R18_640b32_4x.yaml deleted file mode 100644 index 07535ee960..0000000000 --- a/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_R18_640b32_4x.yaml +++ /dev/null @@ -1,14 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - WEIGHTS: '' - TIMM: - BASE_NAME: resnet18 - PRETRAINED: True -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 -DATASETS: - TRAIN: ("lvis_v1_train+coco",) \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_R5021k_640b64_4x.yaml b/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_R5021k_640b64_4x.yaml deleted file mode 100644 index 8b5ae72d95..0000000000 --- a/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_R5021k_640b64_4x.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True -DATASETS: - TRAIN: ("lvis_v1_train+coco",) \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_SwinB_896b32_4x.yaml b/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_SwinB_896b32_4x.yaml deleted file mode 100644 index 39ee45ac96..0000000000 --- a/dimos/models/Detic/configs/BoxSup-C2_LCOCO_CLIP_SwinB_896b32_4x.yaml +++ /dev/null @@ -1,19 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - WEIGHTS: "models/swin_base_patch4_window7_224_22k.pkl" - BACKBONE: - NAME: build_swintransformer_fpn_backbone - SWIN: - SIZE: B-22k - FPN: - IN_FEATURES: ["swin1", "swin2", "swin3"] -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 -INPUT: - TRAIN_SIZE: 896 -DATASETS: - TRAIN: ("lvis_v1_train+coco",) \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-C2_L_CLIP_R5021k_640b64_4x.yaml b/dimos/models/Detic/configs/BoxSup-C2_L_CLIP_R5021k_640b64_4x.yaml deleted file mode 100644 index 91a25ee2ad..0000000000 --- a/dimos/models/Detic/configs/BoxSup-C2_L_CLIP_R5021k_640b64_4x.yaml +++ /dev/null @@ -1,4 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-C2_L_CLIP_SwinB_896b32_4x.yaml b/dimos/models/Detic/configs/BoxSup-C2_L_CLIP_SwinB_896b32_4x.yaml deleted file mode 100644 index bf6e93a830..0000000000 --- a/dimos/models/Detic/configs/BoxSup-C2_L_CLIP_SwinB_896b32_4x.yaml +++ /dev/null @@ -1,17 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - WEIGHTS: "models/swin_base_patch4_window7_224_22k.pkl" - BACKBONE: - NAME: build_swintransformer_fpn_backbone - SWIN: - SIZE: B-22k - FPN: - IN_FEATURES: ["swin1", "swin2", "swin3"] -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 -INPUT: - TRAIN_SIZE: 896 \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.yaml b/dimos/models/Detic/configs/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.yaml deleted file mode 100644 index a4d73a060f..0000000000 --- a/dimos/models/Detic/configs/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True -DATASETS: - TRAIN: ("lvis_v1_train_norare",) \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-C2_Lbase_CLIP_SwinB_896b32_4x.yaml b/dimos/models/Detic/configs/BoxSup-C2_Lbase_CLIP_SwinB_896b32_4x.yaml deleted file mode 100644 index f271ac558c..0000000000 --- a/dimos/models/Detic/configs/BoxSup-C2_Lbase_CLIP_SwinB_896b32_4x.yaml +++ /dev/null @@ -1,19 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - WEIGHTS: "models/swin_base_patch4_window7_224_22k.pkl" - BACKBONE: - NAME: build_swintransformer_fpn_backbone - SWIN: - SIZE: B-22k - FPN: - IN_FEATURES: ["swin1", "swin2", "swin3"] -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 -INPUT: - TRAIN_SIZE: 896 -DATASETS: - TRAIN: ("lvis_v1_train_norare",) \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-DeformDETR_L_R50_2x.yaml b/dimos/models/Detic/configs/BoxSup-DeformDETR_L_R50_2x.yaml deleted file mode 100644 index aed66e1fba..0000000000 --- a/dimos/models/Detic/configs/BoxSup-DeformDETR_L_R50_2x.yaml +++ /dev/null @@ -1,3 +0,0 @@ -_BASE_: "Base-DeformDETR_L_R50_4x.yaml" -SOLVER: - IMS_PER_BATCH: 16 \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup-DeformDETR_L_R50_4x.yaml b/dimos/models/Detic/configs/BoxSup-DeformDETR_L_R50_4x.yaml deleted file mode 100644 index a5ee4566ff..0000000000 --- a/dimos/models/Detic/configs/BoxSup-DeformDETR_L_R50_4x.yaml +++ /dev/null @@ -1 +0,0 @@ -_BASE_: "Base-DeformDETR_L_R50_4x.yaml" \ No newline at end of file diff --git a/dimos/models/Detic/configs/BoxSup_OVCOCO_CLIP_R50_1x.yaml b/dimos/models/Detic/configs/BoxSup_OVCOCO_CLIP_R50_1x.yaml deleted file mode 100644 index b6c977fbac..0000000000 --- a/dimos/models/Detic/configs/BoxSup_OVCOCO_CLIP_R50_1x.yaml +++ /dev/null @@ -1 +0,0 @@ -_BASE_: "Base_OVCOCO_C4_1x.yaml" diff --git a/dimos/models/Detic/configs/BoxSup_ViLD_200e.py b/dimos/models/Detic/configs/BoxSup_ViLD_200e.py deleted file mode 100644 index b189c7b54f..0000000000 --- a/dimos/models/Detic/configs/BoxSup_ViLD_200e.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import os - -from detectron2.config import LazyCall as L -from detectron2.data.samplers import RepeatFactorTrainingSampler -import detectron2.data.transforms as T -from detectron2.evaluation.lvis_evaluation import LVISEvaluator -from detectron2.layers import ShapeSpec -from detectron2.layers.batch_norm import NaiveSyncBatchNorm -from detectron2.model_zoo import get_config -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.modeling.matcher import Matcher -from detectron2.modeling.roi_heads import FastRCNNConvFCHead -from detectron2.solver import WarmupParamScheduler -from detectron2.solver.build import get_default_optimizer_params -from detic.modeling.roi_heads.detic_fast_rcnn import DeticFastRCNNOutputLayers -from detic.modeling.roi_heads.detic_roi_heads import DeticCascadeROIHeads -from detic.modeling.roi_heads.zero_shot_classifier import ZeroShotClassifier -from fvcore.common.param_scheduler import CosineParamScheduler -import torch - -default_configs = get_config("new_baselines/mask_rcnn_R_50_FPN_100ep_LSJ.py") -dataloader = default_configs["dataloader"] -model = default_configs["model"] -train = default_configs["train"] - -[model.roi_heads.pop(k) for k in ["box_head", "box_predictor", "proposal_matcher"]] - -model.roi_heads.update( - _target_=DeticCascadeROIHeads, - num_classes=1203, - box_heads=[ - L(FastRCNNConvFCHead)( - input_shape=ShapeSpec(channels=256, height=7, width=7), - conv_dims=[256, 256, 256, 256], - fc_dims=[1024], - conv_norm=lambda c: NaiveSyncBatchNorm(c, stats_mode="N"), - ) - for _ in range(1) - ], - box_predictors=[ - L(DeticFastRCNNOutputLayers)( - input_shape=ShapeSpec(channels=1024), - test_score_thresh=0.0001, - test_topk_per_image=300, - box2box_transform=L(Box2BoxTransform)(weights=(w1, w1, w2, w2)), - cls_agnostic_bbox_reg=True, - num_classes="${...num_classes}", - cls_score=L(ZeroShotClassifier)( - input_shape=ShapeSpec(channels=1024), - num_classes=1203, - zs_weight_path="datasets/metadata/lvis_v1_clip_a+cname.npy", - norm_weight=True, - # use_bias=-4.6, - ), - use_zeroshot_cls=True, - use_sigmoid_ce=True, - ignore_zero_cats=True, - cat_freq_path="datasets/lvis/lvis_v1_train_norare_cat_info.json", - ) - for (w1, w2) in [(10, 5)] - ], - proposal_matchers=[ - L(Matcher)(thresholds=[th], labels=[0, 1], allow_low_quality_matches=False) for th in [0.5] - ], -) -model.roi_heads.mask_head.num_classes = 1 - -dataloader.train.dataset.names = "lvis_v1_train_norare" -dataloader.train.sampler = L(RepeatFactorTrainingSampler)( - repeat_factors=L(RepeatFactorTrainingSampler.repeat_factors_from_category_frequency)( - dataset_dicts="${dataloader.train.dataset}", repeat_thresh=0.001 - ) -) -image_size = 896 -dataloader.train.mapper.augmentations = [ - L(T.ResizeScale)( - min_scale=0.1, max_scale=2.0, target_height=image_size, target_width=image_size - ), - L(T.FixedSizeCrop)(crop_size=(image_size, image_size)), - L(T.RandomFlip)(horizontal=True), -] -dataloader.train.num_workers = 32 - -dataloader.test.dataset.names = "lvis_v1_val" -dataloader.evaluator = L(LVISEvaluator)( - dataset_name="${..test.dataset.names}", -) - -num_nodes = 4 - -dataloader.train.total_batch_size = 64 * num_nodes -train.max_iter = 184375 * 2 // num_nodes - -lr_multiplier = L(WarmupParamScheduler)( - scheduler=CosineParamScheduler(1.0, 0.0), - warmup_length=500 / train.max_iter, - warmup_factor=0.067, -) - -optimizer = L(torch.optim.AdamW)( - params=L(get_default_optimizer_params)(weight_decay_norm=0.0), - lr=0.0002 * num_nodes, - weight_decay=1e-4, -) - -train.checkpointer.period = 20000 // num_nodes -train.output_dir = f"./output/Lazy/{os.path.basename(__file__)[:-3]}" diff --git a/dimos/models/Detic/configs/Detic_DeformDETR_LI_R50_4x_ft4x.yaml b/dimos/models/Detic/configs/Detic_DeformDETR_LI_R50_4x_ft4x.yaml deleted file mode 100644 index 2da679cd4a..0000000000 --- a/dimos/models/Detic/configs/Detic_DeformDETR_LI_R50_4x_ft4x.yaml +++ /dev/null @@ -1,22 +0,0 @@ -_BASE_: "Base-DeformDETR_L_R50_4x.yaml" -MODEL: - WEIGHTS: "models/BoxSup-DeformDETR_L_R50_4x.pth" -INPUT: - CUSTOM_AUG: ResizeShortestEdge - MIN_SIZE_TRAIN_SAMPLING: range - MIN_SIZE_TRAIN: [480, 800] -DATASETS: - TRAIN: ("lvis_v1_train","imagenet_lvis_v1") - TEST: ("lvis_v1_val",) -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [4, 16] - USE_RFS: [True, False] - DATASET_MIN_SIZES: [[480, 800], [240, 400]] - DATASET_MAX_SIZES: [1333, 667] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] -WITH_IMAGE_LABELS: True diff --git a/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_CXT21k_640b32_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_CXT21k_640b32_4x_ft4x_max-size.yaml deleted file mode 100644 index 8c5befdbdc..0000000000 --- a/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_CXT21k_640b32_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,39 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - DYNAMIC_CLASSIFIER: True - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - ZEROSHOT_WEIGHT_PATH: 'datasets/metadata/lvis-21k_clip_a+cname.npy' - USE_FED_LOSS: False # Federated loss is enabled when DYNAMIC_CLASSIFIER is on - ROI_HEADS: - NUM_CLASSES: 22047 - WEIGHTS: "output/Detic/BoxSup-C2_LCOCO_CLIP_CXT21k_640b32_4x/model_final.pth" - TIMM: - BASE_NAME: convnext_tiny_21k - OUT_LEVELS: [2, 3, 4] - PRETRAINED: True - FPN: - IN_FEATURES: ["layer2", "layer3", "layer4"] -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train+coco","imagenet_lvis-22k") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [4, 16] - DATASET_INPUT_SIZE: [640, 320] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 2 - USE_TAR_DATASET: True -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_R18_640b32_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_R18_640b32_4x_ft4x_max-size.yaml deleted file mode 100644 index e57e579dfd..0000000000 --- a/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_R18_640b32_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,36 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - DYNAMIC_CLASSIFIER: True - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - ZEROSHOT_WEIGHT_PATH: 'datasets/metadata/lvis-21k_clip_a+cname.npy' - USE_FED_LOSS: False # Federated loss is enabled when DYNAMIC_CLASSIFIER is on - ROI_HEADS: - NUM_CLASSES: 22047 - WEIGHTS: "output/Detic/BoxSup-C2_LCOCO_CLIP_R18_640b64_4x/model_final.pth" - TIMM: - BASE_NAME: resnet18 - PRETRAINED: True -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train+coco","imagenet_lvis-22k") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [4, 16] - DATASET_INPUT_SIZE: [640, 320] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 2 - USE_TAR_DATASET: True -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_R5021k_640b32_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_R5021k_640b32_4x_ft4x_max-size.yaml deleted file mode 100644 index 3d71d29c2f..0000000000 --- a/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_R5021k_640b32_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,33 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - DYNAMIC_CLASSIFIER: True - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - ZEROSHOT_WEIGHT_PATH: 'datasets/metadata/lvis-21k_clip_a+cname.npy' - USE_FED_LOSS: False # Federated loss is enabled when DYNAMIC_CLASSIFIER is on - ROI_HEADS: - NUM_CLASSES: 22047 - WEIGHTS: "output/Detic/BoxSup-C2_LCOCO_CLIP_R5021k_640b64_4x/model_final.pth" -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train+coco","imagenet_lvis-22k") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [4, 16] - DATASET_INPUT_SIZE: [640, 320] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 2 - USE_TAR_DATASET: True -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml deleted file mode 100644 index a3dba8d072..0000000000 --- a/dimos/models/Detic/configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,43 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - WEIGHTS: "models/BoxSup-C2_LCOCO_CLIP_SwinB_896b32_4x.pth" - DYNAMIC_CLASSIFIER: True - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - ZEROSHOT_WEIGHT_PATH: 'datasets/metadata/lvis-21k_clip_a+cname.npy' - USE_FED_LOSS: False # Federated loss is enabled when DYNAMIC_CLASSIFIER is on - ROI_HEADS: - NUM_CLASSES: 22047 - BACKBONE: - NAME: build_swintransformer_fpn_backbone - SWIN: - SIZE: B-22k - FPN: - IN_FEATURES: ["swin1", "swin2", "swin3"] - RESET_CLS_TESTS: True - TEST_CLASSIFIERS: ("datasets/metadata/oid_clip_a+cname.npy","datasets/metadata/o365_clip_a+cnamefix.npy") - TEST_NUM_CLASSES: [500, 365] -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train+coco","imagenet_lvis-22k") - TEST: ('oid_val_expanded', 'objects365_v2_val') -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 16] - USE_DIFF_BS_SIZE: True - DATASET_BS: [4, 16] - DATASET_INPUT_SIZE: [896, 448] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 4 - USE_TAR_DATASET: True -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml deleted file mode 100644 index 3b8633caac..0000000000 --- a/dimos/models/Detic/configs/Detic_LI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,43 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - WEIGHTS: "models/BoxSup-C2_L_CLIP_SwinB_896b32_4x.pth" - DYNAMIC_CLASSIFIER: True - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - ZEROSHOT_WEIGHT_PATH: 'datasets/metadata/lvis-21k_clip_a+cname.npy' - USE_FED_LOSS: False # Federated loss is enabled when DYNAMIC_CLASSIFIER is on - ROI_HEADS: - NUM_CLASSES: 22047 - BACKBONE: - NAME: build_swintransformer_fpn_backbone - SWIN: - SIZE: B-22k - FPN: - IN_FEATURES: ["swin1", "swin2", "swin3"] - RESET_CLS_TESTS: True - TEST_CLASSIFIERS: ("datasets/metadata/oid_clip_a+cname.npy","datasets/metadata/o365_clip_a+cnamefix.npy") - TEST_NUM_CLASSES: [500, 365] -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train","imagenet_lvis-22k") - TEST: ('oid_val_expanded', 'objects365_v2_val') -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 16] - USE_DIFF_BS_SIZE: True - DATASET_BS: [4, 16] - DATASET_INPUT_SIZE: [896, 448] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 4 - USE_TAR_DATASET: True -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LI_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LI_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml deleted file mode 100644 index ca93318e64..0000000000 --- a/dimos/models/Detic/configs/Detic_LI_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,27 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - WEIGHTS: "models/BoxSup-C2_L_CLIP_R5021k_640b64_4x.pth" -SOLVER: - MAX_ITER: 90000 - IMS_PER_BATCH: 64 - BASE_LR: 0.0002 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train","imagenet_lvis_v1") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [8, 32] - DATASET_INPUT_SIZE: [640, 320] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml deleted file mode 100644 index 57ffa48ce6..0000000000 --- a/dimos/models/Detic/configs/Detic_LI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,33 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - BACKBONE: - NAME: build_swintransformer_fpn_backbone - SWIN: - SIZE: B-22k - FPN: - IN_FEATURES: ["swin1", "swin2", "swin3"] - WEIGHTS: "models/BoxSup-C2_L_CLIP_SwinB_896b32_4x.pth" -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train","imagenet_lvis_v1") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [4, 16] - DATASET_INPUT_SIZE: [896, 448] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LbaseCCcapimg_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LbaseCCcapimg_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml deleted file mode 100644 index ada6ffed06..0000000000 --- a/dimos/models/Detic/configs/Detic_LbaseCCcapimg_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,30 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - WITH_CAPTION: True - SYNC_CAPTION_BATCH: True - ROI_BOX_HEAD: - ADD_IMAGE_BOX: True # caption loss is added to the image-box - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - WEIGHTS: "models/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.pth" -SOLVER: - MAX_ITER: 90000 - IMS_PER_BATCH: 64 - BASE_LR: 0.0002 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train_norare","cc3m_v1_train_tags") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [8, 32] - DATASET_INPUT_SIZE: [640, 320] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'captiontag'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LbaseCCimg_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LbaseCCimg_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml deleted file mode 100644 index aadcbc0ccd..0000000000 --- a/dimos/models/Detic/configs/Detic_LbaseCCimg_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,27 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - WEIGHTS: "models/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.pth" -SOLVER: - MAX_ITER: 90000 - IMS_PER_BATCH: 64 - BASE_LR: 0.0002 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train_norare","cc3m_v1_train_tags") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [8, 32] - DATASET_INPUT_SIZE: [640, 320] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LbaseI_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LbaseI_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml deleted file mode 100644 index 3ef1e9a02a..0000000000 --- a/dimos/models/Detic/configs/Detic_LbaseI_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,27 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - WEIGHTS: "models/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.pth" -SOLVER: - MAX_ITER: 90000 - IMS_PER_BATCH: 64 - BASE_LR: 0.0002 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train_norare","imagenet_lvis_v1") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [8, 32] - DATASET_INPUT_SIZE: [640, 320] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LbaseI_CLIP_R5021k_640b64_4x_ft4x_predicted.yaml b/dimos/models/Detic/configs/Detic_LbaseI_CLIP_R5021k_640b64_4x_ft4x_predicted.yaml deleted file mode 100644 index 9d6f1b350f..0000000000 --- a/dimos/models/Detic/configs/Detic_LbaseI_CLIP_R5021k_640b64_4x_ft4x_predicted.yaml +++ /dev/null @@ -1,27 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_score' - WEIGHTS: "models/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.pth" -SOLVER: - MAX_ITER: 90000 - IMS_PER_BATCH: 64 - BASE_LR: 0.0002 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train_norare","imagenet_lvis_v1") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [8, 32] - DATASET_INPUT_SIZE: [640, 320] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_LbaseI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml b/dimos/models/Detic/configs/Detic_LbaseI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml deleted file mode 100644 index b25e2b6651..0000000000 --- a/dimos/models/Detic/configs/Detic_LbaseI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml +++ /dev/null @@ -1,33 +0,0 @@ -_BASE_: "Base-C2_L_R5021k_640b64_4x.yaml" -MODEL: - ROI_BOX_HEAD: - USE_ZEROSHOT_CLS: True - IMAGE_LABEL_LOSS: 'max_size' - BACKBONE: - NAME: build_swintransformer_fpn_backbone - SWIN: - SIZE: B-22k - FPN: - IN_FEATURES: ["swin1", "swin2", "swin3"] - WEIGHTS: "models/BoxSup-C2_Lbase_CLIP_SwinB_896b32_4x.pth" -SOLVER: - MAX_ITER: 180000 - IMS_PER_BATCH: 32 - BASE_LR: 0.0001 - WARMUP_ITERS: 1000 - WARMUP_FACTOR: 0.001 -DATASETS: - TRAIN: ("lvis_v1_train_norare","imagenet_lvis_v1") -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [4, 16] - DATASET_INPUT_SIZE: [896, 448] - USE_RFS: [True, False] - DATASET_INPUT_SCALE: [[0.1, 2.0], [0.5, 1.5]] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_caption.yaml b/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_caption.yaml deleted file mode 100644 index aeafd50d7c..0000000000 --- a/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_caption.yaml +++ /dev/null @@ -1,33 +0,0 @@ -_BASE_: "Base_OVCOCO_C4_1x.yaml" -MODEL: - WEIGHTS: "models/BoxSup_OVCOCO_CLIP_R50_1x.pth" - WITH_CAPTION: True - SYNC_CAPTION_BATCH: True - ROI_BOX_HEAD: - WS_NUM_PROPS: 1 - ADD_IMAGE_BOX: True - NEG_CAP_WEIGHT: 1.0 -SOLVER: - IMS_PER_BATCH: 16 - BASE_LR: 0.02 - STEPS: (60000, 80000) - MAX_ITER: 90000 -DATASETS: - TRAIN: ("coco_zeroshot_train_oriorder", "coco_caption_train_tags") -INPUT: - CUSTOM_AUG: ResizeShortestEdge - MIN_SIZE_TRAIN_SAMPLING: range - MIN_SIZE_TRAIN: (800, 800) -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [2, 8] - USE_RFS: [False, False] - DATASET_MIN_SIZES: [[800, 800], [400, 400]] - DATASET_MAX_SIZES: [1333, 667] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'caption'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_max-size.yaml b/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_max-size.yaml deleted file mode 100644 index 8daa4be6bb..0000000000 --- a/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_max-size.yaml +++ /dev/null @@ -1,30 +0,0 @@ -_BASE_: "Base_OVCOCO_C4_1x.yaml" -MODEL: - WEIGHTS: "models/BoxSup_OVCOCO_CLIP_R50_1x.pth" - ROI_BOX_HEAD: - WS_NUM_PROPS: 32 - IMAGE_LABEL_LOSS: 'max_size' -SOLVER: - IMS_PER_BATCH: 16 - BASE_LR: 0.02 - STEPS: (60000, 80000) - MAX_ITER: 90000 -DATASETS: - TRAIN: ("coco_zeroshot_train_oriorder", "coco_caption_train_tags") -INPUT: - CUSTOM_AUG: ResizeShortestEdge - MIN_SIZE_TRAIN_SAMPLING: range - MIN_SIZE_TRAIN: (800, 800) -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [2, 8] - USE_RFS: [False, False] - DATASET_MIN_SIZES: [[800, 800], [400, 400]] - DATASET_MAX_SIZES: [1333, 667] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'image'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_max-size_caption.yaml b/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_max-size_caption.yaml deleted file mode 100644 index 3ba0a20a18..0000000000 --- a/dimos/models/Detic/configs/Detic_OVCOCO_CLIP_R50_1x_max-size_caption.yaml +++ /dev/null @@ -1,35 +0,0 @@ -_BASE_: "Base_OVCOCO_C4_1x.yaml" -MODEL: - WEIGHTS: "models/BoxSup_OVCOCO_CLIP_R50_1x.pth" - WITH_CAPTION: True - SYNC_CAPTION_BATCH: True - ROI_BOX_HEAD: - WS_NUM_PROPS: 32 - ADD_IMAGE_BOX: True # caption loss is added to the image-box - IMAGE_LABEL_LOSS: 'max_size' - - NEG_CAP_WEIGHT: 1.0 -SOLVER: - IMS_PER_BATCH: 16 - BASE_LR: 0.02 - STEPS: (60000, 80000) - MAX_ITER: 90000 -DATASETS: - TRAIN: ("coco_zeroshot_train_oriorder", "coco_caption_train_tags") -INPUT: - CUSTOM_AUG: ResizeShortestEdge - MIN_SIZE_TRAIN_SAMPLING: range - MIN_SIZE_TRAIN: (800, 800) -DATALOADER: - SAMPLER_TRAIN: "MultiDatasetSampler" - DATASET_RATIO: [1, 4] - USE_DIFF_BS_SIZE: True - DATASET_BS: [2, 8] - USE_RFS: [False, False] - DATASET_MIN_SIZES: [[800, 800], [400, 400]] - DATASET_MAX_SIZES: [1333, 667] - FILTER_EMPTY_ANNOTATIONS: False - MULTI_DATASET_GROUPING: True - DATASET_ANN: ['box', 'captiontag'] - NUM_WORKERS: 8 -WITH_IMAGE_LABELS: True \ No newline at end of file diff --git a/dimos/models/Detic/configs/Detic_ViLD_200e.py b/dimos/models/Detic/configs/Detic_ViLD_200e.py deleted file mode 100644 index 470124a109..0000000000 --- a/dimos/models/Detic/configs/Detic_ViLD_200e.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import os - -from detectron2.config import LazyCall as L -import detectron2.data.transforms as T -from detectron2.evaluation.lvis_evaluation import LVISEvaluator -from detectron2.layers import ShapeSpec -from detectron2.layers.batch_norm import NaiveSyncBatchNorm -from detectron2.model_zoo import get_config -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.modeling.matcher import Matcher -from detectron2.modeling.roi_heads import FastRCNNConvFCHead -from detectron2.solver import WarmupParamScheduler -from detectron2.solver.build import get_default_optimizer_params -from detic.data.custom_dataset_dataloader import ( - MultiDatasetSampler, - build_custom_train_loader, - get_detection_dataset_dicts_with_source, -) -from detic.data.custom_dataset_mapper import CustomDatasetMapper -from detic.modeling.meta_arch.custom_rcnn import CustomRCNN -from detic.modeling.roi_heads.detic_fast_rcnn import DeticFastRCNNOutputLayers -from detic.modeling.roi_heads.detic_roi_heads import DeticCascadeROIHeads -from detic.modeling.roi_heads.zero_shot_classifier import ZeroShotClassifier -from fvcore.common.param_scheduler import CosineParamScheduler -import torch - -default_configs = get_config("new_baselines/mask_rcnn_R_50_FPN_100ep_LSJ.py") -dataloader = default_configs["dataloader"] -model = default_configs["model"] -train = default_configs["train"] - -train.init_checkpoint = "models/BoxSup_ViLD_200e.pth" - -[model.roi_heads.pop(k) for k in ["box_head", "box_predictor", "proposal_matcher"]] - -model.roi_heads.update( - _target_=DeticCascadeROIHeads, - num_classes=1203, - box_heads=[ - L(FastRCNNConvFCHead)( - input_shape=ShapeSpec(channels=256, height=7, width=7), - conv_dims=[256, 256, 256, 256], - fc_dims=[1024], - conv_norm=lambda c: NaiveSyncBatchNorm(c, stats_mode="N"), - ) - for _ in range(1) - ], - box_predictors=[ - L(DeticFastRCNNOutputLayers)( - input_shape=ShapeSpec(channels=1024), - test_score_thresh=0.0001, - test_topk_per_image=300, - box2box_transform=L(Box2BoxTransform)(weights=(w1, w1, w2, w2)), - cls_agnostic_bbox_reg=True, - num_classes="${...num_classes}", - cls_score=L(ZeroShotClassifier)( - input_shape=ShapeSpec(channels=1024), - num_classes=1203, - zs_weight_path="datasets/metadata/lvis_v1_clip_a+cname.npy", - norm_weight=True, - # use_bias=-4.6, - ), - use_zeroshot_cls=True, - use_sigmoid_ce=True, - ignore_zero_cats=True, - cat_freq_path="datasets/lvis/lvis_v1_train_norare_cat_info.json", - image_label_loss="max_size", - image_loss_weight=0.1, - ) - for (w1, w2) in [(10, 5)] - ], - proposal_matchers=[ - L(Matcher)(thresholds=[th], labels=[0, 1], allow_low_quality_matches=False) for th in [0.5] - ], - with_image_labels=True, - ws_num_props=128, -) -model.update( - _target_=CustomRCNN, - with_image_labels=True, -) -model.roi_heads.mask_head.num_classes = 1 - -train.ddp.find_unused_parameters = True - -num_nodes = 4 -image_size = 896 -image_size_weak = 448 -dataloader.train = L(build_custom_train_loader)( - dataset=L(get_detection_dataset_dicts_with_source)( - dataset_names=["lvis_v1_train_norare", "imagenet_lvis_v1"], - filter_empty=False, - ), - mapper=L(CustomDatasetMapper)( - is_train=True, - augmentations=[], - with_ann_type=True, - dataset_ann=["box", "image"], - use_diff_bs_size=True, - dataset_augs=[ - [ - L(T.ResizeScale)( - min_scale=0.1, max_scale=2.0, target_height=image_size, target_width=image_size - ), - L(T.FixedSizeCrop)(crop_size=(image_size, image_size)), - L(T.RandomFlip)(horizontal=True), - ], - [ - L(T.ResizeScale)( - min_scale=0.5, - max_scale=1.5, - target_height=image_size_weak, - target_width=image_size_weak, - ), - L(T.FixedSizeCrop)(crop_size=(image_size_weak, image_size_weak)), - L(T.RandomFlip)(horizontal=True), - ], - ], - image_format="BGR", - use_instance_mask=True, - ), - sampler=L(MultiDatasetSampler)( - dataset_dicts="${dataloader.train.dataset}", - dataset_ratio=[1, 4], - use_rfs=[True, False], - dataset_ann="${dataloader.train.mapper.dataset_ann}", - repeat_threshold=0.001, - ), - total_batch_size=64 * num_nodes, - multi_dataset_grouping=True, - use_diff_bs_size=True, - dataset_bs=[8, 8 * 4], - num_datasets=2, - num_workers=8, -) - -dataloader.test.dataset.names = "lvis_v1_val" -dataloader.evaluator = L(LVISEvaluator)( - dataset_name="${..test.dataset.names}", -) - -train.max_iter = 184375 * 2 // num_nodes -lr_multiplier = L(WarmupParamScheduler)( - scheduler=CosineParamScheduler(1.0, 0.0), - warmup_length=500 / train.max_iter, - warmup_factor=0.067, -) - -optimizer = L(torch.optim.AdamW)( - params=L(get_default_optimizer_params)(weight_decay_norm=0.0), - lr=0.0002 * num_nodes, - weight_decay=1e-4, -) - -train.checkpointer.period = 20000 // num_nodes -train.output_dir = f"./output/Lazy/{os.path.basename(__file__)[:-3]}" diff --git a/dimos/models/Detic/datasets/README.md b/dimos/models/Detic/datasets/README.md deleted file mode 100644 index e9f4a0b3fb..0000000000 --- a/dimos/models/Detic/datasets/README.md +++ /dev/null @@ -1,207 +0,0 @@ -# Prepare datasets for Detic - -The basic training of our model uses [LVIS](https://www.lvisdataset.org/) (which uses [COCO](https://cocodataset.org/) images) and [ImageNet-21K](https://www.image-net.org/download.php). -Some models are trained on [Conceptual Caption (CC3M)](https://ai.google.com/research/ConceptualCaptions/). -Optionally, we use [Objects365](https://www.objects365.org/) and [OpenImages (Challenge 2019 version)](https://storage.googleapis.com/openimages/web/challenge2019.html) for cross-dataset evaluation. -Before starting processing, please download the (selected) datasets from the official websites and place or sim-link them under `$Detic_ROOT/datasets/`. - -``` -$Detic_ROOT/datasets/ - metadata/ - lvis/ - coco/ - imagenet/ - cc3m/ - objects365/ - oid/ -``` -`metadata/` is our preprocessed meta-data (included in the repo). See the below [section](#Metadata) for details. -Please follow the following instruction to pre-process individual datasets. - -### COCO and LVIS - -First, download COCO and LVIS data place them in the following way: - -``` -lvis/ - lvis_v1_train.json - lvis_v1_val.json -coco/ - train2017/ - val2017/ - annotations/ - captions_train2017.json - instances_train2017.json - instances_val2017.json -``` - -Next, prepare the open-vocabulary LVIS training set using - -``` -python tools/remove_lvis_rare.py --ann datasets/lvis/lvis_v1_train.json -``` - -This will generate `datasets/lvis/lvis_v1_train_norare.json`. - -### ImageNet-21K - -The ImageNet-21K folder should look like: -``` -imagenet/ - ImageNet-21K/ - n01593028.tar - n01593282.tar - ... -``` - -We first unzip the overlapping classes of LVIS (we will directly work with the .tar file for the rest classes) and convert them into LVIS annotation format. - -~~~ -mkdir imagenet/annotations -python tools/unzip_imagenet_lvis.py --dst_path datasets/imagenet/ImageNet-LVIS -python tools/create_imagenetlvis_json.py --imagenet_path datasets/imagenet/ImageNet-LVIS --out_path datasets/imagenet/annotations/imagenet_lvis_image_info.json -~~~ -This creates `datasets/imagenet/annotations/imagenet_lvis_image_info.json`. - -[Optional] To train with all the 21K classes, run - -~~~ -python tools/get_imagenet_21k_full_tar_json.py -python tools/create_lvis_21k.py -~~~ -This creates `datasets/imagenet/annotations/imagenet-21k_image_info_lvis-21k.json` and `datasets/lvis/lvis_v1_train_lvis-21k.json` (combined LVIS and ImageNet-21K classes in `categories`). - -[Optional] To train on combined LVIS and COCO, run - -~~~ -python tools/merge_lvis_coco.py -~~~ -This creates `datasets/lvis/lvis_v1_train+coco_mask.json` - -### Conceptual Caption - - -Download the dataset from [this](https://ai.google.com/research/ConceptualCaptions/download) page and place them as: -``` -cc3m/ - GCC-training.tsv -``` - -Run the following command to download the images and convert the annotations to LVIS format (Note: download images takes long). - -~~~ -python tools/download_cc.py --ann datasets/cc3m/GCC-training.tsv --save_image_path datasets/cc3m/training/ --out_path datasets/cc3m/train_image_info.json -python tools/get_cc_tags.py -~~~ - -This creates `datasets/cc3m/train_image_info_tags.json`. - -### Objects365 -Download Objects365 (v2) from the website. We only need the validation set in this project: -``` -objects365/ - annotations/ - zhiyuan_objv2_val.json - val/ - images/ - v1/ - patch0/ - ... - patch15/ - v2/ - patch16/ - ... - patch49/ - -``` - -The original annotation has typos in the class names, we first fix them for our following use of language embeddings. - -``` -python tools/fix_o365_names.py --ann datasets/objects365/annotations/zhiyuan_objv2_val.json -``` -This creates `datasets/objects365/zhiyuan_objv2_val_fixname.json`. - -To train on Objects365, download the training images and use the command above. We note some images in the training annotation do not exist. -We use the following command to filter the missing images. -~~~ -python tools/fix_0365_path.py -~~~ -This creates `datasets/objects365/zhiyuan_objv2_train_fixname_fixmiss.json`. - -### OpenImages - -We followed the instructions in [UniDet](https://github.com/xingyizhou/UniDet/blob/master/docs/DATASETS.md#openimages) to convert the metadata for OpenImages. - -The converted folder should look like - -``` -oid/ - annotations/ - oid_challenge_2019_train_bbox.json - oid_challenge_2019_val_expanded.json - images/ - 0/ - 1/ - 2/ - ... -``` - -### Open-vocabulary COCO - -We first follow [OVR-CNN](https://github.com/alirezazareian/ovr-cnn/blob/master/ipynb/003.ipynb) to create the open-vocabulary COCO split. The converted files should be like - -``` -coco/ - zero-shot/ - instances_train2017_seen_2.json - instances_val2017_all_2.json -``` - -We further pre-process the annotation format for easier evaluation: - -``` -python tools/get_coco_zeroshot_oriorder.py --data_path datasets/coco/zero-shot/instances_train2017_seen_2.json -python tools/get_coco_zeroshot_oriorder.py --data_path datasets/coco/zero-shot/instances_val2017_all_2.json -``` - -Next, we preprocess the COCO caption data: - -``` -python tools/get_cc_tags.py --cc_ann datasets/coco/annotations/captions_train2017.json --out_path datasets/coco/captions_train2017_tags_allcaps.json --allcaps --convert_caption --cat_path datasets/coco/annotations/instances_val2017.json -``` -This creates `datasets/coco/captions_train2017_tags_allcaps.json`. - -### Metadata - -``` -metadata/ - lvis_v1_train_cat_info.json - coco_clip_a+cname.npy - lvis_v1_clip_a+cname.npy - o365_clip_a+cnamefix.npy - oid_clip_a+cname.npy - imagenet_lvis_wnid.txt - Objects365_names_fix.csv -``` - -`lvis_v1_train_cat_info.json` is used by the Federated loss. -This is created by -~~~ -python tools/get_lvis_cat_info.py --ann datasets/lvis/lvis_v1_train.json -~~~ - -`*_clip_a+cname.npy` is the pre-computed CLIP embeddings for each datasets. -They are created by (taking LVIS as an example) -~~~ -python tools/dump_clip_features.py --ann datasets/lvis/lvis_v1_val.json --out_path metadata/lvis_v1_clip_a+cname.npy -~~~ -Note we do not include the 21K class embeddings due to the large file size. -To create it, run -~~~ -python tools/dump_clip_features.py --ann datasets/lvis/lvis_v1_val_lvis-21k.json --out_path datasets/metadata/lvis-21k_clip_a+cname.npy -~~~ - -`imagenet_lvis_wnid.txt` is the list of matched classes between ImageNet-21K and LVIS. - -`Objects365_names_fix.csv` is our manual fix of the Objects365 names. \ No newline at end of file diff --git a/dimos/models/Detic/datasets/metadata/Objects365_names_fix.csv b/dimos/models/Detic/datasets/metadata/Objects365_names_fix.csv deleted file mode 100644 index c274707cc3..0000000000 --- a/dimos/models/Detic/datasets/metadata/Objects365_names_fix.csv +++ /dev/null @@ -1,365 +0,0 @@ -1,Person,Person -2,Sneakers,Sneakers -3,Chair,Chair -4,Other Shoes,Other Shoes -5,Hat,Hat -6,Car,Car -7,Lamp,Lamp -8,Glasses,Glasses -9,Bottle,Bottle -10,Desk,Desk -11,Cup,Cup -12,Street Lights,Street Lights -13,Cabinet/shelf,Cabinet/shelf -14,Handbag/Satchel,Handbag/Satchel -15,Bracelet,Bracelet -16,Plate,Plate -17,Picture/Frame,Picture/Frame -18,Helmet,Helmet -19,Book,Book -20,Gloves,Gloves -21,Storage box,Storage box -22,Boat,Boat -23,Leather Shoes,Leather Shoes -24,Flower,Flower -25,Bench,Bench -26,Potted Plant,Potted Plant -27,Bowl/Basin,Bowl/Basin -28,Flag,Flag -29,Pillow,Pillow -30,Boots,Boots -31,Vase,Vase -32,Microphone,Microphone -33,Necklace,Necklace -34,Ring,Ring -35,SUV,SUV -36,Wine Glass,Wine Glass -37,Belt,Belt -38,Moniter/TV,Monitor/TV -39,Backpack,Backpack -40,Umbrella,Umbrella -41,Traffic Light,Traffic Light -42,Speaker,Speaker -43,Watch,Watch -44,Tie,Tie -45,Trash bin Can,Trash bin Can -46,Slippers,Slippers -47,Bicycle,Bicycle -48,Stool,Stool -49,Barrel/bucket,Barrel/bucket -50,Van,Van -51,Couch,Couch -52,Sandals,Sandals -53,Bakset,Basket -54,Drum,Drum -55,Pen/Pencil,Pen/Pencil -56,Bus,Bus -57,Wild Bird,Wild Bird -58,High Heels,High Heels -59,Motorcycle,Motorcycle -60,Guitar,Guitar -61,Carpet,Carpet -62,Cell Phone,Cell Phone -63,Bread,Bread -64,Camera,Camera -65,Canned,Canned -66,Truck,Truck -67,Traffic cone,Traffic cone -68,Cymbal,Cymbal -69,Lifesaver,Lifesaver -70,Towel,Towel -71,Stuffed Toy,Stuffed Toy -72,Candle,Candle -73,Sailboat,Sailboat -74,Laptop,Laptop -75,Awning,Awning -76,Bed,Bed -77,Faucet,Faucet -78,Tent,Tent -79,Horse,Horse -80,Mirror,Mirror -81,Power outlet,Power outlet -82,Sink,Sink -83,Apple,Apple -84,Air Conditioner,Air Conditioner -85,Knife,Knife -86,Hockey Stick,Hockey Stick -87,Paddle,Paddle -88,Pickup Truck,Pickup Truck -89,Fork,Fork -90,Traffic Sign,Traffic Sign -91,Ballon,Ballon -92,Tripod,Tripod -93,Dog,Dog -94,Spoon,Spoon -95,Clock,Clock -96,Pot,Pot -97,Cow,Cow -98,Cake,Cake -99,Dinning Table,Dining Table -100,Sheep,Sheep -101,Hanger,Hanger -102,Blackboard/Whiteboard,Blackboard/Whiteboard -103,Napkin,Napkin -104,Other Fish,Other Fish -105,Orange/Tangerine,Orange/Tangerine -106,Toiletry,Toiletry -107,Keyboard,Keyboard -108,Tomato,Tomato -109,Lantern,Lantern -110,Machinery Vehicle,Machinery Vehicle -111,Fan,Fan -112,Green Vegetables,Green Vegetables -113,Banana,Banana -114,Baseball Glove,Baseball Glove -115,Airplane,Airplane -116,Mouse,Mouse -117,Train,Train -118,Pumpkin,Pumpkin -119,Soccer,Soccer -120,Skiboard,Skiboard -121,Luggage,Luggage -122,Nightstand,Nightstand -123,Tea pot,Teapot -124,Telephone,Telephone -125,Trolley,Trolley -126,Head Phone,Head Phone -127,Sports Car,Sports Car -128,Stop Sign,Stop Sign -129,Dessert,Dessert -130,Scooter,Scooter -131,Stroller,Stroller -132,Crane,Crane -133,Remote,Remote -134,Refrigerator,Refrigerator -135,Oven,Oven -136,Lemon,Lemon -137,Duck,Duck -138,Baseball Bat,Baseball Bat -139,Surveillance Camera,Surveillance Camera -140,Cat,Cat -141,Jug,Jug -142,Broccoli,Broccoli -143,Piano,Piano -144,Pizza,Pizza -145,Elephant,Elephant -146,Skateboard,Skateboard -147,Surfboard,Surfboard -148,Gun,Gun -149,Skating and Skiing shoes,Skating and Skiing shoes -150,Gas stove,Gas stove -151,Donut,Donut -152,Bow Tie,Bow Tie -153,Carrot,Carrot -154,Toilet,Toilet -155,Kite,Kite -156,Strawberry,Strawberry -157,Other Balls,Other Balls -158,Shovel,Shovel -159,Pepper,Pepper -160,Computer Box,Computer Box -161,Toilet Paper,Toilet Paper -162,Cleaning Products,Cleaning Products -163,Chopsticks,Chopsticks -164,Microwave,Microwave -165,Pigeon,Pigeon -166,Baseball,Baseball -167,Cutting/chopping Board,Cutting/chopping Board -168,Coffee Table,Coffee Table -169,Side Table,Side Table -170,Scissors,Scissors -171,Marker,Marker -172,Pie,Pie -173,Ladder,Ladder -174,Snowboard,Snowboard -175,Cookies,Cookies -176,Radiator,Radiator -177,Fire Hydrant,Fire Hydrant -178,Basketball,Basketball -179,Zebra,Zebra -180,Grape,Grape -181,Giraffe,Giraffe -182,Potato,Potato -183,Sausage,Sausage -184,Tricycle,Tricycle -185,Violin,Violin -186,Egg,Egg -187,Fire Extinguisher,Fire Extinguisher -188,Candy,Candy -189,Fire Truck,Fire Truck -190,Billards,Billards -191,Converter,Converter -192,Bathtub,Bathtub -193,Wheelchair,Wheelchair -194,Golf Club,Golf Club -195,Briefcase,Briefcase -196,Cucumber,Cucumber -197,Cigar/Cigarette,Cigar/Cigarette -198,Paint Brush,Paint Brush -199,Pear,Pear -200,Heavy Truck,Heavy Truck -201,Hamburger,Hamburger -202,Extractor,Extractor -203,Extention Cord,Extension Cord -204,Tong,Tong -205,Tennis Racket,Tennis Racket -206,Folder,Folder -207,American Football,American Football -208,earphone,earphone -209,Mask,Mask -210,Kettle,Kettle -211,Tennis,Tennis -212,Ship,Ship -213,Swing,Swing -214,Coffee Machine,Coffee Machine -215,Slide,Slide -216,Carriage,Carriage -217,Onion,Onion -218,Green beans,Green beans -219,Projector,Projector -220,Frisbee,Frisbee -221,Washing Machine/Drying Machine,Washing Machine/Drying Machine -222,Chicken,Chicken -223,Printer,Printer -224,Watermelon,Watermelon -225,Saxophone,Saxophone -226,Tissue,Tissue -227,Toothbrush,Toothbrush -228,Ice cream,Ice cream -229,Hotair ballon,Hot air balloon -230,Cello,Cello -231,French Fries,French Fries -232,Scale,Scale -233,Trophy,Trophy -234,Cabbage,Cabbage -235,Hot dog,Hot dog -236,Blender,Blender -237,Peach,Peach -238,Rice,Rice -239,Wallet/Purse,Wallet/Purse -240,Volleyball,Volleyball -241,Deer,Deer -242,Goose,Goose -243,Tape,Tape -244,Tablet,Tablet -245,Cosmetics,Cosmetics -246,Trumpet,Trumpet -247,Pineapple,Pineapple -248,Golf Ball,Golf Ball -249,Ambulance,Ambulance -250,Parking meter,Parking meter -251,Mango,Mango -252,Key,Key -253,Hurdle,Hurdle -254,Fishing Rod,Fishing Rod -255,Medal,Medal -256,Flute,Flute -257,Brush,Brush -258,Penguin,Penguin -259,Megaphone,Megaphone -260,Corn,Corn -261,Lettuce,Lettuce -262,Garlic,Garlic -263,Swan,Swan -264,Helicopter,Helicopter -265,Green Onion,Green Onion -266,Sandwich,Sandwich -267,Nuts,Nuts -268,Speed Limit Sign,Speed Limit Sign -269,Induction Cooker,Induction Cooker -270,Broom,Broom -271,Trombone,Trombone -272,Plum,Plum -273,Rickshaw,Rickshaw -274,Goldfish,Goldfish -275,Kiwi fruit,Kiwi fruit -276,Router/modem,Router/modem -277,Poker Card,Poker Card -278,Toaster,Toaster -279,Shrimp,Shrimp -280,Sushi,Sushi -281,Cheese,Cheese -282,Notepaper,Notepaper -283,Cherry,Cherry -284,Pliers,Pliers -285,CD,CD -286,Pasta,Pasta -287,Hammer,Hammer -288,Cue,Cue -289,Avocado,Avocado -290,Hamimelon,Hami melon -291,Flask,Flask -292,Mushroon,Mushroom -293,Screwdriver,Screwdriver -294,Soap,Soap -295,Recorder,Recorder -296,Bear,Bear -297,Eggplant,Eggplant -298,Board Eraser,Board Eraser -299,Coconut,Coconut -300,Tape Measur/ Ruler,Tape Measure/ Ruler -301,Pig,Pig -302,Showerhead,Showerhead -303,Globe,Globe -304,Chips,Chips -305,Steak,Steak -306,Crosswalk Sign,Crosswalk Sign -307,Stapler,Stapler -308,Campel,Camel -309,Formula 1,Formula 1 -310,Pomegranate,Pomegranate -311,Dishwasher,Dishwasher -312,Crab,Crab -313,Hoverboard,Hoverboard -314,Meat ball,Meatball -315,Rice Cooker,Rice Cooker -316,Tuba,Tuba -317,Calculator,Calculator -318,Papaya,Papaya -319,Antelope,Antelope -320,Parrot,Parrot -321,Seal,Seal -322,Buttefly,Butterfly -323,Dumbbell,Dumbbell -324,Donkey,Donkey -325,Lion,Lion -326,Urinal,Urinal -327,Dolphin,Dolphin -328,Electric Drill,Electric Drill -329,Hair Dryer,Hair Dryer -330,Egg tart,Egg tart -331,Jellyfish,Jellyfish -332,Treadmill,Treadmill -333,Lighter,Lighter -334,Grapefruit,Grapefruit -335,Game board,Game board -336,Mop,Mop -337,Radish,Radish -338,Baozi,Baozi -339,Target,Target -340,French,French -341,Spring Rolls,Spring Rolls -342,Monkey,Monkey -343,Rabbit,Rabbit -344,Pencil Case,Pencil Case -345,Yak,Yak -346,Red Cabbage,Red Cabbage -347,Binoculars,Binoculars -348,Asparagus,Asparagus -349,Barbell,Barbell -350,Scallop,Scallop -351,Noddles,Noddles -352,Comb,Comb -353,Dumpling,Dumpling -354,Oyster,Oyster -355,Table Teniis paddle,Table Tennis paddle -356,Cosmetics Brush/Eyeliner Pencil,Cosmetics Brush/Eyeliner Pencil -357,Chainsaw,Chainsaw -358,Eraser,Eraser -359,Lobster,Lobster -360,Durian,Durian -361,Okra,Okra -362,Lipstick,Lipstick -363,Cosmetics Mirror,Cosmetics Mirror -364,Curling,Curling -365,Table Tennis,Table Tennis \ No newline at end of file diff --git a/dimos/models/Detic/datasets/metadata/coco_clip_a+cname.npy b/dimos/models/Detic/datasets/metadata/coco_clip_a+cname.npy deleted file mode 100644 index 63b938afaf..0000000000 Binary files a/dimos/models/Detic/datasets/metadata/coco_clip_a+cname.npy and /dev/null differ diff --git a/dimos/models/Detic/datasets/metadata/imagenet_lvis_wnid.txt b/dimos/models/Detic/datasets/metadata/imagenet_lvis_wnid.txt deleted file mode 100644 index 8433aa01af..0000000000 --- a/dimos/models/Detic/datasets/metadata/imagenet_lvis_wnid.txt +++ /dev/null @@ -1,997 +0,0 @@ -n02682922 -n02686379 -n02691156 -n02694662 -n07884567 -n01698434 -n07750586 -n02701002 -n02705944 -n02715229 -n07739125 -n07825850 -n07750872 -n02730930 -n02732072 -n02735538 -n02738449 -n02738535 -n02739550 -n02739668 -n07718747 -n02747177 -n02747802 -n07719213 -n02754103 -n07764847 -n02763901 -n02764044 -n02486410 -n02766534 -n02768226 -n02769748 -n02774152 -n02773838 -n07693725 -n02775483 -n07687381 -n02776205 -n02779435 -n02780815 -n02782093 -n12147226 -n07753592 -n02786058 -n02785648 -n02786198 -n02787622 -n02788021 -n02790996 -n02792552 -n02795169 -n02796318 -n02797295 -n02797881 -n02799071 -n02799175 -n02799323 -n02800213 -n02801938 -n02802426 -n02804252 -n02139199 -n02808304 -n02807616 -n02808440 -n07860805 -n02810471 -n07709881 -n02816656 -n02816768 -n02131653 -n02818832 -n02821202 -n02822220 -n02404186 -n02823124 -n02823428 -n02823510 -n02164464 -n02824448 -n07720875 -n02827606 -n02828299 -n02828884 -n02831237 -n02834778 -n02838728 -n02839110 -n02840245 -n02841315 -n01503061 -n02843553 -n02843158 -n02843276 -n02843684 -n02413050 -n07744811 -n02846511 -n02849154 -n02850358 -n02850732 -n02850950 -n02852173 -n02854926 -n07743544 -n02858304 -n02860415 -n02860640 -n07841495 -n02865351 -n02865931 -n02865665 -n02869837 -n02870880 -n02871147 -n02871824 -n02872752 -n02876657 -n02877962 -n02879087 -n02879718 -n02883205 -n02880940 -n02881757 -n02882301 -n02883344 -n02885462 -n02887489 -n02887970 -n02892201 -n02892767 -n02893692 -n07679356 -n02896294 -n02898585 -n02900705 -n11876803 -n02906734 -n07715221 -n07600285 -n02909870 -n02912557 -n01887623 -n02108672 -n02916179 -n02917067 -n02916936 -n02917377 -n07680932 -n02920259 -n07880968 -n02924116 -n07848338 -n02274259 -n02928608 -n02930766 -n02931294 -n02932523 -n02933112 -n02933462 -n02938886 -n01887896 -n02942349 -n02437136 -n02942699 -n02943241 -n02946348 -n02946921 -n02951585 -n02948072 -n02948557 -n07598256 -n07601572 -n02949202 -n02949542 -n02951358 -n07755929 -n02952374 -n02954340 -n02954938 -n02955767 -n07920349 -n02958343 -n02959942 -n02960352 -n02961225 -n02963159 -n02965300 -n11808468 -n02968473 -n02970408 -n02970849 -n02971356 -n02977438 -n07580359 -n02978881 -n02979836 -n02121620 -n07715103 -n07822518 -n02988304 -n02992529 -n03000247 -n03001627 -n03002711 -n03002948 -n03005285 -n03006903 -n07757132 -n01791625 -n12515925 -n07721456 -n03017168 -n07712559 -n03020416 -n07921360 -n07617611 -n07921455 -n03030353 -n03031012 -n03035715 -n03037709 -n03038281 -n03041114 -n12710415 -n03043958 -n03045074 -n03045337 -n03046257 -n03047052 -n03050864 -n03051249 -n03055418 -n03057021 -n03057920 -n03059103 -n01792158 -n02233338 -n07922764 -n07772935 -n03063338 -n03063968 -n03063689 -n03066849 -n07808587 -n03075370 -n03075768 -n06596364 -n03080497 -n03085013 -n07810907 -n03096960 -n03100240 -n03100346 -n03101156 -n03101986 -n03102654 -n03108853 -n03109150 -n07731952 -n07687789 -n03110669 -n03111296 -n07568095 -n03112869 -n03113835 -n02125311 -n03121897 -n03123917 -n03124170 -n01976957 -n07681926 -n03127925 -n03128248 -n03129001 -n07691650 -n03131574 -n03133415 -n03135917 -n07682197 -n01579028 -n03138344 -n03138669 -n03140292 -n03141327 -n03141065 -n03141823 -n01322685 -n07718472 -n03147509 -n03148324 -n03150232 -n03150511 -n03151077 -n03156279 -n03157348 -n03158885 -n02110341 -n07765073 -n03168217 -n02430045 -n03175843 -n03179701 -n03188531 -n03199901 -n03201208 -n03201776 -n03206908 -n03207305 -n03207743 -n03207835 -n03207941 -n03210683 -n03216710 -n02084071 -n03219135 -n03219483 -n02068974 -n02389559 -n03223299 -n07639069 -n01812337 -n02268443 -n03233905 -n03234164 -n03236735 -n03237416 -n03239054 -n03237340 -n03239726 -n03245889 -n03247083 -n03249569 -n03250847 -n01846331 -n01847170 -n03253886 -n03255030 -n03256032 -n03259009 -n01613294 -n03261776 -n03262248 -n03262809 -n07840804 -n07866723 -n07841345 -n03266371 -n07713074 -n03271030 -n03273913 -n02503517 -n02432983 -n03291819 -n03294833 -n03309356 -n01610955 -n03320046 -n03325088 -n03325941 -n02443346 -n03329302 -n03329663 -n07753113 -n03335030 -n03337140 -n03336839 -n03343737 -n03345487 -n03345837 -n03346455 -n03349469 -n02512053 -n03350204 -n03351979 -n03354903 -n03355925 -n02007558 -n03356982 -n03358172 -n03359137 -n03362639 -n03364008 -n03364156 -n03372549 -n02376542 -n03376595 -n03378174 -n03378765 -n03379051 -n03380724 -n03384352 -n03393912 -n07868200 -n03397947 -n01639765 -n07924033 -n03400231 -n07605474 -n03403643 -n03408444 -n03410740 -n03416900 -n03417042 -n07818277 -n03424325 -n02423022 -n07643981 -n03433877 -n02510455 -n07814925 -n02439033 -n03438071 -n03438257 -n03441112 -n02416519 -n03443912 -n01443537 -n03446070 -n03445924 -n03447447 -n01855672 -n02480855 -n12158031 -n07758680 -n03454885 -n03455488 -n03456024 -n07722485 -n03459328 -n03459591 -n02132580 -n03461288 -n03467517 -n02041246 -n03467984 -n03475581 -n03475961 -n03476313 -n03480579 -n07697100 -n03481172 -n03482252 -n03482405 -n02342885 -n03483316 -n03485198 -n03490006 -n03484083 -n03484576 -n03485794 -n03488188 -n03494537 -n03497657 -n03498441 -n03502331 -n03502200 -n03503997 -n03505504 -n03505667 -n03506028 -n03508101 -n03512147 -n03513137 -n02008041 -n03518445 -n03521076 -n02398521 -n03524150 -n02395406 -n03528901 -n07858978 -n03531546 -n03532342 -n03533014 -n02213107 -n02374451 -n03541923 -n03543254 -n07830593 -n03544143 -n03545470 -n01833805 -n07857731 -n02134084 -n07614500 -n07615774 -n03557692 -n03557840 -n03558404 -n03571280 -n03584254 -n03584829 -n03589791 -n07642933 -n03593526 -n03594734 -n03594945 -n07606669 -n03595614 -n03595860 -n03602883 -n03605598 -n03609235 -n03610418 -n03610524 -n03612814 -n03613294 -n03617312 -n03617480 -n03620967 -n02122948 -n07763629 -n03623198 -n03623556 -n03625646 -n03626760 -n01882714 -n03630383 -n03633091 -n02165456 -n02412440 -n03636649 -n03637181 -n03637318 -n03640988 -n03642806 -n07870167 -n03644858 -n03649909 -n03655072 -n11748002 -n07749582 -n07926250 -n03662719 -n03662887 -n03665924 -n03668067 -n07749731 -n03670208 -n02129165 -n07901587 -n01674464 -n07607605 -n03691459 -n03693474 -n03701391 -n03705379 -n03710193 -n01847806 -n03715892 -n02504770 -n02073831 -n07747951 -n03717131 -n03717447 -n03720163 -n03722007 -n07916041 -n10297234 -n07711569 -n03724417 -n03725035 -n03726760 -n03727946 -n03729402 -n03733805 -n03735637 -n07871436 -n07755411 -n03759954 -n03760671 -n03761084 -n07844042 -n03764736 -n03770679 -n07606278 -n03773035 -n03775071 -n03775199 -n03782190 -n02484322 -n03789946 -n03791053 -n03791235 -n03790512 -n03792334 -n03793489 -n07690273 -n03797390 -n13000891 -n03801880 -n03800933 -n03805280 -n03814817 -n03814906 -n03815615 -n03816136 -n06267145 -n03822656 -n03825080 -n03831203 -n03831382 -n03836602 -n03837422 -n01970164 -n03844045 -n07842753 -n12433081 -n07747607 -n07924834 -n01518878 -n03858418 -n03862676 -n03863108 -n01621127 -n03871628 -n03873416 -n03874599 -n03876231 -n03877472 -n03878674 -n03880531 -n03880323 -n03885904 -n07762244 -n03887697 -n03888257 -n01821203 -n03889726 -n03889871 -n03891051 -n03891332 -n01816887 -n03895866 -n03896103 -n07663899 -n07725376 -n07751004 -n07855510 -n07767847 -n03904909 -n03906106 -n03906224 -n02051845 -n03906997 -n03908204 -n03908618 -n03908714 -n03909160 -n02055803 -n07815588 -n03914337 -n03916031 -n07746186 -n00007846 -n01318894 -n03920867 -n03924069 -n03928116 -n07824988 -n03930630 -n01811909 -n03935335 -n03938244 -n03940256 -n07753275 -n03942813 -n03944138 -n03948459 -n07683617 -n03950228 -n03950359 -n07873807 -n03963198 -n03964495 -n03966976 -n03967562 -n03973839 -n03973628 -n03975926 -n03976657 -n03978966 -n03980874 -n02382437 -n03982430 -n07927512 -n03990474 -n03991062 -n07710616 -n03992703 -n03993180 -n03996416 -n07695742 -n04004475 -n04008634 -n04009552 -n04011827 -n07752602 -n07617188 -n02655020 -n02047614 -n02110958 -n07735510 -n04023249 -n01322604 -n07881205 -n04033995 -n02324045 -n04037443 -n04039381 -n04039848 -n04040759 -n04043733 -n04045397 -n04049405 -n02412080 -n07745466 -n02331046 -n04057215 -n04059516 -n04059947 -n04062428 -n04064401 -n04069276 -n04074963 -n02391994 -n04090263 -n04095210 -n04097866 -n04099969 -n02329401 -n04102618 -n04102162 -n04103206 -n07928887 -n04114844 -n04116098 -n04122825 -n04123740 -n04124202 -n04124098 -n04127249 -n04127904 -n07806221 -n02534734 -n07823460 -n04131690 -n04133789 -n07695965 -n04137217 -n04138977 -n04140631 -n04141076 -n04141975 -n04143897 -n04146614 -n04148054 -n04149813 -n04150980 -n04154565 -n04156140 -n04157320 -n02021795 -n01456756 -n04160586 -n01956764 -n04179913 -n04183329 -n01482330 -n04185071 -n04185529 -n04185804 -n04186051 -n04186455 -n04186848 -n02411705 -n02104523 -n07615289 -n04192698 -n04197391 -n04199027 -n04204081 -n04204347 -n04205318 -n04206225 -n04207343 -n04208210 -n04208936 -n04209133 -n04209239 -n04210120 -n04217882 -n04220250 -n04225987 -n04227900 -n04228054 -n04228581 -n04230387 -n04230603 -n04230808 -n04232153 -n04235291 -n04235860 -n04239436 -n04241394 -n07914271 -n01726692 -n04251791 -n04252077 -n04254680 -n04254777 -n04256520 -n04256891 -n04257790 -n04259630 -n07583197 -n04263257 -n04263502 -n07848093 -n07844867 -n04266014 -n04269944 -n04270891 -n04272054 -n04275175 -n01772222 -n01984695 -n04284002 -n04285803 -n04286575 -n02355227 -n04297098 -n04303497 -n02317335 -n04306847 -n04307986 -n04313503 -n04315713 -n04315948 -n07588947 -n04320871 -n04320973 -n04326896 -n04330340 -n04332243 -n04333129 -n07745940 -n06794110 -n04335886 -n07854707 -n04346511 -n04349401 -n04350581 -n04350905 -n11978233 -n04356056 -n04356595 -n07879450 -n04367480 -n04370288 -n04370048 -n04370456 -n07712063 -n04371563 -n04373894 -n04376876 -n07826091 -n04381587 -n04379243 -n04380533 -n04382880 -n07880751 -n04384910 -n04387400 -n04389033 -n04388743 -n04390577 -n04392113 -n04393549 -n04395024 -n04395106 -n07933154 -n04397452 -n04397768 -n04398044 -n04401088 -n04401680 -n04402449 -n04403413 -n04404997 -n04405907 -n04409515 -n04409806 -n07905979 -n04421872 -n04422727 -n04422875 -n04423845 -n04431745 -n04432203 -n02129604 -n04434932 -n04438304 -n04439712 -n07686873 -n04442312 -n04442441 -n15075141 -n07734017 -n04450749 -n04452615 -n04453156 -n04453390 -n04453910 -n04461696 -n04459362 -n04459773 -n04461879 -n04465501 -n06874185 -n04466871 -n04467665 -n04468005 -n04469514 -n04476259 -n04479046 -n04480853 -n04482393 -n04485082 -n04489008 -n04490091 -n07609632 -n04491769 -n04493381 -n04498389 -n11877646 -n01662784 -n04502197 -n04505036 -n04507155 -n04508949 -n04509417 -n04516116 -n04517823 -n04522168 -n04525305 -n04531873 -n04534520 -n07828987 -n04536866 -n07906111 -n04540053 -n01616318 -n04542943 -n04543158 -n04543772 -n04546194 -n04548280 -n04548362 -n02081571 -n04550184 -n04554684 -n04555897 -n04557648 -n04559166 -n04559451 -n04560113 -n04560804 -n04562122 -n04562262 -n04562935 -n04560292 -n07756951 -n04568069 -n04569063 -n04574067 -n04574999 -n04576002 -n04579667 -n04584207 -n04587559 -n04589325 -n04590746 -n04591713 -n04591887 -n04592099 -n04593629 -n04596742 -n02114100 -n04597913 -n04606574 -n04610013 -n07849336 -n04612840 -n02391049 -n07716358 diff --git a/dimos/models/Detic/datasets/metadata/lvis_v1_clip_a+cname.npy b/dimos/models/Detic/datasets/metadata/lvis_v1_clip_a+cname.npy deleted file mode 100644 index a9e5376ee4..0000000000 Binary files a/dimos/models/Detic/datasets/metadata/lvis_v1_clip_a+cname.npy and /dev/null differ diff --git a/dimos/models/Detic/datasets/metadata/lvis_v1_train_cat_info.json b/dimos/models/Detic/datasets/metadata/lvis_v1_train_cat_info.json deleted file mode 100644 index 95fef09233..0000000000 --- a/dimos/models/Detic/datasets/metadata/lvis_v1_train_cat_info.json +++ /dev/null @@ -1 +0,0 @@ -[{"name": "aerosol_can", "instance_count": 109, "def": "a dispenser that holds a substance under pressure", "synonyms": ["aerosol_can", "spray_can"], "image_count": 64, "id": 1, "frequency": "c", "synset": "aerosol.n.02"}, {"name": "air_conditioner", "instance_count": 1081, "def": "a machine that keeps air cool and dry", "synonyms": ["air_conditioner"], "image_count": 364, "id": 2, "frequency": "f", "synset": "air_conditioner.n.01"}, {"name": "airplane", "instance_count": 3720, "def": "an aircraft that has a fixed wing and is powered by propellers or jets", "synonyms": ["airplane", "aeroplane"], "image_count": 1911, "id": 3, "frequency": "f", "synset": "airplane.n.01"}, {"name": "alarm_clock", "instance_count": 158, "def": "a clock that wakes a sleeper at some preset time", "synonyms": ["alarm_clock"], "image_count": 149, "id": 4, "frequency": "f", "synset": "alarm_clock.n.01"}, {"name": "alcohol", "instance_count": 207, "def": "a liquor or brew containing alcohol as the active agent", "synonyms": ["alcohol", "alcoholic_beverage"], "image_count": 29, "id": 5, "frequency": "c", "synset": "alcohol.n.01"}, {"name": "alligator", "instance_count": 39, "def": "amphibious reptiles related to crocodiles but with shorter broader snouts", "synonyms": ["alligator", "gator"], "image_count": 26, "id": 6, "frequency": "c", "synset": "alligator.n.02"}, {"name": "almond", "instance_count": 1700, "def": "oval-shaped edible seed of the almond tree", "synonyms": ["almond"], "image_count": 59, "id": 7, "frequency": "c", "synset": "almond.n.02"}, {"name": "ambulance", "instance_count": 25, "def": "a vehicle that takes people to and from hospitals", "synonyms": ["ambulance"], "image_count": 22, "id": 8, "frequency": "c", "synset": "ambulance.n.01"}, {"name": "amplifier", "instance_count": 16, "def": "electronic equipment that increases strength of signals", "synonyms": ["amplifier"], "image_count": 12, "id": 9, "frequency": "c", "synset": "amplifier.n.01"}, {"name": "anklet", "instance_count": 39, "def": "an ornament worn around the ankle", "synonyms": ["anklet", "ankle_bracelet"], "image_count": 28, "id": 10, "frequency": "c", "synset": "anklet.n.03"}, {"name": "antenna", "instance_count": 1018, "def": "an electrical device that sends or receives radio or television signals", "synonyms": ["antenna", "aerial", "transmitting_aerial"], "image_count": 505, "id": 11, "frequency": "f", "synset": "antenna.n.01"}, {"name": "apple", "instance_count": 17451, "def": "fruit with red or yellow or green skin and sweet to tart crisp whitish flesh", "synonyms": ["apple"], "image_count": 1207, "id": 12, "frequency": "f", "synset": "apple.n.01"}, {"name": "applesauce", "instance_count": 7, "def": "puree of stewed apples usually sweetened and spiced", "synonyms": ["applesauce"], "image_count": 4, "id": 13, "frequency": "r", "synset": "applesauce.n.01"}, {"name": "apricot", "instance_count": 62, "def": "downy yellow to rosy-colored fruit resembling a small peach", "synonyms": ["apricot"], "image_count": 10, "id": 14, "frequency": "r", "synset": "apricot.n.02"}, {"name": "apron", "instance_count": 881, "def": "a garment of cloth that is tied about the waist and worn to protect clothing", "synonyms": ["apron"], "image_count": 500, "id": 15, "frequency": "f", "synset": "apron.n.01"}, {"name": "aquarium", "instance_count": 36, "def": "a tank/pool/bowl filled with water for keeping live fish and underwater animals", "synonyms": ["aquarium", "fish_tank"], "image_count": 33, "id": 16, "frequency": "c", "synset": "aquarium.n.01"}, {"name": "arctic_(type_of_shoe)", "instance_count": 8, "def": "a waterproof overshoe that protects shoes from water or snow", "synonyms": ["arctic_(type_of_shoe)", "galosh", "golosh", "rubber_(type_of_shoe)", "gumshoe"], "image_count": 3, "id": 17, "frequency": "r", "synset": "arctic.n.02"}, {"name": "armband", "instance_count": 85, "def": "a band worn around the upper arm", "synonyms": ["armband"], "image_count": 44, "id": 18, "frequency": "c", "synset": "armband.n.02"}, {"name": "armchair", "instance_count": 1112, "def": "chair with a support on each side for arms", "synonyms": ["armchair"], "image_count": 561, "id": 19, "frequency": "f", "synset": "armchair.n.01"}, {"name": "armoire", "instance_count": 11, "def": "a large wardrobe or cabinet", "synonyms": ["armoire"], "image_count": 8, "id": 20, "frequency": "r", "synset": "armoire.n.01"}, {"name": "armor", "instance_count": 23, "def": "protective covering made of metal and used in combat", "synonyms": ["armor", "armour"], "image_count": 9, "id": 21, "frequency": "r", "synset": "armor.n.01"}, {"name": "artichoke", "instance_count": 293, "def": "a thistlelike flower head with edible fleshy leaves and heart", "synonyms": ["artichoke"], "image_count": 33, "id": 22, "frequency": "c", "synset": "artichoke.n.02"}, {"name": "trash_can", "instance_count": 2722, "def": "a bin that holds rubbish until it is collected", "synonyms": ["trash_can", "garbage_can", "wastebin", "dustbin", "trash_barrel", "trash_bin"], "image_count": 1883, "id": 23, "frequency": "f", "synset": "ashcan.n.01"}, {"name": "ashtray", "instance_count": 136, "def": "a receptacle for the ash from smokers' cigars or cigarettes", "synonyms": ["ashtray"], "image_count": 98, "id": 24, "frequency": "c", "synset": "ashtray.n.01"}, {"name": "asparagus", "instance_count": 969, "def": "edible young shoots of the asparagus plant", "synonyms": ["asparagus"], "image_count": 70, "id": 25, "frequency": "c", "synset": "asparagus.n.02"}, {"name": "atomizer", "instance_count": 67, "def": "a dispenser that turns a liquid (such as perfume) into a fine mist", "synonyms": ["atomizer", "atomiser", "spray", "sprayer", "nebulizer", "nebuliser"], "image_count": 46, "id": 26, "frequency": "c", "synset": "atomizer.n.01"}, {"name": "avocado", "instance_count": 1048, "def": "a pear-shaped fruit with green or blackish skin and rich yellowish pulp enclosing a single large seed", "synonyms": ["avocado"], "image_count": 117, "id": 27, "frequency": "f", "synset": "avocado.n.01"}, {"name": "award", "instance_count": 163, "def": "a tangible symbol signifying approval or distinction", "synonyms": ["award", "accolade"], "image_count": 41, "id": 28, "frequency": "c", "synset": "award.n.02"}, {"name": "awning", "instance_count": 4270, "def": "a canopy made of canvas to shelter people or things from rain or sun", "synonyms": ["awning"], "image_count": 1395, "id": 29, "frequency": "f", "synset": "awning.n.01"}, {"name": "ax", "instance_count": 8, "def": "an edge tool with a heavy bladed head mounted across a handle", "synonyms": ["ax", "axe"], "image_count": 7, "id": 30, "frequency": "r", "synset": "ax.n.01"}, {"name": "baboon", "instance_count": 3, "def": "large terrestrial monkeys having doglike muzzles", "synonyms": ["baboon"], "image_count": 1, "id": 31, "frequency": "r", "synset": "baboon.n.01"}, {"name": "baby_buggy", "instance_count": 447, "def": "a small vehicle with four wheels in which a baby or child is pushed around", "synonyms": ["baby_buggy", "baby_carriage", "perambulator", "pram", "stroller"], "image_count": 314, "id": 32, "frequency": "f", "synset": "baby_buggy.n.01"}, {"name": "basketball_backboard", "instance_count": 42, "def": "a raised vertical board with basket attached; used to play basketball", "synonyms": ["basketball_backboard"], "image_count": 31, "id": 33, "frequency": "c", "synset": "backboard.n.01"}, {"name": "backpack", "instance_count": 3907, "def": "a bag carried by a strap on your back or shoulder", "synonyms": ["backpack", "knapsack", "packsack", "rucksack", "haversack"], "image_count": 1905, "id": 34, "frequency": "f", "synset": "backpack.n.01"}, {"name": "handbag", "instance_count": 3947, "def": "a container used for carrying money and small personal items or accessories", "synonyms": ["handbag", "purse", "pocketbook"], "image_count": 1859, "id": 35, "frequency": "f", "synset": "bag.n.04"}, {"name": "suitcase", "instance_count": 8537, "def": "cases used to carry belongings when traveling", "synonyms": ["suitcase", "baggage", "luggage"], "image_count": 1623, "id": 36, "frequency": "f", "synset": "bag.n.06"}, {"name": "bagel", "instance_count": 372, "def": "glazed yeast-raised doughnut-shaped roll with hard crust", "synonyms": ["bagel", "beigel"], "image_count": 47, "id": 37, "frequency": "c", "synset": "bagel.n.01"}, {"name": "bagpipe", "instance_count": 6, "def": "a tubular wind instrument; the player blows air into a bag and squeezes it out", "synonyms": ["bagpipe"], "image_count": 3, "id": 38, "frequency": "r", "synset": "bagpipe.n.01"}, {"name": "baguet", "instance_count": 9, "def": "narrow French stick loaf", "synonyms": ["baguet", "baguette"], "image_count": 3, "id": 39, "frequency": "r", "synset": "baguet.n.01"}, {"name": "bait", "instance_count": 1, "def": "something used to lure fish or other animals into danger so they can be trapped or killed", "synonyms": ["bait", "lure"], "image_count": 1, "id": 40, "frequency": "r", "synset": "bait.n.02"}, {"name": "ball", "instance_count": 755, "def": "a spherical object used as a plaything", "synonyms": ["ball"], "image_count": 305, "id": 41, "frequency": "f", "synset": "ball.n.06"}, {"name": "ballet_skirt", "instance_count": 12, "def": "very short skirt worn by ballerinas", "synonyms": ["ballet_skirt", "tutu"], "image_count": 6, "id": 42, "frequency": "r", "synset": "ballet_skirt.n.01"}, {"name": "balloon", "instance_count": 1556, "def": "large tough nonrigid bag filled with gas or heated air", "synonyms": ["balloon"], "image_count": 210, "id": 43, "frequency": "f", "synset": "balloon.n.01"}, {"name": "bamboo", "instance_count": 243, "def": "woody tropical grass having hollow woody stems", "synonyms": ["bamboo"], "image_count": 36, "id": 44, "frequency": "c", "synset": "bamboo.n.02"}, {"name": "banana", "instance_count": 50552, "def": "elongated crescent-shaped yellow fruit with soft sweet flesh", "synonyms": ["banana"], "image_count": 1787, "id": 45, "frequency": "f", "synset": "banana.n.02"}, {"name": "Band_Aid", "instance_count": 19, "def": "trade name for an adhesive bandage to cover small cuts or blisters", "synonyms": ["Band_Aid"], "image_count": 17, "id": 46, "frequency": "c", "synset": "band_aid.n.01"}, {"name": "bandage", "instance_count": 92, "def": "a piece of soft material that covers and protects an injured part of the body", "synonyms": ["bandage"], "image_count": 51, "id": 47, "frequency": "c", "synset": "bandage.n.01"}, {"name": "bandanna", "instance_count": 219, "def": "large and brightly colored handkerchief; often used as a neckerchief", "synonyms": ["bandanna", "bandana"], "image_count": 138, "id": 48, "frequency": "f", "synset": "bandanna.n.01"}, {"name": "banjo", "instance_count": 3, "def": "a stringed instrument of the guitar family with a long neck and circular body", "synonyms": ["banjo"], "image_count": 3, "id": 49, "frequency": "r", "synset": "banjo.n.01"}, {"name": "banner", "instance_count": 5907, "def": "long strip of cloth or paper used for decoration or advertising", "synonyms": ["banner", "streamer"], "image_count": 1470, "id": 50, "frequency": "f", "synset": "banner.n.01"}, {"name": "barbell", "instance_count": 4, "def": "a bar to which heavy discs are attached at each end; used in weightlifting", "synonyms": ["barbell"], "image_count": 3, "id": 51, "frequency": "r", "synset": "barbell.n.01"}, {"name": "barge", "instance_count": 3, "def": "a flatbottom boat for carrying heavy loads (especially on canals)", "synonyms": ["barge"], "image_count": 2, "id": 52, "frequency": "r", "synset": "barge.n.01"}, {"name": "barrel", "instance_count": 707, "def": "a cylindrical container that holds liquids", "synonyms": ["barrel", "cask"], "image_count": 186, "id": 53, "frequency": "f", "synset": "barrel.n.02"}, {"name": "barrette", "instance_count": 119, "def": "a pin for holding women's hair in place", "synonyms": ["barrette"], "image_count": 76, "id": 54, "frequency": "c", "synset": "barrette.n.01"}, {"name": "barrow", "instance_count": 30, "def": "a cart for carrying small loads; has handles and one or more wheels", "synonyms": ["barrow", "garden_cart", "lawn_cart", "wheelbarrow"], "image_count": 26, "id": 55, "frequency": "c", "synset": "barrow.n.03"}, {"name": "baseball_base", "instance_count": 404, "def": "a place that the runner must touch before scoring", "synonyms": ["baseball_base"], "image_count": 303, "id": 56, "frequency": "f", "synset": "base.n.03"}, {"name": "baseball", "instance_count": 1013, "def": "a ball used in playing baseball", "synonyms": ["baseball"], "image_count": 738, "id": 57, "frequency": "f", "synset": "baseball.n.02"}, {"name": "baseball_bat", "instance_count": 2698, "def": "an implement used in baseball by the batter", "synonyms": ["baseball_bat"], "image_count": 1799, "id": 58, "frequency": "f", "synset": "baseball_bat.n.01"}, {"name": "baseball_cap", "instance_count": 9028, "def": "a cap with a bill", "synonyms": ["baseball_cap", "jockey_cap", "golf_cap"], "image_count": 1934, "id": 59, "frequency": "f", "synset": "baseball_cap.n.01"}, {"name": "baseball_glove", "instance_count": 2536, "def": "the handwear used by fielders in playing baseball", "synonyms": ["baseball_glove", "baseball_mitt"], "image_count": 1609, "id": 60, "frequency": "f", "synset": "baseball_glove.n.01"}, {"name": "basket", "instance_count": 3984, "def": "a container that is usually woven and has handles", "synonyms": ["basket", "handbasket"], "image_count": 1622, "id": 61, "frequency": "f", "synset": "basket.n.01"}, {"name": "basketball", "instance_count": 56, "def": "an inflated ball used in playing basketball", "synonyms": ["basketball"], "image_count": 41, "id": 62, "frequency": "c", "synset": "basketball.n.02"}, {"name": "bass_horn", "instance_count": 6, "def": "the lowest brass wind instrument", "synonyms": ["bass_horn", "sousaphone", "tuba"], "image_count": 4, "id": 63, "frequency": "r", "synset": "bass_horn.n.01"}, {"name": "bat_(animal)", "instance_count": 47, "def": "nocturnal mouselike mammal with forelimbs modified to form membranous wings", "synonyms": ["bat_(animal)"], "image_count": 11, "id": 64, "frequency": "c", "synset": "bat.n.01"}, {"name": "bath_mat", "instance_count": 336, "def": "a heavy towel or mat to stand on while drying yourself after a bath", "synonyms": ["bath_mat"], "image_count": 270, "id": 65, "frequency": "f", "synset": "bath_mat.n.01"}, {"name": "bath_towel", "instance_count": 1210, "def": "a large towel; to dry yourself after a bath", "synonyms": ["bath_towel"], "image_count": 349, "id": 66, "frequency": "f", "synset": "bath_towel.n.01"}, {"name": "bathrobe", "instance_count": 53, "def": "a loose-fitting robe of towelling; worn after a bath or swim", "synonyms": ["bathrobe"], "image_count": 42, "id": 67, "frequency": "c", "synset": "bathrobe.n.01"}, {"name": "bathtub", "instance_count": 868, "def": "a large open container that you fill with water and use to wash the body", "synonyms": ["bathtub", "bathing_tub"], "image_count": 823, "id": 68, "frequency": "f", "synset": "bathtub.n.01"}, {"name": "batter_(food)", "instance_count": 26, "def": "a liquid or semiliquid mixture, as of flour, eggs, and milk, used in cooking", "synonyms": ["batter_(food)"], "image_count": 6, "id": 69, "frequency": "r", "synset": "batter.n.02"}, {"name": "battery", "instance_count": 155, "def": "a portable device that produces electricity", "synonyms": ["battery"], "image_count": 48, "id": 70, "frequency": "c", "synset": "battery.n.02"}, {"name": "beachball", "instance_count": 3, "def": "large and light ball; for play at the seaside", "synonyms": ["beachball"], "image_count": 3, "id": 71, "frequency": "r", "synset": "beach_ball.n.01"}, {"name": "bead", "instance_count": 1371, "def": "a small ball with a hole through the middle used for ornamentation, jewellery, etc.", "synonyms": ["bead"], "image_count": 42, "id": 72, "frequency": "c", "synset": "bead.n.01"}, {"name": "bean_curd", "instance_count": 231, "def": "cheeselike food made of curdled soybean milk", "synonyms": ["bean_curd", "tofu"], "image_count": 24, "id": 73, "frequency": "c", "synset": "bean_curd.n.01"}, {"name": "beanbag", "instance_count": 20, "def": "a bag filled with dried beans or similar items; used in games or to sit on", "synonyms": ["beanbag"], "image_count": 16, "id": 74, "frequency": "c", "synset": "beanbag.n.01"}, {"name": "beanie", "instance_count": 1907, "def": "a small skullcap; formerly worn by schoolboys and college freshmen", "synonyms": ["beanie", "beany"], "image_count": 605, "id": 75, "frequency": "f", "synset": "beanie.n.01"}, {"name": "bear", "instance_count": 1069, "def": "large carnivorous or omnivorous mammals with shaggy coats and claws", "synonyms": ["bear"], "image_count": 646, "id": 76, "frequency": "f", "synset": "bear.n.01"}, {"name": "bed", "instance_count": 2137, "def": "a piece of furniture that provides a place to sleep", "synonyms": ["bed"], "image_count": 1765, "id": 77, "frequency": "f", "synset": "bed.n.01"}, {"name": "bedpan", "instance_count": 2, "def": "a shallow vessel used by a bedridden patient for defecation and urination", "synonyms": ["bedpan"], "image_count": 2, "id": 78, "frequency": "r", "synset": "bedpan.n.01"}, {"name": "bedspread", "instance_count": 188, "def": "decorative cover for a bed", "synonyms": ["bedspread", "bedcover", "bed_covering", "counterpane", "spread"], "image_count": 125, "id": 79, "frequency": "f", "synset": "bedspread.n.01"}, {"name": "cow", "instance_count": 8085, "def": "cattle/cow", "synonyms": ["cow"], "image_count": 1420, "id": 80, "frequency": "f", "synset": "beef.n.01"}, {"name": "beef_(food)", "instance_count": 1242, "def": "meat from an adult domestic bovine", "synonyms": ["beef_(food)", "boeuf_(food)"], "image_count": 140, "id": 81, "frequency": "f", "synset": "beef.n.02"}, {"name": "beeper", "instance_count": 4, "def": "an device that beeps when the person carrying it is being paged", "synonyms": ["beeper", "pager"], "image_count": 4, "id": 82, "frequency": "r", "synset": "beeper.n.01"}, {"name": "beer_bottle", "instance_count": 1227, "def": "a bottle that holds beer", "synonyms": ["beer_bottle"], "image_count": 322, "id": 83, "frequency": "f", "synset": "beer_bottle.n.01"}, {"name": "beer_can", "instance_count": 203, "def": "a can that holds beer", "synonyms": ["beer_can"], "image_count": 60, "id": 84, "frequency": "c", "synset": "beer_can.n.01"}, {"name": "beetle", "instance_count": 9, "def": "insect with hard wing covers", "synonyms": ["beetle"], "image_count": 2, "id": 85, "frequency": "r", "synset": "beetle.n.01"}, {"name": "bell", "instance_count": 590, "def": "a hollow device made of metal that makes a ringing sound when struck", "synonyms": ["bell"], "image_count": 231, "id": 86, "frequency": "f", "synset": "bell.n.01"}, {"name": "bell_pepper", "instance_count": 4369, "def": "large bell-shaped sweet pepper in green or red or yellow or orange or black varieties", "synonyms": ["bell_pepper", "capsicum"], "image_count": 333, "id": 87, "frequency": "f", "synset": "bell_pepper.n.02"}, {"name": "belt", "instance_count": 3683, "def": "a band to tie or buckle around the body (usually at the waist)", "synonyms": ["belt"], "image_count": 1941, "id": 88, "frequency": "f", "synset": "belt.n.02"}, {"name": "belt_buckle", "instance_count": 589, "def": "the buckle used to fasten a belt", "synonyms": ["belt_buckle"], "image_count": 367, "id": 89, "frequency": "f", "synset": "belt_buckle.n.01"}, {"name": "bench", "instance_count": 4374, "def": "a long seat for more than one person", "synonyms": ["bench"], "image_count": 1922, "id": 90, "frequency": "f", "synset": "bench.n.01"}, {"name": "beret", "instance_count": 57, "def": "a cap with no brim or bill; made of soft cloth", "synonyms": ["beret"], "image_count": 18, "id": 91, "frequency": "c", "synset": "beret.n.01"}, {"name": "bib", "instance_count": 96, "def": "a napkin tied under the chin of a child while eating", "synonyms": ["bib"], "image_count": 81, "id": 92, "frequency": "c", "synset": "bib.n.02"}, {"name": "Bible", "instance_count": 2, "def": "the sacred writings of the Christian religions", "synonyms": ["Bible"], "image_count": 1, "id": 93, "frequency": "r", "synset": "bible.n.01"}, {"name": "bicycle", "instance_count": 4566, "def": "a wheeled vehicle that has two wheels and is moved by foot pedals", "synonyms": ["bicycle", "bike_(bicycle)"], "image_count": 1852, "id": 94, "frequency": "f", "synset": "bicycle.n.01"}, {"name": "visor", "instance_count": 777, "def": "a brim that projects to the front to shade the eyes", "synonyms": ["visor", "vizor"], "image_count": 430, "id": 95, "frequency": "f", "synset": "bill.n.09"}, {"name": "billboard", "instance_count": 1025, "def": "large outdoor signboard", "synonyms": ["billboard"], "image_count": 247, "id": 96, "frequency": "f", "synset": "billboard.n.01"}, {"name": "binder", "instance_count": 311, "def": "holds loose papers or magazines", "synonyms": ["binder", "ring-binder"], "image_count": 94, "id": 97, "frequency": "c", "synset": "binder.n.03"}, {"name": "binoculars", "instance_count": 22, "def": "an optical instrument designed for simultaneous use by both eyes", "synonyms": ["binoculars", "field_glasses", "opera_glasses"], "image_count": 21, "id": 98, "frequency": "c", "synset": "binoculars.n.01"}, {"name": "bird", "instance_count": 11557, "def": "animal characterized by feathers and wings", "synonyms": ["bird"], "image_count": 1821, "id": 99, "frequency": "f", "synset": "bird.n.01"}, {"name": "birdfeeder", "instance_count": 16, "def": "an outdoor device that supplies food for wild birds", "synonyms": ["birdfeeder"], "image_count": 16, "id": 100, "frequency": "c", "synset": "bird_feeder.n.01"}, {"name": "birdbath", "instance_count": 12, "def": "an ornamental basin (usually in a garden) for birds to bathe in", "synonyms": ["birdbath"], "image_count": 12, "id": 101, "frequency": "c", "synset": "birdbath.n.01"}, {"name": "birdcage", "instance_count": 180, "def": "a cage in which a bird can be kept", "synonyms": ["birdcage"], "image_count": 25, "id": 102, "frequency": "c", "synset": "birdcage.n.01"}, {"name": "birdhouse", "instance_count": 60, "def": "a shelter for birds", "synonyms": ["birdhouse"], "image_count": 41, "id": 103, "frequency": "c", "synset": "birdhouse.n.01"}, {"name": "birthday_cake", "instance_count": 311, "def": "decorated cake served at a birthday party", "synonyms": ["birthday_cake"], "image_count": 244, "id": 104, "frequency": "f", "synset": "birthday_cake.n.01"}, {"name": "birthday_card", "instance_count": 23, "def": "a card expressing a birthday greeting", "synonyms": ["birthday_card"], "image_count": 7, "id": 105, "frequency": "r", "synset": "birthday_card.n.01"}, {"name": "pirate_flag", "instance_count": 1, "def": "a flag usually bearing a white skull and crossbones on a black background", "synonyms": ["pirate_flag"], "image_count": 1, "id": 106, "frequency": "r", "synset": "black_flag.n.01"}, {"name": "black_sheep", "instance_count": 214, "def": "sheep with a black coat", "synonyms": ["black_sheep"], "image_count": 40, "id": 107, "frequency": "c", "synset": "black_sheep.n.02"}, {"name": "blackberry", "instance_count": 406, "def": "large sweet black or very dark purple edible aggregate fruit", "synonyms": ["blackberry"], "image_count": 40, "id": 108, "frequency": "c", "synset": "blackberry.n.01"}, {"name": "blackboard", "instance_count": 154, "def": "sheet of slate; for writing with chalk", "synonyms": ["blackboard", "chalkboard"], "image_count": 104, "id": 109, "frequency": "f", "synset": "blackboard.n.01"}, {"name": "blanket", "instance_count": 3075, "def": "bedding that keeps a person warm in bed", "synonyms": ["blanket"], "image_count": 1671, "id": 110, "frequency": "f", "synset": "blanket.n.01"}, {"name": "blazer", "instance_count": 124, "def": "lightweight jacket; often striped in the colors of a club or school", "synonyms": ["blazer", "sport_jacket", "sport_coat", "sports_jacket", "sports_coat"], "image_count": 49, "id": 111, "frequency": "c", "synset": "blazer.n.01"}, {"name": "blender", "instance_count": 316, "def": "an electrically powered mixer that mix or chop or liquefy foods", "synonyms": ["blender", "liquidizer", "liquidiser"], "image_count": 243, "id": 112, "frequency": "f", "synset": "blender.n.01"}, {"name": "blimp", "instance_count": 3, "def": "a small nonrigid airship used for observation or as a barrage balloon", "synonyms": ["blimp"], "image_count": 2, "id": 113, "frequency": "r", "synset": "blimp.n.02"}, {"name": "blinker", "instance_count": 1269, "def": "a light that flashes on and off; used as a signal or to send messages", "synonyms": ["blinker", "flasher"], "image_count": 242, "id": 114, "frequency": "f", "synset": "blinker.n.01"}, {"name": "blouse", "instance_count": 623, "def": "a top worn by women", "synonyms": ["blouse"], "image_count": 271, "id": 115, "frequency": "f", "synset": "blouse.n.01"}, {"name": "blueberry", "instance_count": 2114, "def": "sweet edible dark-blue berries of blueberry plants", "synonyms": ["blueberry"], "image_count": 104, "id": 116, "frequency": "f", "synset": "blueberry.n.02"}, {"name": "gameboard", "instance_count": 17, "def": "a flat portable surface (usually rectangular) designed for board games", "synonyms": ["gameboard"], "image_count": 8, "id": 117, "frequency": "r", "synset": "board.n.09"}, {"name": "boat", "instance_count": 9981, "def": "a vessel for travel on water", "synonyms": ["boat", "ship_(boat)"], "image_count": 1758, "id": 118, "frequency": "f", "synset": "boat.n.01"}, {"name": "bob", "instance_count": 2, "def": "a small float usually made of cork; attached to a fishing line", "synonyms": ["bob", "bobber", "bobfloat"], "image_count": 1, "id": 119, "frequency": "r", "synset": "bob.n.05"}, {"name": "bobbin", "instance_count": 190, "def": "a thing around which thread/tape/film or other flexible materials can be wound", "synonyms": ["bobbin", "spool", "reel"], "image_count": 48, "id": 120, "frequency": "c", "synset": "bobbin.n.01"}, {"name": "bobby_pin", "instance_count": 43, "def": "a flat wire hairpin used to hold bobbed hair in place", "synonyms": ["bobby_pin", "hairgrip"], "image_count": 14, "id": 121, "frequency": "c", "synset": "bobby_pin.n.01"}, {"name": "boiled_egg", "instance_count": 125, "def": "egg cooked briefly in the shell in gently boiling water", "synonyms": ["boiled_egg", "coddled_egg"], "image_count": 40, "id": 122, "frequency": "c", "synset": "boiled_egg.n.01"}, {"name": "bolo_tie", "instance_count": 1, "def": "a cord fastened around the neck with an ornamental clasp and worn as a necktie", "synonyms": ["bolo_tie", "bolo", "bola_tie", "bola"], "image_count": 1, "id": 123, "frequency": "r", "synset": "bolo_tie.n.01"}, {"name": "deadbolt", "instance_count": 46, "def": "the part of a lock that is engaged or withdrawn with a key", "synonyms": ["deadbolt"], "image_count": 37, "id": 124, "frequency": "c", "synset": "bolt.n.03"}, {"name": "bolt", "instance_count": 11261, "def": "a screw that screws into a nut to form a fastener", "synonyms": ["bolt"], "image_count": 1510, "id": 125, "frequency": "f", "synset": "bolt.n.06"}, {"name": "bonnet", "instance_count": 10, "def": "a hat tied under the chin", "synonyms": ["bonnet"], "image_count": 6, "id": 126, "frequency": "r", "synset": "bonnet.n.01"}, {"name": "book", "instance_count": 33353, "def": "a written work or composition that has been published", "synonyms": ["book"], "image_count": 1903, "id": 127, "frequency": "f", "synset": "book.n.01"}, {"name": "bookcase", "instance_count": 113, "def": "a piece of furniture with shelves for storing books", "synonyms": ["bookcase"], "image_count": 70, "id": 128, "frequency": "c", "synset": "bookcase.n.01"}, {"name": "booklet", "instance_count": 439, "def": "a small book usually having a paper cover", "synonyms": ["booklet", "brochure", "leaflet", "pamphlet"], "image_count": 86, "id": 129, "frequency": "c", "synset": "booklet.n.01"}, {"name": "bookmark", "instance_count": 15, "def": "a marker (a piece of paper or ribbon) placed between the pages of a book", "synonyms": ["bookmark", "bookmarker"], "image_count": 7, "id": 130, "frequency": "r", "synset": "bookmark.n.01"}, {"name": "boom_microphone", "instance_count": 10, "def": "a pole carrying an overhead microphone projected over a film or tv set", "synonyms": ["boom_microphone", "microphone_boom"], "image_count": 5, "id": 131, "frequency": "r", "synset": "boom.n.04"}, {"name": "boot", "instance_count": 4194, "def": "footwear that covers the whole foot and lower leg", "synonyms": ["boot"], "image_count": 1406, "id": 132, "frequency": "f", "synset": "boot.n.01"}, {"name": "bottle", "instance_count": 7969, "def": "a glass or plastic vessel used for storing drinks or other liquids", "synonyms": ["bottle"], "image_count": 1901, "id": 133, "frequency": "f", "synset": "bottle.n.01"}, {"name": "bottle_opener", "instance_count": 15, "def": "an opener for removing caps or corks from bottles", "synonyms": ["bottle_opener"], "image_count": 15, "id": 134, "frequency": "c", "synset": "bottle_opener.n.01"}, {"name": "bouquet", "instance_count": 53, "def": "an arrangement of flowers that is usually given as a present", "synonyms": ["bouquet"], "image_count": 28, "id": 135, "frequency": "c", "synset": "bouquet.n.01"}, {"name": "bow_(weapon)", "instance_count": 6, "def": "a weapon for shooting arrows", "synonyms": ["bow_(weapon)"], "image_count": 6, "id": 136, "frequency": "r", "synset": "bow.n.04"}, {"name": "bow_(decorative_ribbons)", "instance_count": 1144, "def": "a decorative interlacing of ribbons", "synonyms": ["bow_(decorative_ribbons)"], "image_count": 494, "id": 137, "frequency": "f", "synset": "bow.n.08"}, {"name": "bow-tie", "instance_count": 359, "def": "a man's tie that ties in a bow", "synonyms": ["bow-tie", "bowtie"], "image_count": 234, "id": 138, "frequency": "f", "synset": "bow_tie.n.01"}, {"name": "bowl", "instance_count": 5308, "def": "a dish that is round and open at the top for serving foods", "synonyms": ["bowl"], "image_count": 1922, "id": 139, "frequency": "f", "synset": "bowl.n.03"}, {"name": "pipe_bowl", "instance_count": 1, "def": "a small round container that is open at the top for holding tobacco", "synonyms": ["pipe_bowl"], "image_count": 1, "id": 140, "frequency": "r", "synset": "bowl.n.08"}, {"name": "bowler_hat", "instance_count": 89, "def": "a felt hat that is round and hard with a narrow brim", "synonyms": ["bowler_hat", "bowler", "derby_hat", "derby", "plug_hat"], "image_count": 35, "id": 141, "frequency": "c", "synset": "bowler_hat.n.01"}, {"name": "bowling_ball", "instance_count": 38, "def": "a large ball with finger holes used in the sport of bowling", "synonyms": ["bowling_ball"], "image_count": 5, "id": 142, "frequency": "r", "synset": "bowling_ball.n.01"}, {"name": "box", "instance_count": 7855, "def": "a (usually rectangular) container; may have a lid", "synonyms": ["box"], "image_count": 1828, "id": 143, "frequency": "f", "synset": "box.n.01"}, {"name": "boxing_glove", "instance_count": 22, "def": "large glove coverings the fists of a fighter worn for the sport of boxing", "synonyms": ["boxing_glove"], "image_count": 8, "id": 144, "frequency": "r", "synset": "boxing_glove.n.01"}, {"name": "suspenders", "instance_count": 88, "def": "elastic straps that hold trousers up (usually used in the plural)", "synonyms": ["suspenders"], "image_count": 63, "id": 145, "frequency": "c", "synset": "brace.n.06"}, {"name": "bracelet", "instance_count": 3219, "def": "jewelry worn around the wrist for decoration", "synonyms": ["bracelet", "bangle"], "image_count": 1668, "id": 146, "frequency": "f", "synset": "bracelet.n.02"}, {"name": "brass_plaque", "instance_count": 4, "def": "a memorial made of brass", "synonyms": ["brass_plaque"], "image_count": 4, "id": 147, "frequency": "r", "synset": "brass.n.07"}, {"name": "brassiere", "instance_count": 118, "def": "an undergarment worn by women to support their breasts", "synonyms": ["brassiere", "bra", "bandeau"], "image_count": 95, "id": 148, "frequency": "c", "synset": "brassiere.n.01"}, {"name": "bread-bin", "instance_count": 17, "def": "a container used to keep bread or cake in", "synonyms": ["bread-bin", "breadbox"], "image_count": 17, "id": 149, "frequency": "c", "synset": "bread-bin.n.01"}, {"name": "bread", "instance_count": 6550, "def": "food made from dough of flour or meal and usually raised with yeast or baking powder and then baked", "synonyms": ["bread"], "image_count": 1567, "id": 150, "frequency": "f", "synset": "bread.n.01"}, {"name": "breechcloth", "instance_count": 3, "def": "a garment that provides covering for the loins", "synonyms": ["breechcloth", "breechclout", "loincloth"], "image_count": 2, "id": 151, "frequency": "r", "synset": "breechcloth.n.01"}, {"name": "bridal_gown", "instance_count": 118, "def": "a gown worn by the bride at a wedding", "synonyms": ["bridal_gown", "wedding_gown", "wedding_dress"], "image_count": 103, "id": 152, "frequency": "f", "synset": "bridal_gown.n.01"}, {"name": "briefcase", "instance_count": 84, "def": "a case with a handle; for carrying papers or files or books", "synonyms": ["briefcase"], "image_count": 50, "id": 153, "frequency": "c", "synset": "briefcase.n.01"}, {"name": "broccoli", "instance_count": 12166, "def": "plant with dense clusters of tight green flower buds", "synonyms": ["broccoli"], "image_count": 1309, "id": 154, "frequency": "f", "synset": "broccoli.n.01"}, {"name": "broach", "instance_count": 9, "def": "a decorative pin worn by women", "synonyms": ["broach"], "image_count": 6, "id": 155, "frequency": "r", "synset": "brooch.n.01"}, {"name": "broom", "instance_count": 144, "def": "bundle of straws or twigs attached to a long handle; used for cleaning", "synonyms": ["broom"], "image_count": 92, "id": 156, "frequency": "c", "synset": "broom.n.01"}, {"name": "brownie", "instance_count": 217, "def": "square or bar of very rich chocolate cake usually with nuts", "synonyms": ["brownie"], "image_count": 19, "id": 157, "frequency": "c", "synset": "brownie.n.03"}, {"name": "brussels_sprouts", "instance_count": 590, "def": "the small edible cabbage-like buds growing along a stalk", "synonyms": ["brussels_sprouts"], "image_count": 37, "id": 158, "frequency": "c", "synset": "brussels_sprouts.n.01"}, {"name": "bubble_gum", "instance_count": 4, "def": "a kind of chewing gum that can be blown into bubbles", "synonyms": ["bubble_gum"], "image_count": 4, "id": 159, "frequency": "r", "synset": "bubble_gum.n.01"}, {"name": "bucket", "instance_count": 1346, "def": "a roughly cylindrical vessel that is open at the top", "synonyms": ["bucket", "pail"], "image_count": 709, "id": 160, "frequency": "f", "synset": "bucket.n.01"}, {"name": "horse_buggy", "instance_count": 19, "def": "a small lightweight carriage; drawn by a single horse", "synonyms": ["horse_buggy"], "image_count": 9, "id": 161, "frequency": "r", "synset": "buggy.n.01"}, {"name": "bull", "instance_count": 230, "def": "a cow with horns", "synonyms": ["horned_cow"], "image_count": 82, "id": 162, "frequency": "c", "synset": "bull.n.11"}, {"name": "bulldog", "instance_count": 21, "def": "a thickset short-haired dog with a large head and strong undershot lower jaw", "synonyms": ["bulldog"], "image_count": 15, "id": 163, "frequency": "c", "synset": "bulldog.n.01"}, {"name": "bulldozer", "instance_count": 4, "def": "large powerful tractor; a large blade in front flattens areas of ground", "synonyms": ["bulldozer", "dozer"], "image_count": 3, "id": 164, "frequency": "r", "synset": "bulldozer.n.01"}, {"name": "bullet_train", "instance_count": 80, "def": "a high-speed passenger train", "synonyms": ["bullet_train"], "image_count": 61, "id": 165, "frequency": "c", "synset": "bullet_train.n.01"}, {"name": "bulletin_board", "instance_count": 76, "def": "a board that hangs on a wall; displays announcements", "synonyms": ["bulletin_board", "notice_board"], "image_count": 51, "id": 166, "frequency": "c", "synset": "bulletin_board.n.02"}, {"name": "bulletproof_vest", "instance_count": 27, "def": "a vest capable of resisting the impact of a bullet", "synonyms": ["bulletproof_vest"], "image_count": 5, "id": 167, "frequency": "r", "synset": "bulletproof_vest.n.01"}, {"name": "bullhorn", "instance_count": 15, "def": "a portable loudspeaker with built-in microphone and amplifier", "synonyms": ["bullhorn", "megaphone"], "image_count": 13, "id": 168, "frequency": "c", "synset": "bullhorn.n.01"}, {"name": "bun", "instance_count": 1780, "def": "small rounded bread either plain or sweet", "synonyms": ["bun", "roll"], "image_count": 642, "id": 169, "frequency": "f", "synset": "bun.n.01"}, {"name": "bunk_bed", "instance_count": 44, "def": "beds built one above the other", "synonyms": ["bunk_bed"], "image_count": 24, "id": 170, "frequency": "c", "synset": "bunk_bed.n.01"}, {"name": "buoy", "instance_count": 1404, "def": "a float attached by rope to the seabed to mark channels in a harbor or underwater hazards", "synonyms": ["buoy"], "image_count": 255, "id": 171, "frequency": "f", "synset": "buoy.n.01"}, {"name": "burrito", "instance_count": 14, "def": "a flour tortilla folded around a filling", "synonyms": ["burrito"], "image_count": 9, "id": 172, "frequency": "r", "synset": "burrito.n.01"}, {"name": "bus_(vehicle)", "instance_count": 3281, "def": "a vehicle carrying many passengers; used for public transport", "synonyms": ["bus_(vehicle)", "autobus", "charabanc", "double-decker", "motorbus", "motorcoach"], "image_count": 1808, "id": 173, "frequency": "f", "synset": "bus.n.01"}, {"name": "business_card", "instance_count": 84, "def": "a card on which are printed the person's name and business affiliation", "synonyms": ["business_card"], "image_count": 31, "id": 174, "frequency": "c", "synset": "business_card.n.01"}, {"name": "butter", "instance_count": 308, "def": "an edible emulsion of fat globules made by churning milk or cream; for cooking and table use", "synonyms": ["butter"], "image_count": 158, "id": 175, "frequency": "f", "synset": "butter.n.01"}, {"name": "butterfly", "instance_count": 296, "def": "insect typically having a slender body with knobbed antennae and broad colorful wings", "synonyms": ["butterfly"], "image_count": 80, "id": 176, "frequency": "c", "synset": "butterfly.n.01"}, {"name": "button", "instance_count": 7884, "def": "a round fastener sewn to shirts and coats etc to fit through buttonholes", "synonyms": ["button"], "image_count": 1884, "id": 177, "frequency": "f", "synset": "button.n.01"}, {"name": "cab_(taxi)", "instance_count": 414, "def": "a car that takes passengers where they want to go in exchange for money", "synonyms": ["cab_(taxi)", "taxi", "taxicab"], "image_count": 158, "id": 178, "frequency": "f", "synset": "cab.n.03"}, {"name": "cabana", "instance_count": 20, "def": "a small tent used as a dressing room beside the sea or a swimming pool", "synonyms": ["cabana"], "image_count": 2, "id": 179, "frequency": "r", "synset": "cabana.n.01"}, {"name": "cabin_car", "instance_count": 14, "def": "a car on a freight train for use of the train crew; usually the last car on the train", "synonyms": ["cabin_car", "caboose"], "image_count": 12, "id": 180, "frequency": "c", "synset": "cabin_car.n.01"}, {"name": "cabinet", "instance_count": 7371, "def": "a piece of furniture resembling a cupboard with doors and shelves and drawers", "synonyms": ["cabinet"], "image_count": 1659, "id": 181, "frequency": "f", "synset": "cabinet.n.01"}, {"name": "locker", "instance_count": 95, "def": "a storage compartment for clothes and valuables; usually it has a lock", "synonyms": ["locker", "storage_locker"], "image_count": 7, "id": 182, "frequency": "r", "synset": "cabinet.n.03"}, {"name": "cake", "instance_count": 2297, "def": "baked goods made from or based on a mixture of flour, sugar, eggs, and fat", "synonyms": ["cake"], "image_count": 834, "id": 183, "frequency": "f", "synset": "cake.n.03"}, {"name": "calculator", "instance_count": 60, "def": "a small machine that is used for mathematical calculations", "synonyms": ["calculator"], "image_count": 57, "id": 184, "frequency": "c", "synset": "calculator.n.02"}, {"name": "calendar", "instance_count": 251, "def": "a list or register of events (appointments/social events/court cases, etc)", "synonyms": ["calendar"], "image_count": 174, "id": 185, "frequency": "f", "synset": "calendar.n.02"}, {"name": "calf", "instance_count": 301, "def": "young of domestic cattle", "synonyms": ["calf"], "image_count": 95, "id": 186, "frequency": "c", "synset": "calf.n.01"}, {"name": "camcorder", "instance_count": 45, "def": "a portable television camera and videocassette recorder", "synonyms": ["camcorder"], "image_count": 27, "id": 187, "frequency": "c", "synset": "camcorder.n.01"}, {"name": "camel", "instance_count": 34, "def": "cud-chewing mammal used as a draft or saddle animal in desert regions", "synonyms": ["camel"], "image_count": 22, "id": 188, "frequency": "c", "synset": "camel.n.01"}, {"name": "camera", "instance_count": 2471, "def": "equipment for taking photographs", "synonyms": ["camera"], "image_count": 1391, "id": 189, "frequency": "f", "synset": "camera.n.01"}, {"name": "camera_lens", "instance_count": 167, "def": "a lens that focuses the image in a camera", "synonyms": ["camera_lens"], "image_count": 90, "id": 190, "frequency": "c", "synset": "camera_lens.n.01"}, {"name": "camper_(vehicle)", "instance_count": 102, "def": "a recreational vehicle equipped for camping out while traveling", "synonyms": ["camper_(vehicle)", "camping_bus", "motor_home"], "image_count": 40, "id": 191, "frequency": "c", "synset": "camper.n.02"}, {"name": "can", "instance_count": 1424, "def": "airtight sealed metal container for food or drink or paint etc.", "synonyms": ["can", "tin_can"], "image_count": 445, "id": 192, "frequency": "f", "synset": "can.n.01"}, {"name": "can_opener", "instance_count": 22, "def": "a device for cutting cans open", "synonyms": ["can_opener", "tin_opener"], "image_count": 21, "id": 193, "frequency": "c", "synset": "can_opener.n.01"}, {"name": "candle", "instance_count": 4288, "def": "stick of wax with a wick in the middle", "synonyms": ["candle", "candlestick"], "image_count": 1132, "id": 194, "frequency": "f", "synset": "candle.n.01"}, {"name": "candle_holder", "instance_count": 530, "def": "a holder with sockets for candles", "synonyms": ["candle_holder"], "image_count": 177, "id": 195, "frequency": "f", "synset": "candlestick.n.01"}, {"name": "candy_bar", "instance_count": 29, "def": "a candy shaped as a bar", "synonyms": ["candy_bar"], "image_count": 4, "id": 196, "frequency": "r", "synset": "candy_bar.n.01"}, {"name": "candy_cane", "instance_count": 107, "def": "a hard candy in the shape of a rod (usually with stripes)", "synonyms": ["candy_cane"], "image_count": 17, "id": 197, "frequency": "c", "synset": "candy_cane.n.01"}, {"name": "walking_cane", "instance_count": 106, "def": "a stick that people can lean on to help them walk", "synonyms": ["walking_cane"], "image_count": 84, "id": 198, "frequency": "c", "synset": "cane.n.01"}, {"name": "canister", "instance_count": 218, "def": "metal container for storing dry foods such as tea or flour", "synonyms": ["canister", "cannister"], "image_count": 55, "id": 199, "frequency": "c", "synset": "canister.n.02"}, {"name": "canoe", "instance_count": 96, "def": "small and light boat; pointed at both ends; propelled with a paddle", "synonyms": ["canoe"], "image_count": 30, "id": 200, "frequency": "c", "synset": "canoe.n.01"}, {"name": "cantaloup", "instance_count": 193, "def": "the fruit of a cantaloup vine; small to medium-sized melon with yellowish flesh", "synonyms": ["cantaloup", "cantaloupe"], "image_count": 25, "id": 201, "frequency": "c", "synset": "cantaloup.n.02"}, {"name": "canteen", "instance_count": 2, "def": "a flask for carrying water; used by soldiers or travelers", "synonyms": ["canteen"], "image_count": 2, "id": 202, "frequency": "r", "synset": "canteen.n.01"}, {"name": "cap_(headwear)", "instance_count": 636, "def": "a tight-fitting headwear", "synonyms": ["cap_(headwear)"], "image_count": 125, "id": 203, "frequency": "f", "synset": "cap.n.01"}, {"name": "bottle_cap", "instance_count": 5293, "def": "a top (as for a bottle)", "synonyms": ["bottle_cap", "cap_(container_lid)"], "image_count": 1135, "id": 204, "frequency": "f", "synset": "cap.n.02"}, {"name": "cape", "instance_count": 27, "def": "a sleeveless garment like a cloak but shorter", "synonyms": ["cape"], "image_count": 19, "id": 205, "frequency": "c", "synset": "cape.n.02"}, {"name": "cappuccino", "instance_count": 87, "def": "equal parts of espresso and steamed milk", "synonyms": ["cappuccino", "coffee_cappuccino"], "image_count": 72, "id": 206, "frequency": "c", "synset": "cappuccino.n.01"}, {"name": "car_(automobile)", "instance_count": 10528, "def": "a motor vehicle with four wheels", "synonyms": ["car_(automobile)", "auto_(automobile)", "automobile"], "image_count": 1926, "id": 207, "frequency": "f", "synset": "car.n.01"}, {"name": "railcar_(part_of_a_train)", "instance_count": 928, "def": "a wheeled vehicle adapted to the rails of railroad (mark each individual railcar separately)", "synonyms": ["railcar_(part_of_a_train)", "railway_car_(part_of_a_train)", "railroad_car_(part_of_a_train)"], "image_count": 159, "id": 208, "frequency": "f", "synset": "car.n.02"}, {"name": "elevator_car", "instance_count": 10, "def": "where passengers ride up and down", "synonyms": ["elevator_car"], "image_count": 7, "id": 209, "frequency": "r", "synset": "car.n.04"}, {"name": "car_battery", "instance_count": 1, "def": "a battery in a motor vehicle", "synonyms": ["car_battery", "automobile_battery"], "image_count": 1, "id": 210, "frequency": "r", "synset": "car_battery.n.01"}, {"name": "identity_card", "instance_count": 16, "def": "a card certifying the identity of the bearer", "synonyms": ["identity_card"], "image_count": 13, "id": 211, "frequency": "c", "synset": "card.n.02"}, {"name": "card", "instance_count": 122, "def": "a rectangular piece of paper used to send messages (e.g. greetings or pictures)", "synonyms": ["card"], "image_count": 35, "id": 212, "frequency": "c", "synset": "card.n.03"}, {"name": "cardigan", "instance_count": 22, "def": "knitted jacket that is fastened up the front with buttons or a zipper", "synonyms": ["cardigan"], "image_count": 18, "id": 213, "frequency": "c", "synset": "cardigan.n.01"}, {"name": "cargo_ship", "instance_count": 15, "def": "a ship designed to carry cargo", "synonyms": ["cargo_ship", "cargo_vessel"], "image_count": 8, "id": 214, "frequency": "r", "synset": "cargo_ship.n.01"}, {"name": "carnation", "instance_count": 22, "def": "plant with pink to purple-red spice-scented usually double flowers", "synonyms": ["carnation"], "image_count": 6, "id": 215, "frequency": "r", "synset": "carnation.n.01"}, {"name": "horse_carriage", "instance_count": 49, "def": "a vehicle with wheels drawn by one or more horses", "synonyms": ["horse_carriage"], "image_count": 35, "id": 216, "frequency": "c", "synset": "carriage.n.02"}, {"name": "carrot", "instance_count": 18049, "def": "deep orange edible root of the cultivated carrot plant", "synonyms": ["carrot"], "image_count": 1222, "id": 217, "frequency": "f", "synset": "carrot.n.01"}, {"name": "tote_bag", "instance_count": 231, "def": "a capacious bag or basket", "synonyms": ["tote_bag"], "image_count": 103, "id": 218, "frequency": "f", "synset": "carryall.n.01"}, {"name": "cart", "instance_count": 51, "def": "a heavy open wagon usually having two wheels and drawn by an animal", "synonyms": ["cart"], "image_count": 28, "id": 219, "frequency": "c", "synset": "cart.n.01"}, {"name": "carton", "instance_count": 206, "def": "a container made of cardboard for holding food or drink", "synonyms": ["carton"], "image_count": 63, "id": 220, "frequency": "c", "synset": "carton.n.02"}, {"name": "cash_register", "instance_count": 33, "def": "a cashbox with an adding machine to register transactions", "synonyms": ["cash_register", "register_(for_cash_transactions)"], "image_count": 28, "id": 221, "frequency": "c", "synset": "cash_register.n.01"}, {"name": "casserole", "instance_count": 12, "def": "food cooked and served in a casserole", "synonyms": ["casserole"], "image_count": 5, "id": 222, "frequency": "r", "synset": "casserole.n.01"}, {"name": "cassette", "instance_count": 74, "def": "a container that holds a magnetic tape used for recording or playing sound or video", "synonyms": ["cassette"], "image_count": 7, "id": 223, "frequency": "r", "synset": "cassette.n.01"}, {"name": "cast", "instance_count": 15, "def": "bandage consisting of a firm covering that immobilizes broken bones while they heal", "synonyms": ["cast", "plaster_cast", "plaster_bandage"], "image_count": 14, "id": 224, "frequency": "c", "synset": "cast.n.05"}, {"name": "cat", "instance_count": 2387, "def": "a domestic house cat", "synonyms": ["cat"], "image_count": 1918, "id": 225, "frequency": "f", "synset": "cat.n.01"}, {"name": "cauliflower", "instance_count": 1035, "def": "edible compact head of white undeveloped flowers", "synonyms": ["cauliflower"], "image_count": 133, "id": 226, "frequency": "f", "synset": "cauliflower.n.02"}, {"name": "cayenne_(spice)", "instance_count": 49, "def": "ground pods and seeds of pungent red peppers of the genus Capsicum", "synonyms": ["cayenne_(spice)", "cayenne_pepper_(spice)", "red_pepper_(spice)"], "image_count": 16, "id": 227, "frequency": "c", "synset": "cayenne.n.02"}, {"name": "CD_player", "instance_count": 37, "def": "electronic equipment for playing compact discs (CDs)", "synonyms": ["CD_player"], "image_count": 27, "id": 228, "frequency": "c", "synset": "cd_player.n.01"}, {"name": "celery", "instance_count": 911, "def": "widely cultivated herb with aromatic leaf stalks that are eaten raw or cooked", "synonyms": ["celery"], "image_count": 110, "id": 229, "frequency": "f", "synset": "celery.n.01"}, {"name": "cellular_telephone", "instance_count": 2902, "def": "a hand-held mobile telephone", "synonyms": ["cellular_telephone", "cellular_phone", "cellphone", "mobile_phone", "smart_phone"], "image_count": 1895, "id": 230, "frequency": "f", "synset": "cellular_telephone.n.01"}, {"name": "chain_mail", "instance_count": 13, "def": "(Middle Ages) flexible armor made of interlinked metal rings", "synonyms": ["chain_mail", "ring_mail", "chain_armor", "chain_armour", "ring_armor", "ring_armour"], "image_count": 4, "id": 231, "frequency": "r", "synset": "chain_mail.n.01"}, {"name": "chair", "instance_count": 11549, "def": "a seat for one person, with a support for the back", "synonyms": ["chair"], "image_count": 1927, "id": 232, "frequency": "f", "synset": "chair.n.01"}, {"name": "chaise_longue", "instance_count": 15, "def": "a long chair; for reclining", "synonyms": ["chaise_longue", "chaise", "daybed"], "image_count": 8, "id": 233, "frequency": "r", "synset": "chaise_longue.n.01"}, {"name": "chalice", "instance_count": 1, "def": "a bowl-shaped drinking vessel; especially the Eucharistic cup", "synonyms": ["chalice"], "image_count": 1, "id": 234, "frequency": "r", "synset": "chalice.n.01"}, {"name": "chandelier", "instance_count": 392, "def": "branched lighting fixture; often ornate; hangs from the ceiling", "synonyms": ["chandelier"], "image_count": 263, "id": 235, "frequency": "f", "synset": "chandelier.n.01"}, {"name": "chap", "instance_count": 19, "def": "leather leggings without a seat; worn over trousers by cowboys to protect their legs", "synonyms": ["chap"], "image_count": 10, "id": 236, "frequency": "r", "synset": "chap.n.04"}, {"name": "checkbook", "instance_count": 2, "def": "a book issued to holders of checking accounts", "synonyms": ["checkbook", "chequebook"], "image_count": 2, "id": 237, "frequency": "r", "synset": "checkbook.n.01"}, {"name": "checkerboard", "instance_count": 3, "def": "a board having 64 squares of two alternating colors", "synonyms": ["checkerboard"], "image_count": 3, "id": 238, "frequency": "r", "synset": "checkerboard.n.01"}, {"name": "cherry", "instance_count": 903, "def": "a red fruit with a single hard stone", "synonyms": ["cherry"], "image_count": 87, "id": 239, "frequency": "c", "synset": "cherry.n.03"}, {"name": "chessboard", "instance_count": 13, "def": "a checkerboard used to play chess", "synonyms": ["chessboard"], "image_count": 9, "id": 240, "frequency": "r", "synset": "chessboard.n.01"}, {"name": "chicken_(animal)", "instance_count": 417, "def": "a domestic fowl bred for flesh or eggs", "synonyms": ["chicken_(animal)"], "image_count": 71, "id": 241, "frequency": "c", "synset": "chicken.n.02"}, {"name": "chickpea", "instance_count": 265, "def": "the seed of the chickpea plant; usually dried", "synonyms": ["chickpea", "garbanzo"], "image_count": 13, "id": 242, "frequency": "c", "synset": "chickpea.n.01"}, {"name": "chili_(vegetable)", "instance_count": 354, "def": "very hot and finely tapering pepper of special pungency", "synonyms": ["chili_(vegetable)", "chili_pepper_(vegetable)", "chilli_(vegetable)", "chilly_(vegetable)", "chile_(vegetable)"], "image_count": 18, "id": 243, "frequency": "c", "synset": "chili.n.02"}, {"name": "chime", "instance_count": 2, "def": "an instrument consisting of a set of bells that are struck with a hammer", "synonyms": ["chime", "gong"], "image_count": 2, "id": 244, "frequency": "r", "synset": "chime.n.01"}, {"name": "chinaware", "instance_count": 41, "def": "dishware made of high quality porcelain", "synonyms": ["chinaware"], "image_count": 5, "id": 245, "frequency": "r", "synset": "chinaware.n.01"}, {"name": "crisp_(potato_chip)", "instance_count": 541, "def": "a thin crisp slice of potato fried in deep fat", "synonyms": ["crisp_(potato_chip)", "potato_chip"], "image_count": 45, "id": 246, "frequency": "c", "synset": "chip.n.04"}, {"name": "poker_chip", "instance_count": 21, "def": "a small disk-shaped counter used to represent money when gambling", "synonyms": ["poker_chip"], "image_count": 1, "id": 247, "frequency": "r", "synset": "chip.n.06"}, {"name": "chocolate_bar", "instance_count": 179, "def": "a bar of chocolate candy", "synonyms": ["chocolate_bar"], "image_count": 23, "id": 248, "frequency": "c", "synset": "chocolate_bar.n.01"}, {"name": "chocolate_cake", "instance_count": 80, "def": "cake containing chocolate", "synonyms": ["chocolate_cake"], "image_count": 32, "id": 249, "frequency": "c", "synset": "chocolate_cake.n.01"}, {"name": "chocolate_milk", "instance_count": 7, "def": "milk flavored with chocolate syrup", "synonyms": ["chocolate_milk"], "image_count": 4, "id": 250, "frequency": "r", "synset": "chocolate_milk.n.01"}, {"name": "chocolate_mousse", "instance_count": 1, "def": "dessert mousse made with chocolate", "synonyms": ["chocolate_mousse"], "image_count": 1, "id": 251, "frequency": "r", "synset": "chocolate_mousse.n.01"}, {"name": "choker", "instance_count": 1380, "def": "shirt collar, animal collar, or tight-fitting necklace", "synonyms": ["choker", "collar", "neckband"], "image_count": 858, "id": 252, "frequency": "f", "synset": "choker.n.03"}, {"name": "chopping_board", "instance_count": 840, "def": "a wooden board where meats or vegetables can be cut", "synonyms": ["chopping_board", "cutting_board", "chopping_block"], "image_count": 661, "id": 253, "frequency": "f", "synset": "chopping_board.n.01"}, {"name": "chopstick", "instance_count": 557, "def": "one of a pair of slender sticks used as oriental tableware to eat food with", "synonyms": ["chopstick"], "image_count": 168, "id": 254, "frequency": "f", "synset": "chopstick.n.01"}, {"name": "Christmas_tree", "instance_count": 303, "def": "an ornamented evergreen used as a Christmas decoration", "synonyms": ["Christmas_tree"], "image_count": 210, "id": 255, "frequency": "f", "synset": "christmas_tree.n.05"}, {"name": "slide", "instance_count": 106, "def": "sloping channel through which things can descend", "synonyms": ["slide"], "image_count": 65, "id": 256, "frequency": "c", "synset": "chute.n.02"}, {"name": "cider", "instance_count": 38, "def": "a beverage made from juice pressed from apples", "synonyms": ["cider", "cyder"], "image_count": 4, "id": 257, "frequency": "r", "synset": "cider.n.01"}, {"name": "cigar_box", "instance_count": 3, "def": "a box for holding cigars", "synonyms": ["cigar_box"], "image_count": 2, "id": 258, "frequency": "r", "synset": "cigar_box.n.01"}, {"name": "cigarette", "instance_count": 269, "def": "finely ground tobacco wrapped in paper; for smoking", "synonyms": ["cigarette"], "image_count": 159, "id": 259, "frequency": "f", "synset": "cigarette.n.01"}, {"name": "cigarette_case", "instance_count": 35, "def": "a small flat case for holding cigarettes", "synonyms": ["cigarette_case", "cigarette_pack"], "image_count": 31, "id": 260, "frequency": "c", "synset": "cigarette_case.n.01"}, {"name": "cistern", "instance_count": 901, "def": "a tank that holds the water used to flush a toilet", "synonyms": ["cistern", "water_tank"], "image_count": 811, "id": 261, "frequency": "f", "synset": "cistern.n.02"}, {"name": "clarinet", "instance_count": 1, "def": "a single-reed instrument with a straight tube", "synonyms": ["clarinet"], "image_count": 1, "id": 262, "frequency": "r", "synset": "clarinet.n.01"}, {"name": "clasp", "instance_count": 197, "def": "a fastener (as a buckle or hook) that is used to hold two things together", "synonyms": ["clasp"], "image_count": 42, "id": 263, "frequency": "c", "synset": "clasp.n.01"}, {"name": "cleansing_agent", "instance_count": 63, "def": "a preparation used in cleaning something", "synonyms": ["cleansing_agent", "cleanser", "cleaner"], "image_count": 27, "id": 264, "frequency": "c", "synset": "cleansing_agent.n.01"}, {"name": "cleat_(for_securing_rope)", "instance_count": 8, "def": "a fastener (usually with two projecting horns) around which a rope can be secured", "synonyms": ["cleat_(for_securing_rope)"], "image_count": 2, "id": 265, "frequency": "r", "synset": "cleat.n.02"}, {"name": "clementine", "instance_count": 108, "def": "a variety of mandarin orange", "synonyms": ["clementine"], "image_count": 5, "id": 266, "frequency": "r", "synset": "clementine.n.01"}, {"name": "clip", "instance_count": 301, "def": "any of various small fasteners used to hold loose articles together", "synonyms": ["clip"], "image_count": 95, "id": 267, "frequency": "c", "synset": "clip.n.03"}, {"name": "clipboard", "instance_count": 36, "def": "a small writing board with a clip at the top for holding papers", "synonyms": ["clipboard"], "image_count": 32, "id": 268, "frequency": "c", "synset": "clipboard.n.01"}, {"name": "clippers_(for_plants)", "instance_count": 1, "def": "shears for cutting grass or shrubbery (often used in the plural)", "synonyms": ["clippers_(for_plants)"], "image_count": 1, "id": 269, "frequency": "r", "synset": "clipper.n.03"}, {"name": "cloak", "instance_count": 1, "def": "a loose outer garment", "synonyms": ["cloak"], "image_count": 1, "id": 270, "frequency": "r", "synset": "cloak.n.02"}, {"name": "clock", "instance_count": 2677, "def": "a timepiece that shows the time of day", "synonyms": ["clock", "timepiece", "timekeeper"], "image_count": 1844, "id": 271, "frequency": "f", "synset": "clock.n.01"}, {"name": "clock_tower", "instance_count": 932, "def": "a tower with a large clock visible high up on an outside face", "synonyms": ["clock_tower"], "image_count": 897, "id": 272, "frequency": "f", "synset": "clock_tower.n.01"}, {"name": "clothes_hamper", "instance_count": 47, "def": "a hamper that holds dirty clothes to be washed or wet clothes to be dried", "synonyms": ["clothes_hamper", "laundry_basket", "clothes_basket"], "image_count": 31, "id": 273, "frequency": "c", "synset": "clothes_hamper.n.01"}, {"name": "clothespin", "instance_count": 111, "def": "wood or plastic fastener; for holding clothes on a clothesline", "synonyms": ["clothespin", "clothes_peg"], "image_count": 23, "id": 274, "frequency": "c", "synset": "clothespin.n.01"}, {"name": "clutch_bag", "instance_count": 1, "def": "a woman's strapless purse that is carried in the hand", "synonyms": ["clutch_bag"], "image_count": 1, "id": 275, "frequency": "r", "synset": "clutch_bag.n.01"}, {"name": "coaster", "instance_count": 390, "def": "a covering (plate or mat) that protects the surface of a table", "synonyms": ["coaster"], "image_count": 202, "id": 276, "frequency": "f", "synset": "coaster.n.03"}, {"name": "coat", "instance_count": 4145, "def": "an outer garment that has sleeves and covers the body from shoulder down", "synonyms": ["coat"], "image_count": 746, "id": 277, "frequency": "f", "synset": "coat.n.01"}, {"name": "coat_hanger", "instance_count": 282, "def": "a hanger that is shaped like a person's shoulders", "synonyms": ["coat_hanger", "clothes_hanger", "dress_hanger"], "image_count": 44, "id": 278, "frequency": "c", "synset": "coat_hanger.n.01"}, {"name": "coatrack", "instance_count": 16, "def": "a rack with hooks for temporarily holding coats and hats", "synonyms": ["coatrack", "hatrack"], "image_count": 14, "id": 279, "frequency": "c", "synset": "coatrack.n.01"}, {"name": "cock", "instance_count": 132, "def": "adult male chicken", "synonyms": ["cock", "rooster"], "image_count": 26, "id": 280, "frequency": "c", "synset": "cock.n.04"}, {"name": "cockroach", "instance_count": 1, "def": "any of numerous chiefly nocturnal insects; some are domestic pests", "synonyms": ["cockroach"], "image_count": 1, "id": 281, "frequency": "r", "synset": "cockroach.n.01"}, {"name": "cocoa_(beverage)", "instance_count": 4, "def": "a beverage made from cocoa powder and milk and sugar; usually drunk hot", "synonyms": ["cocoa_(beverage)", "hot_chocolate_(beverage)", "drinking_chocolate"], "image_count": 2, "id": 282, "frequency": "r", "synset": "cocoa.n.01"}, {"name": "coconut", "instance_count": 273, "def": "large hard-shelled brown oval nut with a fibrous husk", "synonyms": ["coconut", "cocoanut"], "image_count": 25, "id": 283, "frequency": "c", "synset": "coconut.n.02"}, {"name": "coffee_maker", "instance_count": 271, "def": "a kitchen appliance for brewing coffee automatically", "synonyms": ["coffee_maker", "coffee_machine"], "image_count": 238, "id": 284, "frequency": "f", "synset": "coffee_maker.n.01"}, {"name": "coffee_table", "instance_count": 709, "def": "low table where magazines can be placed and coffee or cocktails are served", "synonyms": ["coffee_table", "cocktail_table"], "image_count": 592, "id": 285, "frequency": "f", "synset": "coffee_table.n.01"}, {"name": "coffeepot", "instance_count": 32, "def": "tall pot in which coffee is brewed", "synonyms": ["coffeepot"], "image_count": 26, "id": 286, "frequency": "c", "synset": "coffeepot.n.01"}, {"name": "coil", "instance_count": 7, "def": "tubing that is wound in a spiral", "synonyms": ["coil"], "image_count": 5, "id": 287, "frequency": "r", "synset": "coil.n.05"}, {"name": "coin", "instance_count": 305, "def": "a flat metal piece (usually a disc) used as money", "synonyms": ["coin"], "image_count": 42, "id": 288, "frequency": "c", "synset": "coin.n.01"}, {"name": "colander", "instance_count": 16, "def": "bowl-shaped strainer; used to wash or drain foods", "synonyms": ["colander", "cullender"], "image_count": 13, "id": 289, "frequency": "c", "synset": "colander.n.01"}, {"name": "coleslaw", "instance_count": 72, "def": "basically shredded cabbage", "synonyms": ["coleslaw", "slaw"], "image_count": 46, "id": 290, "frequency": "c", "synset": "coleslaw.n.01"}, {"name": "coloring_material", "instance_count": 1, "def": "any material used for its color", "synonyms": ["coloring_material", "colouring_material"], "image_count": 1, "id": 291, "frequency": "r", "synset": "coloring_material.n.01"}, {"name": "combination_lock", "instance_count": 13, "def": "lock that can be opened only by turning dials in a special sequence", "synonyms": ["combination_lock"], "image_count": 8, "id": 292, "frequency": "r", "synset": "combination_lock.n.01"}, {"name": "pacifier", "instance_count": 40, "def": "device used for an infant to suck or bite on", "synonyms": ["pacifier", "teething_ring"], "image_count": 34, "id": 293, "frequency": "c", "synset": "comforter.n.04"}, {"name": "comic_book", "instance_count": 97, "def": "a magazine devoted to comic strips", "synonyms": ["comic_book"], "image_count": 5, "id": 294, "frequency": "r", "synset": "comic_book.n.01"}, {"name": "compass", "instance_count": 1, "def": "navigational instrument for finding directions", "synonyms": ["compass"], "image_count": 1, "id": 295, "frequency": "r", "synset": "compass.n.01"}, {"name": "computer_keyboard", "instance_count": 2745, "def": "a keyboard that is a data input device for computers", "synonyms": ["computer_keyboard", "keyboard_(computer)"], "image_count": 1871, "id": 296, "frequency": "f", "synset": "computer_keyboard.n.01"}, {"name": "condiment", "instance_count": 2985, "def": "a preparation (a sauce or relish or spice) to enhance flavor or enjoyment", "synonyms": ["condiment"], "image_count": 717, "id": 297, "frequency": "f", "synset": "condiment.n.01"}, {"name": "cone", "instance_count": 4081, "def": "a cone-shaped object used to direct traffic", "synonyms": ["cone", "traffic_cone"], "image_count": 1010, "id": 298, "frequency": "f", "synset": "cone.n.01"}, {"name": "control", "instance_count": 1775, "def": "a mechanism that controls the operation of a machine", "synonyms": ["control", "controller"], "image_count": 679, "id": 299, "frequency": "f", "synset": "control.n.09"}, {"name": "convertible_(automobile)", "instance_count": 4, "def": "a car that has top that can be folded or removed", "synonyms": ["convertible_(automobile)"], "image_count": 3, "id": 300, "frequency": "r", "synset": "convertible.n.01"}, {"name": "sofa_bed", "instance_count": 5, "def": "a sofa that can be converted into a bed", "synonyms": ["sofa_bed"], "image_count": 4, "id": 301, "frequency": "r", "synset": "convertible.n.03"}, {"name": "cooker", "instance_count": 1, "def": "a utensil for cooking", "synonyms": ["cooker"], "image_count": 1, "id": 302, "frequency": "r", "synset": "cooker.n.01"}, {"name": "cookie", "instance_count": 1920, "def": "any of various small flat sweet cakes (`biscuit' is the British term)", "synonyms": ["cookie", "cooky", "biscuit_(cookie)"], "image_count": 166, "id": 303, "frequency": "f", "synset": "cookie.n.01"}, {"name": "cooking_utensil", "instance_count": 18, "def": "a kitchen utensil made of material that does not melt easily; used for cooking", "synonyms": ["cooking_utensil"], "image_count": 2, "id": 304, "frequency": "r", "synset": "cooking_utensil.n.01"}, {"name": "cooler_(for_food)", "instance_count": 499, "def": "an insulated box for storing food often with ice", "synonyms": ["cooler_(for_food)", "ice_chest"], "image_count": 266, "id": 305, "frequency": "f", "synset": "cooler.n.01"}, {"name": "cork_(bottle_plug)", "instance_count": 326, "def": "the plug in the mouth of a bottle (especially a wine bottle)", "synonyms": ["cork_(bottle_plug)", "bottle_cork"], "image_count": 101, "id": 306, "frequency": "f", "synset": "cork.n.04"}, {"name": "corkboard", "instance_count": 7, "def": "a sheet consisting of cork granules", "synonyms": ["corkboard"], "image_count": 6, "id": 307, "frequency": "r", "synset": "corkboard.n.01"}, {"name": "corkscrew", "instance_count": 15, "def": "a bottle opener that pulls corks", "synonyms": ["corkscrew", "bottle_screw"], "image_count": 14, "id": 308, "frequency": "c", "synset": "corkscrew.n.01"}, {"name": "edible_corn", "instance_count": 1883, "def": "ears or kernels of corn that can be prepared and served for human food (only mark individual ears or kernels)", "synonyms": ["edible_corn", "corn", "maize"], "image_count": 133, "id": 309, "frequency": "f", "synset": "corn.n.03"}, {"name": "cornbread", "instance_count": 10, "def": "bread made primarily of cornmeal", "synonyms": ["cornbread"], "image_count": 2, "id": 310, "frequency": "r", "synset": "cornbread.n.01"}, {"name": "cornet", "instance_count": 65, "def": "a brass musical instrument with a narrow tube and a flared bell and many valves", "synonyms": ["cornet", "horn", "trumpet"], "image_count": 38, "id": 311, "frequency": "c", "synset": "cornet.n.01"}, {"name": "cornice", "instance_count": 149, "def": "a decorative framework to conceal curtain fixtures at the top of a window casing", "synonyms": ["cornice", "valance", "valance_board", "pelmet"], "image_count": 95, "id": 312, "frequency": "c", "synset": "cornice.n.01"}, {"name": "cornmeal", "instance_count": 1, "def": "coarsely ground corn", "synonyms": ["cornmeal"], "image_count": 1, "id": 313, "frequency": "r", "synset": "cornmeal.n.01"}, {"name": "corset", "instance_count": 12, "def": "a woman's close-fitting foundation garment", "synonyms": ["corset", "girdle"], "image_count": 12, "id": 314, "frequency": "c", "synset": "corset.n.01"}, {"name": "costume", "instance_count": 124, "def": "the attire characteristic of a country or a time or a social class", "synonyms": ["costume"], "image_count": 49, "id": 315, "frequency": "c", "synset": "costume.n.04"}, {"name": "cougar", "instance_count": 6, "def": "large American feline resembling a lion", "synonyms": ["cougar", "puma", "catamount", "mountain_lion", "panther"], "image_count": 5, "id": 316, "frequency": "r", "synset": "cougar.n.01"}, {"name": "coverall", "instance_count": 12, "def": "a loose-fitting protective garment that is worn over other clothing", "synonyms": ["coverall"], "image_count": 5, "id": 317, "frequency": "r", "synset": "coverall.n.01"}, {"name": "cowbell", "instance_count": 29, "def": "a bell hung around the neck of cow so that the cow can be easily located", "synonyms": ["cowbell"], "image_count": 16, "id": 318, "frequency": "c", "synset": "cowbell.n.01"}, {"name": "cowboy_hat", "instance_count": 535, "def": "a hat with a wide brim and a soft crown; worn by American ranch hands", "synonyms": ["cowboy_hat", "ten-gallon_hat"], "image_count": 216, "id": 319, "frequency": "f", "synset": "cowboy_hat.n.01"}, {"name": "crab_(animal)", "instance_count": 50, "def": "decapod having eyes on short stalks and a broad flattened shell and pincers", "synonyms": ["crab_(animal)"], "image_count": 12, "id": 320, "frequency": "c", "synset": "crab.n.01"}, {"name": "crabmeat", "instance_count": 5, "def": "the edible flesh of any of various crabs", "synonyms": ["crabmeat"], "image_count": 1, "id": 321, "frequency": "r", "synset": "crab.n.05"}, {"name": "cracker", "instance_count": 510, "def": "a thin crisp wafer", "synonyms": ["cracker"], "image_count": 54, "id": 322, "frequency": "c", "synset": "cracker.n.01"}, {"name": "crape", "instance_count": 12, "def": "small very thin pancake", "synonyms": ["crape", "crepe", "French_pancake"], "image_count": 5, "id": 323, "frequency": "r", "synset": "crape.n.01"}, {"name": "crate", "instance_count": 1832, "def": "a rugged box (usually made of wood); used for shipping", "synonyms": ["crate"], "image_count": 245, "id": 324, "frequency": "f", "synset": "crate.n.01"}, {"name": "crayon", "instance_count": 59, "def": "writing or drawing implement made of a colored stick of composition wax", "synonyms": ["crayon", "wax_crayon"], "image_count": 12, "id": 325, "frequency": "c", "synset": "crayon.n.01"}, {"name": "cream_pitcher", "instance_count": 10, "def": "a small pitcher for serving cream", "synonyms": ["cream_pitcher"], "image_count": 7, "id": 326, "frequency": "r", "synset": "cream_pitcher.n.01"}, {"name": "crescent_roll", "instance_count": 152, "def": "very rich flaky crescent-shaped roll", "synonyms": ["crescent_roll", "croissant"], "image_count": 35, "id": 327, "frequency": "c", "synset": "crescent_roll.n.01"}, {"name": "crib", "instance_count": 40, "def": "baby bed with high sides made of slats", "synonyms": ["crib", "cot"], "image_count": 36, "id": 328, "frequency": "c", "synset": "crib.n.01"}, {"name": "crock_pot", "instance_count": 128, "def": "an earthen jar (made of baked clay) or a modern electric crockpot", "synonyms": ["crock_pot", "earthenware_jar"], "image_count": 32, "id": 329, "frequency": "c", "synset": "crock.n.03"}, {"name": "crossbar", "instance_count": 6991, "def": "a horizontal bar that goes across something", "synonyms": ["crossbar"], "image_count": 1027, "id": 330, "frequency": "f", "synset": "crossbar.n.01"}, {"name": "crouton", "instance_count": 140, "def": "a small piece of toasted or fried bread; served in soup or salads", "synonyms": ["crouton"], "image_count": 10, "id": 331, "frequency": "r", "synset": "crouton.n.01"}, {"name": "crow", "instance_count": 24, "def": "black birds having a raucous call", "synonyms": ["crow"], "image_count": 12, "id": 332, "frequency": "c", "synset": "crow.n.01"}, {"name": "crowbar", "instance_count": 1, "def": "a heavy iron lever with one end forged into a wedge", "synonyms": ["crowbar", "wrecking_bar", "pry_bar"], "image_count": 1, "id": 333, "frequency": "r", "synset": "crowbar.n.01"}, {"name": "crown", "instance_count": 126, "def": "an ornamental jeweled headdress signifying sovereignty", "synonyms": ["crown"], "image_count": 67, "id": 334, "frequency": "c", "synset": "crown.n.04"}, {"name": "crucifix", "instance_count": 99, "def": "representation of the cross on which Jesus died", "synonyms": ["crucifix"], "image_count": 71, "id": 335, "frequency": "c", "synset": "crucifix.n.01"}, {"name": "cruise_ship", "instance_count": 35, "def": "a passenger ship used commercially for pleasure cruises", "synonyms": ["cruise_ship", "cruise_liner"], "image_count": 30, "id": 336, "frequency": "c", "synset": "cruise_ship.n.01"}, {"name": "police_cruiser", "instance_count": 86, "def": "a car in which policemen cruise the streets", "synonyms": ["police_cruiser", "patrol_car", "police_car", "squad_car"], "image_count": 48, "id": 337, "frequency": "c", "synset": "cruiser.n.01"}, {"name": "crumb", "instance_count": 3021, "def": "small piece of e.g. bread or cake", "synonyms": ["crumb"], "image_count": 249, "id": 338, "frequency": "f", "synset": "crumb.n.03"}, {"name": "crutch", "instance_count": 20, "def": "a wooden or metal staff that fits under the armpit and reaches to the ground", "synonyms": ["crutch"], "image_count": 13, "id": 339, "frequency": "c", "synset": "crutch.n.01"}, {"name": "cub_(animal)", "instance_count": 55, "def": "the young of certain carnivorous mammals such as the bear or wolf or lion", "synonyms": ["cub_(animal)"], "image_count": 29, "id": 340, "frequency": "c", "synset": "cub.n.03"}, {"name": "cube", "instance_count": 189, "def": "a block in the (approximate) shape of a cube", "synonyms": ["cube", "square_block"], "image_count": 14, "id": 341, "frequency": "c", "synset": "cube.n.05"}, {"name": "cucumber", "instance_count": 1533, "def": "cylindrical green fruit with thin green rind and white flesh eaten as a vegetable", "synonyms": ["cucumber", "cuke"], "image_count": 236, "id": 342, "frequency": "f", "synset": "cucumber.n.02"}, {"name": "cufflink", "instance_count": 17, "def": "jewelry consisting of linked buttons used to fasten the cuffs of a shirt", "synonyms": ["cufflink"], "image_count": 15, "id": 343, "frequency": "c", "synset": "cufflink.n.01"}, {"name": "cup", "instance_count": 4637, "def": "a small open container usually used for drinking; usually has a handle", "synonyms": ["cup"], "image_count": 1521, "id": 344, "frequency": "f", "synset": "cup.n.01"}, {"name": "trophy_cup", "instance_count": 80, "def": "a metal award or cup-shaped vessel with handles that is awarded as a trophy to a competition winner", "synonyms": ["trophy_cup"], "image_count": 25, "id": 345, "frequency": "c", "synset": "cup.n.08"}, {"name": "cupboard", "instance_count": 1623, "def": "a small room (or recess) or cabinet used for storage space", "synonyms": ["cupboard", "closet"], "image_count": 249, "id": 346, "frequency": "f", "synset": "cupboard.n.01"}, {"name": "cupcake", "instance_count": 1628, "def": "small cake baked in a muffin tin", "synonyms": ["cupcake"], "image_count": 139, "id": 347, "frequency": "f", "synset": "cupcake.n.01"}, {"name": "hair_curler", "instance_count": 20, "def": "a cylindrical tube around which the hair is wound to curl it", "synonyms": ["hair_curler", "hair_roller", "hair_crimper"], "image_count": 2, "id": 348, "frequency": "r", "synset": "curler.n.01"}, {"name": "curling_iron", "instance_count": 2, "def": "a cylindrical home appliance that heats hair that has been curled around it", "synonyms": ["curling_iron"], "image_count": 2, "id": 349, "frequency": "r", "synset": "curling_iron.n.01"}, {"name": "curtain", "instance_count": 4506, "def": "hanging cloth used as a blind (especially for a window)", "synonyms": ["curtain", "drapery"], "image_count": 1890, "id": 350, "frequency": "f", "synset": "curtain.n.01"}, {"name": "cushion", "instance_count": 7174, "def": "a soft bag filled with air or padding such as feathers or foam rubber", "synonyms": ["cushion"], "image_count": 1240, "id": 351, "frequency": "f", "synset": "cushion.n.03"}, {"name": "cylinder", "instance_count": 3, "def": "a cylindrical container", "synonyms": ["cylinder"], "image_count": 1, "id": 352, "frequency": "r", "synset": "cylinder.n.04"}, {"name": "cymbal", "instance_count": 24, "def": "a percussion instrument consisting of a concave brass disk", "synonyms": ["cymbal"], "image_count": 9, "id": 353, "frequency": "r", "synset": "cymbal.n.01"}, {"name": "dagger", "instance_count": 1, "def": "a short knife with a pointed blade used for piercing or stabbing", "synonyms": ["dagger"], "image_count": 1, "id": 354, "frequency": "r", "synset": "dagger.n.01"}, {"name": "dalmatian", "instance_count": 3, "def": "a large breed having a smooth white coat with black or brown spots", "synonyms": ["dalmatian"], "image_count": 3, "id": 355, "frequency": "r", "synset": "dalmatian.n.02"}, {"name": "dartboard", "instance_count": 11, "def": "a circular board of wood or cork used as the target in the game of darts", "synonyms": ["dartboard"], "image_count": 11, "id": 356, "frequency": "c", "synset": "dartboard.n.01"}, {"name": "date_(fruit)", "instance_count": 103, "def": "sweet edible fruit of the date palm with a single long woody seed", "synonyms": ["date_(fruit)"], "image_count": 4, "id": 357, "frequency": "r", "synset": "date.n.08"}, {"name": "deck_chair", "instance_count": 1787, "def": "a folding chair for use outdoors; a wooden frame supports a length of canvas", "synonyms": ["deck_chair", "beach_chair"], "image_count": 236, "id": 358, "frequency": "f", "synset": "deck_chair.n.01"}, {"name": "deer", "instance_count": 130, "def": "distinguished from Bovidae by the male's having solid deciduous antlers", "synonyms": ["deer", "cervid"], "image_count": 44, "id": 359, "frequency": "c", "synset": "deer.n.01"}, {"name": "dental_floss", "instance_count": 20, "def": "a soft thread for cleaning the spaces between the teeth", "synonyms": ["dental_floss", "floss"], "image_count": 19, "id": 360, "frequency": "c", "synset": "dental_floss.n.01"}, {"name": "desk", "instance_count": 1662, "def": "a piece of furniture with a writing surface and usually drawers or other compartments", "synonyms": ["desk"], "image_count": 1100, "id": 361, "frequency": "f", "synset": "desk.n.01"}, {"name": "detergent", "instance_count": 11, "def": "a surface-active chemical widely used in industry and laundering", "synonyms": ["detergent"], "image_count": 7, "id": 362, "frequency": "r", "synset": "detergent.n.01"}, {"name": "diaper", "instance_count": 89, "def": "garment consisting of a folded cloth drawn up between the legs and fastened at the waist", "synonyms": ["diaper"], "image_count": 69, "id": 363, "frequency": "c", "synset": "diaper.n.01"}, {"name": "diary", "instance_count": 2, "def": "yearly planner book", "synonyms": ["diary", "journal"], "image_count": 2, "id": 364, "frequency": "r", "synset": "diary.n.01"}, {"name": "die", "instance_count": 25, "def": "a small cube with 1 to 6 spots on the six faces; used in gambling", "synonyms": ["die", "dice"], "image_count": 8, "id": 365, "frequency": "r", "synset": "die.n.01"}, {"name": "dinghy", "instance_count": 15, "def": "a small boat of shallow draft with seats and oars with which it is propelled", "synonyms": ["dinghy", "dory", "rowboat"], "image_count": 5, "id": 366, "frequency": "r", "synset": "dinghy.n.01"}, {"name": "dining_table", "instance_count": 312, "def": "a table at which meals are served", "synonyms": ["dining_table"], "image_count": 227, "id": 367, "frequency": "f", "synset": "dining_table.n.01"}, {"name": "tux", "instance_count": 10, "def": "semiformal evening dress for men", "synonyms": ["tux", "tuxedo"], "image_count": 6, "id": 368, "frequency": "r", "synset": "dinner_jacket.n.01"}, {"name": "dish", "instance_count": 532, "def": "a piece of dishware normally used as a container for holding or serving food", "synonyms": ["dish"], "image_count": 106, "id": 369, "frequency": "f", "synset": "dish.n.01"}, {"name": "dish_antenna", "instance_count": 153, "def": "directional antenna consisting of a parabolic reflector", "synonyms": ["dish_antenna"], "image_count": 81, "id": 370, "frequency": "c", "synset": "dish.n.05"}, {"name": "dishrag", "instance_count": 32, "def": "a cloth for washing dishes or cleaning in general", "synonyms": ["dishrag", "dishcloth"], "image_count": 17, "id": 371, "frequency": "c", "synset": "dishrag.n.01"}, {"name": "dishtowel", "instance_count": 223, "def": "a towel for drying dishes", "synonyms": ["dishtowel", "tea_towel"], "image_count": 134, "id": 372, "frequency": "f", "synset": "dishtowel.n.01"}, {"name": "dishwasher", "instance_count": 317, "def": "a machine for washing dishes", "synonyms": ["dishwasher", "dishwashing_machine"], "image_count": 312, "id": 373, "frequency": "f", "synset": "dishwasher.n.01"}, {"name": "dishwasher_detergent", "instance_count": 9, "def": "dishsoap or dish detergent designed for use in dishwashers", "synonyms": ["dishwasher_detergent", "dishwashing_detergent", "dishwashing_liquid", "dishsoap"], "image_count": 8, "id": 374, "frequency": "r", "synset": "dishwasher_detergent.n.01"}, {"name": "dispenser", "instance_count": 610, "def": "a container so designed that the contents can be used in prescribed amounts", "synonyms": ["dispenser"], "image_count": 271, "id": 375, "frequency": "f", "synset": "dispenser.n.01"}, {"name": "diving_board", "instance_count": 2, "def": "a springboard from which swimmers can dive", "synonyms": ["diving_board"], "image_count": 2, "id": 376, "frequency": "r", "synset": "diving_board.n.01"}, {"name": "Dixie_cup", "instance_count": 352, "def": "a disposable cup made of paper; for holding drinks", "synonyms": ["Dixie_cup", "paper_cup"], "image_count": 103, "id": 377, "frequency": "f", "synset": "dixie_cup.n.01"}, {"name": "dog", "instance_count": 2684, "def": "a common domesticated dog", "synonyms": ["dog"], "image_count": 1938, "id": 378, "frequency": "f", "synset": "dog.n.01"}, {"name": "dog_collar", "instance_count": 733, "def": "a collar for a dog", "synonyms": ["dog_collar"], "image_count": 574, "id": 379, "frequency": "f", "synset": "dog_collar.n.01"}, {"name": "doll", "instance_count": 398, "def": "a toy replica of a HUMAN (NOT AN ANIMAL)", "synonyms": ["doll"], "image_count": 120, "id": 380, "frequency": "f", "synset": "doll.n.01"}, {"name": "dollar", "instance_count": 2, "def": "a piece of paper money worth one dollar", "synonyms": ["dollar", "dollar_bill", "one_dollar_bill"], "image_count": 2, "id": 381, "frequency": "r", "synset": "dollar.n.02"}, {"name": "dollhouse", "instance_count": 2, "def": "a house so small that it is likened to a child's plaything", "synonyms": ["dollhouse", "doll's_house"], "image_count": 2, "id": 382, "frequency": "r", "synset": "dollhouse.n.01"}, {"name": "dolphin", "instance_count": 38, "def": "any of various small toothed whales with a beaklike snout; larger than porpoises", "synonyms": ["dolphin"], "image_count": 13, "id": 383, "frequency": "c", "synset": "dolphin.n.02"}, {"name": "domestic_ass", "instance_count": 49, "def": "domestic beast of burden descended from the African wild ass; patient but stubborn", "synonyms": ["domestic_ass", "donkey"], "image_count": 29, "id": 384, "frequency": "c", "synset": "domestic_ass.n.01"}, {"name": "doorknob", "instance_count": 4072, "def": "a knob used to open a door (often called `doorhandle' in Great Britain)", "synonyms": ["doorknob", "doorhandle"], "image_count": 1710, "id": 385, "frequency": "f", "synset": "doorknob.n.01"}, {"name": "doormat", "instance_count": 78, "def": "a mat placed outside an exterior door for wiping the shoes before entering", "synonyms": ["doormat", "welcome_mat"], "image_count": 66, "id": 386, "frequency": "c", "synset": "doormat.n.02"}, {"name": "doughnut", "instance_count": 11911, "def": "a small ring-shaped friedcake", "synonyms": ["doughnut", "donut"], "image_count": 1008, "id": 387, "frequency": "f", "synset": "doughnut.n.02"}, {"name": "dove", "instance_count": 2, "def": "any of numerous small pigeons", "synonyms": ["dove"], "image_count": 1, "id": 388, "frequency": "r", "synset": "dove.n.01"}, {"name": "dragonfly", "instance_count": 8, "def": "slender-bodied non-stinging insect having iridescent wings that are outspread at rest", "synonyms": ["dragonfly"], "image_count": 3, "id": 389, "frequency": "r", "synset": "dragonfly.n.01"}, {"name": "drawer", "instance_count": 7927, "def": "a boxlike container in a piece of furniture; made so as to slide in and out", "synonyms": ["drawer"], "image_count": 1942, "id": 390, "frequency": "f", "synset": "drawer.n.01"}, {"name": "underdrawers", "instance_count": 23, "def": "underpants worn by men", "synonyms": ["underdrawers", "boxers", "boxershorts"], "image_count": 19, "id": 391, "frequency": "c", "synset": "drawers.n.01"}, {"name": "dress", "instance_count": 2842, "def": "a one-piece garment for a woman; has skirt and bodice", "synonyms": ["dress", "frock"], "image_count": 1488, "id": 392, "frequency": "f", "synset": "dress.n.01"}, {"name": "dress_hat", "instance_count": 76, "def": "a man's hat with a tall crown; usually covered with silk or with beaver fur", "synonyms": ["dress_hat", "high_hat", "opera_hat", "silk_hat", "top_hat"], "image_count": 46, "id": 393, "frequency": "c", "synset": "dress_hat.n.01"}, {"name": "dress_suit", "instance_count": 306, "def": "formalwear consisting of full evening dress for men", "synonyms": ["dress_suit"], "image_count": 106, "id": 394, "frequency": "f", "synset": "dress_suit.n.01"}, {"name": "dresser", "instance_count": 152, "def": "a cabinet with shelves", "synonyms": ["dresser"], "image_count": 115, "id": 395, "frequency": "f", "synset": "dresser.n.05"}, {"name": "drill", "instance_count": 24, "def": "a tool with a sharp rotating point for making holes in hard materials", "synonyms": ["drill"], "image_count": 19, "id": 396, "frequency": "c", "synset": "drill.n.01"}, {"name": "drone", "instance_count": 2, "def": "an aircraft without a pilot that is operated by remote control", "synonyms": ["drone"], "image_count": 2, "id": 397, "frequency": "r", "synset": "drone.n.04"}, {"name": "dropper", "instance_count": 1, "def": "pipet consisting of a small tube with a vacuum bulb at one end for drawing liquid in and releasing it a drop at a time", "synonyms": ["dropper", "eye_dropper"], "image_count": 1, "id": 398, "frequency": "r", "synset": "dropper.n.01"}, {"name": "drum_(musical_instrument)", "instance_count": 59, "def": "a musical percussion instrument; usually consists of a hollow cylinder with a membrane stretched across each end", "synonyms": ["drum_(musical_instrument)"], "image_count": 28, "id": 399, "frequency": "c", "synset": "drum.n.01"}, {"name": "drumstick", "instance_count": 25, "def": "a stick used for playing a drum", "synonyms": ["drumstick"], "image_count": 9, "id": 400, "frequency": "r", "synset": "drumstick.n.02"}, {"name": "duck", "instance_count": 1090, "def": "small web-footed broad-billed swimming bird", "synonyms": ["duck"], "image_count": 192, "id": 401, "frequency": "f", "synset": "duck.n.01"}, {"name": "duckling", "instance_count": 36, "def": "young duck", "synonyms": ["duckling"], "image_count": 12, "id": 402, "frequency": "c", "synset": "duckling.n.02"}, {"name": "duct_tape", "instance_count": 77, "def": "a wide silvery adhesive tape", "synonyms": ["duct_tape"], "image_count": 21, "id": 403, "frequency": "c", "synset": "duct_tape.n.01"}, {"name": "duffel_bag", "instance_count": 666, "def": "a large cylindrical bag of heavy cloth (does not include suitcases)", "synonyms": ["duffel_bag", "duffle_bag", "duffel", "duffle"], "image_count": 247, "id": 404, "frequency": "f", "synset": "duffel_bag.n.01"}, {"name": "dumbbell", "instance_count": 13, "def": "an exercising weight with two ball-like ends connected by a short handle", "synonyms": ["dumbbell"], "image_count": 6, "id": 405, "frequency": "r", "synset": "dumbbell.n.01"}, {"name": "dumpster", "instance_count": 95, "def": "a container designed to receive and transport and dump waste", "synonyms": ["dumpster"], "image_count": 64, "id": 406, "frequency": "c", "synset": "dumpster.n.01"}, {"name": "dustpan", "instance_count": 7, "def": "a short-handled receptacle into which dust can be swept", "synonyms": ["dustpan"], "image_count": 7, "id": 407, "frequency": "r", "synset": "dustpan.n.02"}, {"name": "eagle", "instance_count": 48, "def": "large birds of prey noted for their broad wings and strong soaring flight", "synonyms": ["eagle"], "image_count": 40, "id": 408, "frequency": "c", "synset": "eagle.n.01"}, {"name": "earphone", "instance_count": 767, "def": "device for listening to audio that is held over or inserted into the ear", "synonyms": ["earphone", "earpiece", "headphone"], "image_count": 542, "id": 409, "frequency": "f", "synset": "earphone.n.01"}, {"name": "earplug", "instance_count": 39, "def": "a soft plug that is inserted into the ear canal to block sound", "synonyms": ["earplug"], "image_count": 2, "id": 410, "frequency": "r", "synset": "earplug.n.01"}, {"name": "earring", "instance_count": 3070, "def": "jewelry to ornament the ear", "synonyms": ["earring"], "image_count": 1898, "id": 411, "frequency": "f", "synset": "earring.n.01"}, {"name": "easel", "instance_count": 43, "def": "an upright tripod for displaying something (usually an artist's canvas)", "synonyms": ["easel"], "image_count": 36, "id": 412, "frequency": "c", "synset": "easel.n.01"}, {"name": "eclair", "instance_count": 39, "def": "oblong cream puff", "synonyms": ["eclair"], "image_count": 4, "id": 413, "frequency": "r", "synset": "eclair.n.01"}, {"name": "eel", "instance_count": 1, "def": "an elongate fish with fatty flesh", "synonyms": ["eel"], "image_count": 1, "id": 414, "frequency": "r", "synset": "eel.n.01"}, {"name": "egg", "instance_count": 813, "def": "oval reproductive body of a fowl (especially a hen) used as food", "synonyms": ["egg", "eggs"], "image_count": 191, "id": 415, "frequency": "f", "synset": "egg.n.02"}, {"name": "egg_roll", "instance_count": 15, "def": "minced vegetables and meat wrapped in a pancake and fried", "synonyms": ["egg_roll", "spring_roll"], "image_count": 6, "id": 416, "frequency": "r", "synset": "egg_roll.n.01"}, {"name": "egg_yolk", "instance_count": 90, "def": "the yellow spherical part of an egg", "synonyms": ["egg_yolk", "yolk_(egg)"], "image_count": 41, "id": 417, "frequency": "c", "synset": "egg_yolk.n.01"}, {"name": "eggbeater", "instance_count": 52, "def": "a mixer for beating eggs or whipping cream", "synonyms": ["eggbeater", "eggwhisk"], "image_count": 39, "id": 418, "frequency": "c", "synset": "eggbeater.n.02"}, {"name": "eggplant", "instance_count": 337, "def": "egg-shaped vegetable having a shiny skin typically dark purple", "synonyms": ["eggplant", "aubergine"], "image_count": 46, "id": 419, "frequency": "c", "synset": "eggplant.n.01"}, {"name": "electric_chair", "instance_count": 1, "def": "a chair-shaped instrument of execution by electrocution", "synonyms": ["electric_chair"], "image_count": 1, "id": 420, "frequency": "r", "synset": "electric_chair.n.01"}, {"name": "refrigerator", "instance_count": 1702, "def": "a refrigerator in which the coolant is pumped around by an electric motor", "synonyms": ["refrigerator"], "image_count": 1451, "id": 421, "frequency": "f", "synset": "electric_refrigerator.n.01"}, {"name": "elephant", "instance_count": 5325, "def": "a common elephant", "synonyms": ["elephant"], "image_count": 1878, "id": 422, "frequency": "f", "synset": "elephant.n.01"}, {"name": "elk", "instance_count": 29, "def": "large northern deer with enormous flattened antlers in the male", "synonyms": ["elk", "moose"], "image_count": 11, "id": 423, "frequency": "c", "synset": "elk.n.01"}, {"name": "envelope", "instance_count": 210, "def": "a flat (usually rectangular) container for a letter, thin package, etc.", "synonyms": ["envelope"], "image_count": 82, "id": 424, "frequency": "c", "synset": "envelope.n.01"}, {"name": "eraser", "instance_count": 41, "def": "an implement used to erase something", "synonyms": ["eraser"], "image_count": 18, "id": 425, "frequency": "c", "synset": "eraser.n.01"}, {"name": "escargot", "instance_count": 5, "def": "edible snail usually served in the shell with a sauce of melted butter and garlic", "synonyms": ["escargot"], "image_count": 1, "id": 426, "frequency": "r", "synset": "escargot.n.01"}, {"name": "eyepatch", "instance_count": 9, "def": "a protective cloth covering for an injured eye", "synonyms": ["eyepatch"], "image_count": 7, "id": 427, "frequency": "r", "synset": "eyepatch.n.01"}, {"name": "falcon", "instance_count": 3, "def": "birds of prey having long pointed powerful wings adapted for swift flight", "synonyms": ["falcon"], "image_count": 3, "id": 428, "frequency": "r", "synset": "falcon.n.01"}, {"name": "fan", "instance_count": 737, "def": "a device for creating a current of air by movement of a surface or surfaces", "synonyms": ["fan"], "image_count": 575, "id": 429, "frequency": "f", "synset": "fan.n.01"}, {"name": "faucet", "instance_count": 3185, "def": "a regulator for controlling the flow of a liquid from a reservoir", "synonyms": ["faucet", "spigot", "tap"], "image_count": 1907, "id": 430, "frequency": "f", "synset": "faucet.n.01"}, {"name": "fedora", "instance_count": 14, "def": "a hat made of felt with a creased crown", "synonyms": ["fedora"], "image_count": 8, "id": 431, "frequency": "r", "synset": "fedora.n.01"}, {"name": "ferret", "instance_count": 5, "def": "domesticated albino variety of the European polecat bred for hunting rats and rabbits", "synonyms": ["ferret"], "image_count": 4, "id": 432, "frequency": "r", "synset": "ferret.n.02"}, {"name": "Ferris_wheel", "instance_count": 32, "def": "a large wheel with suspended seats that remain upright as the wheel rotates", "synonyms": ["Ferris_wheel"], "image_count": 32, "id": 433, "frequency": "c", "synset": "ferris_wheel.n.01"}, {"name": "ferry", "instance_count": 17, "def": "a boat that transports people or vehicles across a body of water and operates on a regular schedule", "synonyms": ["ferry", "ferryboat"], "image_count": 11, "id": 434, "frequency": "c", "synset": "ferry.n.01"}, {"name": "fig_(fruit)", "instance_count": 147, "def": "fleshy sweet pear-shaped yellowish or purple fruit eaten fresh or preserved or dried", "synonyms": ["fig_(fruit)"], "image_count": 4, "id": 435, "frequency": "r", "synset": "fig.n.04"}, {"name": "fighter_jet", "instance_count": 115, "def": "a high-speed military or naval airplane designed to destroy enemy targets", "synonyms": ["fighter_jet", "fighter_aircraft", "attack_aircraft"], "image_count": 54, "id": 436, "frequency": "c", "synset": "fighter.n.02"}, {"name": "figurine", "instance_count": 1056, "def": "a small carved or molded figure", "synonyms": ["figurine"], "image_count": 202, "id": 437, "frequency": "f", "synset": "figurine.n.01"}, {"name": "file_cabinet", "instance_count": 53, "def": "office furniture consisting of a container for keeping papers in order", "synonyms": ["file_cabinet", "filing_cabinet"], "image_count": 32, "id": 438, "frequency": "c", "synset": "file.n.03"}, {"name": "file_(tool)", "instance_count": 3, "def": "a steel hand tool with small sharp teeth on some or all of its surfaces; used for smoothing wood or metal", "synonyms": ["file_(tool)"], "image_count": 3, "id": 439, "frequency": "r", "synset": "file.n.04"}, {"name": "fire_alarm", "instance_count": 151, "def": "an alarm that is tripped off by fire or smoke", "synonyms": ["fire_alarm", "smoke_alarm"], "image_count": 130, "id": 440, "frequency": "f", "synset": "fire_alarm.n.02"}, {"name": "fire_engine", "instance_count": 179, "def": "large trucks that carry firefighters and equipment to the site of a fire", "synonyms": ["fire_engine", "fire_truck"], "image_count": 119, "id": 441, "frequency": "f", "synset": "fire_engine.n.01"}, {"name": "fire_extinguisher", "instance_count": 165, "def": "a manually operated device for extinguishing small fires", "synonyms": ["fire_extinguisher", "extinguisher"], "image_count": 141, "id": 442, "frequency": "f", "synset": "fire_extinguisher.n.01"}, {"name": "fire_hose", "instance_count": 67, "def": "a large hose that carries water from a fire hydrant to the site of the fire", "synonyms": ["fire_hose"], "image_count": 29, "id": 443, "frequency": "c", "synset": "fire_hose.n.01"}, {"name": "fireplace", "instance_count": 530, "def": "an open recess in a wall at the base of a chimney where a fire can be built", "synonyms": ["fireplace"], "image_count": 525, "id": 444, "frequency": "f", "synset": "fireplace.n.01"}, {"name": "fireplug", "instance_count": 1458, "def": "an upright hydrant for drawing water to use in fighting a fire", "synonyms": ["fireplug", "fire_hydrant", "hydrant"], "image_count": 1323, "id": 445, "frequency": "f", "synset": "fireplug.n.01"}, {"name": "first-aid_kit", "instance_count": 2, "def": "kit consisting of a set of bandages and medicines for giving first aid", "synonyms": ["first-aid_kit"], "image_count": 2, "id": 446, "frequency": "r", "synset": "first-aid_kit.n.01"}, {"name": "fish", "instance_count": 525, "def": "any of various mostly cold-blooded aquatic vertebrates usually having scales and breathing through gills", "synonyms": ["fish"], "image_count": 113, "id": 447, "frequency": "f", "synset": "fish.n.01"}, {"name": "fish_(food)", "instance_count": 96, "def": "the flesh of fish used as food", "synonyms": ["fish_(food)"], "image_count": 16, "id": 448, "frequency": "c", "synset": "fish.n.02"}, {"name": "fishbowl", "instance_count": 33, "def": "a transparent bowl in which small fish are kept", "synonyms": ["fishbowl", "goldfish_bowl"], "image_count": 7, "id": 449, "frequency": "r", "synset": "fishbowl.n.02"}, {"name": "fishing_rod", "instance_count": 84, "def": "a rod that is used in fishing to extend the fishing line", "synonyms": ["fishing_rod", "fishing_pole"], "image_count": 35, "id": 450, "frequency": "c", "synset": "fishing_rod.n.01"}, {"name": "flag", "instance_count": 7007, "def": "emblem usually consisting of a rectangular piece of cloth of distinctive design (do not include pole)", "synonyms": ["flag"], "image_count": 1908, "id": 451, "frequency": "f", "synset": "flag.n.01"}, {"name": "flagpole", "instance_count": 1082, "def": "a tall staff or pole on which a flag is raised", "synonyms": ["flagpole", "flagstaff"], "image_count": 353, "id": 452, "frequency": "f", "synset": "flagpole.n.02"}, {"name": "flamingo", "instance_count": 309, "def": "large pink web-footed bird with down-bent bill", "synonyms": ["flamingo"], "image_count": 18, "id": 453, "frequency": "c", "synset": "flamingo.n.01"}, {"name": "flannel", "instance_count": 18, "def": "a soft light woolen fabric; used for clothing", "synonyms": ["flannel"], "image_count": 14, "id": 454, "frequency": "c", "synset": "flannel.n.01"}, {"name": "flap", "instance_count": 218, "def": "any broad thin covering attached at one edge, such as a mud flap next to a wheel or a flap on an airplane wing", "synonyms": ["flap"], "image_count": 77, "id": 455, "frequency": "c", "synset": "flap.n.01"}, {"name": "flash", "instance_count": 10, "def": "a lamp for providing momentary light to take a photograph", "synonyms": ["flash", "flashbulb"], "image_count": 8, "id": 456, "frequency": "r", "synset": "flash.n.10"}, {"name": "flashlight", "instance_count": 48, "def": "a small portable battery-powered electric lamp", "synonyms": ["flashlight", "torch"], "image_count": 37, "id": 457, "frequency": "c", "synset": "flashlight.n.01"}, {"name": "fleece", "instance_count": 2, "def": "a soft bulky fabric with deep pile; used chiefly for clothing", "synonyms": ["fleece"], "image_count": 1, "id": 458, "frequency": "r", "synset": "fleece.n.03"}, {"name": "flip-flop_(sandal)", "instance_count": 1103, "def": "a backless sandal held to the foot by a thong between two toes", "synonyms": ["flip-flop_(sandal)"], "image_count": 346, "id": 459, "frequency": "f", "synset": "flip-flop.n.02"}, {"name": "flipper_(footwear)", "instance_count": 49, "def": "a shoe to aid a person in swimming", "synonyms": ["flipper_(footwear)", "fin_(footwear)"], "image_count": 19, "id": 460, "frequency": "c", "synset": "flipper.n.01"}, {"name": "flower_arrangement", "instance_count": 3960, "def": "a decorative arrangement of flowers", "synonyms": ["flower_arrangement", "floral_arrangement"], "image_count": 1779, "id": 461, "frequency": "f", "synset": "flower_arrangement.n.01"}, {"name": "flute_glass", "instance_count": 86, "def": "a tall narrow wineglass", "synonyms": ["flute_glass", "champagne_flute"], "image_count": 23, "id": 462, "frequency": "c", "synset": "flute.n.02"}, {"name": "foal", "instance_count": 30, "def": "a young horse", "synonyms": ["foal"], "image_count": 25, "id": 463, "frequency": "c", "synset": "foal.n.01"}, {"name": "folding_chair", "instance_count": 303, "def": "a chair that can be folded flat for storage", "synonyms": ["folding_chair"], "image_count": 67, "id": 464, "frequency": "c", "synset": "folding_chair.n.01"}, {"name": "food_processor", "instance_count": 22, "def": "a kitchen appliance for shredding, blending, chopping, or slicing food", "synonyms": ["food_processor"], "image_count": 19, "id": 465, "frequency": "c", "synset": "food_processor.n.01"}, {"name": "football_(American)", "instance_count": 35, "def": "the inflated oblong ball used in playing American football", "synonyms": ["football_(American)"], "image_count": 28, "id": 466, "frequency": "c", "synset": "football.n.02"}, {"name": "football_helmet", "instance_count": 7, "def": "a padded helmet with a face mask to protect the head of football players", "synonyms": ["football_helmet"], "image_count": 4, "id": 467, "frequency": "r", "synset": "football_helmet.n.01"}, {"name": "footstool", "instance_count": 41, "def": "a low seat or a stool to rest the feet of a seated person", "synonyms": ["footstool", "footrest"], "image_count": 27, "id": 468, "frequency": "c", "synset": "footstool.n.01"}, {"name": "fork", "instance_count": 3137, "def": "cutlery used for serving and eating food", "synonyms": ["fork"], "image_count": 1861, "id": 469, "frequency": "f", "synset": "fork.n.01"}, {"name": "forklift", "instance_count": 14, "def": "an industrial vehicle with a power operated fork in front that can be inserted under loads to lift and move them", "synonyms": ["forklift"], "image_count": 11, "id": 470, "frequency": "c", "synset": "forklift.n.01"}, {"name": "freight_car", "instance_count": 121, "def": "a railway car that carries freight", "synonyms": ["freight_car"], "image_count": 13, "id": 471, "frequency": "c", "synset": "freight_car.n.01"}, {"name": "French_toast", "instance_count": 41, "def": "bread slice dipped in egg and milk and fried", "synonyms": ["French_toast"], "image_count": 13, "id": 472, "frequency": "c", "synset": "french_toast.n.01"}, {"name": "freshener", "instance_count": 39, "def": "anything that freshens air by removing or covering odor", "synonyms": ["freshener", "air_freshener"], "image_count": 32, "id": 473, "frequency": "c", "synset": "freshener.n.01"}, {"name": "frisbee", "instance_count": 2332, "def": "a light, plastic disk propelled with a flip of the wrist for recreation or competition", "synonyms": ["frisbee"], "image_count": 1767, "id": 474, "frequency": "f", "synset": "frisbee.n.01"}, {"name": "frog", "instance_count": 84, "def": "a tailless stout-bodied amphibians with long hind limbs for leaping", "synonyms": ["frog", "toad", "toad_frog"], "image_count": 42, "id": 475, "frequency": "c", "synset": "frog.n.01"}, {"name": "fruit_juice", "instance_count": 37, "def": "drink produced by squeezing or crushing fruit", "synonyms": ["fruit_juice"], "image_count": 17, "id": 476, "frequency": "c", "synset": "fruit_juice.n.01"}, {"name": "frying_pan", "instance_count": 310, "def": "a pan used for frying foods", "synonyms": ["frying_pan", "frypan", "skillet"], "image_count": 128, "id": 477, "frequency": "f", "synset": "frying_pan.n.01"}, {"name": "fudge", "instance_count": 4, "def": "soft creamy candy", "synonyms": ["fudge"], "image_count": 1, "id": 478, "frequency": "r", "synset": "fudge.n.01"}, {"name": "funnel", "instance_count": 9, "def": "a cone-shaped utensil used to channel a substance into a container with a small mouth", "synonyms": ["funnel"], "image_count": 9, "id": 479, "frequency": "r", "synset": "funnel.n.02"}, {"name": "futon", "instance_count": 11, "def": "a pad that is used for sleeping on the floor or on a raised frame", "synonyms": ["futon"], "image_count": 10, "id": 480, "frequency": "r", "synset": "futon.n.01"}, {"name": "gag", "instance_count": 4, "def": "restraint put into a person's mouth to prevent speaking or shouting", "synonyms": ["gag", "muzzle"], "image_count": 4, "id": 481, "frequency": "r", "synset": "gag.n.02"}, {"name": "garbage", "instance_count": 18, "def": "a receptacle where waste can be discarded", "synonyms": ["garbage"], "image_count": 9, "id": 482, "frequency": "r", "synset": "garbage.n.03"}, {"name": "garbage_truck", "instance_count": 18, "def": "a truck for collecting domestic refuse", "synonyms": ["garbage_truck"], "image_count": 18, "id": 483, "frequency": "c", "synset": "garbage_truck.n.01"}, {"name": "garden_hose", "instance_count": 50, "def": "a hose used for watering a lawn or garden", "synonyms": ["garden_hose"], "image_count": 41, "id": 484, "frequency": "c", "synset": "garden_hose.n.01"}, {"name": "gargle", "instance_count": 38, "def": "a medicated solution used for gargling and rinsing the mouth", "synonyms": ["gargle", "mouthwash"], "image_count": 28, "id": 485, "frequency": "c", "synset": "gargle.n.01"}, {"name": "gargoyle", "instance_count": 8, "def": "an ornament consisting of a grotesquely carved figure of a person or animal", "synonyms": ["gargoyle"], "image_count": 3, "id": 486, "frequency": "r", "synset": "gargoyle.n.02"}, {"name": "garlic", "instance_count": 487, "def": "aromatic bulb used as seasoning", "synonyms": ["garlic", "ail"], "image_count": 65, "id": 487, "frequency": "c", "synset": "garlic.n.02"}, {"name": "gasmask", "instance_count": 12, "def": "a protective face mask with a filter", "synonyms": ["gasmask", "respirator", "gas_helmet"], "image_count": 9, "id": 488, "frequency": "r", "synset": "gasmask.n.01"}, {"name": "gazelle", "instance_count": 82, "def": "small swift graceful antelope of Africa and Asia having lustrous eyes", "synonyms": ["gazelle"], "image_count": 23, "id": 489, "frequency": "c", "synset": "gazelle.n.01"}, {"name": "gelatin", "instance_count": 248, "def": "an edible jelly made with gelatin and used as a dessert or salad base or a coating for foods", "synonyms": ["gelatin", "jelly"], "image_count": 24, "id": 490, "frequency": "c", "synset": "gelatin.n.02"}, {"name": "gemstone", "instance_count": 2, "def": "a crystalline rock that can be cut and polished for jewelry", "synonyms": ["gemstone"], "image_count": 1, "id": 491, "frequency": "r", "synset": "gem.n.02"}, {"name": "generator", "instance_count": 2, "def": "engine that converts mechanical energy into electrical energy by electromagnetic induction", "synonyms": ["generator"], "image_count": 2, "id": 492, "frequency": "r", "synset": "generator.n.02"}, {"name": "giant_panda", "instance_count": 112, "def": "large black-and-white herbivorous mammal of bamboo forests of China and Tibet", "synonyms": ["giant_panda", "panda", "panda_bear"], "image_count": 59, "id": 493, "frequency": "c", "synset": "giant_panda.n.01"}, {"name": "gift_wrap", "instance_count": 247, "def": "attractive wrapping paper suitable for wrapping gifts", "synonyms": ["gift_wrap"], "image_count": 48, "id": 494, "frequency": "c", "synset": "gift_wrap.n.01"}, {"name": "ginger", "instance_count": 93, "def": "the root of the common ginger plant; used fresh as a seasoning", "synonyms": ["ginger", "gingerroot"], "image_count": 17, "id": 495, "frequency": "c", "synset": "ginger.n.03"}, {"name": "giraffe", "instance_count": 3923, "def": "tall animal having a spotted coat and small horns and very long neck and legs", "synonyms": ["giraffe"], "image_count": 1877, "id": 496, "frequency": "f", "synset": "giraffe.n.01"}, {"name": "cincture", "instance_count": 56, "def": "a band of material around the waist that strengthens a skirt or trousers", "synonyms": ["cincture", "sash", "waistband", "waistcloth"], "image_count": 18, "id": 497, "frequency": "c", "synset": "girdle.n.02"}, {"name": "glass_(drink_container)", "instance_count": 6420, "def": "a container for holding liquids while drinking", "synonyms": ["glass_(drink_container)", "drinking_glass"], "image_count": 1920, "id": 498, "frequency": "f", "synset": "glass.n.02"}, {"name": "globe", "instance_count": 59, "def": "a sphere on which a map (especially of the earth) is represented", "synonyms": ["globe"], "image_count": 50, "id": 499, "frequency": "c", "synset": "globe.n.03"}, {"name": "glove", "instance_count": 5951, "def": "handwear covering the hand", "synonyms": ["glove"], "image_count": 1890, "id": 500, "frequency": "f", "synset": "glove.n.02"}, {"name": "goat", "instance_count": 842, "def": "a common goat", "synonyms": ["goat"], "image_count": 99, "id": 501, "frequency": "c", "synset": "goat.n.01"}, {"name": "goggles", "instance_count": 3202, "def": "tight-fitting spectacles worn to protect the eyes", "synonyms": ["goggles"], "image_count": 1530, "id": 502, "frequency": "f", "synset": "goggles.n.01"}, {"name": "goldfish", "instance_count": 11, "def": "small golden or orange-red freshwater fishes used as pond or aquarium pets", "synonyms": ["goldfish"], "image_count": 3, "id": 503, "frequency": "r", "synset": "goldfish.n.01"}, {"name": "golf_club", "instance_count": 14, "def": "golf equipment used by a golfer to hit a golf ball", "synonyms": ["golf_club", "golf-club"], "image_count": 11, "id": 504, "frequency": "c", "synset": "golf_club.n.02"}, {"name": "golfcart", "instance_count": 25, "def": "a small motor vehicle in which golfers can ride between shots", "synonyms": ["golfcart"], "image_count": 19, "id": 505, "frequency": "c", "synset": "golfcart.n.01"}, {"name": "gondola_(boat)", "instance_count": 8, "def": "long narrow flat-bottomed boat propelled by sculling; traditionally used on canals of Venice", "synonyms": ["gondola_(boat)"], "image_count": 3, "id": 506, "frequency": "r", "synset": "gondola.n.02"}, {"name": "goose", "instance_count": 413, "def": "loud, web-footed long-necked aquatic birds usually larger than ducks", "synonyms": ["goose"], "image_count": 63, "id": 507, "frequency": "c", "synset": "goose.n.01"}, {"name": "gorilla", "instance_count": 10, "def": "largest ape", "synonyms": ["gorilla"], "image_count": 5, "id": 508, "frequency": "r", "synset": "gorilla.n.01"}, {"name": "gourd", "instance_count": 101, "def": "any of numerous inedible fruits with hard rinds", "synonyms": ["gourd"], "image_count": 6, "id": 509, "frequency": "r", "synset": "gourd.n.02"}, {"name": "grape", "instance_count": 6377, "def": "any of various juicy fruit with green or purple skins; grow in clusters", "synonyms": ["grape"], "image_count": 233, "id": 510, "frequency": "f", "synset": "grape.n.01"}, {"name": "grater", "instance_count": 64, "def": "utensil with sharp perforations for shredding foods (as vegetables or cheese)", "synonyms": ["grater"], "image_count": 54, "id": 511, "frequency": "c", "synset": "grater.n.01"}, {"name": "gravestone", "instance_count": 778, "def": "a stone that is used to mark a grave", "synonyms": ["gravestone", "headstone", "tombstone"], "image_count": 36, "id": 512, "frequency": "c", "synset": "gravestone.n.01"}, {"name": "gravy_boat", "instance_count": 10, "def": "a dish (often boat-shaped) for serving gravy or sauce", "synonyms": ["gravy_boat", "gravy_holder"], "image_count": 10, "id": 513, "frequency": "r", "synset": "gravy_boat.n.01"}, {"name": "green_bean", "instance_count": 2571, "def": "a common bean plant cultivated for its slender green edible pods", "synonyms": ["green_bean"], "image_count": 124, "id": 514, "frequency": "f", "synset": "green_bean.n.02"}, {"name": "green_onion", "instance_count": 1618, "def": "a young onion before the bulb has enlarged", "synonyms": ["green_onion", "spring_onion", "scallion"], "image_count": 101, "id": 515, "frequency": "f", "synset": "green_onion.n.01"}, {"name": "griddle", "instance_count": 4, "def": "cooking utensil consisting of a flat heated surface on which food is cooked", "synonyms": ["griddle"], "image_count": 3, "id": 516, "frequency": "r", "synset": "griddle.n.01"}, {"name": "grill", "instance_count": 747, "def": "a framework of metal bars used as a partition or a grate", "synonyms": ["grill", "grille", "grillwork", "radiator_grille"], "image_count": 363, "id": 517, "frequency": "f", "synset": "grill.n.02"}, {"name": "grits", "instance_count": 3, "def": "coarsely ground corn boiled as a breakfast dish", "synonyms": ["grits", "hominy_grits"], "image_count": 3, "id": 518, "frequency": "r", "synset": "grits.n.01"}, {"name": "grizzly", "instance_count": 44, "def": "powerful brownish-yellow bear of the uplands of western North America", "synonyms": ["grizzly", "grizzly_bear"], "image_count": 30, "id": 519, "frequency": "c", "synset": "grizzly.n.01"}, {"name": "grocery_bag", "instance_count": 46, "def": "a sack for holding customer's groceries", "synonyms": ["grocery_bag"], "image_count": 18, "id": 520, "frequency": "c", "synset": "grocery_bag.n.01"}, {"name": "guitar", "instance_count": 315, "def": "a stringed instrument usually having six strings; played by strumming or plucking", "synonyms": ["guitar"], "image_count": 199, "id": 521, "frequency": "f", "synset": "guitar.n.01"}, {"name": "gull", "instance_count": 1398, "def": "mostly white aquatic bird having long pointed wings and short legs", "synonyms": ["gull", "seagull"], "image_count": 97, "id": 522, "frequency": "c", "synset": "gull.n.02"}, {"name": "gun", "instance_count": 68, "def": "a weapon that discharges a bullet at high velocity from a metal tube", "synonyms": ["gun"], "image_count": 32, "id": 523, "frequency": "c", "synset": "gun.n.01"}, {"name": "hairbrush", "instance_count": 165, "def": "a brush used to groom a person's hair", "synonyms": ["hairbrush"], "image_count": 121, "id": 524, "frequency": "f", "synset": "hairbrush.n.01"}, {"name": "hairnet", "instance_count": 53, "def": "a small net that someone wears over their hair to keep it in place", "synonyms": ["hairnet"], "image_count": 16, "id": 525, "frequency": "c", "synset": "hairnet.n.01"}, {"name": "hairpin", "instance_count": 20, "def": "a double pronged pin used to hold women's hair in place", "synonyms": ["hairpin"], "image_count": 12, "id": 526, "frequency": "c", "synset": "hairpin.n.01"}, {"name": "halter_top", "instance_count": 3, "def": "a woman's top that fastens behind the back and neck leaving the back and arms uncovered", "synonyms": ["halter_top"], "image_count": 2, "id": 527, "frequency": "r", "synset": "halter.n.03"}, {"name": "ham", "instance_count": 1765, "def": "meat cut from the thigh of a hog (usually smoked)", "synonyms": ["ham", "jambon", "gammon"], "image_count": 214, "id": 528, "frequency": "f", "synset": "ham.n.01"}, {"name": "hamburger", "instance_count": 126, "def": "a sandwich consisting of a patty of minced beef served on a bun", "synonyms": ["hamburger", "beefburger", "burger"], "image_count": 48, "id": 529, "frequency": "c", "synset": "hamburger.n.01"}, {"name": "hammer", "instance_count": 41, "def": "a hand tool with a heavy head and a handle; used to deliver an impulsive force by striking", "synonyms": ["hammer"], "image_count": 26, "id": 530, "frequency": "c", "synset": "hammer.n.02"}, {"name": "hammock", "instance_count": 15, "def": "a hanging bed of canvas or rope netting (usually suspended between two trees)", "synonyms": ["hammock"], "image_count": 13, "id": 531, "frequency": "c", "synset": "hammock.n.02"}, {"name": "hamper", "instance_count": 5, "def": "a basket usually with a cover", "synonyms": ["hamper"], "image_count": 4, "id": 532, "frequency": "r", "synset": "hamper.n.02"}, {"name": "hamster", "instance_count": 12, "def": "short-tailed burrowing rodent with large cheek pouches", "synonyms": ["hamster"], "image_count": 11, "id": 533, "frequency": "c", "synset": "hamster.n.01"}, {"name": "hair_dryer", "instance_count": 144, "def": "a hand-held electric blower that can blow warm air onto the hair", "synonyms": ["hair_dryer"], "image_count": 123, "id": 534, "frequency": "f", "synset": "hand_blower.n.01"}, {"name": "hand_glass", "instance_count": 7, "def": "a mirror intended to be held in the hand", "synonyms": ["hand_glass", "hand_mirror"], "image_count": 7, "id": 535, "frequency": "r", "synset": "hand_glass.n.01"}, {"name": "hand_towel", "instance_count": 619, "def": "a small towel used to dry the hands or face", "synonyms": ["hand_towel", "face_towel"], "image_count": 200, "id": 536, "frequency": "f", "synset": "hand_towel.n.01"}, {"name": "handcart", "instance_count": 204, "def": "wheeled vehicle that can be pushed by a person", "synonyms": ["handcart", "pushcart", "hand_truck"], "image_count": 91, "id": 537, "frequency": "c", "synset": "handcart.n.01"}, {"name": "handcuff", "instance_count": 10, "def": "shackle that consists of a metal loop that can be locked around the wrist", "synonyms": ["handcuff"], "image_count": 9, "id": 538, "frequency": "r", "synset": "handcuff.n.01"}, {"name": "handkerchief", "instance_count": 86, "def": "a square piece of cloth used for wiping the eyes or nose or as a costume accessory", "synonyms": ["handkerchief"], "image_count": 72, "id": 539, "frequency": "c", "synset": "handkerchief.n.01"}, {"name": "handle", "instance_count": 8314, "def": "the appendage to an object that is designed to be held in order to use or move it", "synonyms": ["handle", "grip", "handgrip"], "image_count": 1886, "id": 540, "frequency": "f", "synset": "handle.n.01"}, {"name": "handsaw", "instance_count": 5, "def": "a saw used with one hand for cutting wood", "synonyms": ["handsaw", "carpenter's_saw"], "image_count": 4, "id": 541, "frequency": "r", "synset": "handsaw.n.01"}, {"name": "hardback_book", "instance_count": 2, "def": "a book with cardboard or cloth or leather covers", "synonyms": ["hardback_book", "hardcover_book"], "image_count": 1, "id": 542, "frequency": "r", "synset": "hardback.n.01"}, {"name": "harmonium", "instance_count": 2, "def": "a free-reed instrument in which air is forced through the reeds by bellows", "synonyms": ["harmonium", "organ_(musical_instrument)", "reed_organ_(musical_instrument)"], "image_count": 1, "id": 543, "frequency": "r", "synset": "harmonium.n.01"}, {"name": "hat", "instance_count": 7213, "def": "headwear that protects the head from bad weather, sun, or worn for fashion", "synonyms": ["hat"], "image_count": 1932, "id": 544, "frequency": "f", "synset": "hat.n.01"}, {"name": "hatbox", "instance_count": 7, "def": "a round piece of luggage for carrying hats", "synonyms": ["hatbox"], "image_count": 4, "id": 545, "frequency": "r", "synset": "hatbox.n.01"}, {"name": "veil", "instance_count": 57, "def": "a garment that covers the head OR face", "synonyms": ["veil"], "image_count": 56, "id": 546, "frequency": "c", "synset": "head_covering.n.01"}, {"name": "headband", "instance_count": 1114, "def": "a band worn around or over the head", "synonyms": ["headband"], "image_count": 854, "id": 547, "frequency": "f", "synset": "headband.n.01"}, {"name": "headboard", "instance_count": 850, "def": "a vertical board or panel forming the head of a bedstead", "synonyms": ["headboard"], "image_count": 755, "id": 548, "frequency": "f", "synset": "headboard.n.01"}, {"name": "headlight", "instance_count": 7326, "def": "a powerful light with reflector; attached to the front of an automobile or locomotive", "synonyms": ["headlight", "headlamp"], "image_count": 1843, "id": 549, "frequency": "f", "synset": "headlight.n.01"}, {"name": "headscarf", "instance_count": 235, "def": "a kerchief worn over the head and tied under the chin", "synonyms": ["headscarf"], "image_count": 96, "id": 550, "frequency": "c", "synset": "headscarf.n.01"}, {"name": "headset", "instance_count": 10, "def": "receiver consisting of a pair of headphones", "synonyms": ["headset"], "image_count": 7, "id": 551, "frequency": "r", "synset": "headset.n.01"}, {"name": "headstall_(for_horses)", "instance_count": 133, "def": "the band that is the part of a bridle that fits around a horse's head", "synonyms": ["headstall_(for_horses)", "headpiece_(for_horses)"], "image_count": 74, "id": 552, "frequency": "c", "synset": "headstall.n.01"}, {"name": "heart", "instance_count": 347, "def": "a muscular organ; its contractions move the blood through the body", "synonyms": ["heart"], "image_count": 66, "id": 553, "frequency": "c", "synset": "heart.n.02"}, {"name": "heater", "instance_count": 64, "def": "device that heats water or supplies warmth to a room", "synonyms": ["heater", "warmer"], "image_count": 57, "id": 554, "frequency": "c", "synset": "heater.n.01"}, {"name": "helicopter", "instance_count": 68, "def": "an aircraft without wings that obtains its lift from the rotation of overhead blades", "synonyms": ["helicopter"], "image_count": 44, "id": 555, "frequency": "c", "synset": "helicopter.n.01"}, {"name": "helmet", "instance_count": 4845, "def": "a protective headgear made of hard material to resist blows", "synonyms": ["helmet"], "image_count": 1905, "id": 556, "frequency": "f", "synset": "helmet.n.02"}, {"name": "heron", "instance_count": 6, "def": "grey or white wading bird with long neck and long legs and (usually) long bill", "synonyms": ["heron"], "image_count": 4, "id": 557, "frequency": "r", "synset": "heron.n.02"}, {"name": "highchair", "instance_count": 98, "def": "a chair for feeding a very young child", "synonyms": ["highchair", "feeding_chair"], "image_count": 90, "id": 558, "frequency": "c", "synset": "highchair.n.01"}, {"name": "hinge", "instance_count": 5283, "def": "a joint that holds two parts together so that one can swing relative to the other", "synonyms": ["hinge"], "image_count": 1635, "id": 559, "frequency": "f", "synset": "hinge.n.01"}, {"name": "hippopotamus", "instance_count": 24, "def": "massive thick-skinned animal living in or around rivers of tropical Africa", "synonyms": ["hippopotamus"], "image_count": 8, "id": 560, "frequency": "r", "synset": "hippopotamus.n.01"}, {"name": "hockey_stick", "instance_count": 15, "def": "sports implement consisting of a stick used by hockey players to move the puck", "synonyms": ["hockey_stick"], "image_count": 5, "id": 561, "frequency": "r", "synset": "hockey_stick.n.01"}, {"name": "hog", "instance_count": 73, "def": "domestic swine", "synonyms": ["hog", "pig"], "image_count": 50, "id": 562, "frequency": "c", "synset": "hog.n.03"}, {"name": "home_plate_(baseball)", "instance_count": 551, "def": "(baseball) a rubber slab where the batter stands; it must be touched by a base runner in order to score", "synonyms": ["home_plate_(baseball)", "home_base_(baseball)"], "image_count": 545, "id": 563, "frequency": "f", "synset": "home_plate.n.01"}, {"name": "honey", "instance_count": 90, "def": "a sweet yellow liquid produced by bees", "synonyms": ["honey"], "image_count": 20, "id": 564, "frequency": "c", "synset": "honey.n.01"}, {"name": "fume_hood", "instance_count": 208, "def": "metal covering leading to a vent that exhausts smoke or fumes", "synonyms": ["fume_hood", "exhaust_hood"], "image_count": 193, "id": 565, "frequency": "f", "synset": "hood.n.06"}, {"name": "hook", "instance_count": 1157, "def": "a curved or bent implement for suspending or pulling something", "synonyms": ["hook"], "image_count": 285, "id": 566, "frequency": "f", "synset": "hook.n.05"}, {"name": "hookah", "instance_count": 3, "def": "a tobacco pipe with a long flexible tube connected to a container where the smoke is cooled by passing through water", "synonyms": ["hookah", "narghile", "nargileh", "sheesha", "shisha", "water_pipe"], "image_count": 3, "id": 567, "frequency": "r", "synset": "hookah.n.01"}, {"name": "hornet", "instance_count": 1, "def": "large stinging wasp", "synonyms": ["hornet"], "image_count": 1, "id": 568, "frequency": "r", "synset": "hornet.n.01"}, {"name": "horse", "instance_count": 4744, "def": "a common horse", "synonyms": ["horse"], "image_count": 1904, "id": 569, "frequency": "f", "synset": "horse.n.01"}, {"name": "hose", "instance_count": 610, "def": "a flexible pipe for conveying a liquid or gas", "synonyms": ["hose", "hosepipe"], "image_count": 294, "id": 570, "frequency": "f", "synset": "hose.n.03"}, {"name": "hot-air_balloon", "instance_count": 4, "def": "balloon for travel through the air in a basket suspended below a large bag of heated air", "synonyms": ["hot-air_balloon"], "image_count": 3, "id": 571, "frequency": "r", "synset": "hot-air_balloon.n.01"}, {"name": "hotplate", "instance_count": 6, "def": "a portable electric appliance for heating or cooking or keeping food warm", "synonyms": ["hotplate"], "image_count": 5, "id": 572, "frequency": "r", "synset": "hot_plate.n.01"}, {"name": "hot_sauce", "instance_count": 70, "def": "a pungent peppery sauce", "synonyms": ["hot_sauce"], "image_count": 24, "id": 573, "frequency": "c", "synset": "hot_sauce.n.01"}, {"name": "hourglass", "instance_count": 2, "def": "a sandglass timer that runs for sixty minutes", "synonyms": ["hourglass"], "image_count": 2, "id": 574, "frequency": "r", "synset": "hourglass.n.01"}, {"name": "houseboat", "instance_count": 4, "def": "a barge that is designed and equipped for use as a dwelling", "synonyms": ["houseboat"], "image_count": 2, "id": 575, "frequency": "r", "synset": "houseboat.n.01"}, {"name": "hummingbird", "instance_count": 18, "def": "tiny American bird having brilliant iridescent plumage and long slender bills", "synonyms": ["hummingbird"], "image_count": 16, "id": 576, "frequency": "c", "synset": "hummingbird.n.01"}, {"name": "hummus", "instance_count": 9, "def": "a thick spread made from mashed chickpeas", "synonyms": ["hummus", "humus", "hommos", "hoummos", "humous"], "image_count": 8, "id": 577, "frequency": "r", "synset": "hummus.n.01"}, {"name": "polar_bear", "instance_count": 196, "def": "white bear of Arctic regions", "synonyms": ["polar_bear"], "image_count": 154, "id": 578, "frequency": "f", "synset": "ice_bear.n.01"}, {"name": "icecream", "instance_count": 180, "def": "frozen dessert containing cream and sugar and flavoring", "synonyms": ["icecream"], "image_count": 66, "id": 579, "frequency": "c", "synset": "ice_cream.n.01"}, {"name": "popsicle", "instance_count": 1, "def": "ice cream or water ice on a small wooden stick", "synonyms": ["popsicle"], "image_count": 1, "id": 580, "frequency": "r", "synset": "ice_lolly.n.01"}, {"name": "ice_maker", "instance_count": 26, "def": "an appliance included in some electric refrigerators for making ice cubes", "synonyms": ["ice_maker"], "image_count": 24, "id": 581, "frequency": "c", "synset": "ice_maker.n.01"}, {"name": "ice_pack", "instance_count": 4, "def": "a waterproof bag filled with ice: applied to the body (especially the head) to cool or reduce swelling", "synonyms": ["ice_pack", "ice_bag"], "image_count": 1, "id": 582, "frequency": "r", "synset": "ice_pack.n.01"}, {"name": "ice_skate", "instance_count": 14, "def": "skate consisting of a boot with a steel blade fitted to the sole", "synonyms": ["ice_skate"], "image_count": 4, "id": 583, "frequency": "r", "synset": "ice_skate.n.01"}, {"name": "igniter", "instance_count": 77, "def": "a substance or device used to start a fire", "synonyms": ["igniter", "ignitor", "lighter"], "image_count": 75, "id": 584, "frequency": "c", "synset": "igniter.n.01"}, {"name": "inhaler", "instance_count": 7, "def": "a dispenser that produces a chemical vapor to be inhaled through mouth or nose", "synonyms": ["inhaler", "inhalator"], "image_count": 6, "id": 585, "frequency": "r", "synset": "inhaler.n.01"}, {"name": "iPod", "instance_count": 172, "def": "a pocket-sized device used to play music files", "synonyms": ["iPod"], "image_count": 126, "id": 586, "frequency": "f", "synset": "ipod.n.01"}, {"name": "iron_(for_clothing)", "instance_count": 38, "def": "home appliance consisting of a flat metal base that is heated and used to smooth cloth", "synonyms": ["iron_(for_clothing)", "smoothing_iron_(for_clothing)"], "image_count": 24, "id": 587, "frequency": "c", "synset": "iron.n.04"}, {"name": "ironing_board", "instance_count": 24, "def": "narrow padded board on collapsible supports; used for ironing clothes", "synonyms": ["ironing_board"], "image_count": 22, "id": 588, "frequency": "c", "synset": "ironing_board.n.01"}, {"name": "jacket", "instance_count": 8013, "def": "a waist-length coat", "synonyms": ["jacket"], "image_count": 1872, "id": 589, "frequency": "f", "synset": "jacket.n.01"}, {"name": "jam", "instance_count": 29, "def": "preserve of crushed fruit", "synonyms": ["jam"], "image_count": 16, "id": 590, "frequency": "c", "synset": "jam.n.01"}, {"name": "jar", "instance_count": 2002, "def": "a vessel (usually cylindrical) with a wide mouth and without handles", "synonyms": ["jar"], "image_count": 423, "id": 591, "frequency": "f", "synset": "jar.n.01"}, {"name": "jean", "instance_count": 5421, "def": "(usually plural) close-fitting trousers of heavy denim for manual work or casual wear", "synonyms": ["jean", "blue_jean", "denim"], "image_count": 1927, "id": 592, "frequency": "f", "synset": "jean.n.01"}, {"name": "jeep", "instance_count": 55, "def": "a car suitable for traveling over rough terrain", "synonyms": ["jeep", "landrover"], "image_count": 38, "id": 593, "frequency": "c", "synset": "jeep.n.01"}, {"name": "jelly_bean", "instance_count": 116, "def": "sugar-glazed jellied candy", "synonyms": ["jelly_bean", "jelly_egg"], "image_count": 3, "id": 594, "frequency": "r", "synset": "jelly_bean.n.01"}, {"name": "jersey", "instance_count": 8117, "def": "a close-fitting pullover shirt", "synonyms": ["jersey", "T-shirt", "tee_shirt"], "image_count": 1945, "id": 595, "frequency": "f", "synset": "jersey.n.03"}, {"name": "jet_plane", "instance_count": 87, "def": "an airplane powered by one or more jet engines", "synonyms": ["jet_plane", "jet-propelled_plane"], "image_count": 35, "id": 596, "frequency": "c", "synset": "jet.n.01"}, {"name": "jewel", "instance_count": 1, "def": "a precious or semiprecious stone incorporated into a piece of jewelry", "synonyms": ["jewel", "gem", "precious_stone"], "image_count": 1, "id": 597, "frequency": "r", "synset": "jewel.n.01"}, {"name": "jewelry", "instance_count": 51, "def": "an adornment (as a bracelet or ring or necklace) made of precious metals and set with gems (or imitation gems)", "synonyms": ["jewelry", "jewellery"], "image_count": 13, "id": 598, "frequency": "c", "synset": "jewelry.n.01"}, {"name": "joystick", "instance_count": 12, "def": "a control device for computers consisting of a vertical handle that can move freely in two directions", "synonyms": ["joystick"], "image_count": 9, "id": 599, "frequency": "r", "synset": "joystick.n.02"}, {"name": "jumpsuit", "instance_count": 21, "def": "one-piece garment fashioned after a parachutist's uniform", "synonyms": ["jumpsuit"], "image_count": 14, "id": 600, "frequency": "c", "synset": "jump_suit.n.01"}, {"name": "kayak", "instance_count": 124, "def": "a small canoe consisting of a light frame made watertight with animal skins", "synonyms": ["kayak"], "image_count": 37, "id": 601, "frequency": "c", "synset": "kayak.n.01"}, {"name": "keg", "instance_count": 6, "def": "small cask or barrel", "synonyms": ["keg"], "image_count": 3, "id": 602, "frequency": "r", "synset": "keg.n.02"}, {"name": "kennel", "instance_count": 4, "def": "outbuilding that serves as a shelter for a dog", "synonyms": ["kennel", "doghouse"], "image_count": 4, "id": 603, "frequency": "r", "synset": "kennel.n.01"}, {"name": "kettle", "instance_count": 130, "def": "a metal pot for stewing or boiling; usually has a lid", "synonyms": ["kettle", "boiler"], "image_count": 100, "id": 604, "frequency": "c", "synset": "kettle.n.01"}, {"name": "key", "instance_count": 447, "def": "metal instrument used to unlock a lock", "synonyms": ["key"], "image_count": 195, "id": 605, "frequency": "f", "synset": "key.n.01"}, {"name": "keycard", "instance_count": 1, "def": "a plastic card used to gain access typically to a door", "synonyms": ["keycard"], "image_count": 1, "id": 606, "frequency": "r", "synset": "keycard.n.01"}, {"name": "kilt", "instance_count": 19, "def": "a knee-length pleated tartan skirt worn by men as part of the traditional dress in the Highlands of northern Scotland", "synonyms": ["kilt"], "image_count": 12, "id": 607, "frequency": "c", "synset": "kilt.n.01"}, {"name": "kimono", "instance_count": 38, "def": "a loose robe; imitated from robes originally worn by Japanese", "synonyms": ["kimono"], "image_count": 24, "id": 608, "frequency": "c", "synset": "kimono.n.01"}, {"name": "kitchen_sink", "instance_count": 519, "def": "a sink in a kitchen", "synonyms": ["kitchen_sink"], "image_count": 489, "id": 609, "frequency": "f", "synset": "kitchen_sink.n.01"}, {"name": "kitchen_table", "instance_count": 11, "def": "a table in the kitchen", "synonyms": ["kitchen_table"], "image_count": 10, "id": 610, "frequency": "r", "synset": "kitchen_table.n.01"}, {"name": "kite", "instance_count": 11174, "def": "plaything consisting of a light frame covered with tissue paper; flown in wind at end of a string", "synonyms": ["kite"], "image_count": 1689, "id": 611, "frequency": "f", "synset": "kite.n.03"}, {"name": "kitten", "instance_count": 60, "def": "young domestic cat", "synonyms": ["kitten", "kitty"], "image_count": 42, "id": 612, "frequency": "c", "synset": "kitten.n.01"}, {"name": "kiwi_fruit", "instance_count": 702, "def": "fuzzy brown egg-shaped fruit with slightly tart green flesh", "synonyms": ["kiwi_fruit"], "image_count": 81, "id": 613, "frequency": "c", "synset": "kiwi.n.03"}, {"name": "knee_pad", "instance_count": 1765, "def": "protective garment consisting of a pad worn by football or baseball or hockey players", "synonyms": ["knee_pad"], "image_count": 894, "id": 614, "frequency": "f", "synset": "knee_pad.n.01"}, {"name": "knife", "instance_count": 3515, "def": "tool with a blade and point used as a cutting instrument", "synonyms": ["knife"], "image_count": 1868, "id": 615, "frequency": "f", "synset": "knife.n.01"}, {"name": "knitting_needle", "instance_count": 16, "def": "needle consisting of a slender rod with pointed ends; usually used in pairs", "synonyms": ["knitting_needle"], "image_count": 7, "id": 616, "frequency": "r", "synset": "knitting_needle.n.01"}, {"name": "knob", "instance_count": 8432, "def": "a round handle often found on a door", "synonyms": ["knob"], "image_count": 1567, "id": 617, "frequency": "f", "synset": "knob.n.02"}, {"name": "knocker_(on_a_door)", "instance_count": 10, "def": "a device (usually metal and ornamental) attached by a hinge to a door", "synonyms": ["knocker_(on_a_door)", "doorknocker"], "image_count": 10, "id": 618, "frequency": "r", "synset": "knocker.n.05"}, {"name": "koala", "instance_count": 15, "def": "sluggish tailless Australian marsupial with grey furry ears and coat", "synonyms": ["koala", "koala_bear"], "image_count": 8, "id": 619, "frequency": "r", "synset": "koala.n.01"}, {"name": "lab_coat", "instance_count": 42, "def": "a light coat worn to protect clothing from substances used while working in a laboratory", "synonyms": ["lab_coat", "laboratory_coat"], "image_count": 7, "id": 620, "frequency": "r", "synset": "lab_coat.n.01"}, {"name": "ladder", "instance_count": 975, "def": "steps consisting of two parallel members connected by rungs", "synonyms": ["ladder"], "image_count": 629, "id": 621, "frequency": "f", "synset": "ladder.n.01"}, {"name": "ladle", "instance_count": 226, "def": "a spoon-shaped vessel with a long handle frequently used to transfer liquids", "synonyms": ["ladle"], "image_count": 89, "id": 622, "frequency": "c", "synset": "ladle.n.01"}, {"name": "ladybug", "instance_count": 68, "def": "small round bright-colored and spotted beetle, typically red and black", "synonyms": ["ladybug", "ladybeetle", "ladybird_beetle"], "image_count": 15, "id": 623, "frequency": "c", "synset": "ladybug.n.01"}, {"name": "lamb_(animal)", "instance_count": 618, "def": "young sheep", "synonyms": ["lamb_(animal)"], "image_count": 134, "id": 624, "frequency": "f", "synset": "lamb.n.01"}, {"name": "lamb-chop", "instance_count": 8, "def": "chop cut from a lamb", "synonyms": ["lamb-chop", "lambchop"], "image_count": 4, "id": 625, "frequency": "r", "synset": "lamb_chop.n.01"}, {"name": "lamp", "instance_count": 4139, "def": "a piece of furniture holding one or more electric light bulbs", "synonyms": ["lamp"], "image_count": 1802, "id": 626, "frequency": "f", "synset": "lamp.n.02"}, {"name": "lamppost", "instance_count": 2234, "def": "a metal post supporting an outdoor lamp (such as a streetlight)", "synonyms": ["lamppost"], "image_count": 595, "id": 627, "frequency": "f", "synset": "lamppost.n.01"}, {"name": "lampshade", "instance_count": 2475, "def": "a protective ornamental shade used to screen a light bulb from direct view", "synonyms": ["lampshade"], "image_count": 1210, "id": 628, "frequency": "f", "synset": "lampshade.n.01"}, {"name": "lantern", "instance_count": 364, "def": "light in a transparent protective case", "synonyms": ["lantern"], "image_count": 48, "id": 629, "frequency": "c", "synset": "lantern.n.01"}, {"name": "lanyard", "instance_count": 1065, "def": "a cord worn around the neck to hold a knife or whistle, etc.", "synonyms": ["lanyard", "laniard"], "image_count": 418, "id": 630, "frequency": "f", "synset": "lanyard.n.02"}, {"name": "laptop_computer", "instance_count": 2852, "def": "a portable computer small enough to use in your lap", "synonyms": ["laptop_computer", "notebook_computer"], "image_count": 1846, "id": 631, "frequency": "f", "synset": "laptop.n.01"}, {"name": "lasagna", "instance_count": 7, "def": "baked dish of layers of lasagna pasta with sauce and cheese and meat or vegetables", "synonyms": ["lasagna", "lasagne"], "image_count": 5, "id": 632, "frequency": "r", "synset": "lasagna.n.01"}, {"name": "latch", "instance_count": 702, "def": "a bar that can be lowered or slid into a groove to fasten a door or gate", "synonyms": ["latch"], "image_count": 221, "id": 633, "frequency": "f", "synset": "latch.n.02"}, {"name": "lawn_mower", "instance_count": 12, "def": "garden tool for mowing grass on lawns", "synonyms": ["lawn_mower"], "image_count": 10, "id": 634, "frequency": "r", "synset": "lawn_mower.n.01"}, {"name": "leather", "instance_count": 20, "def": "an animal skin made smooth and flexible by removing the hair and then tanning", "synonyms": ["leather"], "image_count": 7, "id": 635, "frequency": "r", "synset": "leather.n.01"}, {"name": "legging_(clothing)", "instance_count": 154, "def": "a garment covering the leg (usually extending from the knee to the ankle)", "synonyms": ["legging_(clothing)", "leging_(clothing)", "leg_covering"], "image_count": 76, "id": 636, "frequency": "c", "synset": "legging.n.01"}, {"name": "Lego", "instance_count": 331, "def": "a child's plastic construction set for making models from blocks", "synonyms": ["Lego", "Lego_set"], "image_count": 22, "id": 637, "frequency": "c", "synset": "lego.n.01"}, {"name": "legume", "instance_count": 333, "def": "the fruit or seed of bean or pea plants", "synonyms": ["legume"], "image_count": 10, "id": 638, "frequency": "r", "synset": "legume.n.02"}, {"name": "lemon", "instance_count": 2168, "def": "yellow oval fruit with juicy acidic flesh", "synonyms": ["lemon"], "image_count": 341, "id": 639, "frequency": "f", "synset": "lemon.n.01"}, {"name": "lemonade", "instance_count": 2, "def": "sweetened beverage of diluted lemon juice", "synonyms": ["lemonade"], "image_count": 1, "id": 640, "frequency": "r", "synset": "lemonade.n.01"}, {"name": "lettuce", "instance_count": 5500, "def": "leafy plant commonly eaten in salad or on sandwiches", "synonyms": ["lettuce"], "image_count": 705, "id": 641, "frequency": "f", "synset": "lettuce.n.02"}, {"name": "license_plate", "instance_count": 4392, "def": "a plate mounted on the front and back of car and bearing the car's registration number", "synonyms": ["license_plate", "numberplate"], "image_count": 1900, "id": 642, "frequency": "f", "synset": "license_plate.n.01"}, {"name": "life_buoy", "instance_count": 524, "def": "a ring-shaped life preserver used to prevent drowning (NOT a life-jacket or vest)", "synonyms": ["life_buoy", "lifesaver", "life_belt", "life_ring"], "image_count": 188, "id": 643, "frequency": "f", "synset": "life_buoy.n.01"}, {"name": "life_jacket", "instance_count": 689, "def": "life preserver consisting of a sleeveless jacket of buoyant or inflatable design", "synonyms": ["life_jacket", "life_vest"], "image_count": 227, "id": 644, "frequency": "f", "synset": "life_jacket.n.01"}, {"name": "lightbulb", "instance_count": 7075, "def": "lightblub/source of light", "synonyms": ["lightbulb"], "image_count": 861, "id": 645, "frequency": "f", "synset": "light_bulb.n.01"}, {"name": "lightning_rod", "instance_count": 6, "def": "a metallic conductor that is attached to a high point and leads to the ground", "synonyms": ["lightning_rod", "lightning_conductor"], "image_count": 6, "id": 646, "frequency": "r", "synset": "lightning_rod.n.02"}, {"name": "lime", "instance_count": 1134, "def": "the green acidic fruit of any of various lime trees", "synonyms": ["lime"], "image_count": 115, "id": 647, "frequency": "f", "synset": "lime.n.06"}, {"name": "limousine", "instance_count": 6, "def": "long luxurious car; usually driven by a chauffeur", "synonyms": ["limousine"], "image_count": 5, "id": 648, "frequency": "r", "synset": "limousine.n.01"}, {"name": "lion", "instance_count": 69, "def": "large gregarious predatory cat of Africa and India", "synonyms": ["lion"], "image_count": 43, "id": 649, "frequency": "c", "synset": "lion.n.01"}, {"name": "lip_balm", "instance_count": 29, "def": "a balm applied to the lips", "synonyms": ["lip_balm"], "image_count": 14, "id": 650, "frequency": "c", "synset": "lip_balm.n.01"}, {"name": "liquor", "instance_count": 66, "def": "liquor or beer", "synonyms": ["liquor", "spirits", "hard_liquor", "liqueur", "cordial"], "image_count": 6, "id": 651, "frequency": "r", "synset": "liquor.n.01"}, {"name": "lizard", "instance_count": 22, "def": "a reptile with usually two pairs of legs and a tapering tail", "synonyms": ["lizard"], "image_count": 15, "id": 652, "frequency": "c", "synset": "lizard.n.01"}, {"name": "log", "instance_count": 7363, "def": "a segment of the trunk of a tree when stripped of branches", "synonyms": ["log"], "image_count": 1167, "id": 653, "frequency": "f", "synset": "log.n.01"}, {"name": "lollipop", "instance_count": 59, "def": "hard candy on a stick", "synonyms": ["lollipop"], "image_count": 15, "id": 654, "frequency": "c", "synset": "lollipop.n.02"}, {"name": "speaker_(stero_equipment)", "instance_count": 2029, "def": "electronic device that produces sound often as part of a stereo system", "synonyms": ["speaker_(stero_equipment)"], "image_count": 994, "id": 655, "frequency": "f", "synset": "loudspeaker.n.01"}, {"name": "loveseat", "instance_count": 41, "def": "small sofa that seats two people", "synonyms": ["loveseat"], "image_count": 28, "id": 656, "frequency": "c", "synset": "love_seat.n.01"}, {"name": "machine_gun", "instance_count": 5, "def": "a rapidly firing automatic gun", "synonyms": ["machine_gun"], "image_count": 2, "id": 657, "frequency": "r", "synset": "machine_gun.n.01"}, {"name": "magazine", "instance_count": 1379, "def": "a paperback periodic publication", "synonyms": ["magazine"], "image_count": 338, "id": 658, "frequency": "f", "synset": "magazine.n.02"}, {"name": "magnet", "instance_count": 5638, "def": "a device that attracts iron and produces a magnetic field", "synonyms": ["magnet"], "image_count": 334, "id": 659, "frequency": "f", "synset": "magnet.n.01"}, {"name": "mail_slot", "instance_count": 16, "def": "a slot (usually in a door) through which mail can be delivered", "synonyms": ["mail_slot"], "image_count": 15, "id": 660, "frequency": "c", "synset": "mail_slot.n.01"}, {"name": "mailbox_(at_home)", "instance_count": 240, "def": "a private box for delivery of mail", "synonyms": ["mailbox_(at_home)", "letter_box_(at_home)"], "image_count": 102, "id": 661, "frequency": "f", "synset": "mailbox.n.01"}, {"name": "mallard", "instance_count": 2, "def": "wild dabbling duck from which domestic ducks are descended", "synonyms": ["mallard"], "image_count": 1, "id": 662, "frequency": "r", "synset": "mallard.n.01"}, {"name": "mallet", "instance_count": 16, "def": "a sports implement with a long handle and a hammer-like head used to hit a ball", "synonyms": ["mallet"], "image_count": 8, "id": 663, "frequency": "r", "synset": "mallet.n.01"}, {"name": "mammoth", "instance_count": 2, "def": "any of numerous extinct elephants widely distributed in the Pleistocene", "synonyms": ["mammoth"], "image_count": 1, "id": 664, "frequency": "r", "synset": "mammoth.n.01"}, {"name": "manatee", "instance_count": 1, "def": "sirenian mammal of tropical coastal waters of America", "synonyms": ["manatee"], "image_count": 1, "id": 665, "frequency": "r", "synset": "manatee.n.01"}, {"name": "mandarin_orange", "instance_count": 401, "def": "a somewhat flat reddish-orange loose skinned citrus of China", "synonyms": ["mandarin_orange"], "image_count": 28, "id": 666, "frequency": "c", "synset": "mandarin.n.05"}, {"name": "manger", "instance_count": 126, "def": "a container (usually in a barn or stable) from which cattle or horses feed", "synonyms": ["manger", "trough"], "image_count": 91, "id": 667, "frequency": "c", "synset": "manger.n.01"}, {"name": "manhole", "instance_count": 445, "def": "a hole (usually with a flush cover) through which a person can gain access to an underground structure", "synonyms": ["manhole"], "image_count": 260, "id": 668, "frequency": "f", "synset": "manhole.n.01"}, {"name": "map", "instance_count": 186, "def": "a diagrammatic representation of the earth's surface (or part of it)", "synonyms": ["map"], "image_count": 131, "id": 669, "frequency": "f", "synset": "map.n.01"}, {"name": "marker", "instance_count": 501, "def": "a writing implement for making a mark", "synonyms": ["marker"], "image_count": 128, "id": 670, "frequency": "f", "synset": "marker.n.03"}, {"name": "martini", "instance_count": 3, "def": "a cocktail made of gin (or vodka) with dry vermouth", "synonyms": ["martini"], "image_count": 3, "id": 671, "frequency": "r", "synset": "martini.n.01"}, {"name": "mascot", "instance_count": 10, "def": "a person or animal that is adopted by a team or other group as a symbolic figure", "synonyms": ["mascot"], "image_count": 10, "id": 672, "frequency": "r", "synset": "mascot.n.01"}, {"name": "mashed_potato", "instance_count": 58, "def": "potato that has been peeled and boiled and then mashed", "synonyms": ["mashed_potato"], "image_count": 39, "id": 673, "frequency": "c", "synset": "mashed_potato.n.01"}, {"name": "masher", "instance_count": 2, "def": "a kitchen utensil used for mashing (e.g. potatoes)", "synonyms": ["masher"], "image_count": 2, "id": 674, "frequency": "r", "synset": "masher.n.02"}, {"name": "mask", "instance_count": 1595, "def": "a protective covering worn over the face", "synonyms": ["mask", "facemask"], "image_count": 925, "id": 675, "frequency": "f", "synset": "mask.n.04"}, {"name": "mast", "instance_count": 2985, "def": "a vertical spar for supporting sails", "synonyms": ["mast"], "image_count": 354, "id": 676, "frequency": "f", "synset": "mast.n.01"}, {"name": "mat_(gym_equipment)", "instance_count": 114, "def": "sports equipment consisting of a piece of thick padding on the floor for gymnastics", "synonyms": ["mat_(gym_equipment)", "gym_mat"], "image_count": 31, "id": 677, "frequency": "c", "synset": "mat.n.03"}, {"name": "matchbox", "instance_count": 11, "def": "a box for holding matches", "synonyms": ["matchbox"], "image_count": 10, "id": 678, "frequency": "r", "synset": "matchbox.n.01"}, {"name": "mattress", "instance_count": 354, "def": "a thick pad filled with resilient material used as a bed or part of a bed", "synonyms": ["mattress"], "image_count": 215, "id": 679, "frequency": "f", "synset": "mattress.n.01"}, {"name": "measuring_cup", "instance_count": 139, "def": "graduated cup used to measure liquid or granular ingredients", "synonyms": ["measuring_cup"], "image_count": 71, "id": 680, "frequency": "c", "synset": "measuring_cup.n.01"}, {"name": "measuring_stick", "instance_count": 57, "def": "measuring instrument having a sequence of marks at regular intervals", "synonyms": ["measuring_stick", "ruler_(measuring_stick)", "measuring_rod"], "image_count": 43, "id": 681, "frequency": "c", "synset": "measuring_stick.n.01"}, {"name": "meatball", "instance_count": 174, "def": "ground meat formed into a ball and fried or simmered in broth", "synonyms": ["meatball"], "image_count": 28, "id": 682, "frequency": "c", "synset": "meatball.n.01"}, {"name": "medicine", "instance_count": 243, "def": "something that treats or prevents or alleviates the symptoms of disease", "synonyms": ["medicine"], "image_count": 34, "id": 683, "frequency": "c", "synset": "medicine.n.02"}, {"name": "melon", "instance_count": 167, "def": "fruit of the gourd family having a hard rind and sweet juicy flesh", "synonyms": ["melon"], "image_count": 16, "id": 684, "frequency": "c", "synset": "melon.n.01"}, {"name": "microphone", "instance_count": 435, "def": "device for converting sound waves into electrical energy", "synonyms": ["microphone"], "image_count": 273, "id": 685, "frequency": "f", "synset": "microphone.n.01"}, {"name": "microscope", "instance_count": 3, "def": "magnifier of the image of small objects", "synonyms": ["microscope"], "image_count": 2, "id": 686, "frequency": "r", "synset": "microscope.n.01"}, {"name": "microwave_oven", "instance_count": 1105, "def": "kitchen appliance that cooks food by passing an electromagnetic wave through it", "synonyms": ["microwave_oven"], "image_count": 999, "id": 687, "frequency": "f", "synset": "microwave.n.02"}, {"name": "milestone", "instance_count": 5, "def": "stone post at side of a road to show distances", "synonyms": ["milestone", "milepost"], "image_count": 4, "id": 688, "frequency": "r", "synset": "milestone.n.01"}, {"name": "milk", "instance_count": 227, "def": "a white nutritious liquid secreted by mammals and used as food by human beings", "synonyms": ["milk"], "image_count": 107, "id": 689, "frequency": "f", "synset": "milk.n.01"}, {"name": "milk_can", "instance_count": 8, "def": "can for transporting milk", "synonyms": ["milk_can"], "image_count": 2, "id": 690, "frequency": "r", "synset": "milk_can.n.01"}, {"name": "milkshake", "instance_count": 1, "def": "frothy drink of milk and flavoring and sometimes fruit or ice cream", "synonyms": ["milkshake"], "image_count": 1, "id": 691, "frequency": "r", "synset": "milkshake.n.01"}, {"name": "minivan", "instance_count": 1046, "def": "a small box-shaped passenger van", "synonyms": ["minivan"], "image_count": 454, "id": 692, "frequency": "f", "synset": "minivan.n.01"}, {"name": "mint_candy", "instance_count": 27, "def": "a candy that is flavored with a mint oil", "synonyms": ["mint_candy"], "image_count": 9, "id": 693, "frequency": "r", "synset": "mint.n.05"}, {"name": "mirror", "instance_count": 3490, "def": "polished surface that forms images by reflecting light", "synonyms": ["mirror"], "image_count": 1901, "id": 694, "frequency": "f", "synset": "mirror.n.01"}, {"name": "mitten", "instance_count": 156, "def": "glove that encases the thumb separately and the other four fingers together", "synonyms": ["mitten"], "image_count": 61, "id": 695, "frequency": "c", "synset": "mitten.n.01"}, {"name": "mixer_(kitchen_tool)", "instance_count": 108, "def": "a kitchen utensil that is used for mixing foods", "synonyms": ["mixer_(kitchen_tool)", "stand_mixer"], "image_count": 91, "id": 696, "frequency": "c", "synset": "mixer.n.04"}, {"name": "money", "instance_count": 122, "def": "the official currency issued by a government or national bank", "synonyms": ["money"], "image_count": 46, "id": 697, "frequency": "c", "synset": "money.n.03"}, {"name": "monitor_(computer_equipment) computer_monitor", "instance_count": 2955, "def": "a computer monitor", "synonyms": ["monitor_(computer_equipment) computer_monitor"], "image_count": 1402, "id": 698, "frequency": "f", "synset": "monitor.n.04"}, {"name": "monkey", "instance_count": 166, "def": "any of various long-tailed primates", "synonyms": ["monkey"], "image_count": 74, "id": 699, "frequency": "c", "synset": "monkey.n.01"}, {"name": "motor", "instance_count": 985, "def": "machine that converts other forms of energy into mechanical energy and so imparts motion", "synonyms": ["motor"], "image_count": 421, "id": 700, "frequency": "f", "synset": "motor.n.01"}, {"name": "motor_scooter", "instance_count": 720, "def": "a wheeled vehicle with small wheels and a low-powered engine", "synonyms": ["motor_scooter", "scooter"], "image_count": 226, "id": 701, "frequency": "f", "synset": "motor_scooter.n.01"}, {"name": "motor_vehicle", "instance_count": 64, "def": "a self-propelled wheeled vehicle that does not run on rails", "synonyms": ["motor_vehicle", "automotive_vehicle"], "image_count": 10, "id": 702, "frequency": "r", "synset": "motor_vehicle.n.01"}, {"name": "motorcycle", "instance_count": 5247, "def": "a motor vehicle with two wheels and a strong frame", "synonyms": ["motorcycle"], "image_count": 1720, "id": 703, "frequency": "f", "synset": "motorcycle.n.01"}, {"name": "mound_(baseball)", "instance_count": 269, "def": "(baseball) the slight elevation on which the pitcher stands", "synonyms": ["mound_(baseball)", "pitcher's_mound"], "image_count": 261, "id": 704, "frequency": "f", "synset": "mound.n.01"}, {"name": "mouse_(computer_equipment)", "instance_count": 1832, "def": "a computer input device that controls an on-screen pointer (does not include trackpads / touchpads)", "synonyms": ["mouse_(computer_equipment)", "computer_mouse"], "image_count": 1337, "id": 705, "frequency": "f", "synset": "mouse.n.04"}, {"name": "mousepad", "instance_count": 333, "def": "a small portable pad that provides an operating surface for a computer mouse", "synonyms": ["mousepad"], "image_count": 293, "id": 706, "frequency": "f", "synset": "mousepad.n.01"}, {"name": "muffin", "instance_count": 352, "def": "a sweet quick bread baked in a cup-shaped pan", "synonyms": ["muffin"], "image_count": 62, "id": 707, "frequency": "c", "synset": "muffin.n.01"}, {"name": "mug", "instance_count": 1785, "def": "with handle and usually cylindrical", "synonyms": ["mug"], "image_count": 814, "id": 708, "frequency": "f", "synset": "mug.n.04"}, {"name": "mushroom", "instance_count": 6257, "def": "a common mushroom", "synonyms": ["mushroom"], "image_count": 407, "id": 709, "frequency": "f", "synset": "mushroom.n.02"}, {"name": "music_stool", "instance_count": 6, "def": "a stool for piano players; usually adjustable in height", "synonyms": ["music_stool", "piano_stool"], "image_count": 6, "id": 710, "frequency": "r", "synset": "music_stool.n.01"}, {"name": "musical_instrument", "instance_count": 33, "def": "any of various devices or contrivances that can be used to produce musical tones or sounds", "synonyms": ["musical_instrument", "instrument_(musical)"], "image_count": 16, "id": 711, "frequency": "c", "synset": "musical_instrument.n.01"}, {"name": "nailfile", "instance_count": 10, "def": "a small flat file for shaping the nails", "synonyms": ["nailfile"], "image_count": 7, "id": 712, "frequency": "r", "synset": "nailfile.n.01"}, {"name": "napkin", "instance_count": 3979, "def": "a small piece of table linen or paper that is used to wipe the mouth and to cover the lap in order to protect clothing", "synonyms": ["napkin", "table_napkin", "serviette"], "image_count": 1791, "id": 713, "frequency": "f", "synset": "napkin.n.01"}, {"name": "neckerchief", "instance_count": 4, "def": "a kerchief worn around the neck", "synonyms": ["neckerchief"], "image_count": 2, "id": 714, "frequency": "r", "synset": "neckerchief.n.01"}, {"name": "necklace", "instance_count": 2709, "def": "jewelry consisting of a cord or chain (often bearing gems) worn about the neck as an ornament", "synonyms": ["necklace"], "image_count": 1915, "id": 715, "frequency": "f", "synset": "necklace.n.01"}, {"name": "necktie", "instance_count": 4069, "def": "neckwear consisting of a long narrow piece of material worn under a collar and tied in knot at the front", "synonyms": ["necktie", "tie_(necktie)"], "image_count": 1940, "id": 716, "frequency": "f", "synset": "necktie.n.01"}, {"name": "needle", "instance_count": 61, "def": "a sharp pointed implement (usually metal)", "synonyms": ["needle"], "image_count": 13, "id": 717, "frequency": "c", "synset": "needle.n.03"}, {"name": "nest", "instance_count": 20, "def": "a structure in which animals lay eggs or give birth to their young", "synonyms": ["nest"], "image_count": 16, "id": 718, "frequency": "c", "synset": "nest.n.01"}, {"name": "newspaper", "instance_count": 1179, "def": "a daily or weekly publication on folded sheets containing news, articles, and advertisements", "synonyms": ["newspaper", "paper_(newspaper)"], "image_count": 448, "id": 719, "frequency": "f", "synset": "newspaper.n.01"}, {"name": "newsstand", "instance_count": 39, "def": "a stall where newspapers and other periodicals are sold", "synonyms": ["newsstand"], "image_count": 12, "id": 720, "frequency": "c", "synset": "newsstand.n.01"}, {"name": "nightshirt", "instance_count": 35, "def": "garments designed to be worn in bed", "synonyms": ["nightshirt", "nightwear", "sleepwear", "nightclothes"], "image_count": 18, "id": 721, "frequency": "c", "synset": "nightwear.n.01"}, {"name": "nosebag_(for_animals)", "instance_count": 4, "def": "a canvas bag that is used to feed an animal (such as a horse); covers the muzzle and fastens at the top of the head", "synonyms": ["nosebag_(for_animals)", "feedbag"], "image_count": 4, "id": 722, "frequency": "r", "synset": "nosebag.n.01"}, {"name": "noseband_(for_animals)", "instance_count": 120, "def": "a strap that is the part of a bridle that goes over the animal's nose", "synonyms": ["noseband_(for_animals)", "nosepiece_(for_animals)"], "image_count": 71, "id": 723, "frequency": "c", "synset": "noseband.n.01"}, {"name": "notebook", "instance_count": 290, "def": "a book with blank pages for recording notes or memoranda", "synonyms": ["notebook"], "image_count": 189, "id": 724, "frequency": "f", "synset": "notebook.n.01"}, {"name": "notepad", "instance_count": 187, "def": "a pad of paper for keeping notes", "synonyms": ["notepad"], "image_count": 74, "id": 725, "frequency": "c", "synset": "notepad.n.01"}, {"name": "nut", "instance_count": 790, "def": "a small metal block (usually square or hexagonal) with internal screw thread to be fitted onto a bolt", "synonyms": ["nut"], "image_count": 103, "id": 726, "frequency": "f", "synset": "nut.n.03"}, {"name": "nutcracker", "instance_count": 7, "def": "a hand tool used to crack nuts open", "synonyms": ["nutcracker"], "image_count": 3, "id": 727, "frequency": "r", "synset": "nutcracker.n.01"}, {"name": "oar", "instance_count": 488, "def": "an implement used to propel or steer a boat", "synonyms": ["oar"], "image_count": 110, "id": 728, "frequency": "f", "synset": "oar.n.01"}, {"name": "octopus_(food)", "instance_count": 5, "def": "tentacles of octopus prepared as food", "synonyms": ["octopus_(food)"], "image_count": 5, "id": 729, "frequency": "r", "synset": "octopus.n.01"}, {"name": "octopus_(animal)", "instance_count": 17, "def": "bottom-living cephalopod having a soft oval body with eight long tentacles", "synonyms": ["octopus_(animal)"], "image_count": 9, "id": 730, "frequency": "r", "synset": "octopus.n.02"}, {"name": "oil_lamp", "instance_count": 28, "def": "a lamp that burns oil (as kerosine) for light", "synonyms": ["oil_lamp", "kerosene_lamp", "kerosine_lamp"], "image_count": 15, "id": 731, "frequency": "c", "synset": "oil_lamp.n.01"}, {"name": "olive_oil", "instance_count": 36, "def": "oil from olives", "synonyms": ["olive_oil"], "image_count": 25, "id": 732, "frequency": "c", "synset": "olive_oil.n.01"}, {"name": "omelet", "instance_count": 10, "def": "beaten eggs cooked until just set; may be folded around e.g. ham or cheese or jelly", "synonyms": ["omelet", "omelette"], "image_count": 7, "id": 733, "frequency": "r", "synset": "omelet.n.01"}, {"name": "onion", "instance_count": 9779, "def": "the bulb of an onion plant", "synonyms": ["onion"], "image_count": 647, "id": 734, "frequency": "f", "synset": "onion.n.01"}, {"name": "orange_(fruit)", "instance_count": 13034, "def": "orange (FRUIT of an orange tree)", "synonyms": ["orange_(fruit)"], "image_count": 824, "id": 735, "frequency": "f", "synset": "orange.n.01"}, {"name": "orange_juice", "instance_count": 223, "def": "bottled or freshly squeezed juice of oranges", "synonyms": ["orange_juice"], "image_count": 100, "id": 736, "frequency": "c", "synset": "orange_juice.n.01"}, {"name": "ostrich", "instance_count": 71, "def": "fast-running African flightless bird with two-toed feet; largest living bird", "synonyms": ["ostrich"], "image_count": 47, "id": 737, "frequency": "c", "synset": "ostrich.n.02"}, {"name": "ottoman", "instance_count": 157, "def": "a thick standalone cushion used as a seat or footrest, often next to a chair", "synonyms": ["ottoman", "pouf", "pouffe", "hassock"], "image_count": 121, "id": 738, "frequency": "f", "synset": "ottoman.n.03"}, {"name": "oven", "instance_count": 929, "def": "kitchen appliance used for baking or roasting", "synonyms": ["oven"], "image_count": 731, "id": 739, "frequency": "f", "synset": "oven.n.01"}, {"name": "overalls_(clothing)", "instance_count": 76, "def": "work clothing consisting of denim trousers usually with a bib and shoulder straps", "synonyms": ["overalls_(clothing)"], "image_count": 73, "id": 740, "frequency": "c", "synset": "overall.n.01"}, {"name": "owl", "instance_count": 73, "def": "nocturnal bird of prey with hawk-like beak and claws and large head with front-facing eyes", "synonyms": ["owl"], "image_count": 49, "id": 741, "frequency": "c", "synset": "owl.n.01"}, {"name": "packet", "instance_count": 109, "def": "a small package or bundle", "synonyms": ["packet"], "image_count": 23, "id": 742, "frequency": "c", "synset": "packet.n.03"}, {"name": "inkpad", "instance_count": 12, "def": "absorbent material saturated with ink used to transfer ink evenly to a rubber stamp", "synonyms": ["inkpad", "inking_pad", "stamp_pad"], "image_count": 4, "id": 743, "frequency": "r", "synset": "pad.n.03"}, {"name": "pad", "instance_count": 264, "def": "mostly arm/knee pads labeled", "synonyms": ["pad"], "image_count": 62, "id": 744, "frequency": "c", "synset": "pad.n.04"}, {"name": "paddle", "instance_count": 306, "def": "a short light oar used without an oarlock to propel a canoe or small boat", "synonyms": ["paddle", "boat_paddle"], "image_count": 118, "id": 745, "frequency": "f", "synset": "paddle.n.04"}, {"name": "padlock", "instance_count": 184, "def": "a detachable, portable lock", "synonyms": ["padlock"], "image_count": 99, "id": 746, "frequency": "c", "synset": "padlock.n.01"}, {"name": "paintbrush", "instance_count": 91, "def": "a brush used as an applicator to apply paint", "synonyms": ["paintbrush"], "image_count": 40, "id": 747, "frequency": "c", "synset": "paintbrush.n.01"}, {"name": "painting", "instance_count": 2645, "def": "graphic art consisting of an artistic composition made by applying paints to a surface", "synonyms": ["painting"], "image_count": 1036, "id": 748, "frequency": "f", "synset": "painting.n.01"}, {"name": "pajamas", "instance_count": 163, "def": "loose-fitting nightclothes worn for sleeping or lounging", "synonyms": ["pajamas", "pyjamas"], "image_count": 105, "id": 749, "frequency": "f", "synset": "pajama.n.02"}, {"name": "palette", "instance_count": 68, "def": "board that provides a flat surface on which artists mix paints and the range of colors used", "synonyms": ["palette", "pallet"], "image_count": 21, "id": 750, "frequency": "c", "synset": "palette.n.02"}, {"name": "pan_(for_cooking)", "instance_count": 643, "def": "cooking utensil consisting of a wide metal vessel", "synonyms": ["pan_(for_cooking)", "cooking_pan"], "image_count": 229, "id": 751, "frequency": "f", "synset": "pan.n.01"}, {"name": "pan_(metal_container)", "instance_count": 21, "def": "shallow container made of metal", "synonyms": ["pan_(metal_container)"], "image_count": 7, "id": 752, "frequency": "r", "synset": "pan.n.03"}, {"name": "pancake", "instance_count": 295, "def": "a flat cake of thin batter fried on both sides on a griddle", "synonyms": ["pancake"], "image_count": 72, "id": 753, "frequency": "c", "synset": "pancake.n.01"}, {"name": "pantyhose", "instance_count": 11, "def": "a woman's tights consisting of underpants and stockings", "synonyms": ["pantyhose"], "image_count": 9, "id": 754, "frequency": "r", "synset": "pantyhose.n.01"}, {"name": "papaya", "instance_count": 206, "def": "large oval melon-like tropical fruit with yellowish flesh", "synonyms": ["papaya"], "image_count": 10, "id": 755, "frequency": "r", "synset": "papaya.n.02"}, {"name": "paper_plate", "instance_count": 957, "def": "a disposable plate made of cardboard", "synonyms": ["paper_plate"], "image_count": 328, "id": 756, "frequency": "f", "synset": "paper_plate.n.01"}, {"name": "paper_towel", "instance_count": 600, "def": "a disposable towel made of absorbent paper", "synonyms": ["paper_towel"], "image_count": 468, "id": 757, "frequency": "f", "synset": "paper_towel.n.01"}, {"name": "paperback_book", "instance_count": 3, "def": "a book with paper covers", "synonyms": ["paperback_book", "paper-back_book", "softback_book", "soft-cover_book"], "image_count": 1, "id": 758, "frequency": "r", "synset": "paperback_book.n.01"}, {"name": "paperweight", "instance_count": 4, "def": "a weight used to hold down a stack of papers", "synonyms": ["paperweight"], "image_count": 2, "id": 759, "frequency": "r", "synset": "paperweight.n.01"}, {"name": "parachute", "instance_count": 61, "def": "rescue equipment consisting of a device that fills with air and retards your fall", "synonyms": ["parachute"], "image_count": 24, "id": 760, "frequency": "c", "synset": "parachute.n.01"}, {"name": "parakeet", "instance_count": 46, "def": "any of numerous small slender long-tailed parrots", "synonyms": ["parakeet", "parrakeet", "parroket", "paraquet", "paroquet", "parroquet"], "image_count": 11, "id": 761, "frequency": "c", "synset": "parakeet.n.01"}, {"name": "parasail_(sports)", "instance_count": 385, "def": "parachute that will lift a person up into the air when it is towed by a motorboat or a car", "synonyms": ["parasail_(sports)"], "image_count": 72, "id": 762, "frequency": "c", "synset": "parasail.n.01"}, {"name": "parasol", "instance_count": 45, "def": "a handheld collapsible source of shade", "synonyms": ["parasol", "sunshade"], "image_count": 17, "id": 763, "frequency": "c", "synset": "parasol.n.01"}, {"name": "parchment", "instance_count": 17, "def": "a superior paper resembling sheepskin", "synonyms": ["parchment"], "image_count": 10, "id": 764, "frequency": "r", "synset": "parchment.n.01"}, {"name": "parka", "instance_count": 89, "def": "a kind of heavy jacket (`windcheater' is a British term)", "synonyms": ["parka", "anorak"], "image_count": 17, "id": 765, "frequency": "c", "synset": "parka.n.01"}, {"name": "parking_meter", "instance_count": 1075, "def": "a coin-operated timer located next to a parking space", "synonyms": ["parking_meter"], "image_count": 489, "id": 766, "frequency": "f", "synset": "parking_meter.n.01"}, {"name": "parrot", "instance_count": 76, "def": "usually brightly colored tropical birds with short hooked beaks and the ability to mimic sounds", "synonyms": ["parrot"], "image_count": 47, "id": 767, "frequency": "c", "synset": "parrot.n.01"}, {"name": "passenger_car_(part_of_a_train)", "instance_count": 465, "def": "a railcar where passengers ride", "synonyms": ["passenger_car_(part_of_a_train)", "coach_(part_of_a_train)"], "image_count": 93, "id": 768, "frequency": "c", "synset": "passenger_car.n.01"}, {"name": "passenger_ship", "instance_count": 1, "def": "a ship built to carry passengers", "synonyms": ["passenger_ship"], "image_count": 1, "id": 769, "frequency": "r", "synset": "passenger_ship.n.01"}, {"name": "passport", "instance_count": 12, "def": "a document issued by a country to a citizen allowing that person to travel abroad and re-enter the home country", "synonyms": ["passport"], "image_count": 12, "id": 770, "frequency": "c", "synset": "passport.n.02"}, {"name": "pastry", "instance_count": 4972, "def": "any of various baked foods made of dough or batter", "synonyms": ["pastry"], "image_count": 228, "id": 771, "frequency": "f", "synset": "pastry.n.02"}, {"name": "patty_(food)", "instance_count": 20, "def": "small flat mass of chopped food", "synonyms": ["patty_(food)"], "image_count": 5, "id": 772, "frequency": "r", "synset": "patty.n.01"}, {"name": "pea_(food)", "instance_count": 1869, "def": "seed of a pea plant used for food", "synonyms": ["pea_(food)"], "image_count": 76, "id": 773, "frequency": "c", "synset": "pea.n.01"}, {"name": "peach", "instance_count": 1041, "def": "downy juicy fruit with sweet yellowish or whitish flesh", "synonyms": ["peach"], "image_count": 71, "id": 774, "frequency": "c", "synset": "peach.n.03"}, {"name": "peanut_butter", "instance_count": 50, "def": "a spread made from ground peanuts", "synonyms": ["peanut_butter"], "image_count": 30, "id": 775, "frequency": "c", "synset": "peanut_butter.n.01"}, {"name": "pear", "instance_count": 1069, "def": "sweet juicy gritty-textured fruit available in many varieties", "synonyms": ["pear"], "image_count": 109, "id": 776, "frequency": "f", "synset": "pear.n.01"}, {"name": "peeler_(tool_for_fruit_and_vegetables)", "instance_count": 18, "def": "a device for peeling vegetables or fruits", "synonyms": ["peeler_(tool_for_fruit_and_vegetables)"], "image_count": 14, "id": 777, "frequency": "c", "synset": "peeler.n.03"}, {"name": "wooden_leg", "instance_count": 1, "def": "a prosthesis that replaces a missing leg", "synonyms": ["wooden_leg", "pegleg"], "image_count": 1, "id": 778, "frequency": "r", "synset": "peg.n.04"}, {"name": "pegboard", "instance_count": 9, "def": "a board perforated with regularly spaced holes into which pegs can be fitted", "synonyms": ["pegboard"], "image_count": 8, "id": 779, "frequency": "r", "synset": "pegboard.n.01"}, {"name": "pelican", "instance_count": 76, "def": "large long-winged warm-water seabird having a large bill with a distensible pouch for fish", "synonyms": ["pelican"], "image_count": 26, "id": 780, "frequency": "c", "synset": "pelican.n.01"}, {"name": "pen", "instance_count": 987, "def": "a writing implement with a point from which ink flows", "synonyms": ["pen"], "image_count": 339, "id": 781, "frequency": "f", "synset": "pen.n.01"}, {"name": "pencil", "instance_count": 543, "def": "a thin cylindrical pointed writing implement made of wood and graphite", "synonyms": ["pencil"], "image_count": 153, "id": 782, "frequency": "f", "synset": "pencil.n.01"}, {"name": "pencil_box", "instance_count": 2, "def": "a box for holding pencils", "synonyms": ["pencil_box", "pencil_case"], "image_count": 2, "id": 783, "frequency": "r", "synset": "pencil_box.n.01"}, {"name": "pencil_sharpener", "instance_count": 4, "def": "a rotary implement for sharpening the point on pencils", "synonyms": ["pencil_sharpener"], "image_count": 3, "id": 784, "frequency": "r", "synset": "pencil_sharpener.n.01"}, {"name": "pendulum", "instance_count": 18, "def": "an apparatus consisting of an object mounted so that it swings freely under the influence of gravity", "synonyms": ["pendulum"], "image_count": 8, "id": 785, "frequency": "r", "synset": "pendulum.n.01"}, {"name": "penguin", "instance_count": 229, "def": "short-legged flightless birds of cold southern regions having webbed feet and wings modified as flippers", "synonyms": ["penguin"], "image_count": 47, "id": 786, "frequency": "c", "synset": "penguin.n.01"}, {"name": "pennant", "instance_count": 235, "def": "a flag longer than it is wide (and often tapering)", "synonyms": ["pennant"], "image_count": 8, "id": 787, "frequency": "r", "synset": "pennant.n.02"}, {"name": "penny_(coin)", "instance_count": 15, "def": "a coin worth one-hundredth of the value of the basic unit", "synonyms": ["penny_(coin)"], "image_count": 6, "id": 788, "frequency": "r", "synset": "penny.n.02"}, {"name": "pepper", "instance_count": 697, "def": "pungent seasoning from the berry of the common pepper plant; whole or ground", "synonyms": ["pepper", "peppercorn"], "image_count": 116, "id": 789, "frequency": "f", "synset": "pepper.n.03"}, {"name": "pepper_mill", "instance_count": 91, "def": "a mill for grinding pepper", "synonyms": ["pepper_mill", "pepper_grinder"], "image_count": 69, "id": 790, "frequency": "c", "synset": "pepper_mill.n.01"}, {"name": "perfume", "instance_count": 28, "def": "a toiletry that emits and diffuses a fragrant odor", "synonyms": ["perfume"], "image_count": 13, "id": 791, "frequency": "c", "synset": "perfume.n.02"}, {"name": "persimmon", "instance_count": 22, "def": "orange fruit resembling a plum; edible when fully ripe", "synonyms": ["persimmon"], "image_count": 6, "id": 792, "frequency": "r", "synset": "persimmon.n.02"}, {"name": "person", "instance_count": 13439, "def": "a human being", "synonyms": ["person", "baby", "child", "boy", "girl", "man", "woman", "human"], "image_count": 1928, "id": 793, "frequency": "f", "synset": "person.n.01"}, {"name": "pet", "instance_count": 103, "def": "a domesticated animal kept for companionship or amusement", "synonyms": ["pet"], "image_count": 79, "id": 794, "frequency": "c", "synset": "pet.n.01"}, {"name": "pew_(church_bench)", "instance_count": 194, "def": "long bench with backs; used in church by the congregation", "synonyms": ["pew_(church_bench)", "church_bench"], "image_count": 14, "id": 795, "frequency": "c", "synset": "pew.n.01"}, {"name": "phonebook", "instance_count": 24, "def": "a directory containing an alphabetical list of telephone subscribers and their telephone numbers", "synonyms": ["phonebook", "telephone_book", "telephone_directory"], "image_count": 7, "id": 796, "frequency": "r", "synset": "phonebook.n.01"}, {"name": "phonograph_record", "instance_count": 138, "def": "sound recording consisting of a typically black disk with a continuous groove", "synonyms": ["phonograph_record", "phonograph_recording", "record_(phonograph_recording)"], "image_count": 20, "id": 797, "frequency": "c", "synset": "phonograph_record.n.01"}, {"name": "piano", "instance_count": 126, "def": "a keyboard instrument that is played by depressing keys that cause hammers to strike tuned strings and produce sounds", "synonyms": ["piano"], "image_count": 114, "id": 798, "frequency": "f", "synset": "piano.n.01"}, {"name": "pickle", "instance_count": 632, "def": "vegetables (especially cucumbers) preserved in brine or vinegar", "synonyms": ["pickle"], "image_count": 221, "id": 799, "frequency": "f", "synset": "pickle.n.01"}, {"name": "pickup_truck", "instance_count": 838, "def": "a light truck with an open body and low sides and a tailboard", "synonyms": ["pickup_truck"], "image_count": 502, "id": 800, "frequency": "f", "synset": "pickup.n.01"}, {"name": "pie", "instance_count": 228, "def": "dish baked in pastry-lined pan often with a pastry top", "synonyms": ["pie"], "image_count": 62, "id": 801, "frequency": "c", "synset": "pie.n.01"}, {"name": "pigeon", "instance_count": 1850, "def": "wild and domesticated birds having a heavy body and short legs", "synonyms": ["pigeon"], "image_count": 87, "id": 802, "frequency": "c", "synset": "pigeon.n.01"}, {"name": "piggy_bank", "instance_count": 5, "def": "a child's coin bank (often shaped like a pig)", "synonyms": ["piggy_bank", "penny_bank"], "image_count": 4, "id": 803, "frequency": "r", "synset": "piggy_bank.n.01"}, {"name": "pillow", "instance_count": 6115, "def": "a cushion to support the head of a sleeping person", "synonyms": ["pillow"], "image_count": 1912, "id": 804, "frequency": "f", "synset": "pillow.n.01"}, {"name": "pin_(non_jewelry)", "instance_count": 112, "def": "a small slender (often pointed) piece of wood or metal used to support or fasten or attach things", "synonyms": ["pin_(non_jewelry)"], "image_count": 7, "id": 805, "frequency": "r", "synset": "pin.n.09"}, {"name": "pineapple", "instance_count": 1636, "def": "large sweet fleshy tropical fruit with a tuft of stiff leaves", "synonyms": ["pineapple"], "image_count": 186, "id": 806, "frequency": "f", "synset": "pineapple.n.02"}, {"name": "pinecone", "instance_count": 141, "def": "the seed-producing cone of a pine tree", "synonyms": ["pinecone"], "image_count": 18, "id": 807, "frequency": "c", "synset": "pinecone.n.01"}, {"name": "ping-pong_ball", "instance_count": 4, "def": "light hollow ball used in playing table tennis", "synonyms": ["ping-pong_ball"], "image_count": 4, "id": 808, "frequency": "r", "synset": "ping-pong_ball.n.01"}, {"name": "pinwheel", "instance_count": 172, "def": "a toy consisting of vanes of colored paper or plastic that is pinned to a stick and spins when it is pointed into the wind", "synonyms": ["pinwheel"], "image_count": 3, "id": 809, "frequency": "r", "synset": "pinwheel.n.03"}, {"name": "tobacco_pipe", "instance_count": 7, "def": "a tube with a small bowl at one end; used for smoking tobacco", "synonyms": ["tobacco_pipe"], "image_count": 7, "id": 810, "frequency": "r", "synset": "pipe.n.01"}, {"name": "pipe", "instance_count": 4762, "def": "a long tube made of metal or plastic that is used to carry water or oil or gas etc.", "synonyms": ["pipe", "piping"], "image_count": 1413, "id": 811, "frequency": "f", "synset": "pipe.n.02"}, {"name": "pistol", "instance_count": 9, "def": "a firearm that is held and fired with one hand", "synonyms": ["pistol", "handgun"], "image_count": 7, "id": 812, "frequency": "r", "synset": "pistol.n.01"}, {"name": "pita_(bread)", "instance_count": 28, "def": "usually small round bread that can open into a pocket for filling", "synonyms": ["pita_(bread)", "pocket_bread"], "image_count": 12, "id": 813, "frequency": "c", "synset": "pita.n.01"}, {"name": "pitcher_(vessel_for_liquid)", "instance_count": 488, "def": "an open vessel with a handle and a spout for pouring", "synonyms": ["pitcher_(vessel_for_liquid)", "ewer"], "image_count": 248, "id": 814, "frequency": "f", "synset": "pitcher.n.02"}, {"name": "pitchfork", "instance_count": 4, "def": "a long-handled hand tool with sharp widely spaced prongs for lifting and pitching hay", "synonyms": ["pitchfork"], "image_count": 4, "id": 815, "frequency": "r", "synset": "pitchfork.n.01"}, {"name": "pizza", "instance_count": 4103, "def": "Italian open pie made of thin bread dough spread with a spiced mixture of e.g. tomato sauce and cheese", "synonyms": ["pizza"], "image_count": 1881, "id": 816, "frequency": "f", "synset": "pizza.n.01"}, {"name": "place_mat", "instance_count": 1123, "def": "a mat placed on a table for an individual place setting", "synonyms": ["place_mat"], "image_count": 529, "id": 817, "frequency": "f", "synset": "place_mat.n.01"}, {"name": "plate", "instance_count": 5214, "def": "dish on which food is served or from which food is eaten", "synonyms": ["plate"], "image_count": 1932, "id": 818, "frequency": "f", "synset": "plate.n.04"}, {"name": "platter", "instance_count": 148, "def": "a large shallow dish used for serving food", "synonyms": ["platter"], "image_count": 50, "id": 819, "frequency": "c", "synset": "platter.n.01"}, {"name": "playpen", "instance_count": 3, "def": "a portable enclosure in which babies may be left to play", "synonyms": ["playpen"], "image_count": 3, "id": 820, "frequency": "r", "synset": "playpen.n.01"}, {"name": "pliers", "instance_count": 49, "def": "a gripping hand tool with two hinged arms and (usually) serrated jaws", "synonyms": ["pliers", "plyers"], "image_count": 28, "id": 821, "frequency": "c", "synset": "pliers.n.01"}, {"name": "plow_(farm_equipment)", "instance_count": 12, "def": "a farm tool having one or more heavy blades to break the soil and cut a furrow prior to sowing", "synonyms": ["plow_(farm_equipment)", "plough_(farm_equipment)"], "image_count": 10, "id": 822, "frequency": "r", "synset": "plow.n.01"}, {"name": "plume", "instance_count": 11, "def": "a feather or cluster of feathers worn as an ornament", "synonyms": ["plume"], "image_count": 5, "id": 823, "frequency": "r", "synset": "plume.n.02"}, {"name": "pocket_watch", "instance_count": 20, "def": "a watch that is carried in a small watch pocket", "synonyms": ["pocket_watch"], "image_count": 5, "id": 824, "frequency": "r", "synset": "pocket_watch.n.01"}, {"name": "pocketknife", "instance_count": 21, "def": "a knife with a blade that folds into the handle; suitable for carrying in the pocket", "synonyms": ["pocketknife"], "image_count": 18, "id": 825, "frequency": "c", "synset": "pocketknife.n.01"}, {"name": "poker_(fire_stirring_tool)", "instance_count": 34, "def": "fire iron consisting of a metal rod with a handle; used to stir a fire", "synonyms": ["poker_(fire_stirring_tool)", "stove_poker", "fire_hook"], "image_count": 14, "id": 826, "frequency": "c", "synset": "poker.n.01"}, {"name": "pole", "instance_count": 14276, "def": "a long (usually round) rod of wood or metal or plastic", "synonyms": ["pole", "post"], "image_count": 1890, "id": 827, "frequency": "f", "synset": "pole.n.01"}, {"name": "polo_shirt", "instance_count": 1695, "def": "a shirt with short sleeves designed for comfort and casual wear", "synonyms": ["polo_shirt", "sport_shirt"], "image_count": 660, "id": 828, "frequency": "f", "synset": "polo_shirt.n.01"}, {"name": "poncho", "instance_count": 14, "def": "a blanket-like cloak with a hole in the center for the head", "synonyms": ["poncho"], "image_count": 8, "id": 829, "frequency": "r", "synset": "poncho.n.01"}, {"name": "pony", "instance_count": 57, "def": "any of various breeds of small gentle horses usually less than five feet high at the shoulder", "synonyms": ["pony"], "image_count": 25, "id": 830, "frequency": "c", "synset": "pony.n.05"}, {"name": "pool_table", "instance_count": 10, "def": "game equipment consisting of a heavy table on which pool is played", "synonyms": ["pool_table", "billiard_table", "snooker_table"], "image_count": 10, "id": 831, "frequency": "r", "synset": "pool_table.n.01"}, {"name": "pop_(soda)", "instance_count": 951, "def": "a sweet drink containing carbonated water and flavoring", "synonyms": ["pop_(soda)", "soda_(pop)", "tonic", "soft_drink"], "image_count": 218, "id": 832, "frequency": "f", "synset": "pop.n.02"}, {"name": "postbox_(public)", "instance_count": 57, "def": "public box for deposit of mail", "synonyms": ["postbox_(public)", "mailbox_(public)"], "image_count": 36, "id": 833, "frequency": "c", "synset": "postbox.n.01"}, {"name": "postcard", "instance_count": 276, "def": "a card for sending messages by post without an envelope", "synonyms": ["postcard", "postal_card", "mailing-card"], "image_count": 16, "id": 834, "frequency": "c", "synset": "postcard.n.01"}, {"name": "poster", "instance_count": 3378, "def": "a sign posted in a public place as an advertisement", "synonyms": ["poster", "placard"], "image_count": 808, "id": 835, "frequency": "f", "synset": "poster.n.01"}, {"name": "pot", "instance_count": 1719, "def": "metal or earthenware cooking vessel that is usually round and deep; often has a handle and lid", "synonyms": ["pot"], "image_count": 479, "id": 836, "frequency": "f", "synset": "pot.n.01"}, {"name": "flowerpot", "instance_count": 3902, "def": "a container in which plants are cultivated", "synonyms": ["flowerpot"], "image_count": 1404, "id": 837, "frequency": "f", "synset": "pot.n.04"}, {"name": "potato", "instance_count": 4393, "def": "an edible tuber native to South America", "synonyms": ["potato"], "image_count": 307, "id": 838, "frequency": "f", "synset": "potato.n.01"}, {"name": "potholder", "instance_count": 112, "def": "an insulated pad for holding hot pots", "synonyms": ["potholder"], "image_count": 57, "id": 839, "frequency": "c", "synset": "potholder.n.01"}, {"name": "pottery", "instance_count": 272, "def": "ceramic ware made from clay and baked in a kiln", "synonyms": ["pottery", "clayware"], "image_count": 28, "id": 840, "frequency": "c", "synset": "pottery.n.01"}, {"name": "pouch", "instance_count": 131, "def": "a small or medium size container for holding or carrying things", "synonyms": ["pouch"], "image_count": 80, "id": 841, "frequency": "c", "synset": "pouch.n.01"}, {"name": "power_shovel", "instance_count": 16, "def": "a machine for excavating", "synonyms": ["power_shovel", "excavator", "digger"], "image_count": 11, "id": 842, "frequency": "c", "synset": "power_shovel.n.01"}, {"name": "prawn", "instance_count": 779, "def": "any of various edible decapod crustaceans", "synonyms": ["prawn", "shrimp"], "image_count": 92, "id": 843, "frequency": "c", "synset": "prawn.n.01"}, {"name": "pretzel", "instance_count": 179, "def": "glazed and salted cracker typically in the shape of a loose knot", "synonyms": ["pretzel"], "image_count": 20, "id": 844, "frequency": "c", "synset": "pretzel.n.01"}, {"name": "printer", "instance_count": 217, "def": "a machine that prints", "synonyms": ["printer", "printing_machine"], "image_count": 194, "id": 845, "frequency": "f", "synset": "printer.n.03"}, {"name": "projectile_(weapon)", "instance_count": 64, "def": "a weapon that is forcibly thrown or projected at a targets", "synonyms": ["projectile_(weapon)", "missile"], "image_count": 23, "id": 846, "frequency": "c", "synset": "projectile.n.01"}, {"name": "projector", "instance_count": 54, "def": "an optical instrument that projects an enlarged image onto a screen", "synonyms": ["projector"], "image_count": 52, "id": 847, "frequency": "c", "synset": "projector.n.02"}, {"name": "propeller", "instance_count": 1458, "def": "a mechanical device that rotates to push against air or water", "synonyms": ["propeller", "propellor"], "image_count": 673, "id": 848, "frequency": "f", "synset": "propeller.n.01"}, {"name": "prune", "instance_count": 8, "def": "dried plum", "synonyms": ["prune"], "image_count": 2, "id": 849, "frequency": "r", "synset": "prune.n.01"}, {"name": "pudding", "instance_count": 2, "def": "any of various soft thick unsweetened baked dishes", "synonyms": ["pudding"], "image_count": 2, "id": 850, "frequency": "r", "synset": "pudding.n.01"}, {"name": "puffer_(fish)", "instance_count": 2, "def": "fishes whose elongated spiny body can inflate itself with water or air to form a globe", "synonyms": ["puffer_(fish)", "pufferfish", "blowfish", "globefish"], "image_count": 1, "id": 851, "frequency": "r", "synset": "puffer.n.02"}, {"name": "puffin", "instance_count": 4, "def": "seabirds having short necks and brightly colored compressed bills", "synonyms": ["puffin"], "image_count": 2, "id": 852, "frequency": "r", "synset": "puffin.n.01"}, {"name": "pug-dog", "instance_count": 13, "def": "small compact smooth-coated breed of Asiatic origin having a tightly curled tail and broad flat wrinkled muzzle", "synonyms": ["pug-dog"], "image_count": 8, "id": 853, "frequency": "r", "synset": "pug.n.01"}, {"name": "pumpkin", "instance_count": 1192, "def": "usually large pulpy deep-yellow round fruit of the squash family maturing in late summer or early autumn", "synonyms": ["pumpkin"], "image_count": 80, "id": 854, "frequency": "c", "synset": "pumpkin.n.02"}, {"name": "puncher", "instance_count": 6, "def": "a tool for making holes or indentations", "synonyms": ["puncher"], "image_count": 3, "id": 855, "frequency": "r", "synset": "punch.n.03"}, {"name": "puppet", "instance_count": 18, "def": "a small figure of a person operated from above with strings by a puppeteer", "synonyms": ["puppet", "marionette"], "image_count": 3, "id": 856, "frequency": "r", "synset": "puppet.n.01"}, {"name": "puppy", "instance_count": 57, "def": "a young dog", "synonyms": ["puppy"], "image_count": 15, "id": 857, "frequency": "c", "synset": "puppy.n.01"}, {"name": "quesadilla", "instance_count": 6, "def": "a tortilla that is filled with cheese and heated", "synonyms": ["quesadilla"], "image_count": 2, "id": 858, "frequency": "r", "synset": "quesadilla.n.01"}, {"name": "quiche", "instance_count": 33, "def": "a tart filled with rich unsweetened custard; often contains other ingredients (as cheese or ham or seafood or vegetables)", "synonyms": ["quiche"], "image_count": 10, "id": 859, "frequency": "r", "synset": "quiche.n.02"}, {"name": "quilt", "instance_count": 513, "def": "bedding made of two layers of cloth filled with stuffing and stitched together", "synonyms": ["quilt", "comforter"], "image_count": 386, "id": 860, "frequency": "f", "synset": "quilt.n.01"}, {"name": "rabbit", "instance_count": 139, "def": "any of various burrowing animals of the family Leporidae having long ears and short tails", "synonyms": ["rabbit"], "image_count": 65, "id": 861, "frequency": "c", "synset": "rabbit.n.01"}, {"name": "race_car", "instance_count": 6, "def": "a fast car that competes in races", "synonyms": ["race_car", "racing_car"], "image_count": 3, "id": 862, "frequency": "r", "synset": "racer.n.02"}, {"name": "racket", "instance_count": 64, "def": "a sports implement used to strike a ball in various games", "synonyms": ["racket", "racquet"], "image_count": 35, "id": 863, "frequency": "c", "synset": "racket.n.04"}, {"name": "radar", "instance_count": 13, "def": "measuring instrument in which the echo of a pulse of microwave radiation is used to detect and locate distant objects", "synonyms": ["radar"], "image_count": 5, "id": 864, "frequency": "r", "synset": "radar.n.01"}, {"name": "radiator", "instance_count": 195, "def": "a mechanism consisting of a metal honeycomb through which hot fluids circulate", "synonyms": ["radiator"], "image_count": 180, "id": 865, "frequency": "f", "synset": "radiator.n.03"}, {"name": "radio_receiver", "instance_count": 123, "def": "an electronic receiver that detects and demodulates and amplifies transmitted radio signals", "synonyms": ["radio_receiver", "radio_set", "radio", "tuner_(radio)"], "image_count": 99, "id": 866, "frequency": "c", "synset": "radio_receiver.n.01"}, {"name": "radish", "instance_count": 519, "def": "pungent edible root of any of various cultivated radish plants", "synonyms": ["radish", "daikon"], "image_count": 49, "id": 867, "frequency": "c", "synset": "radish.n.03"}, {"name": "raft", "instance_count": 66, "def": "a flat float (usually made of logs or planks) that can be used for transport or as a platform for swimmers", "synonyms": ["raft"], "image_count": 28, "id": 868, "frequency": "c", "synset": "raft.n.01"}, {"name": "rag_doll", "instance_count": 3, "def": "a cloth doll that is stuffed and (usually) painted", "synonyms": ["rag_doll"], "image_count": 1, "id": 869, "frequency": "r", "synset": "rag_doll.n.01"}, {"name": "raincoat", "instance_count": 303, "def": "a water-resistant coat", "synonyms": ["raincoat", "waterproof_jacket"], "image_count": 52, "id": 870, "frequency": "c", "synset": "raincoat.n.01"}, {"name": "ram_(animal)", "instance_count": 132, "def": "uncastrated adult male sheep", "synonyms": ["ram_(animal)"], "image_count": 36, "id": 871, "frequency": "c", "synset": "ram.n.05"}, {"name": "raspberry", "instance_count": 778, "def": "red or black edible aggregate berries usually smaller than the related blackberries", "synonyms": ["raspberry"], "image_count": 70, "id": 872, "frequency": "c", "synset": "raspberry.n.02"}, {"name": "rat", "instance_count": 6, "def": "any of various long-tailed rodents similar to but larger than a mouse", "synonyms": ["rat"], "image_count": 6, "id": 873, "frequency": "r", "synset": "rat.n.01"}, {"name": "razorblade", "instance_count": 35, "def": "a blade that has very sharp edge", "synonyms": ["razorblade"], "image_count": 29, "id": 874, "frequency": "c", "synset": "razorblade.n.01"}, {"name": "reamer_(juicer)", "instance_count": 26, "def": "a squeezer with a conical ridged center that is used for squeezing juice from citrus fruit", "synonyms": ["reamer_(juicer)", "juicer", "juice_reamer"], "image_count": 24, "id": 875, "frequency": "c", "synset": "reamer.n.01"}, {"name": "rearview_mirror", "instance_count": 3650, "def": "vehicle mirror (side or rearview)", "synonyms": ["rearview_mirror"], "image_count": 1115, "id": 876, "frequency": "f", "synset": "rearview_mirror.n.01"}, {"name": "receipt", "instance_count": 89, "def": "an acknowledgment (usually tangible) that payment has been made", "synonyms": ["receipt"], "image_count": 61, "id": 877, "frequency": "c", "synset": "receipt.n.02"}, {"name": "recliner", "instance_count": 28, "def": "an armchair whose back can be lowered and foot can be raised to allow the sitter to recline in it", "synonyms": ["recliner", "reclining_chair", "lounger_(chair)"], "image_count": 18, "id": 878, "frequency": "c", "synset": "recliner.n.01"}, {"name": "record_player", "instance_count": 22, "def": "machine in which rotating records cause a stylus to vibrate and the vibrations are amplified acoustically or electronically", "synonyms": ["record_player", "phonograph_(record_player)", "turntable"], "image_count": 18, "id": 879, "frequency": "c", "synset": "record_player.n.01"}, {"name": "reflector", "instance_count": 3426, "def": "device that reflects light, radiation, etc.", "synonyms": ["reflector"], "image_count": 665, "id": 880, "frequency": "f", "synset": "reflector.n.01"}, {"name": "remote_control", "instance_count": 2467, "def": "a device that can be used to control a machine or apparatus from a distance", "synonyms": ["remote_control"], "image_count": 1096, "id": 881, "frequency": "f", "synset": "remote_control.n.01"}, {"name": "rhinoceros", "instance_count": 50, "def": "massive powerful herbivorous odd-toed ungulate of southeast Asia and Africa having very thick skin and one or two horns on the snout", "synonyms": ["rhinoceros"], "image_count": 29, "id": 882, "frequency": "c", "synset": "rhinoceros.n.01"}, {"name": "rib_(food)", "instance_count": 32, "def": "cut of meat including one or more ribs", "synonyms": ["rib_(food)"], "image_count": 8, "id": 883, "frequency": "r", "synset": "rib.n.03"}, {"name": "rifle", "instance_count": 37, "def": "a shoulder firearm with a long barrel", "synonyms": ["rifle"], "image_count": 14, "id": 884, "frequency": "c", "synset": "rifle.n.01"}, {"name": "ring", "instance_count": 2314, "def": "jewelry consisting of a circlet of precious metal (often set with jewels) worn on the finger", "synonyms": ["ring"], "image_count": 1622, "id": 885, "frequency": "f", "synset": "ring.n.08"}, {"name": "river_boat", "instance_count": 3, "def": "a boat used on rivers or to ply a river", "synonyms": ["river_boat"], "image_count": 2, "id": 886, "frequency": "r", "synset": "river_boat.n.01"}, {"name": "road_map", "instance_count": 3, "def": "(NOT A ROAD) a MAP showing roads (for automobile travel)", "synonyms": ["road_map"], "image_count": 3, "id": 887, "frequency": "r", "synset": "road_map.n.02"}, {"name": "robe", "instance_count": 77, "def": "any loose flowing garment", "synonyms": ["robe"], "image_count": 32, "id": 888, "frequency": "c", "synset": "robe.n.01"}, {"name": "rocking_chair", "instance_count": 70, "def": "a chair mounted on rockers", "synonyms": ["rocking_chair"], "image_count": 55, "id": 889, "frequency": "c", "synset": "rocking_chair.n.01"}, {"name": "rodent", "instance_count": 2, "def": "relatively small placental mammals having a single pair of constantly growing incisor teeth specialized for gnawing", "synonyms": ["rodent"], "image_count": 1, "id": 890, "frequency": "r", "synset": "rodent.n.01"}, {"name": "roller_skate", "instance_count": 35, "def": "a shoe with pairs of rollers (small hard wheels) fixed to the sole", "synonyms": ["roller_skate"], "image_count": 10, "id": 891, "frequency": "r", "synset": "roller_skate.n.01"}, {"name": "Rollerblade", "instance_count": 31, "def": "an in-line variant of a roller skate", "synonyms": ["Rollerblade"], "image_count": 10, "id": 892, "frequency": "r", "synset": "rollerblade.n.01"}, {"name": "rolling_pin", "instance_count": 52, "def": "utensil consisting of a cylinder (usually of wood) with a handle at each end; used to roll out dough", "synonyms": ["rolling_pin"], "image_count": 47, "id": 893, "frequency": "c", "synset": "rolling_pin.n.01"}, {"name": "root_beer", "instance_count": 3, "def": "carbonated drink containing extracts of roots and herbs", "synonyms": ["root_beer"], "image_count": 3, "id": 894, "frequency": "r", "synset": "root_beer.n.01"}, {"name": "router_(computer_equipment)", "instance_count": 41, "def": "a device that forwards data packets between computer networks", "synonyms": ["router_(computer_equipment)"], "image_count": 29, "id": 895, "frequency": "c", "synset": "router.n.02"}, {"name": "rubber_band", "instance_count": 574, "def": "a narrow band of elastic rubber used to hold things (such as papers) together", "synonyms": ["rubber_band", "elastic_band"], "image_count": 342, "id": 896, "frequency": "f", "synset": "rubber_band.n.01"}, {"name": "runner_(carpet)", "instance_count": 32, "def": "a long narrow carpet", "synonyms": ["runner_(carpet)"], "image_count": 25, "id": 897, "frequency": "c", "synset": "runner.n.08"}, {"name": "plastic_bag", "instance_count": 3631, "def": "a bag made of paper or plastic for holding customer's purchases", "synonyms": ["plastic_bag", "paper_bag"], "image_count": 1469, "id": 898, "frequency": "f", "synset": "sack.n.01"}, {"name": "saddle_(on_an_animal)", "instance_count": 955, "def": "a seat for the rider of a horse or camel", "synonyms": ["saddle_(on_an_animal)"], "image_count": 521, "id": 899, "frequency": "f", "synset": "saddle.n.01"}, {"name": "saddle_blanket", "instance_count": 648, "def": "stable gear consisting of a blanket placed under the saddle", "synonyms": ["saddle_blanket", "saddlecloth", "horse_blanket"], "image_count": 347, "id": 900, "frequency": "f", "synset": "saddle_blanket.n.01"}, {"name": "saddlebag", "instance_count": 56, "def": "a large bag (or pair of bags) hung over a saddle", "synonyms": ["saddlebag"], "image_count": 35, "id": 901, "frequency": "c", "synset": "saddlebag.n.01"}, {"name": "safety_pin", "instance_count": 15, "def": "a pin in the form of a clasp; has a guard so the point of the pin will not stick the user", "synonyms": ["safety_pin"], "image_count": 7, "id": 902, "frequency": "r", "synset": "safety_pin.n.01"}, {"name": "sail", "instance_count": 863, "def": "a large piece of fabric by means of which wind is used to propel a sailing vessel", "synonyms": ["sail"], "image_count": 207, "id": 903, "frequency": "f", "synset": "sail.n.01"}, {"name": "salad", "instance_count": 171, "def": "food mixtures either arranged on a plate or tossed and served with a moist dressing; usually consisting of or including greens", "synonyms": ["salad"], "image_count": 108, "id": 904, "frequency": "f", "synset": "salad.n.01"}, {"name": "salad_plate", "instance_count": 6, "def": "a plate or bowl for individual servings of salad", "synonyms": ["salad_plate", "salad_bowl"], "image_count": 2, "id": 905, "frequency": "r", "synset": "salad_plate.n.01"}, {"name": "salami", "instance_count": 290, "def": "highly seasoned fatty sausage of pork and beef usually dried", "synonyms": ["salami"], "image_count": 34, "id": 906, "frequency": "c", "synset": "salami.n.01"}, {"name": "salmon_(fish)", "instance_count": 27, "def": "any of various large food and game fishes of northern waters", "synonyms": ["salmon_(fish)"], "image_count": 12, "id": 907, "frequency": "c", "synset": "salmon.n.01"}, {"name": "salmon_(food)", "instance_count": 14, "def": "flesh of any of various marine or freshwater fish of the family Salmonidae", "synonyms": ["salmon_(food)"], "image_count": 10, "id": 908, "frequency": "r", "synset": "salmon.n.03"}, {"name": "salsa", "instance_count": 22, "def": "spicy sauce of tomatoes and onions and chili peppers to accompany Mexican foods", "synonyms": ["salsa"], "image_count": 13, "id": 909, "frequency": "c", "synset": "salsa.n.01"}, {"name": "saltshaker", "instance_count": 543, "def": "a shaker with a perforated top for sprinkling salt", "synonyms": ["saltshaker"], "image_count": 361, "id": 910, "frequency": "f", "synset": "saltshaker.n.01"}, {"name": "sandal_(type_of_shoe)", "instance_count": 3145, "def": "a shoe consisting of a sole fastened by straps to the foot", "synonyms": ["sandal_(type_of_shoe)"], "image_count": 1023, "id": 911, "frequency": "f", "synset": "sandal.n.01"}, {"name": "sandwich", "instance_count": 2315, "def": "two (or more) slices of bread with a filling between them", "synonyms": ["sandwich"], "image_count": 782, "id": 912, "frequency": "f", "synset": "sandwich.n.01"}, {"name": "satchel", "instance_count": 3, "def": "luggage consisting of a small case with a flat bottom and (usually) a shoulder strap", "synonyms": ["satchel"], "image_count": 2, "id": 913, "frequency": "r", "synset": "satchel.n.01"}, {"name": "saucepan", "instance_count": 26, "def": "a deep pan with a handle; used for stewing or boiling", "synonyms": ["saucepan"], "image_count": 5, "id": 914, "frequency": "r", "synset": "saucepan.n.01"}, {"name": "saucer", "instance_count": 555, "def": "a small shallow dish for holding a cup at the table", "synonyms": ["saucer"], "image_count": 247, "id": 915, "frequency": "f", "synset": "saucer.n.02"}, {"name": "sausage", "instance_count": 2704, "def": "highly seasoned minced meat stuffed in casings", "synonyms": ["sausage"], "image_count": 221, "id": 916, "frequency": "f", "synset": "sausage.n.01"}, {"name": "sawhorse", "instance_count": 5, "def": "a framework for holding wood that is being sawed", "synonyms": ["sawhorse", "sawbuck"], "image_count": 4, "id": 917, "frequency": "r", "synset": "sawhorse.n.01"}, {"name": "saxophone", "instance_count": 13, "def": "a wind instrument with a `J'-shaped form typically made of brass", "synonyms": ["saxophone"], "image_count": 8, "id": 918, "frequency": "r", "synset": "sax.n.02"}, {"name": "scale_(measuring_instrument)", "instance_count": 178, "def": "a measuring instrument for weighing; shows amount of mass", "synonyms": ["scale_(measuring_instrument)"], "image_count": 158, "id": 919, "frequency": "f", "synset": "scale.n.07"}, {"name": "scarecrow", "instance_count": 4, "def": "an effigy in the shape of a man to frighten birds away from seeds", "synonyms": ["scarecrow", "strawman"], "image_count": 3, "id": 920, "frequency": "r", "synset": "scarecrow.n.01"}, {"name": "scarf", "instance_count": 1310, "def": "a garment worn around the head or neck or shoulders for warmth or decoration", "synonyms": ["scarf"], "image_count": 752, "id": 921, "frequency": "f", "synset": "scarf.n.01"}, {"name": "school_bus", "instance_count": 142, "def": "a bus used to transport children to or from school", "synonyms": ["school_bus"], "image_count": 64, "id": 922, "frequency": "c", "synset": "school_bus.n.01"}, {"name": "scissors", "instance_count": 1376, "def": "a tool having two crossed pivoting blades with looped handles", "synonyms": ["scissors"], "image_count": 707, "id": 923, "frequency": "f", "synset": "scissors.n.01"}, {"name": "scoreboard", "instance_count": 161, "def": "a large board for displaying the score of a contest (and some other information)", "synonyms": ["scoreboard"], "image_count": 143, "id": 924, "frequency": "f", "synset": "scoreboard.n.01"}, {"name": "scraper", "instance_count": 1, "def": "any of various hand tools for scraping", "synonyms": ["scraper"], "image_count": 1, "id": 925, "frequency": "r", "synset": "scraper.n.01"}, {"name": "screwdriver", "instance_count": 88, "def": "a hand tool for driving screws; has a tip that fits into the head of a screw", "synonyms": ["screwdriver"], "image_count": 49, "id": 926, "frequency": "c", "synset": "screwdriver.n.01"}, {"name": "scrubbing_brush", "instance_count": 141, "def": "a brush with short stiff bristles for heavy cleaning", "synonyms": ["scrubbing_brush"], "image_count": 126, "id": 927, "frequency": "f", "synset": "scrub_brush.n.01"}, {"name": "sculpture", "instance_count": 202, "def": "a three-dimensional work of art", "synonyms": ["sculpture"], "image_count": 76, "id": 928, "frequency": "c", "synset": "sculpture.n.01"}, {"name": "seabird", "instance_count": 126, "def": "a bird that frequents coastal waters and the open ocean: gulls; pelicans; gannets; cormorants; albatrosses; petrels; etc.", "synonyms": ["seabird", "seafowl"], "image_count": 11, "id": 929, "frequency": "c", "synset": "seabird.n.01"}, {"name": "seahorse", "instance_count": 23, "def": "small fish with horse-like heads bent sharply downward and curled tails", "synonyms": ["seahorse"], "image_count": 11, "id": 930, "frequency": "c", "synset": "seahorse.n.02"}, {"name": "seaplane", "instance_count": 4, "def": "an airplane that can land on or take off from water", "synonyms": ["seaplane", "hydroplane"], "image_count": 4, "id": 931, "frequency": "r", "synset": "seaplane.n.01"}, {"name": "seashell", "instance_count": 451, "def": "the shell of a marine organism", "synonyms": ["seashell"], "image_count": 39, "id": 932, "frequency": "c", "synset": "seashell.n.01"}, {"name": "sewing_machine", "instance_count": 11, "def": "a textile machine used as a home appliance for sewing", "synonyms": ["sewing_machine"], "image_count": 11, "id": 933, "frequency": "c", "synset": "sewing_machine.n.01"}, {"name": "shaker", "instance_count": 24, "def": "a container in which something can be shaken", "synonyms": ["shaker"], "image_count": 13, "id": 934, "frequency": "c", "synset": "shaker.n.03"}, {"name": "shampoo", "instance_count": 254, "def": "cleansing agent consisting of soaps or detergents used for washing the hair", "synonyms": ["shampoo"], "image_count": 91, "id": 935, "frequency": "c", "synset": "shampoo.n.01"}, {"name": "shark", "instance_count": 20, "def": "typically large carnivorous fishes with sharpe teeth", "synonyms": ["shark"], "image_count": 14, "id": 936, "frequency": "c", "synset": "shark.n.01"}, {"name": "sharpener", "instance_count": 7, "def": "any implement that is used to make something (an edge or a point) sharper", "synonyms": ["sharpener"], "image_count": 5, "id": 937, "frequency": "r", "synset": "sharpener.n.01"}, {"name": "Sharpie", "instance_count": 5, "def": "a pen with indelible ink that will write on any surface", "synonyms": ["Sharpie"], "image_count": 3, "id": 938, "frequency": "r", "synset": "sharpie.n.03"}, {"name": "shaver_(electric)", "instance_count": 12, "def": "a razor powered by an electric motor", "synonyms": ["shaver_(electric)", "electric_shaver", "electric_razor"], "image_count": 10, "id": 939, "frequency": "r", "synset": "shaver.n.03"}, {"name": "shaving_cream", "instance_count": 33, "def": "toiletry consisting that forms a rich lather for softening the beard before shaving", "synonyms": ["shaving_cream", "shaving_soap"], "image_count": 18, "id": 940, "frequency": "c", "synset": "shaving_cream.n.01"}, {"name": "shawl", "instance_count": 9, "def": "cloak consisting of an oblong piece of cloth used to cover the head and shoulders", "synonyms": ["shawl"], "image_count": 9, "id": 941, "frequency": "r", "synset": "shawl.n.01"}, {"name": "shears", "instance_count": 38, "def": "large scissors with strong blades", "synonyms": ["shears"], "image_count": 6, "id": 942, "frequency": "r", "synset": "shears.n.01"}, {"name": "sheep", "instance_count": 13304, "def": "woolly usually horned ruminant mammal related to the goat", "synonyms": ["sheep"], "image_count": 951, "id": 943, "frequency": "f", "synset": "sheep.n.01"}, {"name": "shepherd_dog", "instance_count": 2, "def": "any of various usually long-haired breeds of dog reared to herd and guard sheep", "synonyms": ["shepherd_dog", "sheepdog"], "image_count": 2, "id": 944, "frequency": "r", "synset": "shepherd_dog.n.01"}, {"name": "sherbert", "instance_count": 2, "def": "a frozen dessert made primarily of fruit juice and sugar", "synonyms": ["sherbert", "sherbet"], "image_count": 1, "id": 945, "frequency": "r", "synset": "sherbert.n.01"}, {"name": "shield", "instance_count": 41, "def": "armor carried on the arm to intercept blows", "synonyms": ["shield"], "image_count": 19, "id": 946, "frequency": "c", "synset": "shield.n.02"}, {"name": "shirt", "instance_count": 10177, "def": "a garment worn on the upper half of the body", "synonyms": ["shirt"], "image_count": 1942, "id": 947, "frequency": "f", "synset": "shirt.n.01"}, {"name": "shoe", "instance_count": 9374, "def": "common footwear covering the foot", "synonyms": ["shoe", "sneaker_(type_of_shoe)", "tennis_shoe"], "image_count": 1916, "id": 948, "frequency": "f", "synset": "shoe.n.01"}, {"name": "shopping_bag", "instance_count": 377, "def": "a bag made of plastic or strong paper (often with handles); used to transport goods after shopping", "synonyms": ["shopping_bag"], "image_count": 139, "id": 949, "frequency": "f", "synset": "shopping_bag.n.01"}, {"name": "shopping_cart", "instance_count": 90, "def": "a handcart that holds groceries or other goods while shopping", "synonyms": ["shopping_cart"], "image_count": 43, "id": 950, "frequency": "c", "synset": "shopping_cart.n.01"}, {"name": "short_pants", "instance_count": 5305, "def": "trousers that end at or above the knee", "synonyms": ["short_pants", "shorts_(clothing)", "trunks_(clothing)"], "image_count": 1969, "id": 951, "frequency": "f", "synset": "short_pants.n.01"}, {"name": "shot_glass", "instance_count": 24, "def": "a small glass adequate to hold a single swallow of whiskey", "synonyms": ["shot_glass"], "image_count": 5, "id": 952, "frequency": "r", "synset": "shot_glass.n.01"}, {"name": "shoulder_bag", "instance_count": 331, "def": "a large handbag that can be carried by a strap looped over the shoulder", "synonyms": ["shoulder_bag"], "image_count": 134, "id": 953, "frequency": "f", "synset": "shoulder_bag.n.01"}, {"name": "shovel", "instance_count": 110, "def": "a hand tool for lifting loose material such as snow, dirt, etc.", "synonyms": ["shovel"], "image_count": 74, "id": 954, "frequency": "c", "synset": "shovel.n.01"}, {"name": "shower_head", "instance_count": 450, "def": "a plumbing fixture that sprays water over you", "synonyms": ["shower_head"], "image_count": 381, "id": 955, "frequency": "f", "synset": "shower.n.01"}, {"name": "shower_cap", "instance_count": 1, "def": "a tight cap worn to keep hair dry while showering", "synonyms": ["shower_cap"], "image_count": 1, "id": 956, "frequency": "r", "synset": "shower_cap.n.01"}, {"name": "shower_curtain", "instance_count": 479, "def": "a curtain that keeps water from splashing out of the shower area", "synonyms": ["shower_curtain"], "image_count": 381, "id": 957, "frequency": "f", "synset": "shower_curtain.n.01"}, {"name": "shredder_(for_paper)", "instance_count": 6, "def": "a device that shreds documents", "synonyms": ["shredder_(for_paper)"], "image_count": 6, "id": 958, "frequency": "r", "synset": "shredder.n.01"}, {"name": "signboard", "instance_count": 8091, "def": "structure displaying a board on which advertisements can be posted", "synonyms": ["signboard"], "image_count": 1826, "id": 959, "frequency": "f", "synset": "signboard.n.01"}, {"name": "silo", "instance_count": 95, "def": "a cylindrical tower used for storing goods", "synonyms": ["silo"], "image_count": 28, "id": 960, "frequency": "c", "synset": "silo.n.01"}, {"name": "sink", "instance_count": 2182, "def": "plumbing fixture consisting of a water basin fixed to a wall or floor and having a drainpipe", "synonyms": ["sink"], "image_count": 1635, "id": 961, "frequency": "f", "synset": "sink.n.01"}, {"name": "skateboard", "instance_count": 3597, "def": "a board with wheels that is ridden in a standing or crouching position and propelled by foot", "synonyms": ["skateboard"], "image_count": 1967, "id": 962, "frequency": "f", "synset": "skateboard.n.01"}, {"name": "skewer", "instance_count": 81, "def": "a long pin for holding meat in position while it is being roasted", "synonyms": ["skewer"], "image_count": 16, "id": 963, "frequency": "c", "synset": "skewer.n.01"}, {"name": "ski", "instance_count": 8496, "def": "sports equipment for skiing on snow", "synonyms": ["ski"], "image_count": 1926, "id": 964, "frequency": "f", "synset": "ski.n.01"}, {"name": "ski_boot", "instance_count": 8124, "def": "a stiff boot that is fastened to a ski with a ski binding", "synonyms": ["ski_boot"], "image_count": 1789, "id": 965, "frequency": "f", "synset": "ski_boot.n.01"}, {"name": "ski_parka", "instance_count": 1727, "def": "a parka to be worn while skiing", "synonyms": ["ski_parka", "ski_jacket"], "image_count": 401, "id": 966, "frequency": "f", "synset": "ski_parka.n.01"}, {"name": "ski_pole", "instance_count": 8263, "def": "a pole with metal points used as an aid in skiing", "synonyms": ["ski_pole"], "image_count": 1968, "id": 967, "frequency": "f", "synset": "ski_pole.n.01"}, {"name": "skirt", "instance_count": 1784, "def": "a garment hanging from the waist; worn mainly by girls and women", "synonyms": ["skirt"], "image_count": 1167, "id": 968, "frequency": "f", "synset": "skirt.n.02"}, {"name": "skullcap", "instance_count": 1, "def": "rounded brimless cap fitting the crown of the head", "synonyms": ["skullcap"], "image_count": 1, "id": 969, "frequency": "r", "synset": "skullcap.n.01"}, {"name": "sled", "instance_count": 102, "def": "a vehicle or flat object for transportation over snow by sliding or pulled by dogs, etc.", "synonyms": ["sled", "sledge", "sleigh"], "image_count": 56, "id": 970, "frequency": "c", "synset": "sled.n.01"}, {"name": "sleeping_bag", "instance_count": 33, "def": "large padded bag designed to be slept in outdoors", "synonyms": ["sleeping_bag"], "image_count": 17, "id": 971, "frequency": "c", "synset": "sleeping_bag.n.01"}, {"name": "sling_(bandage)", "instance_count": 1, "def": "bandage to support an injured forearm; slung over the shoulder or neck", "synonyms": ["sling_(bandage)", "triangular_bandage"], "image_count": 1, "id": 972, "frequency": "r", "synset": "sling.n.05"}, {"name": "slipper_(footwear)", "instance_count": 121, "def": "low footwear that can be slipped on and off easily; usually worn indoors", "synonyms": ["slipper_(footwear)", "carpet_slipper_(footwear)"], "image_count": 58, "id": 973, "frequency": "c", "synset": "slipper.n.01"}, {"name": "smoothie", "instance_count": 53, "def": "a thick smooth drink consisting of fresh fruit pureed with ice cream or yoghurt or milk", "synonyms": ["smoothie"], "image_count": 9, "id": 974, "frequency": "r", "synset": "smoothie.n.02"}, {"name": "snake", "instance_count": 16, "def": "limbless scaly elongate reptile; some are venomous", "synonyms": ["snake", "serpent"], "image_count": 8, "id": 975, "frequency": "r", "synset": "snake.n.01"}, {"name": "snowboard", "instance_count": 2119, "def": "a board that resembles a broad ski or a small surfboard; used in a standing position to slide down snow-covered slopes", "synonyms": ["snowboard"], "image_count": 1124, "id": 976, "frequency": "f", "synset": "snowboard.n.01"}, {"name": "snowman", "instance_count": 61, "def": "a figure of a person made of packed snow", "synonyms": ["snowman"], "image_count": 31, "id": 977, "frequency": "c", "synset": "snowman.n.01"}, {"name": "snowmobile", "instance_count": 23, "def": "tracked vehicle for travel on snow having skis in front", "synonyms": ["snowmobile"], "image_count": 16, "id": 978, "frequency": "c", "synset": "snowmobile.n.01"}, {"name": "soap", "instance_count": 895, "def": "a cleansing agent made from the salts of vegetable or animal fats", "synonyms": ["soap"], "image_count": 491, "id": 979, "frequency": "f", "synset": "soap.n.01"}, {"name": "soccer_ball", "instance_count": 670, "def": "an inflated ball used in playing soccer (called `football' outside of the United States)", "synonyms": ["soccer_ball"], "image_count": 432, "id": 980, "frequency": "f", "synset": "soccer_ball.n.01"}, {"name": "sock", "instance_count": 6866, "def": "cloth covering for the foot; worn inside the shoe; reaches to between the ankle and the knee", "synonyms": ["sock"], "image_count": 1945, "id": 981, "frequency": "f", "synset": "sock.n.01"}, {"name": "sofa", "instance_count": 2408, "def": "an upholstered seat for more than one person", "synonyms": ["sofa", "couch", "lounge"], "image_count": 1899, "id": 982, "frequency": "f", "synset": "sofa.n.01"}, {"name": "softball", "instance_count": 5, "def": "ball used in playing softball", "synonyms": ["softball"], "image_count": 5, "id": 983, "frequency": "r", "synset": "softball.n.01"}, {"name": "solar_array", "instance_count": 52, "def": "electrical device consisting of a large array of connected solar cells", "synonyms": ["solar_array", "solar_battery", "solar_panel"], "image_count": 28, "id": 984, "frequency": "c", "synset": "solar_array.n.01"}, {"name": "sombrero", "instance_count": 22, "def": "a straw hat with a tall crown and broad brim; worn in American southwest and in Mexico", "synonyms": ["sombrero"], "image_count": 7, "id": 985, "frequency": "r", "synset": "sombrero.n.02"}, {"name": "soup", "instance_count": 193, "def": "liquid food especially of meat or fish or vegetable stock often containing pieces of solid food", "synonyms": ["soup"], "image_count": 146, "id": 986, "frequency": "f", "synset": "soup.n.01"}, {"name": "soup_bowl", "instance_count": 2, "def": "a bowl for serving soup", "synonyms": ["soup_bowl"], "image_count": 1, "id": 987, "frequency": "r", "synset": "soup_bowl.n.01"}, {"name": "soupspoon", "instance_count": 44, "def": "a spoon with a rounded bowl for eating soup", "synonyms": ["soupspoon"], "image_count": 25, "id": 988, "frequency": "c", "synset": "soupspoon.n.01"}, {"name": "sour_cream", "instance_count": 49, "def": "soured light cream", "synonyms": ["sour_cream", "soured_cream"], "image_count": 22, "id": 989, "frequency": "c", "synset": "sour_cream.n.01"}, {"name": "soya_milk", "instance_count": 2, "def": "a milk substitute containing soybean flour and water; used in some infant formulas and in making tofu", "synonyms": ["soya_milk", "soybean_milk", "soymilk"], "image_count": 1, "id": 990, "frequency": "r", "synset": "soya_milk.n.01"}, {"name": "space_shuttle", "instance_count": 10, "def": "a reusable spacecraft with wings for a controlled descent through the Earth's atmosphere", "synonyms": ["space_shuttle"], "image_count": 10, "id": 991, "frequency": "r", "synset": "space_shuttle.n.01"}, {"name": "sparkler_(fireworks)", "instance_count": 12, "def": "a firework that burns slowly and throws out a shower of sparks", "synonyms": ["sparkler_(fireworks)"], "image_count": 9, "id": 992, "frequency": "r", "synset": "sparkler.n.02"}, {"name": "spatula", "instance_count": 508, "def": "a hand tool with a thin flexible blade used to mix or spread soft substances", "synonyms": ["spatula"], "image_count": 308, "id": 993, "frequency": "f", "synset": "spatula.n.02"}, {"name": "spear", "instance_count": 9, "def": "a long pointed rod used as a tool or weapon", "synonyms": ["spear", "lance"], "image_count": 4, "id": 994, "frequency": "r", "synset": "spear.n.01"}, {"name": "spectacles", "instance_count": 3040, "def": "optical instrument consisting of a frame that holds a pair of lenses for correcting defective vision", "synonyms": ["spectacles", "specs", "eyeglasses", "glasses"], "image_count": 1969, "id": 995, "frequency": "f", "synset": "spectacles.n.01"}, {"name": "spice_rack", "instance_count": 54, "def": "a rack for displaying containers filled with spices", "synonyms": ["spice_rack"], "image_count": 45, "id": 996, "frequency": "c", "synset": "spice_rack.n.01"}, {"name": "spider", "instance_count": 19, "def": "predatory arachnid with eight legs, two poison fangs, two feelers, and usually two silk-spinning organs at the back end of the body", "synonyms": ["spider"], "image_count": 12, "id": 997, "frequency": "c", "synset": "spider.n.01"}, {"name": "crawfish", "instance_count": 5, "def": "large edible marine crustacean having a spiny carapace but lacking the large pincers of true lobsters", "synonyms": ["crawfish", "crayfish"], "image_count": 1, "id": 998, "frequency": "r", "synset": "spiny_lobster.n.02"}, {"name": "sponge", "instance_count": 116, "def": "a porous mass usable to absorb water typically used for cleaning", "synonyms": ["sponge"], "image_count": 85, "id": 999, "frequency": "c", "synset": "sponge.n.01"}, {"name": "spoon", "instance_count": 2111, "def": "a piece of cutlery with a shallow bowl-shaped container and a handle", "synonyms": ["spoon"], "image_count": 1127, "id": 1000, "frequency": "f", "synset": "spoon.n.01"}, {"name": "sportswear", "instance_count": 85, "def": "attire worn for sport or for casual wear", "synonyms": ["sportswear", "athletic_wear", "activewear"], "image_count": 11, "id": 1001, "frequency": "c", "synset": "sportswear.n.01"}, {"name": "spotlight", "instance_count": 403, "def": "a lamp that produces a strong beam of light to illuminate a restricted area; used to focus attention of a stage performer", "synonyms": ["spotlight"], "image_count": 60, "id": 1002, "frequency": "c", "synset": "spotlight.n.02"}, {"name": "squid_(food)", "instance_count": 6, "def": "(Italian cuisine) squid prepared as food", "synonyms": ["squid_(food)", "calamari", "calamary"], "image_count": 1, "id": 1003, "frequency": "r", "synset": "squid.n.01"}, {"name": "squirrel", "instance_count": 19, "def": "a kind of arboreal rodent having a long bushy tail", "synonyms": ["squirrel"], "image_count": 16, "id": 1004, "frequency": "c", "synset": "squirrel.n.01"}, {"name": "stagecoach", "instance_count": 1, "def": "a large coach-and-four formerly used to carry passengers and mail on regular routes between towns", "synonyms": ["stagecoach"], "image_count": 1, "id": 1005, "frequency": "r", "synset": "stagecoach.n.01"}, {"name": "stapler_(stapling_machine)", "instance_count": 68, "def": "a machine that inserts staples into sheets of paper in order to fasten them together", "synonyms": ["stapler_(stapling_machine)"], "image_count": 65, "id": 1006, "frequency": "c", "synset": "stapler.n.01"}, {"name": "starfish", "instance_count": 28, "def": "echinoderms characterized by five arms extending from a central disk", "synonyms": ["starfish", "sea_star"], "image_count": 13, "id": 1007, "frequency": "c", "synset": "starfish.n.01"}, {"name": "statue_(sculpture)", "instance_count": 1934, "def": "a sculpture representing a human or animal", "synonyms": ["statue_(sculpture)"], "image_count": 655, "id": 1008, "frequency": "f", "synset": "statue.n.01"}, {"name": "steak_(food)", "instance_count": 139, "def": "a slice of meat cut from the fleshy part of an animal or large fish", "synonyms": ["steak_(food)"], "image_count": 51, "id": 1009, "frequency": "c", "synset": "steak.n.01"}, {"name": "steak_knife", "instance_count": 1, "def": "a sharp table knife used in eating steak", "synonyms": ["steak_knife"], "image_count": 1, "id": 1010, "frequency": "r", "synset": "steak_knife.n.01"}, {"name": "steering_wheel", "instance_count": 901, "def": "a handwheel that is used for steering", "synonyms": ["steering_wheel"], "image_count": 673, "id": 1011, "frequency": "f", "synset": "steering_wheel.n.01"}, {"name": "stepladder", "instance_count": 5, "def": "a folding portable ladder hinged at the top", "synonyms": ["stepladder"], "image_count": 5, "id": 1012, "frequency": "r", "synset": "step_ladder.n.01"}, {"name": "step_stool", "instance_count": 43, "def": "a stool that has one or two steps that fold under the seat", "synonyms": ["step_stool"], "image_count": 36, "id": 1013, "frequency": "c", "synset": "step_stool.n.01"}, {"name": "stereo_(sound_system)", "instance_count": 77, "def": "electronic device for playing audio", "synonyms": ["stereo_(sound_system)"], "image_count": 54, "id": 1014, "frequency": "c", "synset": "stereo.n.01"}, {"name": "stew", "instance_count": 7, "def": "food prepared by stewing especially meat or fish with vegetables", "synonyms": ["stew"], "image_count": 5, "id": 1015, "frequency": "r", "synset": "stew.n.02"}, {"name": "stirrer", "instance_count": 18, "def": "an implement used for stirring", "synonyms": ["stirrer"], "image_count": 8, "id": 1016, "frequency": "r", "synset": "stirrer.n.02"}, {"name": "stirrup", "instance_count": 625, "def": "support consisting of metal loops into which rider's feet go", "synonyms": ["stirrup"], "image_count": 305, "id": 1017, "frequency": "f", "synset": "stirrup.n.01"}, {"name": "stool", "instance_count": 583, "def": "a simple seat without a back or arms", "synonyms": ["stool"], "image_count": 297, "id": 1018, "frequency": "f", "synset": "stool.n.01"}, {"name": "stop_sign", "instance_count": 1349, "def": "a traffic sign to notify drivers that they must come to a complete stop", "synonyms": ["stop_sign"], "image_count": 1053, "id": 1019, "frequency": "f", "synset": "stop_sign.n.01"}, {"name": "brake_light", "instance_count": 1334, "def": "a red light on the rear of a motor vehicle that signals when the brakes are applied", "synonyms": ["brake_light"], "image_count": 223, "id": 1020, "frequency": "f", "synset": "stoplight.n.01"}, {"name": "stove", "instance_count": 1133, "def": "a kitchen appliance used for cooking food", "synonyms": ["stove", "kitchen_stove", "range_(kitchen_appliance)", "kitchen_range", "cooking_stove"], "image_count": 1037, "id": 1021, "frequency": "f", "synset": "stove.n.01"}, {"name": "strainer", "instance_count": 99, "def": "a filter to retain larger pieces while smaller pieces and liquids pass through", "synonyms": ["strainer"], "image_count": 63, "id": 1022, "frequency": "c", "synset": "strainer.n.01"}, {"name": "strap", "instance_count": 7435, "def": "an elongated strip of material for binding things together or holding", "synonyms": ["strap"], "image_count": 1881, "id": 1023, "frequency": "f", "synset": "strap.n.01"}, {"name": "straw_(for_drinking)", "instance_count": 1154, "def": "a thin paper or plastic tube used to suck liquids into the mouth", "synonyms": ["straw_(for_drinking)", "drinking_straw"], "image_count": 507, "id": 1024, "frequency": "f", "synset": "straw.n.04"}, {"name": "strawberry", "instance_count": 4386, "def": "sweet fleshy red fruit", "synonyms": ["strawberry"], "image_count": 333, "id": 1025, "frequency": "f", "synset": "strawberry.n.01"}, {"name": "street_sign", "instance_count": 8350, "def": "a sign visible from the street", "synonyms": ["street_sign"], "image_count": 1911, "id": 1026, "frequency": "f", "synset": "street_sign.n.01"}, {"name": "streetlight", "instance_count": 7381, "def": "a lamp supported on a lamppost; for illuminating a street", "synonyms": ["streetlight", "street_lamp"], "image_count": 1765, "id": 1027, "frequency": "f", "synset": "streetlight.n.01"}, {"name": "string_cheese", "instance_count": 1, "def": "cheese formed in long strings twisted together", "synonyms": ["string_cheese"], "image_count": 1, "id": 1028, "frequency": "r", "synset": "string_cheese.n.01"}, {"name": "stylus", "instance_count": 11, "def": "a pointed tool for writing or drawing or engraving, including pens", "synonyms": ["stylus"], "image_count": 5, "id": 1029, "frequency": "r", "synset": "stylus.n.02"}, {"name": "subwoofer", "instance_count": 1, "def": "a loudspeaker that is designed to reproduce very low bass frequencies", "synonyms": ["subwoofer"], "image_count": 1, "id": 1030, "frequency": "r", "synset": "subwoofer.n.01"}, {"name": "sugar_bowl", "instance_count": 10, "def": "a dish in which sugar is served", "synonyms": ["sugar_bowl"], "image_count": 9, "id": 1031, "frequency": "r", "synset": "sugar_bowl.n.01"}, {"name": "sugarcane_(plant)", "instance_count": 31, "def": "juicy canes whose sap is a source of molasses and commercial sugar; fresh canes are sometimes chewed for the juice", "synonyms": ["sugarcane_(plant)"], "image_count": 2, "id": 1032, "frequency": "r", "synset": "sugarcane.n.01"}, {"name": "suit_(clothing)", "instance_count": 461, "def": "a set of garments (usually including a jacket and trousers or skirt) for outerwear all of the same fabric and color", "synonyms": ["suit_(clothing)"], "image_count": 151, "id": 1033, "frequency": "f", "synset": "suit.n.01"}, {"name": "sunflower", "instance_count": 618, "def": "any plant of the genus Helianthus having large flower heads with dark disk florets and showy yellow rays", "synonyms": ["sunflower"], "image_count": 82, "id": 1034, "frequency": "c", "synset": "sunflower.n.01"}, {"name": "sunglasses", "instance_count": 5603, "def": "spectacles that are darkened or polarized to protect the eyes from the glare of the sun", "synonyms": ["sunglasses"], "image_count": 1931, "id": 1035, "frequency": "f", "synset": "sunglasses.n.01"}, {"name": "sunhat", "instance_count": 170, "def": "a hat with a broad brim that protects the face from direct exposure to the sun", "synonyms": ["sunhat"], "image_count": 41, "id": 1036, "frequency": "c", "synset": "sunhat.n.01"}, {"name": "surfboard", "instance_count": 3835, "def": "a narrow buoyant board for riding surf", "synonyms": ["surfboard"], "image_count": 1895, "id": 1037, "frequency": "f", "synset": "surfboard.n.01"}, {"name": "sushi", "instance_count": 337, "def": "rice (with raw fish) wrapped in seaweed", "synonyms": ["sushi"], "image_count": 24, "id": 1038, "frequency": "c", "synset": "sushi.n.01"}, {"name": "mop", "instance_count": 22, "def": "cleaning implement consisting of absorbent material fastened to a handle; for cleaning floors", "synonyms": ["mop"], "image_count": 22, "id": 1039, "frequency": "c", "synset": "swab.n.02"}, {"name": "sweat_pants", "instance_count": 56, "def": "loose-fitting trousers with elastic cuffs; worn by athletes", "synonyms": ["sweat_pants"], "image_count": 35, "id": 1040, "frequency": "c", "synset": "sweat_pants.n.01"}, {"name": "sweatband", "instance_count": 145, "def": "a band of material tied around the forehead or wrist to absorb sweat", "synonyms": ["sweatband"], "image_count": 69, "id": 1041, "frequency": "c", "synset": "sweatband.n.02"}, {"name": "sweater", "instance_count": 1894, "def": "a crocheted or knitted garment covering the upper part of the body", "synonyms": ["sweater"], "image_count": 962, "id": 1042, "frequency": "f", "synset": "sweater.n.01"}, {"name": "sweatshirt", "instance_count": 1482, "def": "cotton knit pullover with long sleeves worn during athletic activity", "synonyms": ["sweatshirt"], "image_count": 588, "id": 1043, "frequency": "f", "synset": "sweatshirt.n.01"}, {"name": "sweet_potato", "instance_count": 137, "def": "the edible tuberous root of the sweet potato vine", "synonyms": ["sweet_potato"], "image_count": 21, "id": 1044, "frequency": "c", "synset": "sweet_potato.n.02"}, {"name": "swimsuit", "instance_count": 3141, "def": "garment worn for swimming", "synonyms": ["swimsuit", "swimwear", "bathing_suit", "swimming_costume", "bathing_costume", "swimming_trunks", "bathing_trunks"], "image_count": 825, "id": 1045, "frequency": "f", "synset": "swimsuit.n.01"}, {"name": "sword", "instance_count": 72, "def": "a cutting or thrusting weapon that has a long metal blade", "synonyms": ["sword"], "image_count": 52, "id": 1046, "frequency": "c", "synset": "sword.n.01"}, {"name": "syringe", "instance_count": 14, "def": "a medical instrument used to inject or withdraw fluids", "synonyms": ["syringe"], "image_count": 5, "id": 1047, "frequency": "r", "synset": "syringe.n.01"}, {"name": "Tabasco_sauce", "instance_count": 5, "def": "very spicy sauce (trade name Tabasco) made from fully-aged red peppers", "synonyms": ["Tabasco_sauce"], "image_count": 5, "id": 1048, "frequency": "r", "synset": "tabasco.n.02"}, {"name": "table-tennis_table", "instance_count": 5, "def": "a table used for playing table tennis", "synonyms": ["table-tennis_table", "ping-pong_table"], "image_count": 5, "id": 1049, "frequency": "r", "synset": "table-tennis_table.n.01"}, {"name": "table", "instance_count": 2804, "def": "a piece of furniture having a smooth flat top that is usually supported by one or more vertical legs", "synonyms": ["table"], "image_count": 1860, "id": 1050, "frequency": "f", "synset": "table.n.02"}, {"name": "table_lamp", "instance_count": 81, "def": "a lamp that sits on a table", "synonyms": ["table_lamp"], "image_count": 56, "id": 1051, "frequency": "c", "synset": "table_lamp.n.01"}, {"name": "tablecloth", "instance_count": 2496, "def": "a covering spread over a dining table", "synonyms": ["tablecloth"], "image_count": 1582, "id": 1052, "frequency": "f", "synset": "tablecloth.n.01"}, {"name": "tachometer", "instance_count": 10, "def": "measuring instrument for indicating speed of rotation", "synonyms": ["tachometer"], "image_count": 7, "id": 1053, "frequency": "r", "synset": "tachometer.n.01"}, {"name": "taco", "instance_count": 21, "def": "a small tortilla cupped around a filling", "synonyms": ["taco"], "image_count": 2, "id": 1054, "frequency": "r", "synset": "taco.n.02"}, {"name": "tag", "instance_count": 7550, "def": "a label associated with something for the purpose of identification or information", "synonyms": ["tag"], "image_count": 1562, "id": 1055, "frequency": "f", "synset": "tag.n.02"}, {"name": "taillight", "instance_count": 9222, "def": "lamp (usually red) mounted at the rear of a motor vehicle", "synonyms": ["taillight", "rear_light"], "image_count": 1885, "id": 1056, "frequency": "f", "synset": "taillight.n.01"}, {"name": "tambourine", "instance_count": 1, "def": "a shallow drum with a single drumhead and with metallic disks in the sides", "synonyms": ["tambourine"], "image_count": 1, "id": 1057, "frequency": "r", "synset": "tambourine.n.01"}, {"name": "army_tank", "instance_count": 7, "def": "an enclosed armored military vehicle; has a cannon and moves on caterpillar treads", "synonyms": ["army_tank", "armored_combat_vehicle", "armoured_combat_vehicle"], "image_count": 5, "id": 1058, "frequency": "r", "synset": "tank.n.01"}, {"name": "tank_(storage_vessel)", "instance_count": 304, "def": "a large (usually metallic) vessel for holding gases or liquids", "synonyms": ["tank_(storage_vessel)", "storage_tank"], "image_count": 137, "id": 1059, "frequency": "f", "synset": "tank.n.02"}, {"name": "tank_top_(clothing)", "instance_count": 1799, "def": "a tight-fitting sleeveless shirt with wide shoulder straps and low neck and no front opening", "synonyms": ["tank_top_(clothing)"], "image_count": 1094, "id": 1060, "frequency": "f", "synset": "tank_top.n.01"}, {"name": "tape_(sticky_cloth_or_paper)", "instance_count": 560, "def": "a long thin piece of cloth or paper as used for binding or fastening", "synonyms": ["tape_(sticky_cloth_or_paper)"], "image_count": 134, "id": 1061, "frequency": "f", "synset": "tape.n.01"}, {"name": "tape_measure", "instance_count": 35, "def": "measuring instrument consisting of a narrow strip (cloth or metal) marked in inches or centimeters and used for measuring lengths", "synonyms": ["tape_measure", "measuring_tape"], "image_count": 29, "id": 1062, "frequency": "c", "synset": "tape.n.04"}, {"name": "tapestry", "instance_count": 29, "def": "a heavy textile with a woven design; used for curtains and upholstery", "synonyms": ["tapestry"], "image_count": 22, "id": 1063, "frequency": "c", "synset": "tapestry.n.02"}, {"name": "tarp", "instance_count": 1315, "def": "waterproofed canvas", "synonyms": ["tarp"], "image_count": 522, "id": 1064, "frequency": "f", "synset": "tarpaulin.n.01"}, {"name": "tartan", "instance_count": 68, "def": "a cloth having a crisscross design", "synonyms": ["tartan", "plaid"], "image_count": 50, "id": 1065, "frequency": "c", "synset": "tartan.n.01"}, {"name": "tassel", "instance_count": 276, "def": "adornment consisting of a bunch of cords fastened at one end", "synonyms": ["tassel"], "image_count": 68, "id": 1066, "frequency": "c", "synset": "tassel.n.01"}, {"name": "tea_bag", "instance_count": 42, "def": "a measured amount of tea in a bag for an individual serving of tea", "synonyms": ["tea_bag"], "image_count": 16, "id": 1067, "frequency": "c", "synset": "tea_bag.n.01"}, {"name": "teacup", "instance_count": 152, "def": "a cup from which tea is drunk", "synonyms": ["teacup"], "image_count": 40, "id": 1068, "frequency": "c", "synset": "teacup.n.02"}, {"name": "teakettle", "instance_count": 40, "def": "kettle for boiling water to make tea", "synonyms": ["teakettle"], "image_count": 35, "id": 1069, "frequency": "c", "synset": "teakettle.n.01"}, {"name": "teapot", "instance_count": 209, "def": "pot for brewing tea; usually has a spout and handle", "synonyms": ["teapot"], "image_count": 135, "id": 1070, "frequency": "f", "synset": "teapot.n.01"}, {"name": "teddy_bear", "instance_count": 4886, "def": "plaything consisting of a child's toy bear (usually plush and stuffed with soft materials)", "synonyms": ["teddy_bear"], "image_count": 1413, "id": 1071, "frequency": "f", "synset": "teddy.n.01"}, {"name": "telephone", "instance_count": 945, "def": "electronic device for communicating by voice over long distances (includes wired and wireless/cell phones)", "synonyms": ["telephone", "phone", "telephone_set"], "image_count": 772, "id": 1072, "frequency": "f", "synset": "telephone.n.01"}, {"name": "telephone_booth", "instance_count": 62, "def": "booth for using a telephone", "synonyms": ["telephone_booth", "phone_booth", "call_box", "telephone_box", "telephone_kiosk"], "image_count": 50, "id": 1073, "frequency": "c", "synset": "telephone_booth.n.01"}, {"name": "telephone_pole", "instance_count": 3725, "def": "tall pole supporting telephone wires", "synonyms": ["telephone_pole", "telegraph_pole", "telegraph_post"], "image_count": 1015, "id": 1074, "frequency": "f", "synset": "telephone_pole.n.01"}, {"name": "telephoto_lens", "instance_count": 1, "def": "a camera lens that magnifies the image", "synonyms": ["telephoto_lens", "zoom_lens"], "image_count": 1, "id": 1075, "frequency": "r", "synset": "telephoto_lens.n.01"}, {"name": "television_camera", "instance_count": 117, "def": "television equipment for capturing and recording video", "synonyms": ["television_camera", "tv_camera"], "image_count": 65, "id": 1076, "frequency": "c", "synset": "television_camera.n.01"}, {"name": "television_set", "instance_count": 2205, "def": "an electronic device that receives television signals and displays them on a screen", "synonyms": ["television_set", "tv", "tv_set"], "image_count": 1900, "id": 1077, "frequency": "f", "synset": "television_receiver.n.01"}, {"name": "tennis_ball", "instance_count": 2835, "def": "ball about the size of a fist used in playing tennis", "synonyms": ["tennis_ball"], "image_count": 1302, "id": 1078, "frequency": "f", "synset": "tennis_ball.n.01"}, {"name": "tennis_racket", "instance_count": 3035, "def": "a racket used to play tennis", "synonyms": ["tennis_racket"], "image_count": 1977, "id": 1079, "frequency": "f", "synset": "tennis_racket.n.01"}, {"name": "tequila", "instance_count": 2, "def": "Mexican liquor made from fermented juices of an agave plant", "synonyms": ["tequila"], "image_count": 2, "id": 1080, "frequency": "r", "synset": "tequila.n.01"}, {"name": "thermometer", "instance_count": 33, "def": "measuring instrument for measuring temperature", "synonyms": ["thermometer"], "image_count": 29, "id": 1081, "frequency": "c", "synset": "thermometer.n.01"}, {"name": "thermos_bottle", "instance_count": 49, "def": "vacuum flask that preserves temperature of hot or cold drinks", "synonyms": ["thermos_bottle"], "image_count": 36, "id": 1082, "frequency": "c", "synset": "thermos.n.01"}, {"name": "thermostat", "instance_count": 153, "def": "a regulator for automatically regulating temperature by starting or stopping the supply of heat", "synonyms": ["thermostat"], "image_count": 138, "id": 1083, "frequency": "f", "synset": "thermostat.n.01"}, {"name": "thimble", "instance_count": 6, "def": "a small metal cap to protect the finger while sewing; can be used as a small container", "synonyms": ["thimble"], "image_count": 4, "id": 1084, "frequency": "r", "synset": "thimble.n.02"}, {"name": "thread", "instance_count": 320, "def": "a fine cord of twisted fibers (of cotton or silk or wool or nylon etc.) used in sewing and weaving", "synonyms": ["thread", "yarn"], "image_count": 67, "id": 1085, "frequency": "c", "synset": "thread.n.01"}, {"name": "thumbtack", "instance_count": 224, "def": "a tack for attaching papers to a bulletin board or drawing board", "synonyms": ["thumbtack", "drawing_pin", "pushpin"], "image_count": 26, "id": 1086, "frequency": "c", "synset": "thumbtack.n.01"}, {"name": "tiara", "instance_count": 31, "def": "a jeweled headdress worn by women on formal occasions", "synonyms": ["tiara"], "image_count": 25, "id": 1087, "frequency": "c", "synset": "tiara.n.01"}, {"name": "tiger", "instance_count": 67, "def": "large feline of forests in most of Asia having a tawny coat with black stripes", "synonyms": ["tiger"], "image_count": 33, "id": 1088, "frequency": "c", "synset": "tiger.n.02"}, {"name": "tights_(clothing)", "instance_count": 45, "def": "skintight knit hose covering the body from the waist to the feet worn by acrobats and dancers and as stockings by women and girls", "synonyms": ["tights_(clothing)", "leotards"], "image_count": 37, "id": 1089, "frequency": "c", "synset": "tights.n.01"}, {"name": "timer", "instance_count": 62, "def": "a timepiece that measures a time interval and signals its end", "synonyms": ["timer", "stopwatch"], "image_count": 50, "id": 1090, "frequency": "c", "synset": "timer.n.01"}, {"name": "tinfoil", "instance_count": 421, "def": "foil made of tin or an alloy of tin and lead", "synonyms": ["tinfoil"], "image_count": 270, "id": 1091, "frequency": "f", "synset": "tinfoil.n.01"}, {"name": "tinsel", "instance_count": 70, "def": "a showy decoration that is basically valueless", "synonyms": ["tinsel"], "image_count": 12, "id": 1092, "frequency": "c", "synset": "tinsel.n.01"}, {"name": "tissue_paper", "instance_count": 587, "def": "a soft thin (usually translucent) paper", "synonyms": ["tissue_paper"], "image_count": 316, "id": 1093, "frequency": "f", "synset": "tissue.n.02"}, {"name": "toast_(food)", "instance_count": 125, "def": "slice of bread that has been toasted", "synonyms": ["toast_(food)"], "image_count": 41, "id": 1094, "frequency": "c", "synset": "toast.n.01"}, {"name": "toaster", "instance_count": 240, "def": "a kitchen appliance (usually electric) for toasting bread", "synonyms": ["toaster"], "image_count": 224, "id": 1095, "frequency": "f", "synset": "toaster.n.02"}, {"name": "toaster_oven", "instance_count": 114, "def": "kitchen appliance consisting of a small electric oven for toasting or warming food", "synonyms": ["toaster_oven"], "image_count": 105, "id": 1096, "frequency": "f", "synset": "toaster_oven.n.01"}, {"name": "toilet", "instance_count": 2295, "def": "a plumbing fixture for defecation and urination", "synonyms": ["toilet"], "image_count": 1925, "id": 1097, "frequency": "f", "synset": "toilet.n.02"}, {"name": "toilet_tissue", "instance_count": 1683, "def": "a soft thin absorbent paper for use in toilets", "synonyms": ["toilet_tissue", "toilet_paper", "bathroom_tissue"], "image_count": 1021, "id": 1098, "frequency": "f", "synset": "toilet_tissue.n.01"}, {"name": "tomato", "instance_count": 12338, "def": "mildly acid red or yellow pulpy fruit eaten as a vegetable", "synonyms": ["tomato"], "image_count": 1213, "id": 1099, "frequency": "f", "synset": "tomato.n.01"}, {"name": "tongs", "instance_count": 294, "def": "any of various devices for taking hold of objects; usually have two hinged legs with handles above and pointed hooks below", "synonyms": ["tongs"], "image_count": 172, "id": 1100, "frequency": "f", "synset": "tongs.n.01"}, {"name": "toolbox", "instance_count": 39, "def": "a box or chest or cabinet for holding hand tools", "synonyms": ["toolbox"], "image_count": 28, "id": 1101, "frequency": "c", "synset": "toolbox.n.01"}, {"name": "toothbrush", "instance_count": 1683, "def": "small brush; has long handle; used to clean teeth", "synonyms": ["toothbrush"], "image_count": 745, "id": 1102, "frequency": "f", "synset": "toothbrush.n.01"}, {"name": "toothpaste", "instance_count": 326, "def": "a dentifrice in the form of a paste", "synonyms": ["toothpaste"], "image_count": 187, "id": 1103, "frequency": "f", "synset": "toothpaste.n.01"}, {"name": "toothpick", "instance_count": 423, "def": "pick consisting of a small strip of wood or plastic; used to pick food from between the teeth", "synonyms": ["toothpick"], "image_count": 147, "id": 1104, "frequency": "f", "synset": "toothpick.n.01"}, {"name": "cover", "instance_count": 306, "def": "covering for a hole (especially a hole in the top of a container)", "synonyms": ["cover"], "image_count": 136, "id": 1105, "frequency": "f", "synset": "top.n.09"}, {"name": "tortilla", "instance_count": 135, "def": "thin unleavened pancake made from cornmeal or wheat flour", "synonyms": ["tortilla"], "image_count": 34, "id": 1106, "frequency": "c", "synset": "tortilla.n.01"}, {"name": "tow_truck", "instance_count": 45, "def": "a truck equipped to hoist and pull wrecked cars (or to remove cars from no-parking zones)", "synonyms": ["tow_truck"], "image_count": 41, "id": 1107, "frequency": "c", "synset": "tow_truck.n.01"}, {"name": "towel", "instance_count": 2212, "def": "a rectangular piece of absorbent cloth (or paper) for drying or wiping", "synonyms": ["towel"], "image_count": 636, "id": 1108, "frequency": "f", "synset": "towel.n.01"}, {"name": "towel_rack", "instance_count": 987, "def": "a rack consisting of one or more bars on which towels can be hung", "synonyms": ["towel_rack", "towel_rail", "towel_bar"], "image_count": 570, "id": 1109, "frequency": "f", "synset": "towel_rack.n.01"}, {"name": "toy", "instance_count": 6756, "def": "a device regarded as providing amusement", "synonyms": ["toy"], "image_count": 1149, "id": 1110, "frequency": "f", "synset": "toy.n.03"}, {"name": "tractor_(farm_equipment)", "instance_count": 80, "def": "a wheeled vehicle with large wheels; used in farming and other applications", "synonyms": ["tractor_(farm_equipment)"], "image_count": 61, "id": 1111, "frequency": "c", "synset": "tractor.n.01"}, {"name": "traffic_light", "instance_count": 7298, "def": "a device to control vehicle traffic often consisting of three or more lights", "synonyms": ["traffic_light"], "image_count": 1890, "id": 1112, "frequency": "f", "synset": "traffic_light.n.01"}, {"name": "dirt_bike", "instance_count": 47, "def": "a lightweight motorcycle equipped with rugged tires and suspension for off-road use", "synonyms": ["dirt_bike"], "image_count": 18, "id": 1113, "frequency": "c", "synset": "trail_bike.n.01"}, {"name": "trailer_truck", "instance_count": 297, "def": "a truck consisting of a tractor and trailer together", "synonyms": ["trailer_truck", "tractor_trailer", "trucking_rig", "articulated_lorry", "semi_truck"], "image_count": 143, "id": 1114, "frequency": "f", "synset": "trailer_truck.n.01"}, {"name": "train_(railroad_vehicle)", "instance_count": 2192, "def": "public or private transport provided by a line of railway cars coupled together and drawn by a locomotive", "synonyms": ["train_(railroad_vehicle)", "railroad_train"], "image_count": 1517, "id": 1115, "frequency": "f", "synset": "train.n.01"}, {"name": "trampoline", "instance_count": 7, "def": "gymnastic apparatus consisting of a strong canvas sheet attached with springs to a metal frame", "synonyms": ["trampoline"], "image_count": 7, "id": 1116, "frequency": "r", "synset": "trampoline.n.01"}, {"name": "tray", "instance_count": 2397, "def": "an open receptacle for holding or displaying or serving articles or food", "synonyms": ["tray"], "image_count": 943, "id": 1117, "frequency": "f", "synset": "tray.n.01"}, {"name": "trench_coat", "instance_count": 16, "def": "a military style raincoat; belted with deep pockets", "synonyms": ["trench_coat"], "image_count": 6, "id": 1118, "frequency": "r", "synset": "trench_coat.n.01"}, {"name": "triangle_(musical_instrument)", "instance_count": 1, "def": "a percussion instrument consisting of a metal bar bent in the shape of an open triangle", "synonyms": ["triangle_(musical_instrument)"], "image_count": 1, "id": 1119, "frequency": "r", "synset": "triangle.n.05"}, {"name": "tricycle", "instance_count": 15, "def": "a vehicle with three wheels that is moved by foot pedals", "synonyms": ["tricycle"], "image_count": 11, "id": 1120, "frequency": "c", "synset": "tricycle.n.01"}, {"name": "tripod", "instance_count": 132, "def": "a three-legged rack used for support", "synonyms": ["tripod"], "image_count": 101, "id": 1121, "frequency": "f", "synset": "tripod.n.01"}, {"name": "trousers", "instance_count": 7806, "def": "a garment extending from the waist to the knee or ankle, covering each leg separately", "synonyms": ["trousers", "pants_(clothing)"], "image_count": 1909, "id": 1122, "frequency": "f", "synset": "trouser.n.01"}, {"name": "truck", "instance_count": 1797, "def": "an automotive vehicle suitable for hauling", "synonyms": ["truck"], "image_count": 800, "id": 1123, "frequency": "f", "synset": "truck.n.01"}, {"name": "truffle_(chocolate)", "instance_count": 4, "def": "creamy chocolate candy", "synonyms": ["truffle_(chocolate)", "chocolate_truffle"], "image_count": 1, "id": 1124, "frequency": "r", "synset": "truffle.n.03"}, {"name": "trunk", "instance_count": 334, "def": "luggage consisting of a large strong case used when traveling or for storage", "synonyms": ["trunk"], "image_count": 44, "id": 1125, "frequency": "c", "synset": "trunk.n.02"}, {"name": "vat", "instance_count": 15, "def": "a large vessel for holding or storing liquids", "synonyms": ["vat"], "image_count": 3, "id": 1126, "frequency": "r", "synset": "tub.n.02"}, {"name": "turban", "instance_count": 124, "def": "a traditional headdress consisting of a long scarf wrapped around the head", "synonyms": ["turban"], "image_count": 44, "id": 1127, "frequency": "c", "synset": "turban.n.01"}, {"name": "turkey_(food)", "instance_count": 120, "def": "flesh of large domesticated fowl usually roasted", "synonyms": ["turkey_(food)"], "image_count": 31, "id": 1128, "frequency": "c", "synset": "turkey.n.04"}, {"name": "turnip", "instance_count": 109, "def": "widely cultivated plant having a large fleshy edible white or yellow root", "synonyms": ["turnip"], "image_count": 7, "id": 1129, "frequency": "r", "synset": "turnip.n.01"}, {"name": "turtle", "instance_count": 31, "def": "any of various aquatic and land reptiles having a bony shell and flipper-like limbs for swimming", "synonyms": ["turtle"], "image_count": 20, "id": 1130, "frequency": "c", "synset": "turtle.n.02"}, {"name": "turtleneck_(clothing)", "instance_count": 13, "def": "a sweater or jersey with a high close-fitting collar", "synonyms": ["turtleneck_(clothing)", "polo-neck"], "image_count": 11, "id": 1131, "frequency": "c", "synset": "turtleneck.n.01"}, {"name": "typewriter", "instance_count": 14, "def": "hand-operated character printer for printing written messages one character at a time", "synonyms": ["typewriter"], "image_count": 13, "id": 1132, "frequency": "c", "synset": "typewriter.n.01"}, {"name": "umbrella", "instance_count": 9161, "def": "a lightweight handheld collapsible canopy", "synonyms": ["umbrella"], "image_count": 1924, "id": 1133, "frequency": "f", "synset": "umbrella.n.01"}, {"name": "underwear", "instance_count": 164, "def": "undergarment worn next to the skin and under the outer garments", "synonyms": ["underwear", "underclothes", "underclothing", "underpants"], "image_count": 113, "id": 1134, "frequency": "f", "synset": "underwear.n.01"}, {"name": "unicycle", "instance_count": 2, "def": "a vehicle with a single wheel that is driven by pedals", "synonyms": ["unicycle"], "image_count": 2, "id": 1135, "frequency": "r", "synset": "unicycle.n.01"}, {"name": "urinal", "instance_count": 381, "def": "a plumbing fixture (usually attached to the wall) used by men to urinate", "synonyms": ["urinal"], "image_count": 139, "id": 1136, "frequency": "f", "synset": "urinal.n.01"}, {"name": "urn", "instance_count": 81, "def": "a large vase that usually has a pedestal or feet", "synonyms": ["urn"], "image_count": 12, "id": 1137, "frequency": "c", "synset": "urn.n.01"}, {"name": "vacuum_cleaner", "instance_count": 38, "def": "an electrical home appliance that cleans by suction", "synonyms": ["vacuum_cleaner"], "image_count": 37, "id": 1138, "frequency": "c", "synset": "vacuum.n.04"}, {"name": "vase", "instance_count": 4971, "def": "an open jar of glass or porcelain used as an ornament or to hold flowers", "synonyms": ["vase"], "image_count": 1866, "id": 1139, "frequency": "f", "synset": "vase.n.01"}, {"name": "vending_machine", "instance_count": 65, "def": "a slot machine for selling goods", "synonyms": ["vending_machine"], "image_count": 47, "id": 1140, "frequency": "c", "synset": "vending_machine.n.01"}, {"name": "vent", "instance_count": 3370, "def": "a hole for the escape of gas or air", "synonyms": ["vent", "blowhole", "air_vent"], "image_count": 1468, "id": 1141, "frequency": "f", "synset": "vent.n.01"}, {"name": "vest", "instance_count": 1313, "def": "a man's sleeveless garment worn underneath a coat", "synonyms": ["vest", "waistcoat"], "image_count": 729, "id": 1142, "frequency": "f", "synset": "vest.n.01"}, {"name": "videotape", "instance_count": 228, "def": "a video recording made on magnetic tape", "synonyms": ["videotape"], "image_count": 24, "id": 1143, "frequency": "c", "synset": "videotape.n.01"}, {"name": "vinegar", "instance_count": 1, "def": "sour-tasting liquid produced usually by oxidation of the alcohol in wine or cider and used as a condiment or food preservative", "synonyms": ["vinegar"], "image_count": 1, "id": 1144, "frequency": "r", "synset": "vinegar.n.01"}, {"name": "violin", "instance_count": 10, "def": "bowed stringed instrument that is the highest member of the violin family", "synonyms": ["violin", "fiddle"], "image_count": 10, "id": 1145, "frequency": "r", "synset": "violin.n.01"}, {"name": "vodka", "instance_count": 3, "def": "unaged colorless liquor originating in Russia", "synonyms": ["vodka"], "image_count": 3, "id": 1146, "frequency": "r", "synset": "vodka.n.01"}, {"name": "volleyball", "instance_count": 33, "def": "an inflated ball used in playing volleyball", "synonyms": ["volleyball"], "image_count": 14, "id": 1147, "frequency": "c", "synset": "volleyball.n.02"}, {"name": "vulture", "instance_count": 16, "def": "any of various large birds of prey having naked heads and weak claws and feeding chiefly on carrion", "synonyms": ["vulture"], "image_count": 4, "id": 1148, "frequency": "r", "synset": "vulture.n.01"}, {"name": "waffle", "instance_count": 61, "def": "pancake batter baked in a waffle iron", "synonyms": ["waffle"], "image_count": 29, "id": 1149, "frequency": "c", "synset": "waffle.n.01"}, {"name": "waffle_iron", "instance_count": 4, "def": "a kitchen appliance for baking waffles", "synonyms": ["waffle_iron"], "image_count": 4, "id": 1150, "frequency": "r", "synset": "waffle_iron.n.01"}, {"name": "wagon", "instance_count": 121, "def": "any of various kinds of wheeled vehicles drawn by an animal or a tractor", "synonyms": ["wagon"], "image_count": 70, "id": 1151, "frequency": "c", "synset": "wagon.n.01"}, {"name": "wagon_wheel", "instance_count": 209, "def": "a wheel of a wagon", "synonyms": ["wagon_wheel"], "image_count": 46, "id": 1152, "frequency": "c", "synset": "wagon_wheel.n.01"}, {"name": "walking_stick", "instance_count": 21, "def": "a stick carried in the hand for support in walking", "synonyms": ["walking_stick"], "image_count": 14, "id": 1153, "frequency": "c", "synset": "walking_stick.n.01"}, {"name": "wall_clock", "instance_count": 100, "def": "a clock mounted on a wall", "synonyms": ["wall_clock"], "image_count": 48, "id": 1154, "frequency": "c", "synset": "wall_clock.n.01"}, {"name": "wall_socket", "instance_count": 3069, "def": "receptacle providing a place in a wiring system where current can be taken to run electrical devices", "synonyms": ["wall_socket", "wall_plug", "electric_outlet", "electrical_outlet", "outlet", "electric_receptacle"], "image_count": 1855, "id": 1155, "frequency": "f", "synset": "wall_socket.n.01"}, {"name": "wallet", "instance_count": 123, "def": "a pocket-size case for holding papers and paper money", "synonyms": ["wallet", "billfold"], "image_count": 113, "id": 1156, "frequency": "f", "synset": "wallet.n.01"}, {"name": "walrus", "instance_count": 1, "def": "either of two large northern marine mammals having ivory tusks and tough hide over thick blubber", "synonyms": ["walrus"], "image_count": 1, "id": 1157, "frequency": "r", "synset": "walrus.n.01"}, {"name": "wardrobe", "instance_count": 1, "def": "a tall piece of furniture that provides storage space for clothes; has a door and rails or hooks for hanging clothes", "synonyms": ["wardrobe"], "image_count": 1, "id": 1158, "frequency": "r", "synset": "wardrobe.n.01"}, {"name": "washbasin", "instance_count": 15, "def": "a bathroom sink that is permanently installed and connected to a water supply and drainpipe; where you can wash your hands and face", "synonyms": ["washbasin", "basin_(for_washing)", "washbowl", "washstand", "handbasin"], "image_count": 10, "id": 1159, "frequency": "r", "synset": "washbasin.n.01"}, {"name": "automatic_washer", "instance_count": 68, "def": "a home appliance for washing clothes and linens automatically", "synonyms": ["automatic_washer", "washing_machine"], "image_count": 54, "id": 1160, "frequency": "c", "synset": "washer.n.03"}, {"name": "watch", "instance_count": 2703, "def": "a small, portable timepiece", "synonyms": ["watch", "wristwatch"], "image_count": 1923, "id": 1161, "frequency": "f", "synset": "watch.n.01"}, {"name": "water_bottle", "instance_count": 1449, "def": "a bottle for holding water", "synonyms": ["water_bottle"], "image_count": 630, "id": 1162, "frequency": "f", "synset": "water_bottle.n.01"}, {"name": "water_cooler", "instance_count": 39, "def": "a device for cooling and dispensing drinking water", "synonyms": ["water_cooler"], "image_count": 31, "id": 1163, "frequency": "c", "synset": "water_cooler.n.01"}, {"name": "water_faucet", "instance_count": 109, "def": "a faucet for drawing water from a pipe or cask", "synonyms": ["water_faucet", "water_tap", "tap_(water_faucet)"], "image_count": 69, "id": 1164, "frequency": "c", "synset": "water_faucet.n.01"}, {"name": "water_heater", "instance_count": 7, "def": "a heater and storage tank to supply heated water", "synonyms": ["water_heater", "hot-water_heater"], "image_count": 7, "id": 1165, "frequency": "r", "synset": "water_heater.n.01"}, {"name": "water_jug", "instance_count": 23, "def": "a jug that holds water", "synonyms": ["water_jug"], "image_count": 11, "id": 1166, "frequency": "c", "synset": "water_jug.n.01"}, {"name": "water_gun", "instance_count": 1, "def": "plaything consisting of a toy pistol that squirts water", "synonyms": ["water_gun", "squirt_gun"], "image_count": 1, "id": 1167, "frequency": "r", "synset": "water_pistol.n.01"}, {"name": "water_scooter", "instance_count": 54, "def": "a motorboat resembling a motor scooter (NOT A SURFBOARD OR WATER SKI)", "synonyms": ["water_scooter", "sea_scooter", "jet_ski"], "image_count": 30, "id": 1168, "frequency": "c", "synset": "water_scooter.n.01"}, {"name": "water_ski", "instance_count": 98, "def": "broad ski for skimming over water towed by a speedboat (DO NOT MARK WATER)", "synonyms": ["water_ski"], "image_count": 50, "id": 1169, "frequency": "c", "synset": "water_ski.n.01"}, {"name": "water_tower", "instance_count": 60, "def": "a large reservoir for water", "synonyms": ["water_tower"], "image_count": 45, "id": 1170, "frequency": "c", "synset": "water_tower.n.01"}, {"name": "watering_can", "instance_count": 44, "def": "a container with a handle and a spout with a perforated nozzle; used to sprinkle water over plants", "synonyms": ["watering_can"], "image_count": 28, "id": 1171, "frequency": "c", "synset": "watering_can.n.01"}, {"name": "watermelon", "instance_count": 814, "def": "large oblong or roundish melon with a hard green rind and sweet watery red or occasionally yellowish pulp", "synonyms": ["watermelon"], "image_count": 114, "id": 1172, "frequency": "f", "synset": "watermelon.n.02"}, {"name": "weathervane", "instance_count": 237, "def": "mechanical device attached to an elevated structure; rotates freely to show the direction of the wind", "synonyms": ["weathervane", "vane_(weathervane)", "wind_vane"], "image_count": 193, "id": 1173, "frequency": "f", "synset": "weathervane.n.01"}, {"name": "webcam", "instance_count": 27, "def": "a digital camera designed to take digital photographs and transmit them over the internet", "synonyms": ["webcam"], "image_count": 21, "id": 1174, "frequency": "c", "synset": "webcam.n.01"}, {"name": "wedding_cake", "instance_count": 140, "def": "a rich cake with two or more tiers and covered with frosting and decorations; served at a wedding reception", "synonyms": ["wedding_cake", "bridecake"], "image_count": 91, "id": 1175, "frequency": "c", "synset": "wedding_cake.n.01"}, {"name": "wedding_ring", "instance_count": 49, "def": "a ring given to the bride and/or groom at the wedding", "synonyms": ["wedding_ring", "wedding_band"], "image_count": 31, "id": 1176, "frequency": "c", "synset": "wedding_ring.n.01"}, {"name": "wet_suit", "instance_count": 2907, "def": "a close-fitting garment made of a permeable material; worn in cold water to retain body heat", "synonyms": ["wet_suit"], "image_count": 1469, "id": 1177, "frequency": "f", "synset": "wet_suit.n.01"}, {"name": "wheel", "instance_count": 11272, "def": "a circular frame with spokes (or a solid disc) that can rotate on a shaft or axle", "synonyms": ["wheel"], "image_count": 1924, "id": 1178, "frequency": "f", "synset": "wheel.n.01"}, {"name": "wheelchair", "instance_count": 107, "def": "a movable chair mounted on large wheels", "synonyms": ["wheelchair"], "image_count": 87, "id": 1179, "frequency": "c", "synset": "wheelchair.n.01"}, {"name": "whipped_cream", "instance_count": 201, "def": "cream that has been beaten until light and fluffy", "synonyms": ["whipped_cream"], "image_count": 77, "id": 1180, "frequency": "c", "synset": "whipped_cream.n.01"}, {"name": "whistle", "instance_count": 13, "def": "a small wind instrument that produces a whistling sound by blowing into it", "synonyms": ["whistle"], "image_count": 11, "id": 1181, "frequency": "c", "synset": "whistle.n.03"}, {"name": "wig", "instance_count": 69, "def": "hairpiece covering the head and made of real or synthetic hair", "synonyms": ["wig"], "image_count": 47, "id": 1182, "frequency": "c", "synset": "wig.n.01"}, {"name": "wind_chime", "instance_count": 28, "def": "a decorative arrangement of pieces of metal or glass or pottery that hang together loosely so the wind can cause them to tinkle", "synonyms": ["wind_chime"], "image_count": 21, "id": 1183, "frequency": "c", "synset": "wind_chime.n.01"}, {"name": "windmill", "instance_count": 202, "def": "A mill or turbine that is powered by wind", "synonyms": ["windmill"], "image_count": 47, "id": 1184, "frequency": "c", "synset": "windmill.n.01"}, {"name": "window_box_(for_plants)", "instance_count": 253, "def": "a container for growing plants on a windowsill", "synonyms": ["window_box_(for_plants)"], "image_count": 70, "id": 1185, "frequency": "c", "synset": "window_box.n.01"}, {"name": "windshield_wiper", "instance_count": 4793, "def": "a mechanical device that cleans the windshield", "synonyms": ["windshield_wiper", "windscreen_wiper", "wiper_(for_windshield/screen)"], "image_count": 1838, "id": 1186, "frequency": "f", "synset": "windshield_wiper.n.01"}, {"name": "windsock", "instance_count": 26, "def": "a truncated cloth cone mounted on a mast/pole; shows wind direction", "synonyms": ["windsock", "air_sock", "air-sleeve", "wind_sleeve", "wind_cone"], "image_count": 19, "id": 1187, "frequency": "c", "synset": "windsock.n.01"}, {"name": "wine_bottle", "instance_count": 4449, "def": "a bottle for holding wine", "synonyms": ["wine_bottle"], "image_count": 531, "id": 1188, "frequency": "f", "synset": "wine_bottle.n.01"}, {"name": "wine_bucket", "instance_count": 21, "def": "a bucket of ice used to chill a bottle of wine", "synonyms": ["wine_bucket", "wine_cooler"], "image_count": 11, "id": 1189, "frequency": "c", "synset": "wine_bucket.n.01"}, {"name": "wineglass", "instance_count": 4259, "def": "a glass that has a stem and in which wine is served", "synonyms": ["wineglass"], "image_count": 941, "id": 1190, "frequency": "f", "synset": "wineglass.n.01"}, {"name": "blinder_(for_horses)", "instance_count": 271, "def": "blinds that prevent a horse from seeing something on either side", "synonyms": ["blinder_(for_horses)"], "image_count": 113, "id": 1191, "frequency": "f", "synset": "winker.n.02"}, {"name": "wok", "instance_count": 60, "def": "pan with a convex bottom; used for frying in Chinese cooking", "synonyms": ["wok"], "image_count": 26, "id": 1192, "frequency": "c", "synset": "wok.n.01"}, {"name": "wolf", "instance_count": 16, "def": "a wild carnivorous mammal of the dog family, living and hunting in packs", "synonyms": ["wolf"], "image_count": 5, "id": 1193, "frequency": "r", "synset": "wolf.n.01"}, {"name": "wooden_spoon", "instance_count": 123, "def": "a spoon made of wood", "synonyms": ["wooden_spoon"], "image_count": 56, "id": 1194, "frequency": "c", "synset": "wooden_spoon.n.02"}, {"name": "wreath", "instance_count": 119, "def": "an arrangement of flowers, leaves, or stems fastened in a ring", "synonyms": ["wreath"], "image_count": 73, "id": 1195, "frequency": "c", "synset": "wreath.n.01"}, {"name": "wrench", "instance_count": 80, "def": "a hand tool that is used to hold or twist a nut or bolt", "synonyms": ["wrench", "spanner"], "image_count": 32, "id": 1196, "frequency": "c", "synset": "wrench.n.03"}, {"name": "wristband", "instance_count": 268, "def": "band consisting of a part of a sleeve that covers the wrist", "synonyms": ["wristband"], "image_count": 128, "id": 1197, "frequency": "f", "synset": "wristband.n.01"}, {"name": "wristlet", "instance_count": 1330, "def": "a band or bracelet worn around the wrist", "synonyms": ["wristlet", "wrist_band"], "image_count": 623, "id": 1198, "frequency": "f", "synset": "wristlet.n.01"}, {"name": "yacht", "instance_count": 50, "def": "an expensive vessel propelled by sail or power and used for cruising or racing", "synonyms": ["yacht"], "image_count": 12, "id": 1199, "frequency": "c", "synset": "yacht.n.01"}, {"name": "yogurt", "instance_count": 116, "def": "a custard-like food made from curdled milk", "synonyms": ["yogurt", "yoghurt", "yoghourt"], "image_count": 52, "id": 1200, "frequency": "c", "synset": "yogurt.n.01"}, {"name": "yoke_(animal_equipment)", "instance_count": 20, "def": "gear joining two animals at the neck; NOT egg yolk", "synonyms": ["yoke_(animal_equipment)"], "image_count": 11, "id": 1201, "frequency": "c", "synset": "yoke.n.07"}, {"name": "zebra", "instance_count": 5443, "def": "any of several fleet black-and-white striped African equines", "synonyms": ["zebra"], "image_count": 1674, "id": 1202, "frequency": "f", "synset": "zebra.n.01"}, {"name": "zucchini", "instance_count": 798, "def": "small cucumber-shaped vegetable marrow; typically dark green", "synonyms": ["zucchini", "courgette"], "image_count": 81, "id": 1203, "frequency": "c", "synset": "zucchini.n.02"}] \ No newline at end of file diff --git a/dimos/models/Detic/datasets/metadata/o365_clip_a+cnamefix.npy b/dimos/models/Detic/datasets/metadata/o365_clip_a+cnamefix.npy deleted file mode 100644 index 64a2e43c4b..0000000000 Binary files a/dimos/models/Detic/datasets/metadata/o365_clip_a+cnamefix.npy and /dev/null differ diff --git a/dimos/models/Detic/datasets/metadata/oid_clip_a+cname.npy b/dimos/models/Detic/datasets/metadata/oid_clip_a+cname.npy deleted file mode 100644 index 1a2c953b8d..0000000000 Binary files a/dimos/models/Detic/datasets/metadata/oid_clip_a+cname.npy and /dev/null differ diff --git a/dimos/models/Detic/demo.py b/dimos/models/Detic/demo.py deleted file mode 100755 index e982f745a5..0000000000 --- a/dimos/models/Detic/demo.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import glob -import multiprocessing as mp -import os -import sys -import tempfile -import time -import warnings - -import cv2 -from detectron2.config import get_cfg -from detectron2.data.detection_utils import read_image -from detectron2.utils.logger import setup_logger -import mss -import numpy as np -import tqdm - -sys.path.insert(0, "third_party/CenterNet2/") -from centernet.config import add_centernet_config -from detic.config import add_detic_config -from detic.predictor import VisualizationDemo - - -# Fake a video capture object OpenCV style - half width, half height of first screen using MSS -class ScreenGrab: - def __init__(self) -> None: - self.sct = mss.mss() - m0 = self.sct.monitors[0] - self.monitor = {"top": 0, "left": 0, "width": m0["width"] / 2, "height": m0["height"] / 2} - - def read(self): - img = np.array(self.sct.grab(self.monitor)) - nf = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) - return (True, nf) - - def isOpened(self) -> bool: - return True - - def release(self) -> bool: - return True - - -# constants -WINDOW_NAME = "Detic" - - -def setup_cfg(args): - cfg = get_cfg() - if args.cpu: - cfg.MODEL.DEVICE = "cpu" - add_centernet_config(cfg) - add_detic_config(cfg) - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - # Set score_threshold for builtin models - cfg.MODEL.RETINANET.SCORE_THRESH_TEST = args.confidence_threshold - cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = args.confidence_threshold - cfg.MODEL.PANOPTIC_FPN.COMBINE.INSTANCES_CONFIDENCE_THRESH = args.confidence_threshold - cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = "rand" # load later - if not args.pred_all_class: - cfg.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL = True - cfg.freeze() - return cfg - - -def get_parser(): - parser = argparse.ArgumentParser(description="Detectron2 demo for builtin configs") - parser.add_argument( - "--config-file", - default="configs/quick_schedules/mask_rcnn_R_50_FPN_inference_acc_test.yaml", - metavar="FILE", - help="path to config file", - ) - parser.add_argument("--webcam", help="Take inputs from webcam.") - parser.add_argument("--cpu", action="store_true", help="Use CPU only.") - parser.add_argument("--video-input", help="Path to video file.") - parser.add_argument( - "--input", - nargs="+", - help="A list of space separated input images; or a single glob pattern such as 'directory/*.jpg'", - ) - parser.add_argument( - "--output", - help="A file or directory to save output visualizations. If not given, will show output in an OpenCV window.", - ) - parser.add_argument( - "--vocabulary", - default="lvis", - choices=["lvis", "openimages", "objects365", "coco", "custom"], - help="", - ) - parser.add_argument( - "--custom_vocabulary", - default="", - help="", - ) - parser.add_argument("--pred_all_class", action="store_true") - parser.add_argument( - "--confidence-threshold", - type=float, - default=0.5, - help="Minimum score for instance predictions to be shown", - ) - parser.add_argument( - "--opts", - help="Modify config options using the command-line 'KEY VALUE' pairs", - default=[], - nargs=argparse.REMAINDER, - ) - return parser - - -def test_opencv_video_format(codec, file_ext) -> bool: - with tempfile.TemporaryDirectory(prefix="video_format_test") as dir: - filename = os.path.join(dir, "test_file" + file_ext) - writer = cv2.VideoWriter( - filename=filename, - fourcc=cv2.VideoWriter_fourcc(*codec), - fps=float(30), - frameSize=(10, 10), - isColor=True, - ) - [writer.write(np.zeros((10, 10, 3), np.uint8)) for _ in range(30)] - writer.release() - if os.path.isfile(filename): - return True - return False - - -if __name__ == "__main__": - mp.set_start_method("spawn", force=True) - args = get_parser().parse_args() - setup_logger(name="fvcore") - logger = setup_logger() - logger.info("Arguments: " + str(args)) - - cfg = setup_cfg(args) - - demo = VisualizationDemo(cfg, args) - - if args.input: - if len(args.input) == 1: - args.input = glob.glob(os.path.expanduser(args.input[0])) - assert args.input, "The input path(s) was not found" - for path in tqdm.tqdm(args.input, disable=not args.output): - img = read_image(path, format="BGR") - start_time = time.time() - predictions, visualized_output = demo.run_on_image(img) - logger.info( - "{}: {} in {:.2f}s".format( - path, - "detected {} instances".format(len(predictions["instances"])) - if "instances" in predictions - else "finished", - time.time() - start_time, - ) - ) - - if args.output: - if os.path.isdir(args.output): - assert os.path.isdir(args.output), args.output - out_filename = os.path.join(args.output, os.path.basename(path)) - else: - assert len(args.input) == 1, "Please specify a directory with args.output" - out_filename = args.output - visualized_output.save(out_filename) - else: - cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) - cv2.imshow(WINDOW_NAME, visualized_output.get_image()[:, :, ::-1]) - if cv2.waitKey(0) == 27: - break # esc to quit - elif args.webcam: - assert args.input is None, "Cannot have both --input and --webcam!" - assert args.output is None, "output not yet supported with --webcam!" - if args.webcam == "screen": - cam = ScreenGrab() - else: - cam = cv2.VideoCapture(int(args.webcam)) - for vis in tqdm.tqdm(demo.run_on_video(cam)): - cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) - cv2.imshow(WINDOW_NAME, vis) - if cv2.waitKey(1) == 27: - break # esc to quit - cam.release() - cv2.destroyAllWindows() - elif args.video_input: - video = cv2.VideoCapture(args.video_input) - width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)) - frames_per_second = video.get(cv2.CAP_PROP_FPS) - num_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - basename = os.path.basename(args.video_input) - codec, file_ext = ( - ("x264", ".mkv") if test_opencv_video_format("x264", ".mkv") else ("mp4v", ".mp4") - ) - if codec == ".mp4v": - warnings.warn("x264 codec not available, switching to mp4v", stacklevel=2) - if args.output: - if os.path.isdir(args.output): - output_fname = os.path.join(args.output, basename) - output_fname = os.path.splitext(output_fname)[0] + file_ext - else: - output_fname = args.output - assert not os.path.isfile(output_fname), output_fname - output_file = cv2.VideoWriter( - filename=output_fname, - # some installation of opencv may not support x264 (due to its license), - # you can try other format (e.g. MPEG) - fourcc=cv2.VideoWriter_fourcc(*codec), - fps=float(frames_per_second), - frameSize=(width, height), - isColor=True, - ) - assert os.path.isfile(args.video_input) - for vis_frame in tqdm.tqdm(demo.run_on_video(video), total=num_frames): - if args.output: - output_file.write(vis_frame) - else: - cv2.namedWindow(basename, cv2.WINDOW_NORMAL) - cv2.imshow(basename, vis_frame) - if cv2.waitKey(1) == 27: - break # esc to quit - video.release() - if args.output: - output_file.release() - else: - cv2.destroyAllWindows() diff --git a/dimos/models/Detic/detic/__init__.py b/dimos/models/Detic/detic/__init__.py deleted file mode 100644 index 2f8aa0a44e..0000000000 --- a/dimos/models/Detic/detic/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from .data.datasets import cc, coco_zeroshot, imagenet, lvis_v1, objects365, oid -from .modeling.backbone import swintransformer, timm -from .modeling.meta_arch import custom_rcnn -from .modeling.roi_heads import detic_roi_heads, res5_roi_heads - -try: - from .modeling.meta_arch import d2_deformable_detr -except: - pass diff --git a/dimos/models/Detic/detic/config.py b/dimos/models/Detic/detic/config.py deleted file mode 100644 index c053f0bd06..0000000000 --- a/dimos/models/Detic/detic/config.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from detectron2.config import CfgNode as CN - - -def add_detic_config(cfg) -> None: - _C = cfg - - _C.WITH_IMAGE_LABELS = False # Turn on co-training with classification data - - # Open-vocabulary classifier - _C.MODEL.ROI_BOX_HEAD.USE_ZEROSHOT_CLS = ( - False # Use fixed classifier for open-vocabulary detection - ) - _C.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = "datasets/metadata/lvis_v1_clip_a+cname.npy" - _C.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_DIM = 512 - _C.MODEL.ROI_BOX_HEAD.NORM_WEIGHT = True - _C.MODEL.ROI_BOX_HEAD.NORM_TEMP = 50.0 - _C.MODEL.ROI_BOX_HEAD.IGNORE_ZERO_CATS = False - _C.MODEL.ROI_BOX_HEAD.USE_BIAS = 0.0 # >= 0: not use - - _C.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE = False # CenterNet2 - _C.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE = False - _C.MODEL.ROI_BOX_HEAD.PRIOR_PROB = 0.01 - _C.MODEL.ROI_BOX_HEAD.USE_FED_LOSS = False # Federated Loss - _C.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH = "datasets/metadata/lvis_v1_train_cat_info.json" - _C.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT = 50 - _C.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT = 0.5 - - # Classification data configs - _C.MODEL.ROI_BOX_HEAD.IMAGE_LABEL_LOSS = "max_size" # max, softmax, sum - _C.MODEL.ROI_BOX_HEAD.IMAGE_LOSS_WEIGHT = 0.1 - _C.MODEL.ROI_BOX_HEAD.IMAGE_BOX_SIZE = 1.0 - _C.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX = False # Used for image-box loss and caption loss - _C.MODEL.ROI_BOX_HEAD.WS_NUM_PROPS = 128 # num proposals for image-labeled data - _C.MODEL.ROI_BOX_HEAD.WITH_SOFTMAX_PROP = False # Used for WSDDN - _C.MODEL.ROI_BOX_HEAD.CAPTION_WEIGHT = 1.0 # Caption loss weight - _C.MODEL.ROI_BOX_HEAD.NEG_CAP_WEIGHT = 0.125 # Caption loss hyper-parameter - _C.MODEL.ROI_BOX_HEAD.ADD_FEATURE_TO_PROP = False # Used for WSDDN - _C.MODEL.ROI_BOX_HEAD.SOFTMAX_WEAK_LOSS = False # Used when USE_SIGMOID_CE is False - - _C.MODEL.ROI_HEADS.MASK_WEIGHT = 1.0 - _C.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL = False # For demo only - - # Caption losses - _C.MODEL.CAP_BATCH_RATIO = 4 # Ratio between detection data and caption data - _C.MODEL.WITH_CAPTION = False - _C.MODEL.SYNC_CAPTION_BATCH = False # synchronize across GPUs to enlarge # "classes" - - # dynamic class sampling when training with 21K classes - _C.MODEL.DYNAMIC_CLASSIFIER = False - _C.MODEL.NUM_SAMPLE_CATS = 50 - - # Different classifiers in testing, used in cross-dataset evaluation - _C.MODEL.RESET_CLS_TESTS = False - _C.MODEL.TEST_CLASSIFIERS = [] - _C.MODEL.TEST_NUM_CLASSES = [] - - # Backbones - _C.MODEL.SWIN = CN() - _C.MODEL.SWIN.SIZE = "T" # 'T', 'S', 'B' - _C.MODEL.SWIN.USE_CHECKPOINT = False - _C.MODEL.SWIN.OUT_FEATURES = (1, 2, 3) # FPN stride 8 - 32 - - _C.MODEL.TIMM = CN() - _C.MODEL.TIMM.BASE_NAME = "resnet50" - _C.MODEL.TIMM.OUT_LEVELS = (3, 4, 5) - _C.MODEL.TIMM.NORM = "FrozenBN" - _C.MODEL.TIMM.FREEZE_AT = 0 - _C.MODEL.TIMM.PRETRAINED = False - _C.MODEL.DATASET_LOSS_WEIGHT = [] - - # Multi-dataset dataloader - _C.DATALOADER.DATASET_RATIO = [1, 1] # sample ratio - _C.DATALOADER.USE_RFS = [False, False] - _C.DATALOADER.MULTI_DATASET_GROUPING = False # Always true when multi-dataset is enabled - _C.DATALOADER.DATASET_ANN = ["box", "box"] # Annotation type of each dataset - _C.DATALOADER.USE_DIFF_BS_SIZE = False # Use different batchsize for each dataset - _C.DATALOADER.DATASET_BS = [8, 32] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.DATASET_INPUT_SIZE = [896, 384] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.DATASET_INPUT_SCALE = [(0.1, 2.0), (0.5, 1.5)] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.DATASET_MIN_SIZES = [(640, 800), (320, 400)] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.DATASET_MAX_SIZES = [1333, 667] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.USE_TAR_DATASET = False # for ImageNet-21K, directly reading from unziped files - _C.DATALOADER.TARFILE_PATH = "datasets/imagenet/metadata-22k/tar_files.npy" - _C.DATALOADER.TAR_INDEX_DIR = "datasets/imagenet/metadata-22k/tarindex_npy" - - _C.SOLVER.USE_CUSTOM_SOLVER = False - _C.SOLVER.OPTIMIZER = "SGD" - _C.SOLVER.BACKBONE_MULTIPLIER = 1.0 # Used in DETR - _C.SOLVER.CUSTOM_MULTIPLIER = 1.0 # Used in DETR - _C.SOLVER.CUSTOM_MULTIPLIER_NAME = [] # Used in DETR - - # Deformable DETR - _C.MODEL.DETR = CN() - _C.MODEL.DETR.NUM_CLASSES = 80 - _C.MODEL.DETR.FROZEN_WEIGHTS = "" # For Segmentation - _C.MODEL.DETR.GIOU_WEIGHT = 2.0 - _C.MODEL.DETR.L1_WEIGHT = 5.0 - _C.MODEL.DETR.DEEP_SUPERVISION = True - _C.MODEL.DETR.NO_OBJECT_WEIGHT = 0.1 - _C.MODEL.DETR.CLS_WEIGHT = 2.0 - _C.MODEL.DETR.NUM_FEATURE_LEVELS = 4 - _C.MODEL.DETR.TWO_STAGE = False - _C.MODEL.DETR.WITH_BOX_REFINE = False - _C.MODEL.DETR.FOCAL_ALPHA = 0.25 - _C.MODEL.DETR.NHEADS = 8 - _C.MODEL.DETR.DROPOUT = 0.1 - _C.MODEL.DETR.DIM_FEEDFORWARD = 2048 - _C.MODEL.DETR.ENC_LAYERS = 6 - _C.MODEL.DETR.DEC_LAYERS = 6 - _C.MODEL.DETR.PRE_NORM = False - _C.MODEL.DETR.HIDDEN_DIM = 256 - _C.MODEL.DETR.NUM_OBJECT_QUERIES = 100 - - _C.MODEL.DETR.USE_FED_LOSS = False - _C.MODEL.DETR.WEAK_WEIGHT = 0.1 - - _C.INPUT.CUSTOM_AUG = "" - _C.INPUT.TRAIN_SIZE = 640 - _C.INPUT.TEST_SIZE = 640 - _C.INPUT.SCALE_RANGE = (0.1, 2.0) - # 'default' for fixed short/ long edge, 'square' for max size=INPUT.SIZE - _C.INPUT.TEST_INPUT_TYPE = "default" - - _C.FIND_UNUSED_PARAM = True - _C.EVAL_PRED_AR = False - _C.EVAL_PROPOSAL_AR = False - _C.EVAL_CAT_SPEC_AR = False - _C.IS_DEBUG = False - _C.QUICK_DEBUG = False - _C.FP16 = False - _C.EVAL_AP_FIX = False - _C.GEN_PSEDO_LABELS = False - _C.SAVE_DEBUG_PATH = "output/save_debug/" diff --git a/dimos/models/Detic/detic/custom_solver.py b/dimos/models/Detic/detic/custom_solver.py deleted file mode 100644 index a552dea0f1..0000000000 --- a/dimos/models/Detic/detic/custom_solver.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -import itertools -from typing import Any, Dict, List, Set - -from detectron2.config import CfgNode -from detectron2.solver.build import maybe_add_gradient_clipping -import torch - - -def match_name_keywords(n, name_keywords): - out = False - for b in name_keywords: - if b in n: - out = True - break - return out - - -def build_custom_optimizer(cfg: CfgNode, model: torch.nn.Module) -> torch.optim.Optimizer: - """ - Build an optimizer from config. - """ - params: list[dict[str, Any]] = [] - memo: set[torch.nn.parameter.Parameter] = set() - custom_multiplier_name = cfg.SOLVER.CUSTOM_MULTIPLIER_NAME - optimizer_type = cfg.SOLVER.OPTIMIZER - for key, value in model.named_parameters(recurse=True): - if not value.requires_grad: - continue - # Avoid duplicating parameters - if value in memo: - continue - memo.add(value) - lr = cfg.SOLVER.BASE_LR - weight_decay = cfg.SOLVER.WEIGHT_DECAY - if "backbone" in key: - lr = lr * cfg.SOLVER.BACKBONE_MULTIPLIER - if match_name_keywords(key, custom_multiplier_name): - lr = lr * cfg.SOLVER.CUSTOM_MULTIPLIER - print("Costum LR", key, lr) - param = {"params": [value], "lr": lr} - if optimizer_type != "ADAMW": - param["weight_decay"] = weight_decay - params += [param] - - def maybe_add_full_model_gradient_clipping(optim): # optim: the optimizer class - # detectron2 doesn't have full model gradient clipping now - clip_norm_val = cfg.SOLVER.CLIP_GRADIENTS.CLIP_VALUE - enable = ( - cfg.SOLVER.CLIP_GRADIENTS.ENABLED - and cfg.SOLVER.CLIP_GRADIENTS.CLIP_TYPE == "full_model" - and clip_norm_val > 0.0 - ) - - class FullModelGradientClippingOptimizer(optim): - def step(self, closure=None) -> None: - all_params = itertools.chain(*[x["params"] for x in self.param_groups]) - torch.nn.utils.clip_grad_norm_(all_params, clip_norm_val) - super().step(closure=closure) - - return FullModelGradientClippingOptimizer if enable else optim - - if optimizer_type == "SGD": - optimizer = maybe_add_full_model_gradient_clipping(torch.optim.SGD)( - params, cfg.SOLVER.BASE_LR, momentum=cfg.SOLVER.MOMENTUM, nesterov=cfg.SOLVER.NESTEROV - ) - elif optimizer_type == "ADAMW": - optimizer = maybe_add_full_model_gradient_clipping(torch.optim.AdamW)( - params, cfg.SOLVER.BASE_LR, weight_decay=cfg.SOLVER.WEIGHT_DECAY - ) - else: - raise NotImplementedError(f"no optimizer type {optimizer_type}") - if not cfg.SOLVER.CLIP_GRADIENTS.CLIP_TYPE == "full_model": - optimizer = maybe_add_gradient_clipping(cfg, optimizer) - return optimizer diff --git a/dimos/models/Detic/detic/data/custom_build_augmentation.py b/dimos/models/Detic/detic/data/custom_build_augmentation.py deleted file mode 100644 index 5a6049ae02..0000000000 --- a/dimos/models/Detic/detic/data/custom_build_augmentation.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. - - -from detectron2.data import transforms as T - -from .transforms.custom_augmentation_impl import EfficientDetResizeCrop -from typing import Optional - - -def build_custom_augmentation(cfg, is_train: bool, scale=None, size: Optional[int]=None, min_size: Optional[int]=None, max_size: Optional[int]=None): - """ - Create a list of default :class:`Augmentation` from config. - Now it includes resizing and flipping. - - Returns: - list[Augmentation] - """ - if cfg.INPUT.CUSTOM_AUG == "ResizeShortestEdge": - if is_train: - min_size = cfg.INPUT.MIN_SIZE_TRAIN if min_size is None else min_size - max_size = cfg.INPUT.MAX_SIZE_TRAIN if max_size is None else max_size - sample_style = cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING - else: - min_size = cfg.INPUT.MIN_SIZE_TEST - max_size = cfg.INPUT.MAX_SIZE_TEST - sample_style = "choice" - augmentation = [T.ResizeShortestEdge(min_size, max_size, sample_style)] - elif cfg.INPUT.CUSTOM_AUG == "EfficientDetResizeCrop": - if is_train: - scale = cfg.INPUT.SCALE_RANGE if scale is None else scale - size = cfg.INPUT.TRAIN_SIZE if size is None else size - else: - scale = (1, 1) - size = cfg.INPUT.TEST_SIZE - augmentation = [EfficientDetResizeCrop(size, scale)] - else: - assert 0, cfg.INPUT.CUSTOM_AUG - - if is_train: - augmentation.append(T.RandomFlip()) - return augmentation - - -build_custom_transform_gen = build_custom_augmentation -""" -Alias for backward-compatibility. -""" diff --git a/dimos/models/Detic/detic/data/custom_dataset_dataloader.py b/dimos/models/Detic/detic/data/custom_dataset_dataloader.py deleted file mode 100644 index ff4bfc9ea4..0000000000 --- a/dimos/models/Detic/detic/data/custom_dataset_dataloader.py +++ /dev/null @@ -1,322 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -# Part of the code is from https://github.com/xingyizhou/UniDet/blob/master/projects/UniDet/unidet/data/multi_dataset_dataloader.py (Apache-2.0 License) -from collections import defaultdict -import itertools -import math -import operator -from typing import Iterator, Sequence, Optional - -from detectron2.config import configurable -from detectron2.data.build import ( - build_batch_data_loader, - check_metadata_consistency, - filter_images_with_few_keypoints, - filter_images_with_only_crowd_annotations, - get_detection_dataset_dicts, - print_instances_class_histogram, - worker_init_reset_seed, -) -from detectron2.data.catalog import DatasetCatalog, MetadataCatalog -from detectron2.data.common import DatasetFromList, MapDataset -from detectron2.data.dataset_mapper import DatasetMapper -from detectron2.data.samplers import RepeatFactorTrainingSampler, TrainingSampler -from detectron2.utils import comm -from detectron2.utils.comm import get_world_size -import torch -import torch.utils.data -from torch.utils.data.sampler import Sampler - - -def _custom_train_loader_from_config(cfg, mapper=None, *, dataset=None, sampler=None): - sampler_name = cfg.DATALOADER.SAMPLER_TRAIN - if "MultiDataset" in sampler_name: - dataset_dicts = get_detection_dataset_dicts_with_source( - cfg.DATASETS.TRAIN, - filter_empty=cfg.DATALOADER.FILTER_EMPTY_ANNOTATIONS, - min_keypoints=cfg.MODEL.ROI_KEYPOINT_HEAD.MIN_KEYPOINTS_PER_IMAGE - if cfg.MODEL.KEYPOINT_ON - else 0, - proposal_files=cfg.DATASETS.PROPOSAL_FILES_TRAIN if cfg.MODEL.LOAD_PROPOSALS else None, - ) - else: - dataset_dicts = get_detection_dataset_dicts( - cfg.DATASETS.TRAIN, - filter_empty=cfg.DATALOADER.FILTER_EMPTY_ANNOTATIONS, - min_keypoints=cfg.MODEL.ROI_KEYPOINT_HEAD.MIN_KEYPOINTS_PER_IMAGE - if cfg.MODEL.KEYPOINT_ON - else 0, - proposal_files=cfg.DATASETS.PROPOSAL_FILES_TRAIN if cfg.MODEL.LOAD_PROPOSALS else None, - ) - - if mapper is None: - mapper = DatasetMapper(cfg, True) - - if sampler is not None: - pass - elif sampler_name == "TrainingSampler": - sampler = TrainingSampler(len(dataset)) - elif sampler_name == "MultiDatasetSampler": - sampler = MultiDatasetSampler( - dataset_dicts, - dataset_ratio=cfg.DATALOADER.DATASET_RATIO, - use_rfs=cfg.DATALOADER.USE_RFS, - dataset_ann=cfg.DATALOADER.DATASET_ANN, - repeat_threshold=cfg.DATALOADER.REPEAT_THRESHOLD, - ) - elif sampler_name == "RepeatFactorTrainingSampler": - repeat_factors = RepeatFactorTrainingSampler.repeat_factors_from_category_frequency( - dataset_dicts, cfg.DATALOADER.REPEAT_THRESHOLD - ) - sampler = RepeatFactorTrainingSampler(repeat_factors) - else: - raise ValueError(f"Unknown training sampler: {sampler_name}") - - return { - "dataset": dataset_dicts, - "sampler": sampler, - "mapper": mapper, - "total_batch_size": cfg.SOLVER.IMS_PER_BATCH, - "aspect_ratio_grouping": cfg.DATALOADER.ASPECT_RATIO_GROUPING, - "num_workers": cfg.DATALOADER.NUM_WORKERS, - "multi_dataset_grouping": cfg.DATALOADER.MULTI_DATASET_GROUPING, - "use_diff_bs_size": cfg.DATALOADER.USE_DIFF_BS_SIZE, - "dataset_bs": cfg.DATALOADER.DATASET_BS, - "num_datasets": len(cfg.DATASETS.TRAIN), - } - - -@configurable(from_config=_custom_train_loader_from_config) -def build_custom_train_loader( - dataset, - *, - mapper, - sampler, - total_batch_size: int=16, - aspect_ratio_grouping: bool=True, - num_workers: int=0, - num_datasets: int=1, - multi_dataset_grouping: bool=False, - use_diff_bs_size: bool=False, - dataset_bs=None, -): - """ - Modified from detectron2.data.build.build_custom_train_loader, but supports - different samplers - """ - if dataset_bs is None: - dataset_bs = [] - if isinstance(dataset, list): - dataset = DatasetFromList(dataset, copy=False) - if mapper is not None: - dataset = MapDataset(dataset, mapper) - if sampler is None: - sampler = TrainingSampler(len(dataset)) - assert isinstance(sampler, torch.utils.data.sampler.Sampler) - if multi_dataset_grouping: - return build_multi_dataset_batch_data_loader( - use_diff_bs_size, - dataset_bs, - dataset, - sampler, - total_batch_size, - num_datasets=num_datasets, - num_workers=num_workers, - ) - else: - return build_batch_data_loader( - dataset, - sampler, - total_batch_size, - aspect_ratio_grouping=aspect_ratio_grouping, - num_workers=num_workers, - ) - - -def build_multi_dataset_batch_data_loader( - use_diff_bs_size: int, dataset_bs, dataset, sampler, total_batch_size: int, num_datasets: int, num_workers: int=0 -): - """ """ - world_size = get_world_size() - assert total_batch_size > 0 and total_batch_size % world_size == 0, ( - f"Total batch size ({total_batch_size}) must be divisible by the number of gpus ({world_size})." - ) - - batch_size = total_batch_size // world_size - data_loader = torch.utils.data.DataLoader( - dataset, - sampler=sampler, - num_workers=num_workers, - batch_sampler=None, - collate_fn=operator.itemgetter(0), # don't batch, but yield individual elements - worker_init_fn=worker_init_reset_seed, - ) # yield individual mapped dict - if use_diff_bs_size: - return DIFFMDAspectRatioGroupedDataset(data_loader, dataset_bs, num_datasets) - else: - return MDAspectRatioGroupedDataset(data_loader, batch_size, num_datasets) - - -def get_detection_dataset_dicts_with_source( - dataset_names: Sequence[str], filter_empty: bool=True, min_keypoints: int=0, proposal_files=None -): - assert len(dataset_names) - dataset_dicts = [DatasetCatalog.get(dataset_name) for dataset_name in dataset_names] - for dataset_name, dicts in zip(dataset_names, dataset_dicts, strict=False): - assert len(dicts), f"Dataset '{dataset_name}' is empty!" - - for source_id, (dataset_name, dicts) in enumerate(zip(dataset_names, dataset_dicts, strict=False)): - assert len(dicts), f"Dataset '{dataset_name}' is empty!" - for d in dicts: - d["dataset_source"] = source_id - - if "annotations" in dicts[0]: - try: - class_names = MetadataCatalog.get(dataset_name).thing_classes - check_metadata_consistency("thing_classes", dataset_name) - print_instances_class_histogram(dicts, class_names) - except AttributeError: # class names are not available for this dataset - pass - - assert proposal_files is None - - dataset_dicts = list(itertools.chain.from_iterable(dataset_dicts)) - - has_instances = "annotations" in dataset_dicts[0] - if filter_empty and has_instances: - dataset_dicts = filter_images_with_only_crowd_annotations(dataset_dicts) - if min_keypoints > 0 and has_instances: - dataset_dicts = filter_images_with_few_keypoints(dataset_dicts, min_keypoints) - - return dataset_dicts - - -class MultiDatasetSampler(Sampler): - def __init__( - self, - dataset_dicts, - dataset_ratio, - use_rfs, - dataset_ann, - repeat_threshold: float=0.001, - seed: int | None = None, - ) -> None: - """ """ - sizes = [0 for _ in range(len(dataset_ratio))] - for d in dataset_dicts: - sizes[d["dataset_source"]] += 1 - print("dataset sizes", sizes) - self.sizes = sizes - assert len(dataset_ratio) == len(sizes), ( - f"length of dataset ratio {len(dataset_ratio)} should be equal to number if dataset {len(sizes)}" - ) - if seed is None: - seed = comm.shared_random_seed() - self._seed = int(seed) - self._rank = comm.get_rank() - self._world_size = comm.get_world_size() - - self.dataset_ids = torch.tensor( - [d["dataset_source"] for d in dataset_dicts], dtype=torch.long - ) - - dataset_weight = [ - torch.ones(s) * max(sizes) / s * r / sum(dataset_ratio) - for i, (r, s) in enumerate(zip(dataset_ratio, sizes, strict=False)) - ] - dataset_weight = torch.cat(dataset_weight) - - rfs_factors = [] - st = 0 - for i, s in enumerate(sizes): - if use_rfs[i]: - if dataset_ann[i] == "box": - rfs_func = RepeatFactorTrainingSampler.repeat_factors_from_category_frequency - else: - rfs_func = repeat_factors_from_tag_frequency - rfs_factor = rfs_func(dataset_dicts[st : st + s], repeat_thresh=repeat_threshold) - rfs_factor = rfs_factor * (s / rfs_factor.sum()) - else: - rfs_factor = torch.ones(s) - rfs_factors.append(rfs_factor) - st = st + s - rfs_factors = torch.cat(rfs_factors) - - self.weights = dataset_weight * rfs_factors - self.sample_epoch_size = len(self.weights) - - def __iter__(self) -> Iterator: - start = self._rank - yield from itertools.islice(self._infinite_indices(), start, None, self._world_size) - - def _infinite_indices(self): - g = torch.Generator() - g.manual_seed(self._seed) - while True: - ids = torch.multinomial( - self.weights, self.sample_epoch_size, generator=g, replacement=True - ) - [(self.dataset_ids[ids] == i).sum().int().item() for i in range(len(self.sizes))] - yield from ids - - -class MDAspectRatioGroupedDataset(torch.utils.data.IterableDataset): - def __init__(self, dataset, batch_size: int, num_datasets: int) -> None: - """ """ - self.dataset = dataset - self.batch_size = batch_size - self._buckets = [[] for _ in range(2 * num_datasets)] - - def __iter__(self) -> Iterator: - for d in self.dataset: - w, h = d["width"], d["height"] - aspect_ratio_bucket_id = 0 if w > h else 1 - bucket_id = d["dataset_source"] * 2 + aspect_ratio_bucket_id - bucket = self._buckets[bucket_id] - bucket.append(d) - if len(bucket) == self.batch_size: - yield bucket[:] - del bucket[:] - - -class DIFFMDAspectRatioGroupedDataset(torch.utils.data.IterableDataset): - def __init__(self, dataset, batch_sizes: Sequence[int], num_datasets: int) -> None: - """ """ - self.dataset = dataset - self.batch_sizes = batch_sizes - self._buckets = [[] for _ in range(2 * num_datasets)] - - def __iter__(self) -> Iterator: - for d in self.dataset: - w, h = d["width"], d["height"] - aspect_ratio_bucket_id = 0 if w > h else 1 - bucket_id = d["dataset_source"] * 2 + aspect_ratio_bucket_id - bucket = self._buckets[bucket_id] - bucket.append(d) - if len(bucket) == self.batch_sizes[d["dataset_source"]]: - yield bucket[:] - del bucket[:] - - -def repeat_factors_from_tag_frequency(dataset_dicts, repeat_thresh): - """ """ - category_freq = defaultdict(int) - for dataset_dict in dataset_dicts: - cat_ids = dataset_dict["pos_category_ids"] - for cat_id in cat_ids: - category_freq[cat_id] += 1 - num_images = len(dataset_dicts) - for k, v in category_freq.items(): - category_freq[k] = v / num_images - - category_rep = { - cat_id: max(1.0, math.sqrt(repeat_thresh / cat_freq)) - for cat_id, cat_freq in category_freq.items() - } - - rep_factors = [] - for dataset_dict in dataset_dicts: - cat_ids = dataset_dict["pos_category_ids"] - rep_factor = max({category_rep[cat_id] for cat_id in cat_ids}, default=1.0) - rep_factors.append(rep_factor) - - return torch.tensor(rep_factors, dtype=torch.float32) diff --git a/dimos/models/Detic/detic/data/custom_dataset_mapper.py b/dimos/models/Detic/detic/data/custom_dataset_mapper.py deleted file mode 100644 index 46c86ffd84..0000000000 --- a/dimos/models/Detic/detic/data/custom_dataset_mapper.py +++ /dev/null @@ -1,284 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -import copy -import logging - -from detectron2.config import configurable -from detectron2.data import detection_utils as utils, transforms as T -from detectron2.data.dataset_mapper import DatasetMapper -import numpy as np -import torch - -from .custom_build_augmentation import build_custom_augmentation -from .tar_dataset import DiskTarDataset - -__all__ = ["CustomDatasetMapper"] - - -class CustomDatasetMapper(DatasetMapper): - @configurable - def __init__( - self, - is_train: bool, - with_ann_type: bool=False, - dataset_ann=None, - use_diff_bs_size: bool=False, - dataset_augs=None, - is_debug: bool=False, - use_tar_dataset: bool=False, - tarfile_path: str="", - tar_index_dir: str="", - **kwargs, - ) -> None: - """ - add image labels - """ - if dataset_augs is None: - dataset_augs = [] - if dataset_ann is None: - dataset_ann = [] - self.with_ann_type = with_ann_type - self.dataset_ann = dataset_ann - self.use_diff_bs_size = use_diff_bs_size - if self.use_diff_bs_size and is_train: - self.dataset_augs = [T.AugmentationList(x) for x in dataset_augs] - self.is_debug = is_debug - self.use_tar_dataset = use_tar_dataset - if self.use_tar_dataset: - print("Using tar dataset") - self.tar_dataset = DiskTarDataset(tarfile_path, tar_index_dir) - super().__init__(is_train, **kwargs) - - @classmethod - def from_config(cls, cfg, is_train: bool = True): - ret = super().from_config(cfg, is_train) - ret.update( - { - "with_ann_type": cfg.WITH_IMAGE_LABELS, - "dataset_ann": cfg.DATALOADER.DATASET_ANN, - "use_diff_bs_size": cfg.DATALOADER.USE_DIFF_BS_SIZE, - "is_debug": cfg.IS_DEBUG, - "use_tar_dataset": cfg.DATALOADER.USE_TAR_DATASET, - "tarfile_path": cfg.DATALOADER.TARFILE_PATH, - "tar_index_dir": cfg.DATALOADER.TAR_INDEX_DIR, - } - ) - if ret["use_diff_bs_size"] and is_train: - if cfg.INPUT.CUSTOM_AUG == "EfficientDetResizeCrop": - dataset_scales = cfg.DATALOADER.DATASET_INPUT_SCALE - dataset_sizes = cfg.DATALOADER.DATASET_INPUT_SIZE - ret["dataset_augs"] = [ - build_custom_augmentation(cfg, True, scale, size) - for scale, size in zip(dataset_scales, dataset_sizes, strict=False) - ] - else: - assert cfg.INPUT.CUSTOM_AUG == "ResizeShortestEdge" - min_sizes = cfg.DATALOADER.DATASET_MIN_SIZES - max_sizes = cfg.DATALOADER.DATASET_MAX_SIZES - ret["dataset_augs"] = [ - build_custom_augmentation(cfg, True, min_size=mi, max_size=ma) - for mi, ma in zip(min_sizes, max_sizes, strict=False) - ] - else: - ret["dataset_augs"] = [] - - return ret - - def __call__(self, dataset_dict): - """ - include image labels - """ - dataset_dict = copy.deepcopy(dataset_dict) # it will be modified by code below - # USER: Write your own image loading if it's not from a file - if "file_name" in dataset_dict: - ori_image = utils.read_image(dataset_dict["file_name"], format=self.image_format) - else: - ori_image, _, _ = self.tar_dataset[dataset_dict["tar_index"]] - ori_image = utils._apply_exif_orientation(ori_image) - ori_image = utils.convert_PIL_to_numpy(ori_image, self.image_format) - utils.check_image_size(dataset_dict, ori_image) - - # USER: Remove if you don't do semantic/panoptic segmentation. - if "sem_seg_file_name" in dataset_dict: - sem_seg_gt = utils.read_image(dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) - else: - sem_seg_gt = None - - if self.is_debug: - dataset_dict["dataset_source"] = 0 - - ( - "dataset_source" in dataset_dict - and self.with_ann_type - and self.dataset_ann[dataset_dict["dataset_source"]] != "box" - ) - - aug_input = T.AugInput(copy.deepcopy(ori_image), sem_seg=sem_seg_gt) - if self.use_diff_bs_size and self.is_train: - transforms = self.dataset_augs[dataset_dict["dataset_source"]](aug_input) - else: - transforms = self.augmentations(aug_input) - image, sem_seg_gt = aug_input.image, aug_input.sem_seg - - image_shape = image.shape[:2] # h, w - dataset_dict["image"] = torch.as_tensor(np.ascontiguousarray(image.transpose(2, 0, 1))) - - if sem_seg_gt is not None: - dataset_dict["sem_seg"] = torch.as_tensor(sem_seg_gt.astype("long")) - - # USER: Remove if you don't use pre-computed proposals. - # Most users would not need this feature. - if self.proposal_topk is not None: - utils.transform_proposals( - dataset_dict, image_shape, transforms, proposal_topk=self.proposal_topk - ) - - if not self.is_train: - # USER: Modify this if you want to keep them for some reason. - dataset_dict.pop("annotations", None) - dataset_dict.pop("sem_seg_file_name", None) - return dataset_dict - - if "annotations" in dataset_dict: - # USER: Modify this if you want to keep them for some reason. - for anno in dataset_dict["annotations"]: - if not self.use_instance_mask: - anno.pop("segmentation", None) - if not self.use_keypoint: - anno.pop("keypoints", None) - - # USER: Implement additional transformations if you have other types of data - all_annos = [ - ( - utils.transform_instance_annotations( - obj, - transforms, - image_shape, - keypoint_hflip_indices=self.keypoint_hflip_indices, - ), - obj.get("iscrowd", 0), - ) - for obj in dataset_dict.pop("annotations") - ] - annos = [ann[0] for ann in all_annos if ann[1] == 0] - instances = utils.annotations_to_instances( - annos, image_shape, mask_format=self.instance_mask_format - ) - - del all_annos - if self.recompute_boxes: - instances.gt_boxes = instances.gt_masks.get_bounding_boxes() - dataset_dict["instances"] = utils.filter_empty_instances(instances) - if self.with_ann_type: - dataset_dict["pos_category_ids"] = dataset_dict.get("pos_category_ids", []) - dataset_dict["ann_type"] = self.dataset_ann[dataset_dict["dataset_source"]] - if self.is_debug and ( - ("pos_category_ids" not in dataset_dict) or (dataset_dict["pos_category_ids"] == []) - ): - dataset_dict["pos_category_ids"] = [ - x for x in sorted(set(dataset_dict["instances"].gt_classes.tolist())) - ] - return dataset_dict - - -# DETR augmentation -def build_transform_gen(cfg, is_train: bool): - """ """ - if is_train: - min_size = cfg.INPUT.MIN_SIZE_TRAIN - max_size = cfg.INPUT.MAX_SIZE_TRAIN - sample_style = cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING - else: - min_size = cfg.INPUT.MIN_SIZE_TEST - max_size = cfg.INPUT.MAX_SIZE_TEST - sample_style = "choice" - if sample_style == "range": - assert len(min_size) == 2, f"more than 2 ({len(min_size)}) min_size(s) are provided for ranges" - - logger = logging.getLogger(__name__) - tfm_gens = [] - if is_train: - tfm_gens.append(T.RandomFlip()) - tfm_gens.append(T.ResizeShortestEdge(min_size, max_size, sample_style)) - if is_train: - logger.info("TransformGens used in training: " + str(tfm_gens)) - return tfm_gens - - -class DetrDatasetMapper: - """ - A callable which takes a dataset dict in Detectron2 Dataset format, - and map it into a format used by DETR. - The callable currently does the following: - 1. Read the image from "file_name" - 2. Applies geometric transforms to the image and annotation - 3. Find and applies suitable cropping to the image and annotation - 4. Prepare image and annotation to Tensors - """ - - def __init__(self, cfg, is_train: bool=True) -> None: - if cfg.INPUT.CROP.ENABLED and is_train: - self.crop_gen = [ - T.ResizeShortestEdge([400, 500, 600], sample_style="choice"), - T.RandomCrop(cfg.INPUT.CROP.TYPE, cfg.INPUT.CROP.SIZE), - ] - else: - self.crop_gen = None - - self.mask_on = cfg.MODEL.MASK_ON - self.tfm_gens = build_transform_gen(cfg, is_train) - logging.getLogger(__name__).info( - f"Full TransformGens used in training: {self.tfm_gens!s}, crop: {self.crop_gen!s}" - ) - - self.img_format = cfg.INPUT.FORMAT - self.is_train = is_train - - def __call__(self, dataset_dict): - """ - Args: - dataset_dict (dict): Metadata of one image, in Detectron2 Dataset format. - Returns: - dict: a format that builtin models in detectron2 accept - """ - dataset_dict = copy.deepcopy(dataset_dict) # it will be modified by code below - image = utils.read_image(dataset_dict["file_name"], format=self.img_format) - utils.check_image_size(dataset_dict, image) - - if self.crop_gen is None: - image, transforms = T.apply_transform_gens(self.tfm_gens, image) - else: - if np.random.rand() > 0.5: - image, transforms = T.apply_transform_gens(self.tfm_gens, image) - else: - image, transforms = T.apply_transform_gens( - self.tfm_gens[:-1] + self.crop_gen + self.tfm_gens[-1:], image - ) - - image_shape = image.shape[:2] # h, w - - # Pytorch's dataloader is efficient on torch.Tensor due to shared-memory, - # but not efficient on large generic data structures due to the use of pickle & mp.Queue. - # Therefore it's important to use torch.Tensor. - dataset_dict["image"] = torch.as_tensor(np.ascontiguousarray(image.transpose(2, 0, 1))) - - if not self.is_train: - # USER: Modify this if you want to keep them for some reason. - dataset_dict.pop("annotations", None) - return dataset_dict - - if "annotations" in dataset_dict: - # USER: Modify this if you want to keep them for some reason. - for anno in dataset_dict["annotations"]: - if not self.mask_on: - anno.pop("segmentation", None) - anno.pop("keypoints", None) - - # USER: Implement additional transformations if you have other types of data - annos = [ - utils.transform_instance_annotations(obj, transforms, image_shape) - for obj in dataset_dict.pop("annotations") - if obj.get("iscrowd", 0) == 0 - ] - instances = utils.annotations_to_instances(annos, image_shape) - dataset_dict["instances"] = utils.filter_empty_instances(instances) - return dataset_dict diff --git a/dimos/models/Detic/detic/data/datasets/cc.py b/dimos/models/Detic/detic/data/datasets/cc.py deleted file mode 100644 index be9c7f4a8b..0000000000 --- a/dimos/models/Detic/detic/data/datasets/cc.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import os - -from detectron2.data.datasets.lvis import get_lvis_instances_meta - -from .lvis_v1 import custom_register_lvis_instances - -_CUSTOM_SPLITS = { - "cc3m_v1_val": ("cc3m/validation/", "cc3m/val_image_info.json"), - "cc3m_v1_train": ("cc3m/training/", "cc3m/train_image_info.json"), - "cc3m_v1_train_tags": ("cc3m/training/", "cc3m/train_image_info_tags.json"), -} - -for key, (image_root, json_file) in _CUSTOM_SPLITS.items(): - custom_register_lvis_instances( - key, - get_lvis_instances_meta("lvis_v1"), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/detic/data/datasets/coco_zeroshot.py b/dimos/models/Detic/detic/data/datasets/coco_zeroshot.py deleted file mode 100644 index 80c360593d..0000000000 --- a/dimos/models/Detic/detic/data/datasets/coco_zeroshot.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import os - -from detectron2.data.datasets.builtin_meta import _get_builtin_metadata -from detectron2.data.datasets.register_coco import register_coco_instances - -from .lvis_v1 import custom_register_lvis_instances - -categories_seen = [ - {"id": 1, "name": "person"}, - {"id": 2, "name": "bicycle"}, - {"id": 3, "name": "car"}, - {"id": 4, "name": "motorcycle"}, - {"id": 7, "name": "train"}, - {"id": 8, "name": "truck"}, - {"id": 9, "name": "boat"}, - {"id": 15, "name": "bench"}, - {"id": 16, "name": "bird"}, - {"id": 19, "name": "horse"}, - {"id": 20, "name": "sheep"}, - {"id": 23, "name": "bear"}, - {"id": 24, "name": "zebra"}, - {"id": 25, "name": "giraffe"}, - {"id": 27, "name": "backpack"}, - {"id": 31, "name": "handbag"}, - {"id": 33, "name": "suitcase"}, - {"id": 34, "name": "frisbee"}, - {"id": 35, "name": "skis"}, - {"id": 38, "name": "kite"}, - {"id": 42, "name": "surfboard"}, - {"id": 44, "name": "bottle"}, - {"id": 48, "name": "fork"}, - {"id": 50, "name": "spoon"}, - {"id": 51, "name": "bowl"}, - {"id": 52, "name": "banana"}, - {"id": 53, "name": "apple"}, - {"id": 54, "name": "sandwich"}, - {"id": 55, "name": "orange"}, - {"id": 56, "name": "broccoli"}, - {"id": 57, "name": "carrot"}, - {"id": 59, "name": "pizza"}, - {"id": 60, "name": "donut"}, - {"id": 62, "name": "chair"}, - {"id": 65, "name": "bed"}, - {"id": 70, "name": "toilet"}, - {"id": 72, "name": "tv"}, - {"id": 73, "name": "laptop"}, - {"id": 74, "name": "mouse"}, - {"id": 75, "name": "remote"}, - {"id": 78, "name": "microwave"}, - {"id": 79, "name": "oven"}, - {"id": 80, "name": "toaster"}, - {"id": 82, "name": "refrigerator"}, - {"id": 84, "name": "book"}, - {"id": 85, "name": "clock"}, - {"id": 86, "name": "vase"}, - {"id": 90, "name": "toothbrush"}, -] - -categories_unseen = [ - {"id": 5, "name": "airplane"}, - {"id": 6, "name": "bus"}, - {"id": 17, "name": "cat"}, - {"id": 18, "name": "dog"}, - {"id": 21, "name": "cow"}, - {"id": 22, "name": "elephant"}, - {"id": 28, "name": "umbrella"}, - {"id": 32, "name": "tie"}, - {"id": 36, "name": "snowboard"}, - {"id": 41, "name": "skateboard"}, - {"id": 47, "name": "cup"}, - {"id": 49, "name": "knife"}, - {"id": 61, "name": "cake"}, - {"id": 63, "name": "couch"}, - {"id": 76, "name": "keyboard"}, - {"id": 81, "name": "sink"}, - {"id": 87, "name": "scissors"}, -] - - -def _get_metadata(cat): - if cat == "all": - return _get_builtin_metadata("coco") - elif cat == "seen": - id_to_name = {x["id"]: x["name"] for x in categories_seen} - else: - assert cat == "unseen" - id_to_name = {x["id"]: x["name"] for x in categories_unseen} - - thing_dataset_id_to_contiguous_id = {x: i for i, x in enumerate(sorted(id_to_name))} - thing_classes = [id_to_name[k] for k in sorted(id_to_name)] - return { - "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes, - } - - -_PREDEFINED_SPLITS_COCO = { - "coco_zeroshot_train": ( - "coco/train2017", - "coco/zero-shot/instances_train2017_seen_2.json", - "seen", - ), - "coco_zeroshot_val": ( - "coco/val2017", - "coco/zero-shot/instances_val2017_unseen_2.json", - "unseen", - ), - "coco_not_zeroshot_val": ( - "coco/val2017", - "coco/zero-shot/instances_val2017_seen_2.json", - "seen", - ), - "coco_generalized_zeroshot_val": ( - "coco/val2017", - "coco/zero-shot/instances_val2017_all_2_oriorder.json", - "all", - ), - "coco_zeroshot_train_oriorder": ( - "coco/train2017", - "coco/zero-shot/instances_train2017_seen_2_oriorder.json", - "all", - ), -} - -for key, (image_root, json_file, cat) in _PREDEFINED_SPLITS_COCO.items(): - register_coco_instances( - key, - _get_metadata(cat), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) - -_CUSTOM_SPLITS_COCO = { - "cc3m_coco_train_tags": ("cc3m/training/", "cc3m/coco_train_image_info_tags.json"), - "coco_caption_train_tags": ( - "coco/train2017/", - "coco/annotations/captions_train2017_tags_allcaps.json", - ), -} - -for key, (image_root, json_file) in _CUSTOM_SPLITS_COCO.items(): - custom_register_lvis_instances( - key, - _get_builtin_metadata("coco"), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/detic/data/datasets/imagenet.py b/dimos/models/Detic/detic/data/datasets/imagenet.py deleted file mode 100644 index caa7aa8fe0..0000000000 --- a/dimos/models/Detic/detic/data/datasets/imagenet.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import os - -from detectron2.data import DatasetCatalog, MetadataCatalog -from detectron2.data.datasets.lvis import get_lvis_instances_meta - -from .lvis_v1 import custom_load_lvis_json, get_lvis_22k_meta - - -def custom_register_imagenet_instances(name: str, metadata, json_file, image_root) -> None: - """ """ - DatasetCatalog.register(name, lambda: custom_load_lvis_json(json_file, image_root, name)) - MetadataCatalog.get(name).set( - json_file=json_file, image_root=image_root, evaluator_type="imagenet", **metadata - ) - - -_CUSTOM_SPLITS_IMAGENET = { - "imagenet_lvis_v1": ( - "imagenet/ImageNet-LVIS/", - "imagenet/annotations/imagenet_lvis_image_info.json", - ), -} - -for key, (image_root, json_file) in _CUSTOM_SPLITS_IMAGENET.items(): - custom_register_imagenet_instances( - key, - get_lvis_instances_meta("lvis_v1"), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) - - -_CUSTOM_SPLITS_IMAGENET_22K = { - "imagenet_lvis-22k": ( - "imagenet/ImageNet-LVIS/", - "imagenet/annotations/imagenet-22k_image_info_lvis-22k.json", - ), -} - -for key, (image_root, json_file) in _CUSTOM_SPLITS_IMAGENET_22K.items(): - custom_register_imagenet_instances( - key, - get_lvis_22k_meta(), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/detic/data/datasets/lvis_22k_categories.py b/dimos/models/Detic/detic/data/datasets/lvis_22k_categories.py deleted file mode 100644 index d1b3cc370a..0000000000 --- a/dimos/models/Detic/detic/data/datasets/lvis_22k_categories.py +++ /dev/null @@ -1,22383 +0,0 @@ -CATEGORIES = [ - {"name": "aerosol_can", "id": 1, "frequency": "c", "synset": "aerosol.n.02"}, - {"name": "air_conditioner", "id": 2, "frequency": "f", "synset": "air_conditioner.n.01"}, - {"name": "airplane", "id": 3, "frequency": "f", "synset": "airplane.n.01"}, - {"name": "alarm_clock", "id": 4, "frequency": "f", "synset": "alarm_clock.n.01"}, - {"name": "alcohol", "id": 5, "frequency": "c", "synset": "alcohol.n.01"}, - {"name": "alligator", "id": 6, "frequency": "c", "synset": "alligator.n.02"}, - {"name": "almond", "id": 7, "frequency": "c", "synset": "almond.n.02"}, - {"name": "ambulance", "id": 8, "frequency": "c", "synset": "ambulance.n.01"}, - {"name": "amplifier", "id": 9, "frequency": "c", "synset": "amplifier.n.01"}, - {"name": "anklet", "id": 10, "frequency": "c", "synset": "anklet.n.03"}, - {"name": "antenna", "id": 11, "frequency": "f", "synset": "antenna.n.01"}, - {"name": "apple", "id": 12, "frequency": "f", "synset": "apple.n.01"}, - {"name": "applesauce", "id": 13, "frequency": "r", "synset": "applesauce.n.01"}, - {"name": "apricot", "id": 14, "frequency": "r", "synset": "apricot.n.02"}, - {"name": "apron", "id": 15, "frequency": "f", "synset": "apron.n.01"}, - {"name": "aquarium", "id": 16, "frequency": "c", "synset": "aquarium.n.01"}, - {"name": "arctic_(type_of_shoe)", "id": 17, "frequency": "r", "synset": "arctic.n.02"}, - {"name": "armband", "id": 18, "frequency": "c", "synset": "armband.n.02"}, - {"name": "armchair", "id": 19, "frequency": "f", "synset": "armchair.n.01"}, - {"name": "armoire", "id": 20, "frequency": "r", "synset": "armoire.n.01"}, - {"name": "armor", "id": 21, "frequency": "r", "synset": "armor.n.01"}, - {"name": "artichoke", "id": 22, "frequency": "c", "synset": "artichoke.n.02"}, - {"name": "trash_can", "id": 23, "frequency": "f", "synset": "ashcan.n.01"}, - {"name": "ashtray", "id": 24, "frequency": "c", "synset": "ashtray.n.01"}, - {"name": "asparagus", "id": 25, "frequency": "c", "synset": "asparagus.n.02"}, - {"name": "atomizer", "id": 26, "frequency": "c", "synset": "atomizer.n.01"}, - {"name": "avocado", "id": 27, "frequency": "f", "synset": "avocado.n.01"}, - {"name": "award", "id": 28, "frequency": "c", "synset": "award.n.02"}, - {"name": "awning", "id": 29, "frequency": "f", "synset": "awning.n.01"}, - {"name": "ax", "id": 30, "frequency": "r", "synset": "ax.n.01"}, - {"name": "baboon", "id": 31, "frequency": "r", "synset": "baboon.n.01"}, - {"name": "baby_buggy", "id": 32, "frequency": "f", "synset": "baby_buggy.n.01"}, - {"name": "basketball_backboard", "id": 33, "frequency": "c", "synset": "backboard.n.01"}, - {"name": "backpack", "id": 34, "frequency": "f", "synset": "backpack.n.01"}, - {"name": "handbag", "id": 35, "frequency": "f", "synset": "bag.n.04"}, - {"name": "suitcase", "id": 36, "frequency": "f", "synset": "bag.n.06"}, - {"name": "bagel", "id": 37, "frequency": "c", "synset": "bagel.n.01"}, - {"name": "bagpipe", "id": 38, "frequency": "r", "synset": "bagpipe.n.01"}, - {"name": "baguet", "id": 39, "frequency": "r", "synset": "baguet.n.01"}, - {"name": "bait", "id": 40, "frequency": "r", "synset": "bait.n.02"}, - {"name": "ball", "id": 41, "frequency": "f", "synset": "ball.n.06"}, - {"name": "ballet_skirt", "id": 42, "frequency": "r", "synset": "ballet_skirt.n.01"}, - {"name": "balloon", "id": 43, "frequency": "f", "synset": "balloon.n.01"}, - {"name": "bamboo", "id": 44, "frequency": "c", "synset": "bamboo.n.02"}, - {"name": "banana", "id": 45, "frequency": "f", "synset": "banana.n.02"}, - {"name": "Band_Aid", "id": 46, "frequency": "c", "synset": "band_aid.n.01"}, - {"name": "bandage", "id": 47, "frequency": "c", "synset": "bandage.n.01"}, - {"name": "bandanna", "id": 48, "frequency": "f", "synset": "bandanna.n.01"}, - {"name": "banjo", "id": 49, "frequency": "r", "synset": "banjo.n.01"}, - {"name": "banner", "id": 50, "frequency": "f", "synset": "banner.n.01"}, - {"name": "barbell", "id": 51, "frequency": "r", "synset": "barbell.n.01"}, - {"name": "barge", "id": 52, "frequency": "r", "synset": "barge.n.01"}, - {"name": "barrel", "id": 53, "frequency": "f", "synset": "barrel.n.02"}, - {"name": "barrette", "id": 54, "frequency": "c", "synset": "barrette.n.01"}, - {"name": "barrow", "id": 55, "frequency": "c", "synset": "barrow.n.03"}, - {"name": "baseball_base", "id": 56, "frequency": "f", "synset": "base.n.03"}, - {"name": "baseball", "id": 57, "frequency": "f", "synset": "baseball.n.02"}, - {"name": "baseball_bat", "id": 58, "frequency": "f", "synset": "baseball_bat.n.01"}, - {"name": "baseball_cap", "id": 59, "frequency": "f", "synset": "baseball_cap.n.01"}, - {"name": "baseball_glove", "id": 60, "frequency": "f", "synset": "baseball_glove.n.01"}, - {"name": "basket", "id": 61, "frequency": "f", "synset": "basket.n.01"}, - {"name": "basketball", "id": 62, "frequency": "c", "synset": "basketball.n.02"}, - {"name": "bass_horn", "id": 63, "frequency": "r", "synset": "bass_horn.n.01"}, - {"name": "bat_(animal)", "id": 64, "frequency": "c", "synset": "bat.n.01"}, - {"name": "bath_mat", "id": 65, "frequency": "f", "synset": "bath_mat.n.01"}, - {"name": "bath_towel", "id": 66, "frequency": "f", "synset": "bath_towel.n.01"}, - {"name": "bathrobe", "id": 67, "frequency": "c", "synset": "bathrobe.n.01"}, - {"name": "bathtub", "id": 68, "frequency": "f", "synset": "bathtub.n.01"}, - {"name": "batter_(food)", "id": 69, "frequency": "r", "synset": "batter.n.02"}, - {"name": "battery", "id": 70, "frequency": "c", "synset": "battery.n.02"}, - {"name": "beachball", "id": 71, "frequency": "r", "synset": "beach_ball.n.01"}, - {"name": "bead", "id": 72, "frequency": "c", "synset": "bead.n.01"}, - {"name": "bean_curd", "id": 73, "frequency": "c", "synset": "bean_curd.n.01"}, - {"name": "beanbag", "id": 74, "frequency": "c", "synset": "beanbag.n.01"}, - {"name": "beanie", "id": 75, "frequency": "f", "synset": "beanie.n.01"}, - {"name": "bear", "id": 76, "frequency": "f", "synset": "bear.n.01"}, - {"name": "bed", "id": 77, "frequency": "f", "synset": "bed.n.01"}, - {"name": "bedpan", "id": 78, "frequency": "r", "synset": "bedpan.n.01"}, - {"name": "bedspread", "id": 79, "frequency": "f", "synset": "bedspread.n.01"}, - {"name": "cow", "id": 80, "frequency": "f", "synset": "beef.n.01"}, - {"name": "beef_(food)", "id": 81, "frequency": "f", "synset": "beef.n.02"}, - {"name": "beeper", "id": 82, "frequency": "r", "synset": "beeper.n.01"}, - {"name": "beer_bottle", "id": 83, "frequency": "f", "synset": "beer_bottle.n.01"}, - {"name": "beer_can", "id": 84, "frequency": "c", "synset": "beer_can.n.01"}, - {"name": "beetle", "id": 85, "frequency": "r", "synset": "beetle.n.01"}, - {"name": "bell", "id": 86, "frequency": "f", "synset": "bell.n.01"}, - {"name": "bell_pepper", "id": 87, "frequency": "f", "synset": "bell_pepper.n.02"}, - {"name": "belt", "id": 88, "frequency": "f", "synset": "belt.n.02"}, - {"name": "belt_buckle", "id": 89, "frequency": "f", "synset": "belt_buckle.n.01"}, - {"name": "bench", "id": 90, "frequency": "f", "synset": "bench.n.01"}, - {"name": "beret", "id": 91, "frequency": "c", "synset": "beret.n.01"}, - {"name": "bib", "id": 92, "frequency": "c", "synset": "bib.n.02"}, - {"name": "Bible", "id": 93, "frequency": "r", "synset": "bible.n.01"}, - {"name": "bicycle", "id": 94, "frequency": "f", "synset": "bicycle.n.01"}, - {"name": "visor", "id": 95, "frequency": "f", "synset": "bill.n.09"}, - {"name": "billboard", "id": 96, "frequency": "f", "synset": "billboard.n.01"}, - {"name": "binder", "id": 97, "frequency": "c", "synset": "binder.n.03"}, - {"name": "binoculars", "id": 98, "frequency": "c", "synset": "binoculars.n.01"}, - {"name": "bird", "id": 99, "frequency": "f", "synset": "bird.n.01"}, - {"name": "birdfeeder", "id": 100, "frequency": "c", "synset": "bird_feeder.n.01"}, - {"name": "birdbath", "id": 101, "frequency": "c", "synset": "birdbath.n.01"}, - {"name": "birdcage", "id": 102, "frequency": "c", "synset": "birdcage.n.01"}, - {"name": "birdhouse", "id": 103, "frequency": "c", "synset": "birdhouse.n.01"}, - {"name": "birthday_cake", "id": 104, "frequency": "f", "synset": "birthday_cake.n.01"}, - {"name": "birthday_card", "id": 105, "frequency": "r", "synset": "birthday_card.n.01"}, - {"name": "pirate_flag", "id": 106, "frequency": "r", "synset": "black_flag.n.01"}, - {"name": "black_sheep", "id": 107, "frequency": "c", "synset": "black_sheep.n.02"}, - {"name": "blackberry", "id": 108, "frequency": "c", "synset": "blackberry.n.01"}, - {"name": "blackboard", "id": 109, "frequency": "f", "synset": "blackboard.n.01"}, - {"name": "blanket", "id": 110, "frequency": "f", "synset": "blanket.n.01"}, - {"name": "blazer", "id": 111, "frequency": "c", "synset": "blazer.n.01"}, - {"name": "blender", "id": 112, "frequency": "f", "synset": "blender.n.01"}, - {"name": "blimp", "id": 113, "frequency": "r", "synset": "blimp.n.02"}, - {"name": "blinker", "id": 114, "frequency": "f", "synset": "blinker.n.01"}, - {"name": "blouse", "id": 115, "frequency": "f", "synset": "blouse.n.01"}, - {"name": "blueberry", "id": 116, "frequency": "f", "synset": "blueberry.n.02"}, - {"name": "gameboard", "id": 117, "frequency": "r", "synset": "board.n.09"}, - {"name": "boat", "id": 118, "frequency": "f", "synset": "boat.n.01"}, - {"name": "bob", "id": 119, "frequency": "r", "synset": "bob.n.05"}, - {"name": "bobbin", "id": 120, "frequency": "c", "synset": "bobbin.n.01"}, - {"name": "bobby_pin", "id": 121, "frequency": "c", "synset": "bobby_pin.n.01"}, - {"name": "boiled_egg", "id": 122, "frequency": "c", "synset": "boiled_egg.n.01"}, - {"name": "bolo_tie", "id": 123, "frequency": "r", "synset": "bolo_tie.n.01"}, - {"name": "deadbolt", "id": 124, "frequency": "c", "synset": "bolt.n.03"}, - {"name": "bolt", "id": 125, "frequency": "f", "synset": "bolt.n.06"}, - {"name": "bonnet", "id": 126, "frequency": "r", "synset": "bonnet.n.01"}, - {"name": "book", "id": 127, "frequency": "f", "synset": "book.n.01"}, - {"name": "bookcase", "id": 128, "frequency": "c", "synset": "bookcase.n.01"}, - {"name": "booklet", "id": 129, "frequency": "c", "synset": "booklet.n.01"}, - {"name": "bookmark", "id": 130, "frequency": "r", "synset": "bookmark.n.01"}, - {"name": "boom_microphone", "id": 131, "frequency": "r", "synset": "boom.n.04"}, - {"name": "boot", "id": 132, "frequency": "f", "synset": "boot.n.01"}, - {"name": "bottle", "id": 133, "frequency": "f", "synset": "bottle.n.01"}, - {"name": "bottle_opener", "id": 134, "frequency": "c", "synset": "bottle_opener.n.01"}, - {"name": "bouquet", "id": 135, "frequency": "c", "synset": "bouquet.n.01"}, - {"name": "bow_(weapon)", "id": 136, "frequency": "r", "synset": "bow.n.04"}, - {"name": "bow_(decorative_ribbons)", "id": 137, "frequency": "f", "synset": "bow.n.08"}, - {"name": "bow-tie", "id": 138, "frequency": "f", "synset": "bow_tie.n.01"}, - {"name": "bowl", "id": 139, "frequency": "f", "synset": "bowl.n.03"}, - {"name": "pipe_bowl", "id": 140, "frequency": "r", "synset": "bowl.n.08"}, - {"name": "bowler_hat", "id": 141, "frequency": "c", "synset": "bowler_hat.n.01"}, - {"name": "bowling_ball", "id": 142, "frequency": "r", "synset": "bowling_ball.n.01"}, - {"name": "box", "id": 143, "frequency": "f", "synset": "box.n.01"}, - {"name": "boxing_glove", "id": 144, "frequency": "r", "synset": "boxing_glove.n.01"}, - {"name": "suspenders", "id": 145, "frequency": "c", "synset": "brace.n.06"}, - {"name": "bracelet", "id": 146, "frequency": "f", "synset": "bracelet.n.02"}, - {"name": "brass_plaque", "id": 147, "frequency": "r", "synset": "brass.n.07"}, - {"name": "brassiere", "id": 148, "frequency": "c", "synset": "brassiere.n.01"}, - {"name": "bread-bin", "id": 149, "frequency": "c", "synset": "bread-bin.n.01"}, - {"name": "bread", "id": 150, "frequency": "f", "synset": "bread.n.01"}, - {"name": "breechcloth", "id": 151, "frequency": "r", "synset": "breechcloth.n.01"}, - {"name": "bridal_gown", "id": 152, "frequency": "f", "synset": "bridal_gown.n.01"}, - {"name": "briefcase", "id": 153, "frequency": "c", "synset": "briefcase.n.01"}, - {"name": "broccoli", "id": 154, "frequency": "f", "synset": "broccoli.n.01"}, - {"name": "broach", "id": 155, "frequency": "r", "synset": "brooch.n.01"}, - {"name": "broom", "id": 156, "frequency": "c", "synset": "broom.n.01"}, - {"name": "brownie", "id": 157, "frequency": "c", "synset": "brownie.n.03"}, - {"name": "brussels_sprouts", "id": 158, "frequency": "c", "synset": "brussels_sprouts.n.01"}, - {"name": "bubble_gum", "id": 159, "frequency": "r", "synset": "bubble_gum.n.01"}, - {"name": "bucket", "id": 160, "frequency": "f", "synset": "bucket.n.01"}, - {"name": "horse_buggy", "id": 161, "frequency": "r", "synset": "buggy.n.01"}, - {"name": "bull", "id": 162, "frequency": "c", "synset": "bull.n.11"}, - {"name": "bulldog", "id": 163, "frequency": "c", "synset": "bulldog.n.01"}, - {"name": "bulldozer", "id": 164, "frequency": "r", "synset": "bulldozer.n.01"}, - {"name": "bullet_train", "id": 165, "frequency": "c", "synset": "bullet_train.n.01"}, - {"name": "bulletin_board", "id": 166, "frequency": "c", "synset": "bulletin_board.n.02"}, - {"name": "bulletproof_vest", "id": 167, "frequency": "r", "synset": "bulletproof_vest.n.01"}, - {"name": "bullhorn", "id": 168, "frequency": "c", "synset": "bullhorn.n.01"}, - {"name": "bun", "id": 169, "frequency": "f", "synset": "bun.n.01"}, - {"name": "bunk_bed", "id": 170, "frequency": "c", "synset": "bunk_bed.n.01"}, - {"name": "buoy", "id": 171, "frequency": "f", "synset": "buoy.n.01"}, - {"name": "burrito", "id": 172, "frequency": "r", "synset": "burrito.n.01"}, - {"name": "bus_(vehicle)", "id": 173, "frequency": "f", "synset": "bus.n.01"}, - {"name": "business_card", "id": 174, "frequency": "c", "synset": "business_card.n.01"}, - {"name": "butter", "id": 175, "frequency": "f", "synset": "butter.n.01"}, - {"name": "butterfly", "id": 176, "frequency": "c", "synset": "butterfly.n.01"}, - {"name": "button", "id": 177, "frequency": "f", "synset": "button.n.01"}, - {"name": "cab_(taxi)", "id": 178, "frequency": "f", "synset": "cab.n.03"}, - {"name": "cabana", "id": 179, "frequency": "r", "synset": "cabana.n.01"}, - {"name": "cabin_car", "id": 180, "frequency": "c", "synset": "cabin_car.n.01"}, - {"name": "cabinet", "id": 181, "frequency": "f", "synset": "cabinet.n.01"}, - {"name": "locker", "id": 182, "frequency": "r", "synset": "cabinet.n.03"}, - {"name": "cake", "id": 183, "frequency": "f", "synset": "cake.n.03"}, - {"name": "calculator", "id": 184, "frequency": "c", "synset": "calculator.n.02"}, - {"name": "calendar", "id": 185, "frequency": "f", "synset": "calendar.n.02"}, - {"name": "calf", "id": 186, "frequency": "c", "synset": "calf.n.01"}, - {"name": "camcorder", "id": 187, "frequency": "c", "synset": "camcorder.n.01"}, - {"name": "camel", "id": 188, "frequency": "c", "synset": "camel.n.01"}, - {"name": "camera", "id": 189, "frequency": "f", "synset": "camera.n.01"}, - {"name": "camera_lens", "id": 190, "frequency": "c", "synset": "camera_lens.n.01"}, - {"name": "camper_(vehicle)", "id": 191, "frequency": "c", "synset": "camper.n.02"}, - {"name": "can", "id": 192, "frequency": "f", "synset": "can.n.01"}, - {"name": "can_opener", "id": 193, "frequency": "c", "synset": "can_opener.n.01"}, - {"name": "candle", "id": 194, "frequency": "f", "synset": "candle.n.01"}, - {"name": "candle_holder", "id": 195, "frequency": "f", "synset": "candlestick.n.01"}, - {"name": "candy_bar", "id": 196, "frequency": "r", "synset": "candy_bar.n.01"}, - {"name": "candy_cane", "id": 197, "frequency": "c", "synset": "candy_cane.n.01"}, - {"name": "walking_cane", "id": 198, "frequency": "c", "synset": "cane.n.01"}, - {"name": "canister", "id": 199, "frequency": "c", "synset": "canister.n.02"}, - {"name": "canoe", "id": 200, "frequency": "c", "synset": "canoe.n.01"}, - {"name": "cantaloup", "id": 201, "frequency": "c", "synset": "cantaloup.n.02"}, - {"name": "canteen", "id": 202, "frequency": "r", "synset": "canteen.n.01"}, - {"name": "cap_(headwear)", "id": 203, "frequency": "f", "synset": "cap.n.01"}, - {"name": "bottle_cap", "id": 204, "frequency": "f", "synset": "cap.n.02"}, - {"name": "cape", "id": 205, "frequency": "c", "synset": "cape.n.02"}, - {"name": "cappuccino", "id": 206, "frequency": "c", "synset": "cappuccino.n.01"}, - {"name": "car_(automobile)", "id": 207, "frequency": "f", "synset": "car.n.01"}, - {"name": "railcar_(part_of_a_train)", "id": 208, "frequency": "f", "synset": "car.n.02"}, - {"name": "elevator_car", "id": 209, "frequency": "r", "synset": "car.n.04"}, - {"name": "car_battery", "id": 210, "frequency": "r", "synset": "car_battery.n.01"}, - {"name": "identity_card", "id": 211, "frequency": "c", "synset": "card.n.02"}, - {"name": "card", "id": 212, "frequency": "c", "synset": "card.n.03"}, - {"name": "cardigan", "id": 213, "frequency": "c", "synset": "cardigan.n.01"}, - {"name": "cargo_ship", "id": 214, "frequency": "r", "synset": "cargo_ship.n.01"}, - {"name": "carnation", "id": 215, "frequency": "r", "synset": "carnation.n.01"}, - {"name": "horse_carriage", "id": 216, "frequency": "c", "synset": "carriage.n.02"}, - {"name": "carrot", "id": 217, "frequency": "f", "synset": "carrot.n.01"}, - {"name": "tote_bag", "id": 218, "frequency": "f", "synset": "carryall.n.01"}, - {"name": "cart", "id": 219, "frequency": "c", "synset": "cart.n.01"}, - {"name": "carton", "id": 220, "frequency": "c", "synset": "carton.n.02"}, - {"name": "cash_register", "id": 221, "frequency": "c", "synset": "cash_register.n.01"}, - {"name": "casserole", "id": 222, "frequency": "r", "synset": "casserole.n.01"}, - {"name": "cassette", "id": 223, "frequency": "r", "synset": "cassette.n.01"}, - {"name": "cast", "id": 224, "frequency": "c", "synset": "cast.n.05"}, - {"name": "cat", "id": 225, "frequency": "f", "synset": "cat.n.01"}, - {"name": "cauliflower", "id": 226, "frequency": "f", "synset": "cauliflower.n.02"}, - {"name": "cayenne_(spice)", "id": 227, "frequency": "c", "synset": "cayenne.n.02"}, - {"name": "CD_player", "id": 228, "frequency": "c", "synset": "cd_player.n.01"}, - {"name": "celery", "id": 229, "frequency": "f", "synset": "celery.n.01"}, - { - "name": "cellular_telephone", - "id": 230, - "frequency": "f", - "synset": "cellular_telephone.n.01", - }, - {"name": "chain_mail", "id": 231, "frequency": "r", "synset": "chain_mail.n.01"}, - {"name": "chair", "id": 232, "frequency": "f", "synset": "chair.n.01"}, - {"name": "chaise_longue", "id": 233, "frequency": "r", "synset": "chaise_longue.n.01"}, - {"name": "chalice", "id": 234, "frequency": "r", "synset": "chalice.n.01"}, - {"name": "chandelier", "id": 235, "frequency": "f", "synset": "chandelier.n.01"}, - {"name": "chap", "id": 236, "frequency": "r", "synset": "chap.n.04"}, - {"name": "checkbook", "id": 237, "frequency": "r", "synset": "checkbook.n.01"}, - {"name": "checkerboard", "id": 238, "frequency": "r", "synset": "checkerboard.n.01"}, - {"name": "cherry", "id": 239, "frequency": "c", "synset": "cherry.n.03"}, - {"name": "chessboard", "id": 240, "frequency": "r", "synset": "chessboard.n.01"}, - {"name": "chicken_(animal)", "id": 241, "frequency": "c", "synset": "chicken.n.02"}, - {"name": "chickpea", "id": 242, "frequency": "c", "synset": "chickpea.n.01"}, - {"name": "chili_(vegetable)", "id": 243, "frequency": "c", "synset": "chili.n.02"}, - {"name": "chime", "id": 244, "frequency": "r", "synset": "chime.n.01"}, - {"name": "chinaware", "id": 245, "frequency": "r", "synset": "chinaware.n.01"}, - {"name": "crisp_(potato_chip)", "id": 246, "frequency": "c", "synset": "chip.n.04"}, - {"name": "poker_chip", "id": 247, "frequency": "r", "synset": "chip.n.06"}, - {"name": "chocolate_bar", "id": 248, "frequency": "c", "synset": "chocolate_bar.n.01"}, - {"name": "chocolate_cake", "id": 249, "frequency": "c", "synset": "chocolate_cake.n.01"}, - {"name": "chocolate_milk", "id": 250, "frequency": "r", "synset": "chocolate_milk.n.01"}, - {"name": "chocolate_mousse", "id": 251, "frequency": "r", "synset": "chocolate_mousse.n.01"}, - {"name": "choker", "id": 252, "frequency": "f", "synset": "choker.n.03"}, - {"name": "chopping_board", "id": 253, "frequency": "f", "synset": "chopping_board.n.01"}, - {"name": "chopstick", "id": 254, "frequency": "f", "synset": "chopstick.n.01"}, - {"name": "Christmas_tree", "id": 255, "frequency": "f", "synset": "christmas_tree.n.05"}, - {"name": "slide", "id": 256, "frequency": "c", "synset": "chute.n.02"}, - {"name": "cider", "id": 257, "frequency": "r", "synset": "cider.n.01"}, - {"name": "cigar_box", "id": 258, "frequency": "r", "synset": "cigar_box.n.01"}, - {"name": "cigarette", "id": 259, "frequency": "f", "synset": "cigarette.n.01"}, - {"name": "cigarette_case", "id": 260, "frequency": "c", "synset": "cigarette_case.n.01"}, - {"name": "cistern", "id": 261, "frequency": "f", "synset": "cistern.n.02"}, - {"name": "clarinet", "id": 262, "frequency": "r", "synset": "clarinet.n.01"}, - {"name": "clasp", "id": 263, "frequency": "c", "synset": "clasp.n.01"}, - {"name": "cleansing_agent", "id": 264, "frequency": "c", "synset": "cleansing_agent.n.01"}, - {"name": "cleat_(for_securing_rope)", "id": 265, "frequency": "r", "synset": "cleat.n.02"}, - {"name": "clementine", "id": 266, "frequency": "r", "synset": "clementine.n.01"}, - {"name": "clip", "id": 267, "frequency": "c", "synset": "clip.n.03"}, - {"name": "clipboard", "id": 268, "frequency": "c", "synset": "clipboard.n.01"}, - {"name": "clippers_(for_plants)", "id": 269, "frequency": "r", "synset": "clipper.n.03"}, - {"name": "cloak", "id": 270, "frequency": "r", "synset": "cloak.n.02"}, - {"name": "clock", "id": 271, "frequency": "f", "synset": "clock.n.01"}, - {"name": "clock_tower", "id": 272, "frequency": "f", "synset": "clock_tower.n.01"}, - {"name": "clothes_hamper", "id": 273, "frequency": "c", "synset": "clothes_hamper.n.01"}, - {"name": "clothespin", "id": 274, "frequency": "c", "synset": "clothespin.n.01"}, - {"name": "clutch_bag", "id": 275, "frequency": "r", "synset": "clutch_bag.n.01"}, - {"name": "coaster", "id": 276, "frequency": "f", "synset": "coaster.n.03"}, - {"name": "coat", "id": 277, "frequency": "f", "synset": "coat.n.01"}, - {"name": "coat_hanger", "id": 278, "frequency": "c", "synset": "coat_hanger.n.01"}, - {"name": "coatrack", "id": 279, "frequency": "c", "synset": "coatrack.n.01"}, - {"name": "cock", "id": 280, "frequency": "c", "synset": "cock.n.04"}, - {"name": "cockroach", "id": 281, "frequency": "r", "synset": "cockroach.n.01"}, - {"name": "cocoa_(beverage)", "id": 282, "frequency": "r", "synset": "cocoa.n.01"}, - {"name": "coconut", "id": 283, "frequency": "c", "synset": "coconut.n.02"}, - {"name": "coffee_maker", "id": 284, "frequency": "f", "synset": "coffee_maker.n.01"}, - {"name": "coffee_table", "id": 285, "frequency": "f", "synset": "coffee_table.n.01"}, - {"name": "coffeepot", "id": 286, "frequency": "c", "synset": "coffeepot.n.01"}, - {"name": "coil", "id": 287, "frequency": "r", "synset": "coil.n.05"}, - {"name": "coin", "id": 288, "frequency": "c", "synset": "coin.n.01"}, - {"name": "colander", "id": 289, "frequency": "c", "synset": "colander.n.01"}, - {"name": "coleslaw", "id": 290, "frequency": "c", "synset": "coleslaw.n.01"}, - {"name": "coloring_material", "id": 291, "frequency": "r", "synset": "coloring_material.n.01"}, - {"name": "combination_lock", "id": 292, "frequency": "r", "synset": "combination_lock.n.01"}, - {"name": "pacifier", "id": 293, "frequency": "c", "synset": "comforter.n.04"}, - {"name": "comic_book", "id": 294, "frequency": "r", "synset": "comic_book.n.01"}, - {"name": "compass", "id": 295, "frequency": "r", "synset": "compass.n.01"}, - {"name": "computer_keyboard", "id": 296, "frequency": "f", "synset": "computer_keyboard.n.01"}, - {"name": "condiment", "id": 297, "frequency": "f", "synset": "condiment.n.01"}, - {"name": "cone", "id": 298, "frequency": "f", "synset": "cone.n.01"}, - {"name": "control", "id": 299, "frequency": "f", "synset": "control.n.09"}, - {"name": "convertible_(automobile)", "id": 300, "frequency": "r", "synset": "convertible.n.01"}, - {"name": "sofa_bed", "id": 301, "frequency": "r", "synset": "convertible.n.03"}, - {"name": "cooker", "id": 302, "frequency": "r", "synset": "cooker.n.01"}, - {"name": "cookie", "id": 303, "frequency": "f", "synset": "cookie.n.01"}, - {"name": "cooking_utensil", "id": 304, "frequency": "r", "synset": "cooking_utensil.n.01"}, - {"name": "cooler_(for_food)", "id": 305, "frequency": "f", "synset": "cooler.n.01"}, - {"name": "cork_(bottle_plug)", "id": 306, "frequency": "f", "synset": "cork.n.04"}, - {"name": "corkboard", "id": 307, "frequency": "r", "synset": "corkboard.n.01"}, - {"name": "corkscrew", "id": 308, "frequency": "c", "synset": "corkscrew.n.01"}, - {"name": "edible_corn", "id": 309, "frequency": "f", "synset": "corn.n.03"}, - {"name": "cornbread", "id": 310, "frequency": "r", "synset": "cornbread.n.01"}, - {"name": "cornet", "id": 311, "frequency": "c", "synset": "cornet.n.01"}, - {"name": "cornice", "id": 312, "frequency": "c", "synset": "cornice.n.01"}, - {"name": "cornmeal", "id": 313, "frequency": "r", "synset": "cornmeal.n.01"}, - {"name": "corset", "id": 314, "frequency": "c", "synset": "corset.n.01"}, - {"name": "costume", "id": 315, "frequency": "c", "synset": "costume.n.04"}, - {"name": "cougar", "id": 316, "frequency": "r", "synset": "cougar.n.01"}, - {"name": "coverall", "id": 317, "frequency": "r", "synset": "coverall.n.01"}, - {"name": "cowbell", "id": 318, "frequency": "c", "synset": "cowbell.n.01"}, - {"name": "cowboy_hat", "id": 319, "frequency": "f", "synset": "cowboy_hat.n.01"}, - {"name": "crab_(animal)", "id": 320, "frequency": "c", "synset": "crab.n.01"}, - {"name": "crabmeat", "id": 321, "frequency": "r", "synset": "crab.n.05"}, - {"name": "cracker", "id": 322, "frequency": "c", "synset": "cracker.n.01"}, - {"name": "crape", "id": 323, "frequency": "r", "synset": "crape.n.01"}, - {"name": "crate", "id": 324, "frequency": "f", "synset": "crate.n.01"}, - {"name": "crayon", "id": 325, "frequency": "c", "synset": "crayon.n.01"}, - {"name": "cream_pitcher", "id": 326, "frequency": "r", "synset": "cream_pitcher.n.01"}, - {"name": "crescent_roll", "id": 327, "frequency": "c", "synset": "crescent_roll.n.01"}, - {"name": "crib", "id": 328, "frequency": "c", "synset": "crib.n.01"}, - {"name": "crock_pot", "id": 329, "frequency": "c", "synset": "crock.n.03"}, - {"name": "crossbar", "id": 330, "frequency": "f", "synset": "crossbar.n.01"}, - {"name": "crouton", "id": 331, "frequency": "r", "synset": "crouton.n.01"}, - {"name": "crow", "id": 332, "frequency": "c", "synset": "crow.n.01"}, - {"name": "crowbar", "id": 333, "frequency": "r", "synset": "crowbar.n.01"}, - {"name": "crown", "id": 334, "frequency": "c", "synset": "crown.n.04"}, - {"name": "crucifix", "id": 335, "frequency": "c", "synset": "crucifix.n.01"}, - {"name": "cruise_ship", "id": 336, "frequency": "c", "synset": "cruise_ship.n.01"}, - {"name": "police_cruiser", "id": 337, "frequency": "c", "synset": "cruiser.n.01"}, - {"name": "crumb", "id": 338, "frequency": "f", "synset": "crumb.n.03"}, - {"name": "crutch", "id": 339, "frequency": "c", "synset": "crutch.n.01"}, - {"name": "cub_(animal)", "id": 340, "frequency": "c", "synset": "cub.n.03"}, - {"name": "cube", "id": 341, "frequency": "c", "synset": "cube.n.05"}, - {"name": "cucumber", "id": 342, "frequency": "f", "synset": "cucumber.n.02"}, - {"name": "cufflink", "id": 343, "frequency": "c", "synset": "cufflink.n.01"}, - {"name": "cup", "id": 344, "frequency": "f", "synset": "cup.n.01"}, - {"name": "trophy_cup", "id": 345, "frequency": "c", "synset": "cup.n.08"}, - {"name": "cupboard", "id": 346, "frequency": "f", "synset": "cupboard.n.01"}, - {"name": "cupcake", "id": 347, "frequency": "f", "synset": "cupcake.n.01"}, - {"name": "hair_curler", "id": 348, "frequency": "r", "synset": "curler.n.01"}, - {"name": "curling_iron", "id": 349, "frequency": "r", "synset": "curling_iron.n.01"}, - {"name": "curtain", "id": 350, "frequency": "f", "synset": "curtain.n.01"}, - {"name": "cushion", "id": 351, "frequency": "f", "synset": "cushion.n.03"}, - {"name": "cylinder", "id": 352, "frequency": "r", "synset": "cylinder.n.04"}, - {"name": "cymbal", "id": 353, "frequency": "r", "synset": "cymbal.n.01"}, - {"name": "dagger", "id": 354, "frequency": "r", "synset": "dagger.n.01"}, - {"name": "dalmatian", "id": 355, "frequency": "r", "synset": "dalmatian.n.02"}, - {"name": "dartboard", "id": 356, "frequency": "c", "synset": "dartboard.n.01"}, - {"name": "date_(fruit)", "id": 357, "frequency": "r", "synset": "date.n.08"}, - {"name": "deck_chair", "id": 358, "frequency": "f", "synset": "deck_chair.n.01"}, - {"name": "deer", "id": 359, "frequency": "c", "synset": "deer.n.01"}, - {"name": "dental_floss", "id": 360, "frequency": "c", "synset": "dental_floss.n.01"}, - {"name": "desk", "id": 361, "frequency": "f", "synset": "desk.n.01"}, - {"name": "detergent", "id": 362, "frequency": "r", "synset": "detergent.n.01"}, - {"name": "diaper", "id": 363, "frequency": "c", "synset": "diaper.n.01"}, - {"name": "diary", "id": 364, "frequency": "r", "synset": "diary.n.01"}, - {"name": "die", "id": 365, "frequency": "r", "synset": "die.n.01"}, - {"name": "dinghy", "id": 366, "frequency": "r", "synset": "dinghy.n.01"}, - {"name": "dining_table", "id": 367, "frequency": "f", "synset": "dining_table.n.01"}, - {"name": "tux", "id": 368, "frequency": "r", "synset": "dinner_jacket.n.01"}, - {"name": "dish", "id": 369, "frequency": "f", "synset": "dish.n.01"}, - {"name": "dish_antenna", "id": 370, "frequency": "c", "synset": "dish.n.05"}, - {"name": "dishrag", "id": 371, "frequency": "c", "synset": "dishrag.n.01"}, - {"name": "dishtowel", "id": 372, "frequency": "f", "synset": "dishtowel.n.01"}, - {"name": "dishwasher", "id": 373, "frequency": "f", "synset": "dishwasher.n.01"}, - { - "name": "dishwasher_detergent", - "id": 374, - "frequency": "r", - "synset": "dishwasher_detergent.n.01", - }, - {"name": "dispenser", "id": 375, "frequency": "f", "synset": "dispenser.n.01"}, - {"name": "diving_board", "id": 376, "frequency": "r", "synset": "diving_board.n.01"}, - {"name": "Dixie_cup", "id": 377, "frequency": "f", "synset": "dixie_cup.n.01"}, - {"name": "dog", "id": 378, "frequency": "f", "synset": "dog.n.01"}, - {"name": "dog_collar", "id": 379, "frequency": "f", "synset": "dog_collar.n.01"}, - {"name": "doll", "id": 380, "frequency": "f", "synset": "doll.n.01"}, - {"name": "dollar", "id": 381, "frequency": "r", "synset": "dollar.n.02"}, - {"name": "dollhouse", "id": 382, "frequency": "r", "synset": "dollhouse.n.01"}, - {"name": "dolphin", "id": 383, "frequency": "c", "synset": "dolphin.n.02"}, - {"name": "domestic_ass", "id": 384, "frequency": "c", "synset": "domestic_ass.n.01"}, - {"name": "doorknob", "id": 385, "frequency": "f", "synset": "doorknob.n.01"}, - {"name": "doormat", "id": 386, "frequency": "c", "synset": "doormat.n.02"}, - {"name": "doughnut", "id": 387, "frequency": "f", "synset": "doughnut.n.02"}, - {"name": "dove", "id": 388, "frequency": "r", "synset": "dove.n.01"}, - {"name": "dragonfly", "id": 389, "frequency": "r", "synset": "dragonfly.n.01"}, - {"name": "drawer", "id": 390, "frequency": "f", "synset": "drawer.n.01"}, - {"name": "underdrawers", "id": 391, "frequency": "c", "synset": "drawers.n.01"}, - {"name": "dress", "id": 392, "frequency": "f", "synset": "dress.n.01"}, - {"name": "dress_hat", "id": 393, "frequency": "c", "synset": "dress_hat.n.01"}, - {"name": "dress_suit", "id": 394, "frequency": "f", "synset": "dress_suit.n.01"}, - {"name": "dresser", "id": 395, "frequency": "f", "synset": "dresser.n.05"}, - {"name": "drill", "id": 396, "frequency": "c", "synset": "drill.n.01"}, - {"name": "drone", "id": 397, "frequency": "r", "synset": "drone.n.04"}, - {"name": "dropper", "id": 398, "frequency": "r", "synset": "dropper.n.01"}, - {"name": "drum_(musical_instrument)", "id": 399, "frequency": "c", "synset": "drum.n.01"}, - {"name": "drumstick", "id": 400, "frequency": "r", "synset": "drumstick.n.02"}, - {"name": "duck", "id": 401, "frequency": "f", "synset": "duck.n.01"}, - {"name": "duckling", "id": 402, "frequency": "c", "synset": "duckling.n.02"}, - {"name": "duct_tape", "id": 403, "frequency": "c", "synset": "duct_tape.n.01"}, - {"name": "duffel_bag", "id": 404, "frequency": "f", "synset": "duffel_bag.n.01"}, - {"name": "dumbbell", "id": 405, "frequency": "r", "synset": "dumbbell.n.01"}, - {"name": "dumpster", "id": 406, "frequency": "c", "synset": "dumpster.n.01"}, - {"name": "dustpan", "id": 407, "frequency": "r", "synset": "dustpan.n.02"}, - {"name": "eagle", "id": 408, "frequency": "c", "synset": "eagle.n.01"}, - {"name": "earphone", "id": 409, "frequency": "f", "synset": "earphone.n.01"}, - {"name": "earplug", "id": 410, "frequency": "r", "synset": "earplug.n.01"}, - {"name": "earring", "id": 411, "frequency": "f", "synset": "earring.n.01"}, - {"name": "easel", "id": 412, "frequency": "c", "synset": "easel.n.01"}, - {"name": "eclair", "id": 413, "frequency": "r", "synset": "eclair.n.01"}, - {"name": "eel", "id": 414, "frequency": "r", "synset": "eel.n.01"}, - {"name": "egg", "id": 415, "frequency": "f", "synset": "egg.n.02"}, - {"name": "egg_roll", "id": 416, "frequency": "r", "synset": "egg_roll.n.01"}, - {"name": "egg_yolk", "id": 417, "frequency": "c", "synset": "egg_yolk.n.01"}, - {"name": "eggbeater", "id": 418, "frequency": "c", "synset": "eggbeater.n.02"}, - {"name": "eggplant", "id": 419, "frequency": "c", "synset": "eggplant.n.01"}, - {"name": "electric_chair", "id": 420, "frequency": "r", "synset": "electric_chair.n.01"}, - {"name": "refrigerator", "id": 421, "frequency": "f", "synset": "electric_refrigerator.n.01"}, - {"name": "elephant", "id": 422, "frequency": "f", "synset": "elephant.n.01"}, - {"name": "elk", "id": 423, "frequency": "c", "synset": "elk.n.01"}, - {"name": "envelope", "id": 424, "frequency": "c", "synset": "envelope.n.01"}, - {"name": "eraser", "id": 425, "frequency": "c", "synset": "eraser.n.01"}, - {"name": "escargot", "id": 426, "frequency": "r", "synset": "escargot.n.01"}, - {"name": "eyepatch", "id": 427, "frequency": "r", "synset": "eyepatch.n.01"}, - {"name": "falcon", "id": 428, "frequency": "r", "synset": "falcon.n.01"}, - {"name": "fan", "id": 429, "frequency": "f", "synset": "fan.n.01"}, - {"name": "faucet", "id": 430, "frequency": "f", "synset": "faucet.n.01"}, - {"name": "fedora", "id": 431, "frequency": "r", "synset": "fedora.n.01"}, - {"name": "ferret", "id": 432, "frequency": "r", "synset": "ferret.n.02"}, - {"name": "Ferris_wheel", "id": 433, "frequency": "c", "synset": "ferris_wheel.n.01"}, - {"name": "ferry", "id": 434, "frequency": "c", "synset": "ferry.n.01"}, - {"name": "fig_(fruit)", "id": 435, "frequency": "r", "synset": "fig.n.04"}, - {"name": "fighter_jet", "id": 436, "frequency": "c", "synset": "fighter.n.02"}, - {"name": "figurine", "id": 437, "frequency": "f", "synset": "figurine.n.01"}, - {"name": "file_cabinet", "id": 438, "frequency": "c", "synset": "file.n.03"}, - {"name": "file_(tool)", "id": 439, "frequency": "r", "synset": "file.n.04"}, - {"name": "fire_alarm", "id": 440, "frequency": "f", "synset": "fire_alarm.n.02"}, - {"name": "fire_engine", "id": 441, "frequency": "f", "synset": "fire_engine.n.01"}, - {"name": "fire_extinguisher", "id": 442, "frequency": "f", "synset": "fire_extinguisher.n.01"}, - {"name": "fire_hose", "id": 443, "frequency": "c", "synset": "fire_hose.n.01"}, - {"name": "fireplace", "id": 444, "frequency": "f", "synset": "fireplace.n.01"}, - {"name": "fireplug", "id": 445, "frequency": "f", "synset": "fireplug.n.01"}, - {"name": "first-aid_kit", "id": 446, "frequency": "r", "synset": "first-aid_kit.n.01"}, - {"name": "fish", "id": 447, "frequency": "f", "synset": "fish.n.01"}, - {"name": "fish_(food)", "id": 448, "frequency": "c", "synset": "fish.n.02"}, - {"name": "fishbowl", "id": 449, "frequency": "r", "synset": "fishbowl.n.02"}, - {"name": "fishing_rod", "id": 450, "frequency": "c", "synset": "fishing_rod.n.01"}, - {"name": "flag", "id": 451, "frequency": "f", "synset": "flag.n.01"}, - {"name": "flagpole", "id": 452, "frequency": "f", "synset": "flagpole.n.02"}, - {"name": "flamingo", "id": 453, "frequency": "c", "synset": "flamingo.n.01"}, - {"name": "flannel", "id": 454, "frequency": "c", "synset": "flannel.n.01"}, - {"name": "flap", "id": 455, "frequency": "c", "synset": "flap.n.01"}, - {"name": "flash", "id": 456, "frequency": "r", "synset": "flash.n.10"}, - {"name": "flashlight", "id": 457, "frequency": "c", "synset": "flashlight.n.01"}, - {"name": "fleece", "id": 458, "frequency": "r", "synset": "fleece.n.03"}, - {"name": "flip-flop_(sandal)", "id": 459, "frequency": "f", "synset": "flip-flop.n.02"}, - {"name": "flipper_(footwear)", "id": 460, "frequency": "c", "synset": "flipper.n.01"}, - { - "name": "flower_arrangement", - "id": 461, - "frequency": "f", - "synset": "flower_arrangement.n.01", - }, - {"name": "flute_glass", "id": 462, "frequency": "c", "synset": "flute.n.02"}, - {"name": "foal", "id": 463, "frequency": "c", "synset": "foal.n.01"}, - {"name": "folding_chair", "id": 464, "frequency": "c", "synset": "folding_chair.n.01"}, - {"name": "food_processor", "id": 465, "frequency": "c", "synset": "food_processor.n.01"}, - {"name": "football_(American)", "id": 466, "frequency": "c", "synset": "football.n.02"}, - {"name": "football_helmet", "id": 467, "frequency": "r", "synset": "football_helmet.n.01"}, - {"name": "footstool", "id": 468, "frequency": "c", "synset": "footstool.n.01"}, - {"name": "fork", "id": 469, "frequency": "f", "synset": "fork.n.01"}, - {"name": "forklift", "id": 470, "frequency": "c", "synset": "forklift.n.01"}, - {"name": "freight_car", "id": 471, "frequency": "c", "synset": "freight_car.n.01"}, - {"name": "French_toast", "id": 472, "frequency": "c", "synset": "french_toast.n.01"}, - {"name": "freshener", "id": 473, "frequency": "c", "synset": "freshener.n.01"}, - {"name": "frisbee", "id": 474, "frequency": "f", "synset": "frisbee.n.01"}, - {"name": "frog", "id": 475, "frequency": "c", "synset": "frog.n.01"}, - {"name": "fruit_juice", "id": 476, "frequency": "c", "synset": "fruit_juice.n.01"}, - {"name": "frying_pan", "id": 477, "frequency": "f", "synset": "frying_pan.n.01"}, - {"name": "fudge", "id": 478, "frequency": "r", "synset": "fudge.n.01"}, - {"name": "funnel", "id": 479, "frequency": "r", "synset": "funnel.n.02"}, - {"name": "futon", "id": 480, "frequency": "r", "synset": "futon.n.01"}, - {"name": "gag", "id": 481, "frequency": "r", "synset": "gag.n.02"}, - {"name": "garbage", "id": 482, "frequency": "r", "synset": "garbage.n.03"}, - {"name": "garbage_truck", "id": 483, "frequency": "c", "synset": "garbage_truck.n.01"}, - {"name": "garden_hose", "id": 484, "frequency": "c", "synset": "garden_hose.n.01"}, - {"name": "gargle", "id": 485, "frequency": "c", "synset": "gargle.n.01"}, - {"name": "gargoyle", "id": 486, "frequency": "r", "synset": "gargoyle.n.02"}, - {"name": "garlic", "id": 487, "frequency": "c", "synset": "garlic.n.02"}, - {"name": "gasmask", "id": 488, "frequency": "r", "synset": "gasmask.n.01"}, - {"name": "gazelle", "id": 489, "frequency": "c", "synset": "gazelle.n.01"}, - {"name": "gelatin", "id": 490, "frequency": "c", "synset": "gelatin.n.02"}, - {"name": "gemstone", "id": 491, "frequency": "r", "synset": "gem.n.02"}, - {"name": "generator", "id": 492, "frequency": "r", "synset": "generator.n.02"}, - {"name": "giant_panda", "id": 493, "frequency": "c", "synset": "giant_panda.n.01"}, - {"name": "gift_wrap", "id": 494, "frequency": "c", "synset": "gift_wrap.n.01"}, - {"name": "ginger", "id": 495, "frequency": "c", "synset": "ginger.n.03"}, - {"name": "giraffe", "id": 496, "frequency": "f", "synset": "giraffe.n.01"}, - {"name": "cincture", "id": 497, "frequency": "c", "synset": "girdle.n.02"}, - {"name": "glass_(drink_container)", "id": 498, "frequency": "f", "synset": "glass.n.02"}, - {"name": "globe", "id": 499, "frequency": "c", "synset": "globe.n.03"}, - {"name": "glove", "id": 500, "frequency": "f", "synset": "glove.n.02"}, - {"name": "goat", "id": 501, "frequency": "c", "synset": "goat.n.01"}, - {"name": "goggles", "id": 502, "frequency": "f", "synset": "goggles.n.01"}, - {"name": "goldfish", "id": 503, "frequency": "r", "synset": "goldfish.n.01"}, - {"name": "golf_club", "id": 504, "frequency": "c", "synset": "golf_club.n.02"}, - {"name": "golfcart", "id": 505, "frequency": "c", "synset": "golfcart.n.01"}, - {"name": "gondola_(boat)", "id": 506, "frequency": "r", "synset": "gondola.n.02"}, - {"name": "goose", "id": 507, "frequency": "c", "synset": "goose.n.01"}, - {"name": "gorilla", "id": 508, "frequency": "r", "synset": "gorilla.n.01"}, - {"name": "gourd", "id": 509, "frequency": "r", "synset": "gourd.n.02"}, - {"name": "grape", "id": 510, "frequency": "f", "synset": "grape.n.01"}, - {"name": "grater", "id": 511, "frequency": "c", "synset": "grater.n.01"}, - {"name": "gravestone", "id": 512, "frequency": "c", "synset": "gravestone.n.01"}, - {"name": "gravy_boat", "id": 513, "frequency": "r", "synset": "gravy_boat.n.01"}, - {"name": "green_bean", "id": 514, "frequency": "f", "synset": "green_bean.n.02"}, - {"name": "green_onion", "id": 515, "frequency": "f", "synset": "green_onion.n.01"}, - {"name": "griddle", "id": 516, "frequency": "r", "synset": "griddle.n.01"}, - {"name": "grill", "id": 517, "frequency": "f", "synset": "grill.n.02"}, - {"name": "grits", "id": 518, "frequency": "r", "synset": "grits.n.01"}, - {"name": "grizzly", "id": 519, "frequency": "c", "synset": "grizzly.n.01"}, - {"name": "grocery_bag", "id": 520, "frequency": "c", "synset": "grocery_bag.n.01"}, - {"name": "guitar", "id": 521, "frequency": "f", "synset": "guitar.n.01"}, - {"name": "gull", "id": 522, "frequency": "c", "synset": "gull.n.02"}, - {"name": "gun", "id": 523, "frequency": "c", "synset": "gun.n.01"}, - {"name": "hairbrush", "id": 524, "frequency": "f", "synset": "hairbrush.n.01"}, - {"name": "hairnet", "id": 525, "frequency": "c", "synset": "hairnet.n.01"}, - {"name": "hairpin", "id": 526, "frequency": "c", "synset": "hairpin.n.01"}, - {"name": "halter_top", "id": 527, "frequency": "r", "synset": "halter.n.03"}, - {"name": "ham", "id": 528, "frequency": "f", "synset": "ham.n.01"}, - {"name": "hamburger", "id": 529, "frequency": "c", "synset": "hamburger.n.01"}, - {"name": "hammer", "id": 530, "frequency": "c", "synset": "hammer.n.02"}, - {"name": "hammock", "id": 531, "frequency": "c", "synset": "hammock.n.02"}, - {"name": "hamper", "id": 532, "frequency": "r", "synset": "hamper.n.02"}, - {"name": "hamster", "id": 533, "frequency": "c", "synset": "hamster.n.01"}, - {"name": "hair_dryer", "id": 534, "frequency": "f", "synset": "hand_blower.n.01"}, - {"name": "hand_glass", "id": 535, "frequency": "r", "synset": "hand_glass.n.01"}, - {"name": "hand_towel", "id": 536, "frequency": "f", "synset": "hand_towel.n.01"}, - {"name": "handcart", "id": 537, "frequency": "c", "synset": "handcart.n.01"}, - {"name": "handcuff", "id": 538, "frequency": "r", "synset": "handcuff.n.01"}, - {"name": "handkerchief", "id": 539, "frequency": "c", "synset": "handkerchief.n.01"}, - {"name": "handle", "id": 540, "frequency": "f", "synset": "handle.n.01"}, - {"name": "handsaw", "id": 541, "frequency": "r", "synset": "handsaw.n.01"}, - {"name": "hardback_book", "id": 542, "frequency": "r", "synset": "hardback.n.01"}, - {"name": "harmonium", "id": 543, "frequency": "r", "synset": "harmonium.n.01"}, - {"name": "hat", "id": 544, "frequency": "f", "synset": "hat.n.01"}, - {"name": "hatbox", "id": 545, "frequency": "r", "synset": "hatbox.n.01"}, - {"name": "veil", "id": 546, "frequency": "c", "synset": "head_covering.n.01"}, - {"name": "headband", "id": 547, "frequency": "f", "synset": "headband.n.01"}, - {"name": "headboard", "id": 548, "frequency": "f", "synset": "headboard.n.01"}, - {"name": "headlight", "id": 549, "frequency": "f", "synset": "headlight.n.01"}, - {"name": "headscarf", "id": 550, "frequency": "c", "synset": "headscarf.n.01"}, - {"name": "headset", "id": 551, "frequency": "r", "synset": "headset.n.01"}, - {"name": "headstall_(for_horses)", "id": 552, "frequency": "c", "synset": "headstall.n.01"}, - {"name": "heart", "id": 553, "frequency": "c", "synset": "heart.n.02"}, - {"name": "heater", "id": 554, "frequency": "c", "synset": "heater.n.01"}, - {"name": "helicopter", "id": 555, "frequency": "c", "synset": "helicopter.n.01"}, - {"name": "helmet", "id": 556, "frequency": "f", "synset": "helmet.n.02"}, - {"name": "heron", "id": 557, "frequency": "r", "synset": "heron.n.02"}, - {"name": "highchair", "id": 558, "frequency": "c", "synset": "highchair.n.01"}, - {"name": "hinge", "id": 559, "frequency": "f", "synset": "hinge.n.01"}, - {"name": "hippopotamus", "id": 560, "frequency": "r", "synset": "hippopotamus.n.01"}, - {"name": "hockey_stick", "id": 561, "frequency": "r", "synset": "hockey_stick.n.01"}, - {"name": "hog", "id": 562, "frequency": "c", "synset": "hog.n.03"}, - {"name": "home_plate_(baseball)", "id": 563, "frequency": "f", "synset": "home_plate.n.01"}, - {"name": "honey", "id": 564, "frequency": "c", "synset": "honey.n.01"}, - {"name": "fume_hood", "id": 565, "frequency": "f", "synset": "hood.n.06"}, - {"name": "hook", "id": 566, "frequency": "f", "synset": "hook.n.05"}, - {"name": "hookah", "id": 567, "frequency": "r", "synset": "hookah.n.01"}, - {"name": "hornet", "id": 568, "frequency": "r", "synset": "hornet.n.01"}, - {"name": "horse", "id": 569, "frequency": "f", "synset": "horse.n.01"}, - {"name": "hose", "id": 570, "frequency": "f", "synset": "hose.n.03"}, - {"name": "hot-air_balloon", "id": 571, "frequency": "r", "synset": "hot-air_balloon.n.01"}, - {"name": "hotplate", "id": 572, "frequency": "r", "synset": "hot_plate.n.01"}, - {"name": "hot_sauce", "id": 573, "frequency": "c", "synset": "hot_sauce.n.01"}, - {"name": "hourglass", "id": 574, "frequency": "r", "synset": "hourglass.n.01"}, - {"name": "houseboat", "id": 575, "frequency": "r", "synset": "houseboat.n.01"}, - {"name": "hummingbird", "id": 576, "frequency": "c", "synset": "hummingbird.n.01"}, - {"name": "hummus", "id": 577, "frequency": "r", "synset": "hummus.n.01"}, - {"name": "polar_bear", "id": 578, "frequency": "f", "synset": "ice_bear.n.01"}, - {"name": "icecream", "id": 579, "frequency": "c", "synset": "ice_cream.n.01"}, - {"name": "popsicle", "id": 580, "frequency": "r", "synset": "ice_lolly.n.01"}, - {"name": "ice_maker", "id": 581, "frequency": "c", "synset": "ice_maker.n.01"}, - {"name": "ice_pack", "id": 582, "frequency": "r", "synset": "ice_pack.n.01"}, - {"name": "ice_skate", "id": 583, "frequency": "r", "synset": "ice_skate.n.01"}, - {"name": "igniter", "id": 584, "frequency": "c", "synset": "igniter.n.01"}, - {"name": "inhaler", "id": 585, "frequency": "r", "synset": "inhaler.n.01"}, - {"name": "iPod", "id": 586, "frequency": "f", "synset": "ipod.n.01"}, - {"name": "iron_(for_clothing)", "id": 587, "frequency": "c", "synset": "iron.n.04"}, - {"name": "ironing_board", "id": 588, "frequency": "c", "synset": "ironing_board.n.01"}, - {"name": "jacket", "id": 589, "frequency": "f", "synset": "jacket.n.01"}, - {"name": "jam", "id": 590, "frequency": "c", "synset": "jam.n.01"}, - {"name": "jar", "id": 591, "frequency": "f", "synset": "jar.n.01"}, - {"name": "jean", "id": 592, "frequency": "f", "synset": "jean.n.01"}, - {"name": "jeep", "id": 593, "frequency": "c", "synset": "jeep.n.01"}, - {"name": "jelly_bean", "id": 594, "frequency": "r", "synset": "jelly_bean.n.01"}, - {"name": "jersey", "id": 595, "frequency": "f", "synset": "jersey.n.03"}, - {"name": "jet_plane", "id": 596, "frequency": "c", "synset": "jet.n.01"}, - {"name": "jewel", "id": 597, "frequency": "r", "synset": "jewel.n.01"}, - {"name": "jewelry", "id": 598, "frequency": "c", "synset": "jewelry.n.01"}, - {"name": "joystick", "id": 599, "frequency": "r", "synset": "joystick.n.02"}, - {"name": "jumpsuit", "id": 600, "frequency": "c", "synset": "jump_suit.n.01"}, - {"name": "kayak", "id": 601, "frequency": "c", "synset": "kayak.n.01"}, - {"name": "keg", "id": 602, "frequency": "r", "synset": "keg.n.02"}, - {"name": "kennel", "id": 603, "frequency": "r", "synset": "kennel.n.01"}, - {"name": "kettle", "id": 604, "frequency": "c", "synset": "kettle.n.01"}, - {"name": "key", "id": 605, "frequency": "f", "synset": "key.n.01"}, - {"name": "keycard", "id": 606, "frequency": "r", "synset": "keycard.n.01"}, - {"name": "kilt", "id": 607, "frequency": "c", "synset": "kilt.n.01"}, - {"name": "kimono", "id": 608, "frequency": "c", "synset": "kimono.n.01"}, - {"name": "kitchen_sink", "id": 609, "frequency": "f", "synset": "kitchen_sink.n.01"}, - {"name": "kitchen_table", "id": 610, "frequency": "r", "synset": "kitchen_table.n.01"}, - {"name": "kite", "id": 611, "frequency": "f", "synset": "kite.n.03"}, - {"name": "kitten", "id": 612, "frequency": "c", "synset": "kitten.n.01"}, - {"name": "kiwi_fruit", "id": 613, "frequency": "c", "synset": "kiwi.n.03"}, - {"name": "knee_pad", "id": 614, "frequency": "f", "synset": "knee_pad.n.01"}, - {"name": "knife", "id": 615, "frequency": "f", "synset": "knife.n.01"}, - {"name": "knitting_needle", "id": 616, "frequency": "r", "synset": "knitting_needle.n.01"}, - {"name": "knob", "id": 617, "frequency": "f", "synset": "knob.n.02"}, - {"name": "knocker_(on_a_door)", "id": 618, "frequency": "r", "synset": "knocker.n.05"}, - {"name": "koala", "id": 619, "frequency": "r", "synset": "koala.n.01"}, - {"name": "lab_coat", "id": 620, "frequency": "r", "synset": "lab_coat.n.01"}, - {"name": "ladder", "id": 621, "frequency": "f", "synset": "ladder.n.01"}, - {"name": "ladle", "id": 622, "frequency": "c", "synset": "ladle.n.01"}, - {"name": "ladybug", "id": 623, "frequency": "c", "synset": "ladybug.n.01"}, - {"name": "lamb_(animal)", "id": 624, "frequency": "f", "synset": "lamb.n.01"}, - {"name": "lamb-chop", "id": 625, "frequency": "r", "synset": "lamb_chop.n.01"}, - {"name": "lamp", "id": 626, "frequency": "f", "synset": "lamp.n.02"}, - {"name": "lamppost", "id": 627, "frequency": "f", "synset": "lamppost.n.01"}, - {"name": "lampshade", "id": 628, "frequency": "f", "synset": "lampshade.n.01"}, - {"name": "lantern", "id": 629, "frequency": "c", "synset": "lantern.n.01"}, - {"name": "lanyard", "id": 630, "frequency": "f", "synset": "lanyard.n.02"}, - {"name": "laptop_computer", "id": 631, "frequency": "f", "synset": "laptop.n.01"}, - {"name": "lasagna", "id": 632, "frequency": "r", "synset": "lasagna.n.01"}, - {"name": "latch", "id": 633, "frequency": "f", "synset": "latch.n.02"}, - {"name": "lawn_mower", "id": 634, "frequency": "r", "synset": "lawn_mower.n.01"}, - {"name": "leather", "id": 635, "frequency": "r", "synset": "leather.n.01"}, - {"name": "legging_(clothing)", "id": 636, "frequency": "c", "synset": "legging.n.01"}, - {"name": "Lego", "id": 637, "frequency": "c", "synset": "lego.n.01"}, - {"name": "legume", "id": 638, "frequency": "r", "synset": "legume.n.02"}, - {"name": "lemon", "id": 639, "frequency": "f", "synset": "lemon.n.01"}, - {"name": "lemonade", "id": 640, "frequency": "r", "synset": "lemonade.n.01"}, - {"name": "lettuce", "id": 641, "frequency": "f", "synset": "lettuce.n.02"}, - {"name": "license_plate", "id": 642, "frequency": "f", "synset": "license_plate.n.01"}, - {"name": "life_buoy", "id": 643, "frequency": "f", "synset": "life_buoy.n.01"}, - {"name": "life_jacket", "id": 644, "frequency": "f", "synset": "life_jacket.n.01"}, - {"name": "lightbulb", "id": 645, "frequency": "f", "synset": "light_bulb.n.01"}, - {"name": "lightning_rod", "id": 646, "frequency": "r", "synset": "lightning_rod.n.02"}, - {"name": "lime", "id": 647, "frequency": "f", "synset": "lime.n.06"}, - {"name": "limousine", "id": 648, "frequency": "r", "synset": "limousine.n.01"}, - {"name": "lion", "id": 649, "frequency": "c", "synset": "lion.n.01"}, - {"name": "lip_balm", "id": 650, "frequency": "c", "synset": "lip_balm.n.01"}, - {"name": "liquor", "id": 651, "frequency": "r", "synset": "liquor.n.01"}, - {"name": "lizard", "id": 652, "frequency": "c", "synset": "lizard.n.01"}, - {"name": "log", "id": 653, "frequency": "f", "synset": "log.n.01"}, - {"name": "lollipop", "id": 654, "frequency": "c", "synset": "lollipop.n.02"}, - { - "name": "speaker_(stero_equipment)", - "id": 655, - "frequency": "f", - "synset": "loudspeaker.n.01", - }, - {"name": "loveseat", "id": 656, "frequency": "c", "synset": "love_seat.n.01"}, - {"name": "machine_gun", "id": 657, "frequency": "r", "synset": "machine_gun.n.01"}, - {"name": "magazine", "id": 658, "frequency": "f", "synset": "magazine.n.02"}, - {"name": "magnet", "id": 659, "frequency": "f", "synset": "magnet.n.01"}, - {"name": "mail_slot", "id": 660, "frequency": "c", "synset": "mail_slot.n.01"}, - {"name": "mailbox_(at_home)", "id": 661, "frequency": "f", "synset": "mailbox.n.01"}, - {"name": "mallard", "id": 662, "frequency": "r", "synset": "mallard.n.01"}, - {"name": "mallet", "id": 663, "frequency": "r", "synset": "mallet.n.01"}, - {"name": "mammoth", "id": 664, "frequency": "r", "synset": "mammoth.n.01"}, - {"name": "manatee", "id": 665, "frequency": "r", "synset": "manatee.n.01"}, - {"name": "mandarin_orange", "id": 666, "frequency": "c", "synset": "mandarin.n.05"}, - {"name": "manger", "id": 667, "frequency": "c", "synset": "manger.n.01"}, - {"name": "manhole", "id": 668, "frequency": "f", "synset": "manhole.n.01"}, - {"name": "map", "id": 669, "frequency": "f", "synset": "map.n.01"}, - {"name": "marker", "id": 670, "frequency": "f", "synset": "marker.n.03"}, - {"name": "martini", "id": 671, "frequency": "r", "synset": "martini.n.01"}, - {"name": "mascot", "id": 672, "frequency": "r", "synset": "mascot.n.01"}, - {"name": "mashed_potato", "id": 673, "frequency": "c", "synset": "mashed_potato.n.01"}, - {"name": "masher", "id": 674, "frequency": "r", "synset": "masher.n.02"}, - {"name": "mask", "id": 675, "frequency": "f", "synset": "mask.n.04"}, - {"name": "mast", "id": 676, "frequency": "f", "synset": "mast.n.01"}, - {"name": "mat_(gym_equipment)", "id": 677, "frequency": "c", "synset": "mat.n.03"}, - {"name": "matchbox", "id": 678, "frequency": "r", "synset": "matchbox.n.01"}, - {"name": "mattress", "id": 679, "frequency": "f", "synset": "mattress.n.01"}, - {"name": "measuring_cup", "id": 680, "frequency": "c", "synset": "measuring_cup.n.01"}, - {"name": "measuring_stick", "id": 681, "frequency": "c", "synset": "measuring_stick.n.01"}, - {"name": "meatball", "id": 682, "frequency": "c", "synset": "meatball.n.01"}, - {"name": "medicine", "id": 683, "frequency": "c", "synset": "medicine.n.02"}, - {"name": "melon", "id": 684, "frequency": "c", "synset": "melon.n.01"}, - {"name": "microphone", "id": 685, "frequency": "f", "synset": "microphone.n.01"}, - {"name": "microscope", "id": 686, "frequency": "r", "synset": "microscope.n.01"}, - {"name": "microwave_oven", "id": 687, "frequency": "f", "synset": "microwave.n.02"}, - {"name": "milestone", "id": 688, "frequency": "r", "synset": "milestone.n.01"}, - {"name": "milk", "id": 689, "frequency": "f", "synset": "milk.n.01"}, - {"name": "milk_can", "id": 690, "frequency": "r", "synset": "milk_can.n.01"}, - {"name": "milkshake", "id": 691, "frequency": "r", "synset": "milkshake.n.01"}, - {"name": "minivan", "id": 692, "frequency": "f", "synset": "minivan.n.01"}, - {"name": "mint_candy", "id": 693, "frequency": "r", "synset": "mint.n.05"}, - {"name": "mirror", "id": 694, "frequency": "f", "synset": "mirror.n.01"}, - {"name": "mitten", "id": 695, "frequency": "c", "synset": "mitten.n.01"}, - {"name": "mixer_(kitchen_tool)", "id": 696, "frequency": "c", "synset": "mixer.n.04"}, - {"name": "money", "id": 697, "frequency": "c", "synset": "money.n.03"}, - { - "name": "monitor_(computer_equipment) computer_monitor", - "id": 698, - "frequency": "f", - "synset": "monitor.n.04", - }, - {"name": "monkey", "id": 699, "frequency": "c", "synset": "monkey.n.01"}, - {"name": "motor", "id": 700, "frequency": "f", "synset": "motor.n.01"}, - {"name": "motor_scooter", "id": 701, "frequency": "f", "synset": "motor_scooter.n.01"}, - {"name": "motor_vehicle", "id": 702, "frequency": "r", "synset": "motor_vehicle.n.01"}, - {"name": "motorcycle", "id": 703, "frequency": "f", "synset": "motorcycle.n.01"}, - {"name": "mound_(baseball)", "id": 704, "frequency": "f", "synset": "mound.n.01"}, - {"name": "mouse_(computer_equipment)", "id": 705, "frequency": "f", "synset": "mouse.n.04"}, - {"name": "mousepad", "id": 706, "frequency": "f", "synset": "mousepad.n.01"}, - {"name": "muffin", "id": 707, "frequency": "c", "synset": "muffin.n.01"}, - {"name": "mug", "id": 708, "frequency": "f", "synset": "mug.n.04"}, - {"name": "mushroom", "id": 709, "frequency": "f", "synset": "mushroom.n.02"}, - {"name": "music_stool", "id": 710, "frequency": "r", "synset": "music_stool.n.01"}, - { - "name": "musical_instrument", - "id": 711, - "frequency": "c", - "synset": "musical_instrument.n.01", - }, - {"name": "nailfile", "id": 712, "frequency": "r", "synset": "nailfile.n.01"}, - {"name": "napkin", "id": 713, "frequency": "f", "synset": "napkin.n.01"}, - {"name": "neckerchief", "id": 714, "frequency": "r", "synset": "neckerchief.n.01"}, - {"name": "necklace", "id": 715, "frequency": "f", "synset": "necklace.n.01"}, - {"name": "necktie", "id": 716, "frequency": "f", "synset": "necktie.n.01"}, - {"name": "needle", "id": 717, "frequency": "c", "synset": "needle.n.03"}, - {"name": "nest", "id": 718, "frequency": "c", "synset": "nest.n.01"}, - {"name": "newspaper", "id": 719, "frequency": "f", "synset": "newspaper.n.01"}, - {"name": "newsstand", "id": 720, "frequency": "c", "synset": "newsstand.n.01"}, - {"name": "nightshirt", "id": 721, "frequency": "c", "synset": "nightwear.n.01"}, - {"name": "nosebag_(for_animals)", "id": 722, "frequency": "r", "synset": "nosebag.n.01"}, - {"name": "noseband_(for_animals)", "id": 723, "frequency": "c", "synset": "noseband.n.01"}, - {"name": "notebook", "id": 724, "frequency": "f", "synset": "notebook.n.01"}, - {"name": "notepad", "id": 725, "frequency": "c", "synset": "notepad.n.01"}, - {"name": "nut", "id": 726, "frequency": "f", "synset": "nut.n.03"}, - {"name": "nutcracker", "id": 727, "frequency": "r", "synset": "nutcracker.n.01"}, - {"name": "oar", "id": 728, "frequency": "f", "synset": "oar.n.01"}, - {"name": "octopus_(food)", "id": 729, "frequency": "r", "synset": "octopus.n.01"}, - {"name": "octopus_(animal)", "id": 730, "frequency": "r", "synset": "octopus.n.02"}, - {"name": "oil_lamp", "id": 731, "frequency": "c", "synset": "oil_lamp.n.01"}, - {"name": "olive_oil", "id": 732, "frequency": "c", "synset": "olive_oil.n.01"}, - {"name": "omelet", "id": 733, "frequency": "r", "synset": "omelet.n.01"}, - {"name": "onion", "id": 734, "frequency": "f", "synset": "onion.n.01"}, - {"name": "orange_(fruit)", "id": 735, "frequency": "f", "synset": "orange.n.01"}, - {"name": "orange_juice", "id": 736, "frequency": "c", "synset": "orange_juice.n.01"}, - {"name": "ostrich", "id": 737, "frequency": "c", "synset": "ostrich.n.02"}, - {"name": "ottoman", "id": 738, "frequency": "f", "synset": "ottoman.n.03"}, - {"name": "oven", "id": 739, "frequency": "f", "synset": "oven.n.01"}, - {"name": "overalls_(clothing)", "id": 740, "frequency": "c", "synset": "overall.n.01"}, - {"name": "owl", "id": 741, "frequency": "c", "synset": "owl.n.01"}, - {"name": "packet", "id": 742, "frequency": "c", "synset": "packet.n.03"}, - {"name": "inkpad", "id": 743, "frequency": "r", "synset": "pad.n.03"}, - {"name": "pad", "id": 744, "frequency": "c", "synset": "pad.n.04"}, - {"name": "paddle", "id": 745, "frequency": "f", "synset": "paddle.n.04"}, - {"name": "padlock", "id": 746, "frequency": "c", "synset": "padlock.n.01"}, - {"name": "paintbrush", "id": 747, "frequency": "c", "synset": "paintbrush.n.01"}, - {"name": "painting", "id": 748, "frequency": "f", "synset": "painting.n.01"}, - {"name": "pajamas", "id": 749, "frequency": "f", "synset": "pajama.n.02"}, - {"name": "palette", "id": 750, "frequency": "c", "synset": "palette.n.02"}, - {"name": "pan_(for_cooking)", "id": 751, "frequency": "f", "synset": "pan.n.01"}, - {"name": "pan_(metal_container)", "id": 752, "frequency": "r", "synset": "pan.n.03"}, - {"name": "pancake", "id": 753, "frequency": "c", "synset": "pancake.n.01"}, - {"name": "pantyhose", "id": 754, "frequency": "r", "synset": "pantyhose.n.01"}, - {"name": "papaya", "id": 755, "frequency": "r", "synset": "papaya.n.02"}, - {"name": "paper_plate", "id": 756, "frequency": "f", "synset": "paper_plate.n.01"}, - {"name": "paper_towel", "id": 757, "frequency": "f", "synset": "paper_towel.n.01"}, - {"name": "paperback_book", "id": 758, "frequency": "r", "synset": "paperback_book.n.01"}, - {"name": "paperweight", "id": 759, "frequency": "r", "synset": "paperweight.n.01"}, - {"name": "parachute", "id": 760, "frequency": "c", "synset": "parachute.n.01"}, - {"name": "parakeet", "id": 761, "frequency": "c", "synset": "parakeet.n.01"}, - {"name": "parasail_(sports)", "id": 762, "frequency": "c", "synset": "parasail.n.01"}, - {"name": "parasol", "id": 763, "frequency": "c", "synset": "parasol.n.01"}, - {"name": "parchment", "id": 764, "frequency": "r", "synset": "parchment.n.01"}, - {"name": "parka", "id": 765, "frequency": "c", "synset": "parka.n.01"}, - {"name": "parking_meter", "id": 766, "frequency": "f", "synset": "parking_meter.n.01"}, - {"name": "parrot", "id": 767, "frequency": "c", "synset": "parrot.n.01"}, - { - "name": "passenger_car_(part_of_a_train)", - "id": 768, - "frequency": "c", - "synset": "passenger_car.n.01", - }, - {"name": "passenger_ship", "id": 769, "frequency": "r", "synset": "passenger_ship.n.01"}, - {"name": "passport", "id": 770, "frequency": "c", "synset": "passport.n.02"}, - {"name": "pastry", "id": 771, "frequency": "f", "synset": "pastry.n.02"}, - {"name": "patty_(food)", "id": 772, "frequency": "r", "synset": "patty.n.01"}, - {"name": "pea_(food)", "id": 773, "frequency": "c", "synset": "pea.n.01"}, - {"name": "peach", "id": 774, "frequency": "c", "synset": "peach.n.03"}, - {"name": "peanut_butter", "id": 775, "frequency": "c", "synset": "peanut_butter.n.01"}, - {"name": "pear", "id": 776, "frequency": "f", "synset": "pear.n.01"}, - { - "name": "peeler_(tool_for_fruit_and_vegetables)", - "id": 777, - "frequency": "c", - "synset": "peeler.n.03", - }, - {"name": "wooden_leg", "id": 778, "frequency": "r", "synset": "peg.n.04"}, - {"name": "pegboard", "id": 779, "frequency": "r", "synset": "pegboard.n.01"}, - {"name": "pelican", "id": 780, "frequency": "c", "synset": "pelican.n.01"}, - {"name": "pen", "id": 781, "frequency": "f", "synset": "pen.n.01"}, - {"name": "pencil", "id": 782, "frequency": "f", "synset": "pencil.n.01"}, - {"name": "pencil_box", "id": 783, "frequency": "r", "synset": "pencil_box.n.01"}, - {"name": "pencil_sharpener", "id": 784, "frequency": "r", "synset": "pencil_sharpener.n.01"}, - {"name": "pendulum", "id": 785, "frequency": "r", "synset": "pendulum.n.01"}, - {"name": "penguin", "id": 786, "frequency": "c", "synset": "penguin.n.01"}, - {"name": "pennant", "id": 787, "frequency": "r", "synset": "pennant.n.02"}, - {"name": "penny_(coin)", "id": 788, "frequency": "r", "synset": "penny.n.02"}, - {"name": "pepper", "id": 789, "frequency": "f", "synset": "pepper.n.03"}, - {"name": "pepper_mill", "id": 790, "frequency": "c", "synset": "pepper_mill.n.01"}, - {"name": "perfume", "id": 791, "frequency": "c", "synset": "perfume.n.02"}, - {"name": "persimmon", "id": 792, "frequency": "r", "synset": "persimmon.n.02"}, - {"name": "person", "id": 793, "frequency": "f", "synset": "person.n.01"}, - {"name": "pet", "id": 794, "frequency": "c", "synset": "pet.n.01"}, - {"name": "pew_(church_bench)", "id": 795, "frequency": "c", "synset": "pew.n.01"}, - {"name": "phonebook", "id": 796, "frequency": "r", "synset": "phonebook.n.01"}, - {"name": "phonograph_record", "id": 797, "frequency": "c", "synset": "phonograph_record.n.01"}, - {"name": "piano", "id": 798, "frequency": "f", "synset": "piano.n.01"}, - {"name": "pickle", "id": 799, "frequency": "f", "synset": "pickle.n.01"}, - {"name": "pickup_truck", "id": 800, "frequency": "f", "synset": "pickup.n.01"}, - {"name": "pie", "id": 801, "frequency": "c", "synset": "pie.n.01"}, - {"name": "pigeon", "id": 802, "frequency": "c", "synset": "pigeon.n.01"}, - {"name": "piggy_bank", "id": 803, "frequency": "r", "synset": "piggy_bank.n.01"}, - {"name": "pillow", "id": 804, "frequency": "f", "synset": "pillow.n.01"}, - {"name": "pin_(non_jewelry)", "id": 805, "frequency": "r", "synset": "pin.n.09"}, - {"name": "pineapple", "id": 806, "frequency": "f", "synset": "pineapple.n.02"}, - {"name": "pinecone", "id": 807, "frequency": "c", "synset": "pinecone.n.01"}, - {"name": "ping-pong_ball", "id": 808, "frequency": "r", "synset": "ping-pong_ball.n.01"}, - {"name": "pinwheel", "id": 809, "frequency": "r", "synset": "pinwheel.n.03"}, - {"name": "tobacco_pipe", "id": 810, "frequency": "r", "synset": "pipe.n.01"}, - {"name": "pipe", "id": 811, "frequency": "f", "synset": "pipe.n.02"}, - {"name": "pistol", "id": 812, "frequency": "r", "synset": "pistol.n.01"}, - {"name": "pita_(bread)", "id": 813, "frequency": "c", "synset": "pita.n.01"}, - {"name": "pitcher_(vessel_for_liquid)", "id": 814, "frequency": "f", "synset": "pitcher.n.02"}, - {"name": "pitchfork", "id": 815, "frequency": "r", "synset": "pitchfork.n.01"}, - {"name": "pizza", "id": 816, "frequency": "f", "synset": "pizza.n.01"}, - {"name": "place_mat", "id": 817, "frequency": "f", "synset": "place_mat.n.01"}, - {"name": "plate", "id": 818, "frequency": "f", "synset": "plate.n.04"}, - {"name": "platter", "id": 819, "frequency": "c", "synset": "platter.n.01"}, - {"name": "playpen", "id": 820, "frequency": "r", "synset": "playpen.n.01"}, - {"name": "pliers", "id": 821, "frequency": "c", "synset": "pliers.n.01"}, - {"name": "plow_(farm_equipment)", "id": 822, "frequency": "r", "synset": "plow.n.01"}, - {"name": "plume", "id": 823, "frequency": "r", "synset": "plume.n.02"}, - {"name": "pocket_watch", "id": 824, "frequency": "r", "synset": "pocket_watch.n.01"}, - {"name": "pocketknife", "id": 825, "frequency": "c", "synset": "pocketknife.n.01"}, - {"name": "poker_(fire_stirring_tool)", "id": 826, "frequency": "c", "synset": "poker.n.01"}, - {"name": "pole", "id": 827, "frequency": "f", "synset": "pole.n.01"}, - {"name": "polo_shirt", "id": 828, "frequency": "f", "synset": "polo_shirt.n.01"}, - {"name": "poncho", "id": 829, "frequency": "r", "synset": "poncho.n.01"}, - {"name": "pony", "id": 830, "frequency": "c", "synset": "pony.n.05"}, - {"name": "pool_table", "id": 831, "frequency": "r", "synset": "pool_table.n.01"}, - {"name": "pop_(soda)", "id": 832, "frequency": "f", "synset": "pop.n.02"}, - {"name": "postbox_(public)", "id": 833, "frequency": "c", "synset": "postbox.n.01"}, - {"name": "postcard", "id": 834, "frequency": "c", "synset": "postcard.n.01"}, - {"name": "poster", "id": 835, "frequency": "f", "synset": "poster.n.01"}, - {"name": "pot", "id": 836, "frequency": "f", "synset": "pot.n.01"}, - {"name": "flowerpot", "id": 837, "frequency": "f", "synset": "pot.n.04"}, - {"name": "potato", "id": 838, "frequency": "f", "synset": "potato.n.01"}, - {"name": "potholder", "id": 839, "frequency": "c", "synset": "potholder.n.01"}, - {"name": "pottery", "id": 840, "frequency": "c", "synset": "pottery.n.01"}, - {"name": "pouch", "id": 841, "frequency": "c", "synset": "pouch.n.01"}, - {"name": "power_shovel", "id": 842, "frequency": "c", "synset": "power_shovel.n.01"}, - {"name": "prawn", "id": 843, "frequency": "c", "synset": "prawn.n.01"}, - {"name": "pretzel", "id": 844, "frequency": "c", "synset": "pretzel.n.01"}, - {"name": "printer", "id": 845, "frequency": "f", "synset": "printer.n.03"}, - {"name": "projectile_(weapon)", "id": 846, "frequency": "c", "synset": "projectile.n.01"}, - {"name": "projector", "id": 847, "frequency": "c", "synset": "projector.n.02"}, - {"name": "propeller", "id": 848, "frequency": "f", "synset": "propeller.n.01"}, - {"name": "prune", "id": 849, "frequency": "r", "synset": "prune.n.01"}, - {"name": "pudding", "id": 850, "frequency": "r", "synset": "pudding.n.01"}, - {"name": "puffer_(fish)", "id": 851, "frequency": "r", "synset": "puffer.n.02"}, - {"name": "puffin", "id": 852, "frequency": "r", "synset": "puffin.n.01"}, - {"name": "pug-dog", "id": 853, "frequency": "r", "synset": "pug.n.01"}, - {"name": "pumpkin", "id": 854, "frequency": "c", "synset": "pumpkin.n.02"}, - {"name": "puncher", "id": 855, "frequency": "r", "synset": "punch.n.03"}, - {"name": "puppet", "id": 856, "frequency": "r", "synset": "puppet.n.01"}, - {"name": "puppy", "id": 857, "frequency": "c", "synset": "puppy.n.01"}, - {"name": "quesadilla", "id": 858, "frequency": "r", "synset": "quesadilla.n.01"}, - {"name": "quiche", "id": 859, "frequency": "r", "synset": "quiche.n.02"}, - {"name": "quilt", "id": 860, "frequency": "f", "synset": "quilt.n.01"}, - {"name": "rabbit", "id": 861, "frequency": "c", "synset": "rabbit.n.01"}, - {"name": "race_car", "id": 862, "frequency": "r", "synset": "racer.n.02"}, - {"name": "racket", "id": 863, "frequency": "c", "synset": "racket.n.04"}, - {"name": "radar", "id": 864, "frequency": "r", "synset": "radar.n.01"}, - {"name": "radiator", "id": 865, "frequency": "f", "synset": "radiator.n.03"}, - {"name": "radio_receiver", "id": 866, "frequency": "c", "synset": "radio_receiver.n.01"}, - {"name": "radish", "id": 867, "frequency": "c", "synset": "radish.n.03"}, - {"name": "raft", "id": 868, "frequency": "c", "synset": "raft.n.01"}, - {"name": "rag_doll", "id": 869, "frequency": "r", "synset": "rag_doll.n.01"}, - {"name": "raincoat", "id": 870, "frequency": "c", "synset": "raincoat.n.01"}, - {"name": "ram_(animal)", "id": 871, "frequency": "c", "synset": "ram.n.05"}, - {"name": "raspberry", "id": 872, "frequency": "c", "synset": "raspberry.n.02"}, - {"name": "rat", "id": 873, "frequency": "r", "synset": "rat.n.01"}, - {"name": "razorblade", "id": 874, "frequency": "c", "synset": "razorblade.n.01"}, - {"name": "reamer_(juicer)", "id": 875, "frequency": "c", "synset": "reamer.n.01"}, - {"name": "rearview_mirror", "id": 876, "frequency": "f", "synset": "rearview_mirror.n.01"}, - {"name": "receipt", "id": 877, "frequency": "c", "synset": "receipt.n.02"}, - {"name": "recliner", "id": 878, "frequency": "c", "synset": "recliner.n.01"}, - {"name": "record_player", "id": 879, "frequency": "c", "synset": "record_player.n.01"}, - {"name": "reflector", "id": 880, "frequency": "f", "synset": "reflector.n.01"}, - {"name": "remote_control", "id": 881, "frequency": "f", "synset": "remote_control.n.01"}, - {"name": "rhinoceros", "id": 882, "frequency": "c", "synset": "rhinoceros.n.01"}, - {"name": "rib_(food)", "id": 883, "frequency": "r", "synset": "rib.n.03"}, - {"name": "rifle", "id": 884, "frequency": "c", "synset": "rifle.n.01"}, - {"name": "ring", "id": 885, "frequency": "f", "synset": "ring.n.08"}, - {"name": "river_boat", "id": 886, "frequency": "r", "synset": "river_boat.n.01"}, - {"name": "road_map", "id": 887, "frequency": "r", "synset": "road_map.n.02"}, - {"name": "robe", "id": 888, "frequency": "c", "synset": "robe.n.01"}, - {"name": "rocking_chair", "id": 889, "frequency": "c", "synset": "rocking_chair.n.01"}, - {"name": "rodent", "id": 890, "frequency": "r", "synset": "rodent.n.01"}, - {"name": "roller_skate", "id": 891, "frequency": "r", "synset": "roller_skate.n.01"}, - {"name": "Rollerblade", "id": 892, "frequency": "r", "synset": "rollerblade.n.01"}, - {"name": "rolling_pin", "id": 893, "frequency": "c", "synset": "rolling_pin.n.01"}, - {"name": "root_beer", "id": 894, "frequency": "r", "synset": "root_beer.n.01"}, - {"name": "router_(computer_equipment)", "id": 895, "frequency": "c", "synset": "router.n.02"}, - {"name": "rubber_band", "id": 896, "frequency": "f", "synset": "rubber_band.n.01"}, - {"name": "runner_(carpet)", "id": 897, "frequency": "c", "synset": "runner.n.08"}, - {"name": "plastic_bag", "id": 898, "frequency": "f", "synset": "sack.n.01"}, - {"name": "saddle_(on_an_animal)", "id": 899, "frequency": "f", "synset": "saddle.n.01"}, - {"name": "saddle_blanket", "id": 900, "frequency": "f", "synset": "saddle_blanket.n.01"}, - {"name": "saddlebag", "id": 901, "frequency": "c", "synset": "saddlebag.n.01"}, - {"name": "safety_pin", "id": 902, "frequency": "r", "synset": "safety_pin.n.01"}, - {"name": "sail", "id": 903, "frequency": "f", "synset": "sail.n.01"}, - {"name": "salad", "id": 904, "frequency": "f", "synset": "salad.n.01"}, - {"name": "salad_plate", "id": 905, "frequency": "r", "synset": "salad_plate.n.01"}, - {"name": "salami", "id": 906, "frequency": "c", "synset": "salami.n.01"}, - {"name": "salmon_(fish)", "id": 907, "frequency": "c", "synset": "salmon.n.01"}, - {"name": "salmon_(food)", "id": 908, "frequency": "r", "synset": "salmon.n.03"}, - {"name": "salsa", "id": 909, "frequency": "c", "synset": "salsa.n.01"}, - {"name": "saltshaker", "id": 910, "frequency": "f", "synset": "saltshaker.n.01"}, - {"name": "sandal_(type_of_shoe)", "id": 911, "frequency": "f", "synset": "sandal.n.01"}, - {"name": "sandwich", "id": 912, "frequency": "f", "synset": "sandwich.n.01"}, - {"name": "satchel", "id": 913, "frequency": "r", "synset": "satchel.n.01"}, - {"name": "saucepan", "id": 914, "frequency": "r", "synset": "saucepan.n.01"}, - {"name": "saucer", "id": 915, "frequency": "f", "synset": "saucer.n.02"}, - {"name": "sausage", "id": 916, "frequency": "f", "synset": "sausage.n.01"}, - {"name": "sawhorse", "id": 917, "frequency": "r", "synset": "sawhorse.n.01"}, - {"name": "saxophone", "id": 918, "frequency": "r", "synset": "sax.n.02"}, - {"name": "scale_(measuring_instrument)", "id": 919, "frequency": "f", "synset": "scale.n.07"}, - {"name": "scarecrow", "id": 920, "frequency": "r", "synset": "scarecrow.n.01"}, - {"name": "scarf", "id": 921, "frequency": "f", "synset": "scarf.n.01"}, - {"name": "school_bus", "id": 922, "frequency": "c", "synset": "school_bus.n.01"}, - {"name": "scissors", "id": 923, "frequency": "f", "synset": "scissors.n.01"}, - {"name": "scoreboard", "id": 924, "frequency": "f", "synset": "scoreboard.n.01"}, - {"name": "scraper", "id": 925, "frequency": "r", "synset": "scraper.n.01"}, - {"name": "screwdriver", "id": 926, "frequency": "c", "synset": "screwdriver.n.01"}, - {"name": "scrubbing_brush", "id": 927, "frequency": "f", "synset": "scrub_brush.n.01"}, - {"name": "sculpture", "id": 928, "frequency": "c", "synset": "sculpture.n.01"}, - {"name": "seabird", "id": 929, "frequency": "c", "synset": "seabird.n.01"}, - {"name": "seahorse", "id": 930, "frequency": "c", "synset": "seahorse.n.02"}, - {"name": "seaplane", "id": 931, "frequency": "r", "synset": "seaplane.n.01"}, - {"name": "seashell", "id": 932, "frequency": "c", "synset": "seashell.n.01"}, - {"name": "sewing_machine", "id": 933, "frequency": "c", "synset": "sewing_machine.n.01"}, - {"name": "shaker", "id": 934, "frequency": "c", "synset": "shaker.n.03"}, - {"name": "shampoo", "id": 935, "frequency": "c", "synset": "shampoo.n.01"}, - {"name": "shark", "id": 936, "frequency": "c", "synset": "shark.n.01"}, - {"name": "sharpener", "id": 937, "frequency": "r", "synset": "sharpener.n.01"}, - {"name": "Sharpie", "id": 938, "frequency": "r", "synset": "sharpie.n.03"}, - {"name": "shaver_(electric)", "id": 939, "frequency": "r", "synset": "shaver.n.03"}, - {"name": "shaving_cream", "id": 940, "frequency": "c", "synset": "shaving_cream.n.01"}, - {"name": "shawl", "id": 941, "frequency": "r", "synset": "shawl.n.01"}, - {"name": "shears", "id": 942, "frequency": "r", "synset": "shears.n.01"}, - {"name": "sheep", "id": 943, "frequency": "f", "synset": "sheep.n.01"}, - {"name": "shepherd_dog", "id": 944, "frequency": "r", "synset": "shepherd_dog.n.01"}, - {"name": "sherbert", "id": 945, "frequency": "r", "synset": "sherbert.n.01"}, - {"name": "shield", "id": 946, "frequency": "c", "synset": "shield.n.02"}, - {"name": "shirt", "id": 947, "frequency": "f", "synset": "shirt.n.01"}, - {"name": "shoe", "id": 948, "frequency": "f", "synset": "shoe.n.01"}, - {"name": "shopping_bag", "id": 949, "frequency": "f", "synset": "shopping_bag.n.01"}, - {"name": "shopping_cart", "id": 950, "frequency": "c", "synset": "shopping_cart.n.01"}, - {"name": "short_pants", "id": 951, "frequency": "f", "synset": "short_pants.n.01"}, - {"name": "shot_glass", "id": 952, "frequency": "r", "synset": "shot_glass.n.01"}, - {"name": "shoulder_bag", "id": 953, "frequency": "f", "synset": "shoulder_bag.n.01"}, - {"name": "shovel", "id": 954, "frequency": "c", "synset": "shovel.n.01"}, - {"name": "shower_head", "id": 955, "frequency": "f", "synset": "shower.n.01"}, - {"name": "shower_cap", "id": 956, "frequency": "r", "synset": "shower_cap.n.01"}, - {"name": "shower_curtain", "id": 957, "frequency": "f", "synset": "shower_curtain.n.01"}, - {"name": "shredder_(for_paper)", "id": 958, "frequency": "r", "synset": "shredder.n.01"}, - {"name": "signboard", "id": 959, "frequency": "f", "synset": "signboard.n.01"}, - {"name": "silo", "id": 960, "frequency": "c", "synset": "silo.n.01"}, - {"name": "sink", "id": 961, "frequency": "f", "synset": "sink.n.01"}, - {"name": "skateboard", "id": 962, "frequency": "f", "synset": "skateboard.n.01"}, - {"name": "skewer", "id": 963, "frequency": "c", "synset": "skewer.n.01"}, - {"name": "ski", "id": 964, "frequency": "f", "synset": "ski.n.01"}, - {"name": "ski_boot", "id": 965, "frequency": "f", "synset": "ski_boot.n.01"}, - {"name": "ski_parka", "id": 966, "frequency": "f", "synset": "ski_parka.n.01"}, - {"name": "ski_pole", "id": 967, "frequency": "f", "synset": "ski_pole.n.01"}, - {"name": "skirt", "id": 968, "frequency": "f", "synset": "skirt.n.02"}, - {"name": "skullcap", "id": 969, "frequency": "r", "synset": "skullcap.n.01"}, - {"name": "sled", "id": 970, "frequency": "c", "synset": "sled.n.01"}, - {"name": "sleeping_bag", "id": 971, "frequency": "c", "synset": "sleeping_bag.n.01"}, - {"name": "sling_(bandage)", "id": 972, "frequency": "r", "synset": "sling.n.05"}, - {"name": "slipper_(footwear)", "id": 973, "frequency": "c", "synset": "slipper.n.01"}, - {"name": "smoothie", "id": 974, "frequency": "r", "synset": "smoothie.n.02"}, - {"name": "snake", "id": 975, "frequency": "r", "synset": "snake.n.01"}, - {"name": "snowboard", "id": 976, "frequency": "f", "synset": "snowboard.n.01"}, - {"name": "snowman", "id": 977, "frequency": "c", "synset": "snowman.n.01"}, - {"name": "snowmobile", "id": 978, "frequency": "c", "synset": "snowmobile.n.01"}, - {"name": "soap", "id": 979, "frequency": "f", "synset": "soap.n.01"}, - {"name": "soccer_ball", "id": 980, "frequency": "f", "synset": "soccer_ball.n.01"}, - {"name": "sock", "id": 981, "frequency": "f", "synset": "sock.n.01"}, - {"name": "sofa", "id": 982, "frequency": "f", "synset": "sofa.n.01"}, - {"name": "softball", "id": 983, "frequency": "r", "synset": "softball.n.01"}, - {"name": "solar_array", "id": 984, "frequency": "c", "synset": "solar_array.n.01"}, - {"name": "sombrero", "id": 985, "frequency": "r", "synset": "sombrero.n.02"}, - {"name": "soup", "id": 986, "frequency": "f", "synset": "soup.n.01"}, - {"name": "soup_bowl", "id": 987, "frequency": "r", "synset": "soup_bowl.n.01"}, - {"name": "soupspoon", "id": 988, "frequency": "c", "synset": "soupspoon.n.01"}, - {"name": "sour_cream", "id": 989, "frequency": "c", "synset": "sour_cream.n.01"}, - {"name": "soya_milk", "id": 990, "frequency": "r", "synset": "soya_milk.n.01"}, - {"name": "space_shuttle", "id": 991, "frequency": "r", "synset": "space_shuttle.n.01"}, - {"name": "sparkler_(fireworks)", "id": 992, "frequency": "r", "synset": "sparkler.n.02"}, - {"name": "spatula", "id": 993, "frequency": "f", "synset": "spatula.n.02"}, - {"name": "spear", "id": 994, "frequency": "r", "synset": "spear.n.01"}, - {"name": "spectacles", "id": 995, "frequency": "f", "synset": "spectacles.n.01"}, - {"name": "spice_rack", "id": 996, "frequency": "c", "synset": "spice_rack.n.01"}, - {"name": "spider", "id": 997, "frequency": "c", "synset": "spider.n.01"}, - {"name": "crawfish", "id": 998, "frequency": "r", "synset": "spiny_lobster.n.02"}, - {"name": "sponge", "id": 999, "frequency": "c", "synset": "sponge.n.01"}, - {"name": "spoon", "id": 1000, "frequency": "f", "synset": "spoon.n.01"}, - {"name": "sportswear", "id": 1001, "frequency": "c", "synset": "sportswear.n.01"}, - {"name": "spotlight", "id": 1002, "frequency": "c", "synset": "spotlight.n.02"}, - {"name": "squid_(food)", "id": 1003, "frequency": "r", "synset": "squid.n.01"}, - {"name": "squirrel", "id": 1004, "frequency": "c", "synset": "squirrel.n.01"}, - {"name": "stagecoach", "id": 1005, "frequency": "r", "synset": "stagecoach.n.01"}, - {"name": "stapler_(stapling_machine)", "id": 1006, "frequency": "c", "synset": "stapler.n.01"}, - {"name": "starfish", "id": 1007, "frequency": "c", "synset": "starfish.n.01"}, - {"name": "statue_(sculpture)", "id": 1008, "frequency": "f", "synset": "statue.n.01"}, - {"name": "steak_(food)", "id": 1009, "frequency": "c", "synset": "steak.n.01"}, - {"name": "steak_knife", "id": 1010, "frequency": "r", "synset": "steak_knife.n.01"}, - {"name": "steering_wheel", "id": 1011, "frequency": "f", "synset": "steering_wheel.n.01"}, - {"name": "stepladder", "id": 1012, "frequency": "r", "synset": "step_ladder.n.01"}, - {"name": "step_stool", "id": 1013, "frequency": "c", "synset": "step_stool.n.01"}, - {"name": "stereo_(sound_system)", "id": 1014, "frequency": "c", "synset": "stereo.n.01"}, - {"name": "stew", "id": 1015, "frequency": "r", "synset": "stew.n.02"}, - {"name": "stirrer", "id": 1016, "frequency": "r", "synset": "stirrer.n.02"}, - {"name": "stirrup", "id": 1017, "frequency": "f", "synset": "stirrup.n.01"}, - {"name": "stool", "id": 1018, "frequency": "f", "synset": "stool.n.01"}, - {"name": "stop_sign", "id": 1019, "frequency": "f", "synset": "stop_sign.n.01"}, - {"name": "brake_light", "id": 1020, "frequency": "f", "synset": "stoplight.n.01"}, - {"name": "stove", "id": 1021, "frequency": "f", "synset": "stove.n.01"}, - {"name": "strainer", "id": 1022, "frequency": "c", "synset": "strainer.n.01"}, - {"name": "strap", "id": 1023, "frequency": "f", "synset": "strap.n.01"}, - {"name": "straw_(for_drinking)", "id": 1024, "frequency": "f", "synset": "straw.n.04"}, - {"name": "strawberry", "id": 1025, "frequency": "f", "synset": "strawberry.n.01"}, - {"name": "street_sign", "id": 1026, "frequency": "f", "synset": "street_sign.n.01"}, - {"name": "streetlight", "id": 1027, "frequency": "f", "synset": "streetlight.n.01"}, - {"name": "string_cheese", "id": 1028, "frequency": "r", "synset": "string_cheese.n.01"}, - {"name": "stylus", "id": 1029, "frequency": "r", "synset": "stylus.n.02"}, - {"name": "subwoofer", "id": 1030, "frequency": "r", "synset": "subwoofer.n.01"}, - {"name": "sugar_bowl", "id": 1031, "frequency": "r", "synset": "sugar_bowl.n.01"}, - {"name": "sugarcane_(plant)", "id": 1032, "frequency": "r", "synset": "sugarcane.n.01"}, - {"name": "suit_(clothing)", "id": 1033, "frequency": "f", "synset": "suit.n.01"}, - {"name": "sunflower", "id": 1034, "frequency": "c", "synset": "sunflower.n.01"}, - {"name": "sunglasses", "id": 1035, "frequency": "f", "synset": "sunglasses.n.01"}, - {"name": "sunhat", "id": 1036, "frequency": "c", "synset": "sunhat.n.01"}, - {"name": "surfboard", "id": 1037, "frequency": "f", "synset": "surfboard.n.01"}, - {"name": "sushi", "id": 1038, "frequency": "c", "synset": "sushi.n.01"}, - {"name": "mop", "id": 1039, "frequency": "c", "synset": "swab.n.02"}, - {"name": "sweat_pants", "id": 1040, "frequency": "c", "synset": "sweat_pants.n.01"}, - {"name": "sweatband", "id": 1041, "frequency": "c", "synset": "sweatband.n.02"}, - {"name": "sweater", "id": 1042, "frequency": "f", "synset": "sweater.n.01"}, - {"name": "sweatshirt", "id": 1043, "frequency": "f", "synset": "sweatshirt.n.01"}, - {"name": "sweet_potato", "id": 1044, "frequency": "c", "synset": "sweet_potato.n.02"}, - {"name": "swimsuit", "id": 1045, "frequency": "f", "synset": "swimsuit.n.01"}, - {"name": "sword", "id": 1046, "frequency": "c", "synset": "sword.n.01"}, - {"name": "syringe", "id": 1047, "frequency": "r", "synset": "syringe.n.01"}, - {"name": "Tabasco_sauce", "id": 1048, "frequency": "r", "synset": "tabasco.n.02"}, - { - "name": "table-tennis_table", - "id": 1049, - "frequency": "r", - "synset": "table-tennis_table.n.01", - }, - {"name": "table", "id": 1050, "frequency": "f", "synset": "table.n.02"}, - {"name": "table_lamp", "id": 1051, "frequency": "c", "synset": "table_lamp.n.01"}, - {"name": "tablecloth", "id": 1052, "frequency": "f", "synset": "tablecloth.n.01"}, - {"name": "tachometer", "id": 1053, "frequency": "r", "synset": "tachometer.n.01"}, - {"name": "taco", "id": 1054, "frequency": "r", "synset": "taco.n.02"}, - {"name": "tag", "id": 1055, "frequency": "f", "synset": "tag.n.02"}, - {"name": "taillight", "id": 1056, "frequency": "f", "synset": "taillight.n.01"}, - {"name": "tambourine", "id": 1057, "frequency": "r", "synset": "tambourine.n.01"}, - {"name": "army_tank", "id": 1058, "frequency": "r", "synset": "tank.n.01"}, - {"name": "tank_(storage_vessel)", "id": 1059, "frequency": "f", "synset": "tank.n.02"}, - {"name": "tank_top_(clothing)", "id": 1060, "frequency": "f", "synset": "tank_top.n.01"}, - {"name": "tape_(sticky_cloth_or_paper)", "id": 1061, "frequency": "f", "synset": "tape.n.01"}, - {"name": "tape_measure", "id": 1062, "frequency": "c", "synset": "tape.n.04"}, - {"name": "tapestry", "id": 1063, "frequency": "c", "synset": "tapestry.n.02"}, - {"name": "tarp", "id": 1064, "frequency": "f", "synset": "tarpaulin.n.01"}, - {"name": "tartan", "id": 1065, "frequency": "c", "synset": "tartan.n.01"}, - {"name": "tassel", "id": 1066, "frequency": "c", "synset": "tassel.n.01"}, - {"name": "tea_bag", "id": 1067, "frequency": "c", "synset": "tea_bag.n.01"}, - {"name": "teacup", "id": 1068, "frequency": "c", "synset": "teacup.n.02"}, - {"name": "teakettle", "id": 1069, "frequency": "c", "synset": "teakettle.n.01"}, - {"name": "teapot", "id": 1070, "frequency": "f", "synset": "teapot.n.01"}, - {"name": "teddy_bear", "id": 1071, "frequency": "f", "synset": "teddy.n.01"}, - {"name": "telephone", "id": 1072, "frequency": "f", "synset": "telephone.n.01"}, - {"name": "telephone_booth", "id": 1073, "frequency": "c", "synset": "telephone_booth.n.01"}, - {"name": "telephone_pole", "id": 1074, "frequency": "f", "synset": "telephone_pole.n.01"}, - {"name": "telephoto_lens", "id": 1075, "frequency": "r", "synset": "telephoto_lens.n.01"}, - {"name": "television_camera", "id": 1076, "frequency": "c", "synset": "television_camera.n.01"}, - {"name": "television_set", "id": 1077, "frequency": "f", "synset": "television_receiver.n.01"}, - {"name": "tennis_ball", "id": 1078, "frequency": "f", "synset": "tennis_ball.n.01"}, - {"name": "tennis_racket", "id": 1079, "frequency": "f", "synset": "tennis_racket.n.01"}, - {"name": "tequila", "id": 1080, "frequency": "r", "synset": "tequila.n.01"}, - {"name": "thermometer", "id": 1081, "frequency": "c", "synset": "thermometer.n.01"}, - {"name": "thermos_bottle", "id": 1082, "frequency": "c", "synset": "thermos.n.01"}, - {"name": "thermostat", "id": 1083, "frequency": "f", "synset": "thermostat.n.01"}, - {"name": "thimble", "id": 1084, "frequency": "r", "synset": "thimble.n.02"}, - {"name": "thread", "id": 1085, "frequency": "c", "synset": "thread.n.01"}, - {"name": "thumbtack", "id": 1086, "frequency": "c", "synset": "thumbtack.n.01"}, - {"name": "tiara", "id": 1087, "frequency": "c", "synset": "tiara.n.01"}, - {"name": "tiger", "id": 1088, "frequency": "c", "synset": "tiger.n.02"}, - {"name": "tights_(clothing)", "id": 1089, "frequency": "c", "synset": "tights.n.01"}, - {"name": "timer", "id": 1090, "frequency": "c", "synset": "timer.n.01"}, - {"name": "tinfoil", "id": 1091, "frequency": "f", "synset": "tinfoil.n.01"}, - {"name": "tinsel", "id": 1092, "frequency": "c", "synset": "tinsel.n.01"}, - {"name": "tissue_paper", "id": 1093, "frequency": "f", "synset": "tissue.n.02"}, - {"name": "toast_(food)", "id": 1094, "frequency": "c", "synset": "toast.n.01"}, - {"name": "toaster", "id": 1095, "frequency": "f", "synset": "toaster.n.02"}, - {"name": "toaster_oven", "id": 1096, "frequency": "f", "synset": "toaster_oven.n.01"}, - {"name": "toilet", "id": 1097, "frequency": "f", "synset": "toilet.n.02"}, - {"name": "toilet_tissue", "id": 1098, "frequency": "f", "synset": "toilet_tissue.n.01"}, - {"name": "tomato", "id": 1099, "frequency": "f", "synset": "tomato.n.01"}, - {"name": "tongs", "id": 1100, "frequency": "f", "synset": "tongs.n.01"}, - {"name": "toolbox", "id": 1101, "frequency": "c", "synset": "toolbox.n.01"}, - {"name": "toothbrush", "id": 1102, "frequency": "f", "synset": "toothbrush.n.01"}, - {"name": "toothpaste", "id": 1103, "frequency": "f", "synset": "toothpaste.n.01"}, - {"name": "toothpick", "id": 1104, "frequency": "f", "synset": "toothpick.n.01"}, - {"name": "cover", "id": 1105, "frequency": "f", "synset": "top.n.09"}, - {"name": "tortilla", "id": 1106, "frequency": "c", "synset": "tortilla.n.01"}, - {"name": "tow_truck", "id": 1107, "frequency": "c", "synset": "tow_truck.n.01"}, - {"name": "towel", "id": 1108, "frequency": "f", "synset": "towel.n.01"}, - {"name": "towel_rack", "id": 1109, "frequency": "f", "synset": "towel_rack.n.01"}, - {"name": "toy", "id": 1110, "frequency": "f", "synset": "toy.n.03"}, - {"name": "tractor_(farm_equipment)", "id": 1111, "frequency": "c", "synset": "tractor.n.01"}, - {"name": "traffic_light", "id": 1112, "frequency": "f", "synset": "traffic_light.n.01"}, - {"name": "dirt_bike", "id": 1113, "frequency": "c", "synset": "trail_bike.n.01"}, - {"name": "trailer_truck", "id": 1114, "frequency": "f", "synset": "trailer_truck.n.01"}, - {"name": "train_(railroad_vehicle)", "id": 1115, "frequency": "f", "synset": "train.n.01"}, - {"name": "trampoline", "id": 1116, "frequency": "r", "synset": "trampoline.n.01"}, - {"name": "tray", "id": 1117, "frequency": "f", "synset": "tray.n.01"}, - {"name": "trench_coat", "id": 1118, "frequency": "r", "synset": "trench_coat.n.01"}, - { - "name": "triangle_(musical_instrument)", - "id": 1119, - "frequency": "r", - "synset": "triangle.n.05", - }, - {"name": "tricycle", "id": 1120, "frequency": "c", "synset": "tricycle.n.01"}, - {"name": "tripod", "id": 1121, "frequency": "f", "synset": "tripod.n.01"}, - {"name": "trousers", "id": 1122, "frequency": "f", "synset": "trouser.n.01"}, - {"name": "truck", "id": 1123, "frequency": "f", "synset": "truck.n.01"}, - {"name": "truffle_(chocolate)", "id": 1124, "frequency": "r", "synset": "truffle.n.03"}, - {"name": "trunk", "id": 1125, "frequency": "c", "synset": "trunk.n.02"}, - {"name": "vat", "id": 1126, "frequency": "r", "synset": "tub.n.02"}, - {"name": "turban", "id": 1127, "frequency": "c", "synset": "turban.n.01"}, - {"name": "turkey_(food)", "id": 1128, "frequency": "c", "synset": "turkey.n.04"}, - {"name": "turnip", "id": 1129, "frequency": "r", "synset": "turnip.n.01"}, - {"name": "turtle", "id": 1130, "frequency": "c", "synset": "turtle.n.02"}, - {"name": "turtleneck_(clothing)", "id": 1131, "frequency": "c", "synset": "turtleneck.n.01"}, - {"name": "typewriter", "id": 1132, "frequency": "c", "synset": "typewriter.n.01"}, - {"name": "umbrella", "id": 1133, "frequency": "f", "synset": "umbrella.n.01"}, - {"name": "underwear", "id": 1134, "frequency": "f", "synset": "underwear.n.01"}, - {"name": "unicycle", "id": 1135, "frequency": "r", "synset": "unicycle.n.01"}, - {"name": "urinal", "id": 1136, "frequency": "f", "synset": "urinal.n.01"}, - {"name": "urn", "id": 1137, "frequency": "c", "synset": "urn.n.01"}, - {"name": "vacuum_cleaner", "id": 1138, "frequency": "c", "synset": "vacuum.n.04"}, - {"name": "vase", "id": 1139, "frequency": "f", "synset": "vase.n.01"}, - {"name": "vending_machine", "id": 1140, "frequency": "c", "synset": "vending_machine.n.01"}, - {"name": "vent", "id": 1141, "frequency": "f", "synset": "vent.n.01"}, - {"name": "vest", "id": 1142, "frequency": "f", "synset": "vest.n.01"}, - {"name": "videotape", "id": 1143, "frequency": "c", "synset": "videotape.n.01"}, - {"name": "vinegar", "id": 1144, "frequency": "r", "synset": "vinegar.n.01"}, - {"name": "violin", "id": 1145, "frequency": "r", "synset": "violin.n.01"}, - {"name": "vodka", "id": 1146, "frequency": "r", "synset": "vodka.n.01"}, - {"name": "volleyball", "id": 1147, "frequency": "c", "synset": "volleyball.n.02"}, - {"name": "vulture", "id": 1148, "frequency": "r", "synset": "vulture.n.01"}, - {"name": "waffle", "id": 1149, "frequency": "c", "synset": "waffle.n.01"}, - {"name": "waffle_iron", "id": 1150, "frequency": "r", "synset": "waffle_iron.n.01"}, - {"name": "wagon", "id": 1151, "frequency": "c", "synset": "wagon.n.01"}, - {"name": "wagon_wheel", "id": 1152, "frequency": "c", "synset": "wagon_wheel.n.01"}, - {"name": "walking_stick", "id": 1153, "frequency": "c", "synset": "walking_stick.n.01"}, - {"name": "wall_clock", "id": 1154, "frequency": "c", "synset": "wall_clock.n.01"}, - {"name": "wall_socket", "id": 1155, "frequency": "f", "synset": "wall_socket.n.01"}, - {"name": "wallet", "id": 1156, "frequency": "f", "synset": "wallet.n.01"}, - {"name": "walrus", "id": 1157, "frequency": "r", "synset": "walrus.n.01"}, - {"name": "wardrobe", "id": 1158, "frequency": "r", "synset": "wardrobe.n.01"}, - {"name": "washbasin", "id": 1159, "frequency": "r", "synset": "washbasin.n.01"}, - {"name": "automatic_washer", "id": 1160, "frequency": "c", "synset": "washer.n.03"}, - {"name": "watch", "id": 1161, "frequency": "f", "synset": "watch.n.01"}, - {"name": "water_bottle", "id": 1162, "frequency": "f", "synset": "water_bottle.n.01"}, - {"name": "water_cooler", "id": 1163, "frequency": "c", "synset": "water_cooler.n.01"}, - {"name": "water_faucet", "id": 1164, "frequency": "c", "synset": "water_faucet.n.01"}, - {"name": "water_heater", "id": 1165, "frequency": "r", "synset": "water_heater.n.01"}, - {"name": "water_jug", "id": 1166, "frequency": "c", "synset": "water_jug.n.01"}, - {"name": "water_gun", "id": 1167, "frequency": "r", "synset": "water_pistol.n.01"}, - {"name": "water_scooter", "id": 1168, "frequency": "c", "synset": "water_scooter.n.01"}, - {"name": "water_ski", "id": 1169, "frequency": "c", "synset": "water_ski.n.01"}, - {"name": "water_tower", "id": 1170, "frequency": "c", "synset": "water_tower.n.01"}, - {"name": "watering_can", "id": 1171, "frequency": "c", "synset": "watering_can.n.01"}, - {"name": "watermelon", "id": 1172, "frequency": "f", "synset": "watermelon.n.02"}, - {"name": "weathervane", "id": 1173, "frequency": "f", "synset": "weathervane.n.01"}, - {"name": "webcam", "id": 1174, "frequency": "c", "synset": "webcam.n.01"}, - {"name": "wedding_cake", "id": 1175, "frequency": "c", "synset": "wedding_cake.n.01"}, - {"name": "wedding_ring", "id": 1176, "frequency": "c", "synset": "wedding_ring.n.01"}, - {"name": "wet_suit", "id": 1177, "frequency": "f", "synset": "wet_suit.n.01"}, - {"name": "wheel", "id": 1178, "frequency": "f", "synset": "wheel.n.01"}, - {"name": "wheelchair", "id": 1179, "frequency": "c", "synset": "wheelchair.n.01"}, - {"name": "whipped_cream", "id": 1180, "frequency": "c", "synset": "whipped_cream.n.01"}, - {"name": "whistle", "id": 1181, "frequency": "c", "synset": "whistle.n.03"}, - {"name": "wig", "id": 1182, "frequency": "c", "synset": "wig.n.01"}, - {"name": "wind_chime", "id": 1183, "frequency": "c", "synset": "wind_chime.n.01"}, - {"name": "windmill", "id": 1184, "frequency": "c", "synset": "windmill.n.01"}, - {"name": "window_box_(for_plants)", "id": 1185, "frequency": "c", "synset": "window_box.n.01"}, - {"name": "windshield_wiper", "id": 1186, "frequency": "f", "synset": "windshield_wiper.n.01"}, - {"name": "windsock", "id": 1187, "frequency": "c", "synset": "windsock.n.01"}, - {"name": "wine_bottle", "id": 1188, "frequency": "f", "synset": "wine_bottle.n.01"}, - {"name": "wine_bucket", "id": 1189, "frequency": "c", "synset": "wine_bucket.n.01"}, - {"name": "wineglass", "id": 1190, "frequency": "f", "synset": "wineglass.n.01"}, - {"name": "blinder_(for_horses)", "id": 1191, "frequency": "f", "synset": "winker.n.02"}, - {"name": "wok", "id": 1192, "frequency": "c", "synset": "wok.n.01"}, - {"name": "wolf", "id": 1193, "frequency": "r", "synset": "wolf.n.01"}, - {"name": "wooden_spoon", "id": 1194, "frequency": "c", "synset": "wooden_spoon.n.02"}, - {"name": "wreath", "id": 1195, "frequency": "c", "synset": "wreath.n.01"}, - {"name": "wrench", "id": 1196, "frequency": "c", "synset": "wrench.n.03"}, - {"name": "wristband", "id": 1197, "frequency": "f", "synset": "wristband.n.01"}, - {"name": "wristlet", "id": 1198, "frequency": "f", "synset": "wristlet.n.01"}, - {"name": "yacht", "id": 1199, "frequency": "c", "synset": "yacht.n.01"}, - {"name": "yogurt", "id": 1200, "frequency": "c", "synset": "yogurt.n.01"}, - {"name": "yoke_(animal_equipment)", "id": 1201, "frequency": "c", "synset": "yoke.n.07"}, - {"name": "zebra", "id": 1202, "frequency": "f", "synset": "zebra.n.01"}, - {"name": "zucchini", "id": 1203, "frequency": "c", "synset": "zucchini.n.02"}, - {"id": 1204, "synset": "organism.n.01", "name": "organism"}, - {"id": 1205, "synset": "benthos.n.02", "name": "benthos"}, - {"id": 1206, "synset": "heterotroph.n.01", "name": "heterotroph"}, - {"id": 1207, "synset": "cell.n.02", "name": "cell"}, - {"id": 1208, "synset": "animal.n.01", "name": "animal"}, - {"id": 1209, "synset": "plant.n.02", "name": "plant"}, - {"id": 1210, "synset": "food.n.01", "name": "food"}, - {"id": 1211, "synset": "artifact.n.01", "name": "artifact"}, - {"id": 1212, "synset": "hop.n.01", "name": "hop"}, - {"id": 1213, "synset": "check-in.n.01", "name": "check-in"}, - {"id": 1214, "synset": "dressage.n.01", "name": "dressage"}, - {"id": 1215, "synset": "curvet.n.01", "name": "curvet"}, - {"id": 1216, "synset": "piaffe.n.01", "name": "piaffe"}, - {"id": 1217, "synset": "funambulism.n.01", "name": "funambulism"}, - {"id": 1218, "synset": "rock_climbing.n.01", "name": "rock_climbing"}, - {"id": 1219, "synset": "contact_sport.n.01", "name": "contact_sport"}, - {"id": 1220, "synset": "outdoor_sport.n.01", "name": "outdoor_sport"}, - {"id": 1221, "synset": "gymnastics.n.01", "name": "gymnastics"}, - {"id": 1222, "synset": "acrobatics.n.01", "name": "acrobatics"}, - {"id": 1223, "synset": "track_and_field.n.01", "name": "track_and_field"}, - {"id": 1224, "synset": "track.n.11", "name": "track"}, - {"id": 1225, "synset": "jumping.n.01", "name": "jumping"}, - {"id": 1226, "synset": "broad_jump.n.02", "name": "broad_jump"}, - {"id": 1227, "synset": "high_jump.n.02", "name": "high_jump"}, - {"id": 1228, "synset": "fosbury_flop.n.01", "name": "Fosbury_flop"}, - {"id": 1229, "synset": "skiing.n.01", "name": "skiing"}, - {"id": 1230, "synset": "cross-country_skiing.n.01", "name": "cross-country_skiing"}, - {"id": 1231, "synset": "ski_jumping.n.01", "name": "ski_jumping"}, - {"id": 1232, "synset": "water_sport.n.01", "name": "water_sport"}, - {"id": 1233, "synset": "swimming.n.01", "name": "swimming"}, - {"id": 1234, "synset": "bathe.n.01", "name": "bathe"}, - {"id": 1235, "synset": "dip.n.08", "name": "dip"}, - {"id": 1236, "synset": "dive.n.02", "name": "dive"}, - {"id": 1237, "synset": "floating.n.01", "name": "floating"}, - {"id": 1238, "synset": "dead-man's_float.n.01", "name": "dead-man's_float"}, - {"id": 1239, "synset": "belly_flop.n.01", "name": "belly_flop"}, - {"id": 1240, "synset": "cliff_diving.n.01", "name": "cliff_diving"}, - {"id": 1241, "synset": "flip.n.05", "name": "flip"}, - {"id": 1242, "synset": "gainer.n.03", "name": "gainer"}, - {"id": 1243, "synset": "half_gainer.n.01", "name": "half_gainer"}, - {"id": 1244, "synset": "jackknife.n.02", "name": "jackknife"}, - {"id": 1245, "synset": "swan_dive.n.01", "name": "swan_dive"}, - {"id": 1246, "synset": "skin_diving.n.01", "name": "skin_diving"}, - {"id": 1247, "synset": "scuba_diving.n.01", "name": "scuba_diving"}, - {"id": 1248, "synset": "snorkeling.n.01", "name": "snorkeling"}, - {"id": 1249, "synset": "surfing.n.01", "name": "surfing"}, - {"id": 1250, "synset": "water-skiing.n.01", "name": "water-skiing"}, - {"id": 1251, "synset": "rowing.n.01", "name": "rowing"}, - {"id": 1252, "synset": "sculling.n.01", "name": "sculling"}, - {"id": 1253, "synset": "boxing.n.01", "name": "boxing"}, - {"id": 1254, "synset": "professional_boxing.n.01", "name": "professional_boxing"}, - {"id": 1255, "synset": "in-fighting.n.02", "name": "in-fighting"}, - {"id": 1256, "synset": "fight.n.05", "name": "fight"}, - {"id": 1257, "synset": "rope-a-dope.n.01", "name": "rope-a-dope"}, - {"id": 1258, "synset": "spar.n.03", "name": "spar"}, - {"id": 1259, "synset": "archery.n.01", "name": "archery"}, - {"id": 1260, "synset": "sledding.n.01", "name": "sledding"}, - {"id": 1261, "synset": "tobogganing.n.01", "name": "tobogganing"}, - {"id": 1262, "synset": "luging.n.01", "name": "luging"}, - {"id": 1263, "synset": "bobsledding.n.01", "name": "bobsledding"}, - {"id": 1264, "synset": "wrestling.n.02", "name": "wrestling"}, - {"id": 1265, "synset": "greco-roman_wrestling.n.01", "name": "Greco-Roman_wrestling"}, - {"id": 1266, "synset": "professional_wrestling.n.01", "name": "professional_wrestling"}, - {"id": 1267, "synset": "sumo.n.01", "name": "sumo"}, - {"id": 1268, "synset": "skating.n.01", "name": "skating"}, - {"id": 1269, "synset": "ice_skating.n.01", "name": "ice_skating"}, - {"id": 1270, "synset": "figure_skating.n.01", "name": "figure_skating"}, - {"id": 1271, "synset": "rollerblading.n.01", "name": "rollerblading"}, - {"id": 1272, "synset": "roller_skating.n.01", "name": "roller_skating"}, - {"id": 1273, "synset": "skateboarding.n.01", "name": "skateboarding"}, - {"id": 1274, "synset": "speed_skating.n.01", "name": "speed_skating"}, - {"id": 1275, "synset": "racing.n.01", "name": "racing"}, - {"id": 1276, "synset": "auto_racing.n.01", "name": "auto_racing"}, - {"id": 1277, "synset": "boat_racing.n.01", "name": "boat_racing"}, - {"id": 1278, "synset": "hydroplane_racing.n.01", "name": "hydroplane_racing"}, - {"id": 1279, "synset": "camel_racing.n.01", "name": "camel_racing"}, - {"id": 1280, "synset": "greyhound_racing.n.01", "name": "greyhound_racing"}, - {"id": 1281, "synset": "horse_racing.n.01", "name": "horse_racing"}, - {"id": 1282, "synset": "riding.n.01", "name": "riding"}, - {"id": 1283, "synset": "equestrian_sport.n.01", "name": "equestrian_sport"}, - {"id": 1284, "synset": "pony-trekking.n.01", "name": "pony-trekking"}, - {"id": 1285, "synset": "showjumping.n.01", "name": "showjumping"}, - {"id": 1286, "synset": "cross-country_riding.n.01", "name": "cross-country_riding"}, - {"id": 1287, "synset": "cycling.n.01", "name": "cycling"}, - {"id": 1288, "synset": "bicycling.n.01", "name": "bicycling"}, - {"id": 1289, "synset": "motorcycling.n.01", "name": "motorcycling"}, - {"id": 1290, "synset": "dune_cycling.n.01", "name": "dune_cycling"}, - {"id": 1291, "synset": "blood_sport.n.01", "name": "blood_sport"}, - {"id": 1292, "synset": "bullfighting.n.01", "name": "bullfighting"}, - {"id": 1293, "synset": "cockfighting.n.01", "name": "cockfighting"}, - {"id": 1294, "synset": "hunt.n.08", "name": "hunt"}, - {"id": 1295, "synset": "battue.n.01", "name": "battue"}, - {"id": 1296, "synset": "beagling.n.01", "name": "beagling"}, - {"id": 1297, "synset": "coursing.n.01", "name": "coursing"}, - {"id": 1298, "synset": "deer_hunting.n.01", "name": "deer_hunting"}, - {"id": 1299, "synset": "ducking.n.01", "name": "ducking"}, - {"id": 1300, "synset": "fox_hunting.n.01", "name": "fox_hunting"}, - {"id": 1301, "synset": "pigsticking.n.01", "name": "pigsticking"}, - {"id": 1302, "synset": "fishing.n.01", "name": "fishing"}, - {"id": 1303, "synset": "angling.n.01", "name": "angling"}, - {"id": 1304, "synset": "fly-fishing.n.01", "name": "fly-fishing"}, - {"id": 1305, "synset": "troll.n.04", "name": "troll"}, - {"id": 1306, "synset": "casting.n.03", "name": "casting"}, - {"id": 1307, "synset": "bait_casting.n.01", "name": "bait_casting"}, - {"id": 1308, "synset": "fly_casting.n.01", "name": "fly_casting"}, - {"id": 1309, "synset": "overcast.n.04", "name": "overcast"}, - {"id": 1310, "synset": "surf_casting.n.01", "name": "surf_casting"}, - {"id": 1311, "synset": "day_game.n.01", "name": "day_game"}, - {"id": 1312, "synset": "athletic_game.n.01", "name": "athletic_game"}, - {"id": 1313, "synset": "ice_hockey.n.01", "name": "ice_hockey"}, - {"id": 1314, "synset": "tetherball.n.01", "name": "tetherball"}, - {"id": 1315, "synset": "water_polo.n.01", "name": "water_polo"}, - {"id": 1316, "synset": "outdoor_game.n.01", "name": "outdoor_game"}, - {"id": 1317, "synset": "golf.n.01", "name": "golf"}, - {"id": 1318, "synset": "professional_golf.n.01", "name": "professional_golf"}, - {"id": 1319, "synset": "round_of_golf.n.01", "name": "round_of_golf"}, - {"id": 1320, "synset": "medal_play.n.01", "name": "medal_play"}, - {"id": 1321, "synset": "match_play.n.01", "name": "match_play"}, - {"id": 1322, "synset": "miniature_golf.n.01", "name": "miniature_golf"}, - {"id": 1323, "synset": "croquet.n.01", "name": "croquet"}, - {"id": 1324, "synset": "quoits.n.01", "name": "quoits"}, - {"id": 1325, "synset": "shuffleboard.n.01", "name": "shuffleboard"}, - {"id": 1326, "synset": "field_game.n.01", "name": "field_game"}, - {"id": 1327, "synset": "field_hockey.n.01", "name": "field_hockey"}, - {"id": 1328, "synset": "shinny.n.01", "name": "shinny"}, - {"id": 1329, "synset": "football.n.01", "name": "football"}, - {"id": 1330, "synset": "american_football.n.01", "name": "American_football"}, - {"id": 1331, "synset": "professional_football.n.01", "name": "professional_football"}, - {"id": 1332, "synset": "touch_football.n.01", "name": "touch_football"}, - {"id": 1333, "synset": "hurling.n.01", "name": "hurling"}, - {"id": 1334, "synset": "rugby.n.01", "name": "rugby"}, - {"id": 1335, "synset": "ball_game.n.01", "name": "ball_game"}, - {"id": 1336, "synset": "baseball.n.01", "name": "baseball"}, - {"id": 1337, "synset": "ball.n.11", "name": "ball"}, - {"id": 1338, "synset": "professional_baseball.n.01", "name": "professional_baseball"}, - {"id": 1339, "synset": "hardball.n.02", "name": "hardball"}, - {"id": 1340, "synset": "perfect_game.n.01", "name": "perfect_game"}, - {"id": 1341, "synset": "no-hit_game.n.01", "name": "no-hit_game"}, - {"id": 1342, "synset": "one-hitter.n.01", "name": "one-hitter"}, - {"id": 1343, "synset": "two-hitter.n.01", "name": "two-hitter"}, - {"id": 1344, "synset": "three-hitter.n.01", "name": "three-hitter"}, - {"id": 1345, "synset": "four-hitter.n.01", "name": "four-hitter"}, - {"id": 1346, "synset": "five-hitter.n.01", "name": "five-hitter"}, - {"id": 1347, "synset": "softball.n.02", "name": "softball"}, - {"id": 1348, "synset": "rounders.n.01", "name": "rounders"}, - {"id": 1349, "synset": "stickball.n.01", "name": "stickball"}, - {"id": 1350, "synset": "cricket.n.02", "name": "cricket"}, - {"id": 1351, "synset": "lacrosse.n.01", "name": "lacrosse"}, - {"id": 1352, "synset": "polo.n.02", "name": "polo"}, - {"id": 1353, "synset": "pushball.n.01", "name": "pushball"}, - {"id": 1354, "synset": "soccer.n.01", "name": "soccer"}, - {"id": 1355, "synset": "court_game.n.01", "name": "court_game"}, - {"id": 1356, "synset": "handball.n.02", "name": "handball"}, - {"id": 1357, "synset": "racquetball.n.02", "name": "racquetball"}, - {"id": 1358, "synset": "fives.n.01", "name": "fives"}, - {"id": 1359, "synset": "squash.n.03", "name": "squash"}, - {"id": 1360, "synset": "volleyball.n.01", "name": "volleyball"}, - {"id": 1361, "synset": "jai_alai.n.01", "name": "jai_alai"}, - {"id": 1362, "synset": "badminton.n.01", "name": "badminton"}, - {"id": 1363, "synset": "battledore.n.02", "name": "battledore"}, - {"id": 1364, "synset": "basketball.n.01", "name": "basketball"}, - {"id": 1365, "synset": "professional_basketball.n.01", "name": "professional_basketball"}, - {"id": 1366, "synset": "deck_tennis.n.01", "name": "deck_tennis"}, - {"id": 1367, "synset": "netball.n.01", "name": "netball"}, - {"id": 1368, "synset": "tennis.n.01", "name": "tennis"}, - {"id": 1369, "synset": "professional_tennis.n.01", "name": "professional_tennis"}, - {"id": 1370, "synset": "singles.n.02", "name": "singles"}, - {"id": 1371, "synset": "singles.n.01", "name": "singles"}, - {"id": 1372, "synset": "doubles.n.02", "name": "doubles"}, - {"id": 1373, "synset": "doubles.n.01", "name": "doubles"}, - {"id": 1374, "synset": "royal_tennis.n.01", "name": "royal_tennis"}, - {"id": 1375, "synset": "pallone.n.01", "name": "pallone"}, - {"id": 1376, "synset": "sport.n.01", "name": "sport"}, - {"id": 1377, "synset": "clasp.n.02", "name": "clasp"}, - {"id": 1378, "synset": "judo.n.01", "name": "judo"}, - {"id": 1379, "synset": "team_sport.n.01", "name": "team_sport"}, - {"id": 1380, "synset": "last_supper.n.01", "name": "Last_Supper"}, - {"id": 1381, "synset": "seder.n.01", "name": "Seder"}, - {"id": 1382, "synset": "camping.n.01", "name": "camping"}, - {"id": 1383, "synset": "pest.n.04", "name": "pest"}, - {"id": 1384, "synset": "critter.n.01", "name": "critter"}, - {"id": 1385, "synset": "creepy-crawly.n.01", "name": "creepy-crawly"}, - {"id": 1386, "synset": "darter.n.02", "name": "darter"}, - {"id": 1387, "synset": "peeper.n.03", "name": "peeper"}, - {"id": 1388, "synset": "homeotherm.n.01", "name": "homeotherm"}, - {"id": 1389, "synset": "poikilotherm.n.01", "name": "poikilotherm"}, - {"id": 1390, "synset": "range_animal.n.01", "name": "range_animal"}, - {"id": 1391, "synset": "scavenger.n.03", "name": "scavenger"}, - {"id": 1392, "synset": "bottom-feeder.n.02", "name": "bottom-feeder"}, - {"id": 1393, "synset": "bottom-feeder.n.01", "name": "bottom-feeder"}, - {"id": 1394, "synset": "work_animal.n.01", "name": "work_animal"}, - {"id": 1395, "synset": "beast_of_burden.n.01", "name": "beast_of_burden"}, - {"id": 1396, "synset": "draft_animal.n.01", "name": "draft_animal"}, - {"id": 1397, "synset": "pack_animal.n.01", "name": "pack_animal"}, - {"id": 1398, "synset": "domestic_animal.n.01", "name": "domestic_animal"}, - {"id": 1399, "synset": "feeder.n.01", "name": "feeder"}, - {"id": 1400, "synset": "feeder.n.06", "name": "feeder"}, - {"id": 1401, "synset": "stocker.n.01", "name": "stocker"}, - {"id": 1402, "synset": "hatchling.n.01", "name": "hatchling"}, - {"id": 1403, "synset": "head.n.02", "name": "head"}, - {"id": 1404, "synset": "migrator.n.02", "name": "migrator"}, - {"id": 1405, "synset": "molter.n.01", "name": "molter"}, - {"id": 1406, "synset": "stayer.n.01", "name": "stayer"}, - {"id": 1407, "synset": "stunt.n.02", "name": "stunt"}, - {"id": 1408, "synset": "marine_animal.n.01", "name": "marine_animal"}, - {"id": 1409, "synset": "by-catch.n.01", "name": "by-catch"}, - {"id": 1410, "synset": "female.n.01", "name": "female"}, - {"id": 1411, "synset": "hen.n.04", "name": "hen"}, - {"id": 1412, "synset": "male.n.01", "name": "male"}, - {"id": 1413, "synset": "adult.n.02", "name": "adult"}, - {"id": 1414, "synset": "young.n.01", "name": "young"}, - {"id": 1415, "synset": "orphan.n.04", "name": "orphan"}, - {"id": 1416, "synset": "young_mammal.n.01", "name": "young_mammal"}, - {"id": 1417, "synset": "baby.n.06", "name": "baby"}, - {"id": 1418, "synset": "pup.n.01", "name": "pup"}, - {"id": 1419, "synset": "wolf_pup.n.01", "name": "wolf_pup"}, - {"id": 1420, "synset": "lion_cub.n.01", "name": "lion_cub"}, - {"id": 1421, "synset": "bear_cub.n.01", "name": "bear_cub"}, - {"id": 1422, "synset": "tiger_cub.n.01", "name": "tiger_cub"}, - {"id": 1423, "synset": "kit.n.03", "name": "kit"}, - {"id": 1424, "synset": "suckling.n.03", "name": "suckling"}, - {"id": 1425, "synset": "sire.n.03", "name": "sire"}, - {"id": 1426, "synset": "dam.n.03", "name": "dam"}, - {"id": 1427, "synset": "thoroughbred.n.03", "name": "thoroughbred"}, - {"id": 1428, "synset": "giant.n.01", "name": "giant"}, - {"id": 1429, "synset": "mutant.n.02", "name": "mutant"}, - {"id": 1430, "synset": "carnivore.n.02", "name": "carnivore"}, - {"id": 1431, "synset": "herbivore.n.01", "name": "herbivore"}, - {"id": 1432, "synset": "insectivore.n.02", "name": "insectivore"}, - {"id": 1433, "synset": "acrodont.n.01", "name": "acrodont"}, - {"id": 1434, "synset": "pleurodont.n.01", "name": "pleurodont"}, - {"id": 1435, "synset": "microorganism.n.01", "name": "microorganism"}, - {"id": 1436, "synset": "monohybrid.n.01", "name": "monohybrid"}, - {"id": 1437, "synset": "arbovirus.n.01", "name": "arbovirus"}, - {"id": 1438, "synset": "adenovirus.n.01", "name": "adenovirus"}, - {"id": 1439, "synset": "arenavirus.n.01", "name": "arenavirus"}, - {"id": 1440, "synset": "marburg_virus.n.01", "name": "Marburg_virus"}, - {"id": 1441, "synset": "arenaviridae.n.01", "name": "Arenaviridae"}, - {"id": 1442, "synset": "vesiculovirus.n.01", "name": "vesiculovirus"}, - {"id": 1443, "synset": "reoviridae.n.01", "name": "Reoviridae"}, - {"id": 1444, "synset": "variola_major.n.02", "name": "variola_major"}, - {"id": 1445, "synset": "viroid.n.01", "name": "viroid"}, - {"id": 1446, "synset": "coliphage.n.01", "name": "coliphage"}, - {"id": 1447, "synset": "paramyxovirus.n.01", "name": "paramyxovirus"}, - {"id": 1448, "synset": "poliovirus.n.01", "name": "poliovirus"}, - {"id": 1449, "synset": "herpes.n.02", "name": "herpes"}, - {"id": 1450, "synset": "herpes_simplex_1.n.01", "name": "herpes_simplex_1"}, - {"id": 1451, "synset": "herpes_zoster.n.02", "name": "herpes_zoster"}, - {"id": 1452, "synset": "herpes_varicella_zoster.n.01", "name": "herpes_varicella_zoster"}, - {"id": 1453, "synset": "cytomegalovirus.n.01", "name": "cytomegalovirus"}, - {"id": 1454, "synset": "varicella_zoster_virus.n.01", "name": "varicella_zoster_virus"}, - {"id": 1455, "synset": "polyoma.n.01", "name": "polyoma"}, - {"id": 1456, "synset": "lyssavirus.n.01", "name": "lyssavirus"}, - {"id": 1457, "synset": "reovirus.n.01", "name": "reovirus"}, - {"id": 1458, "synset": "rotavirus.n.01", "name": "rotavirus"}, - {"id": 1459, "synset": "moneran.n.01", "name": "moneran"}, - {"id": 1460, "synset": "archaebacteria.n.01", "name": "archaebacteria"}, - {"id": 1461, "synset": "bacteroid.n.01", "name": "bacteroid"}, - {"id": 1462, "synset": "bacillus_anthracis.n.01", "name": "Bacillus_anthracis"}, - {"id": 1463, "synset": "yersinia_pestis.n.01", "name": "Yersinia_pestis"}, - {"id": 1464, "synset": "brucella.n.01", "name": "Brucella"}, - {"id": 1465, "synset": "spirillum.n.02", "name": "spirillum"}, - {"id": 1466, "synset": "botulinus.n.01", "name": "botulinus"}, - {"id": 1467, "synset": "clostridium_perfringens.n.01", "name": "clostridium_perfringens"}, - {"id": 1468, "synset": "cyanobacteria.n.01", "name": "cyanobacteria"}, - {"id": 1469, "synset": "trichodesmium.n.01", "name": "trichodesmium"}, - {"id": 1470, "synset": "nitric_bacteria.n.01", "name": "nitric_bacteria"}, - {"id": 1471, "synset": "spirillum.n.01", "name": "spirillum"}, - {"id": 1472, "synset": "francisella.n.01", "name": "Francisella"}, - {"id": 1473, "synset": "gonococcus.n.01", "name": "gonococcus"}, - { - "id": 1474, - "synset": "corynebacterium_diphtheriae.n.01", - "name": "Corynebacterium_diphtheriae", - }, - {"id": 1475, "synset": "enteric_bacteria.n.01", "name": "enteric_bacteria"}, - {"id": 1476, "synset": "klebsiella.n.01", "name": "klebsiella"}, - {"id": 1477, "synset": "salmonella_typhimurium.n.01", "name": "Salmonella_typhimurium"}, - {"id": 1478, "synset": "typhoid_bacillus.n.01", "name": "typhoid_bacillus"}, - {"id": 1479, "synset": "nitrate_bacterium.n.01", "name": "nitrate_bacterium"}, - {"id": 1480, "synset": "nitrite_bacterium.n.01", "name": "nitrite_bacterium"}, - {"id": 1481, "synset": "actinomycete.n.01", "name": "actinomycete"}, - {"id": 1482, "synset": "streptomyces.n.01", "name": "streptomyces"}, - {"id": 1483, "synset": "streptomyces_erythreus.n.01", "name": "Streptomyces_erythreus"}, - {"id": 1484, "synset": "streptomyces_griseus.n.01", "name": "Streptomyces_griseus"}, - {"id": 1485, "synset": "tubercle_bacillus.n.01", "name": "tubercle_bacillus"}, - {"id": 1486, "synset": "pus-forming_bacteria.n.01", "name": "pus-forming_bacteria"}, - {"id": 1487, "synset": "streptobacillus.n.01", "name": "streptobacillus"}, - {"id": 1488, "synset": "myxobacteria.n.01", "name": "myxobacteria"}, - {"id": 1489, "synset": "staphylococcus.n.01", "name": "staphylococcus"}, - {"id": 1490, "synset": "diplococcus.n.01", "name": "diplococcus"}, - {"id": 1491, "synset": "pneumococcus.n.01", "name": "pneumococcus"}, - {"id": 1492, "synset": "streptococcus.n.01", "name": "streptococcus"}, - {"id": 1493, "synset": "spirochete.n.01", "name": "spirochete"}, - {"id": 1494, "synset": "planktonic_algae.n.01", "name": "planktonic_algae"}, - {"id": 1495, "synset": "zooplankton.n.01", "name": "zooplankton"}, - {"id": 1496, "synset": "parasite.n.01", "name": "parasite"}, - {"id": 1497, "synset": "endoparasite.n.01", "name": "endoparasite"}, - {"id": 1498, "synset": "ectoparasite.n.01", "name": "ectoparasite"}, - {"id": 1499, "synset": "pathogen.n.01", "name": "pathogen"}, - {"id": 1500, "synset": "commensal.n.01", "name": "commensal"}, - {"id": 1501, "synset": "myrmecophile.n.01", "name": "myrmecophile"}, - {"id": 1502, "synset": "protoctist.n.01", "name": "protoctist"}, - {"id": 1503, "synset": "protozoan.n.01", "name": "protozoan"}, - {"id": 1504, "synset": "sarcodinian.n.01", "name": "sarcodinian"}, - {"id": 1505, "synset": "heliozoan.n.01", "name": "heliozoan"}, - {"id": 1506, "synset": "endameba.n.01", "name": "endameba"}, - {"id": 1507, "synset": "ameba.n.01", "name": "ameba"}, - {"id": 1508, "synset": "globigerina.n.01", "name": "globigerina"}, - {"id": 1509, "synset": "testacean.n.01", "name": "testacean"}, - {"id": 1510, "synset": "arcella.n.01", "name": "arcella"}, - {"id": 1511, "synset": "difflugia.n.01", "name": "difflugia"}, - {"id": 1512, "synset": "ciliate.n.01", "name": "ciliate"}, - {"id": 1513, "synset": "paramecium.n.01", "name": "paramecium"}, - {"id": 1514, "synset": "stentor.n.03", "name": "stentor"}, - {"id": 1515, "synset": "alga.n.01", "name": "alga"}, - {"id": 1516, "synset": "arame.n.01", "name": "arame"}, - {"id": 1517, "synset": "seagrass.n.01", "name": "seagrass"}, - {"id": 1518, "synset": "golden_algae.n.01", "name": "golden_algae"}, - {"id": 1519, "synset": "yellow-green_algae.n.01", "name": "yellow-green_algae"}, - {"id": 1520, "synset": "brown_algae.n.01", "name": "brown_algae"}, - {"id": 1521, "synset": "kelp.n.01", "name": "kelp"}, - {"id": 1522, "synset": "fucoid.n.02", "name": "fucoid"}, - {"id": 1523, "synset": "fucoid.n.01", "name": "fucoid"}, - {"id": 1524, "synset": "fucus.n.01", "name": "fucus"}, - {"id": 1525, "synset": "bladderwrack.n.01", "name": "bladderwrack"}, - {"id": 1526, "synset": "green_algae.n.01", "name": "green_algae"}, - {"id": 1527, "synset": "pond_scum.n.01", "name": "pond_scum"}, - {"id": 1528, "synset": "chlorella.n.01", "name": "chlorella"}, - {"id": 1529, "synset": "stonewort.n.01", "name": "stonewort"}, - {"id": 1530, "synset": "desmid.n.01", "name": "desmid"}, - {"id": 1531, "synset": "sea_moss.n.02", "name": "sea_moss"}, - {"id": 1532, "synset": "eukaryote.n.01", "name": "eukaryote"}, - {"id": 1533, "synset": "prokaryote.n.01", "name": "prokaryote"}, - {"id": 1534, "synset": "zooid.n.01", "name": "zooid"}, - {"id": 1535, "synset": "leishmania.n.01", "name": "Leishmania"}, - {"id": 1536, "synset": "zoomastigote.n.01", "name": "zoomastigote"}, - {"id": 1537, "synset": "polymastigote.n.01", "name": "polymastigote"}, - {"id": 1538, "synset": "costia.n.01", "name": "costia"}, - {"id": 1539, "synset": "giardia.n.01", "name": "giardia"}, - {"id": 1540, "synset": "cryptomonad.n.01", "name": "cryptomonad"}, - {"id": 1541, "synset": "sporozoan.n.01", "name": "sporozoan"}, - {"id": 1542, "synset": "sporozoite.n.01", "name": "sporozoite"}, - {"id": 1543, "synset": "trophozoite.n.01", "name": "trophozoite"}, - {"id": 1544, "synset": "merozoite.n.01", "name": "merozoite"}, - {"id": 1545, "synset": "coccidium.n.01", "name": "coccidium"}, - {"id": 1546, "synset": "gregarine.n.01", "name": "gregarine"}, - {"id": 1547, "synset": "plasmodium.n.02", "name": "plasmodium"}, - {"id": 1548, "synset": "leucocytozoan.n.01", "name": "leucocytozoan"}, - {"id": 1549, "synset": "microsporidian.n.01", "name": "microsporidian"}, - {"id": 1550, "synset": "ostariophysi.n.01", "name": "Ostariophysi"}, - {"id": 1551, "synset": "cypriniform_fish.n.01", "name": "cypriniform_fish"}, - {"id": 1552, "synset": "loach.n.01", "name": "loach"}, - {"id": 1553, "synset": "cyprinid.n.01", "name": "cyprinid"}, - {"id": 1554, "synset": "carp.n.02", "name": "carp"}, - {"id": 1555, "synset": "domestic_carp.n.01", "name": "domestic_carp"}, - {"id": 1556, "synset": "leather_carp.n.01", "name": "leather_carp"}, - {"id": 1557, "synset": "mirror_carp.n.01", "name": "mirror_carp"}, - {"id": 1558, "synset": "european_bream.n.01", "name": "European_bream"}, - {"id": 1559, "synset": "tench.n.01", "name": "tench"}, - {"id": 1560, "synset": "dace.n.01", "name": "dace"}, - {"id": 1561, "synset": "chub.n.01", "name": "chub"}, - {"id": 1562, "synset": "shiner.n.04", "name": "shiner"}, - {"id": 1563, "synset": "common_shiner.n.01", "name": "common_shiner"}, - {"id": 1564, "synset": "roach.n.05", "name": "roach"}, - {"id": 1565, "synset": "rudd.n.01", "name": "rudd"}, - {"id": 1566, "synset": "minnow.n.01", "name": "minnow"}, - {"id": 1567, "synset": "gudgeon.n.02", "name": "gudgeon"}, - {"id": 1568, "synset": "crucian_carp.n.01", "name": "crucian_carp"}, - {"id": 1569, "synset": "electric_eel.n.01", "name": "electric_eel"}, - {"id": 1570, "synset": "catostomid.n.01", "name": "catostomid"}, - {"id": 1571, "synset": "buffalo_fish.n.01", "name": "buffalo_fish"}, - {"id": 1572, "synset": "black_buffalo.n.01", "name": "black_buffalo"}, - {"id": 1573, "synset": "hog_sucker.n.01", "name": "hog_sucker"}, - {"id": 1574, "synset": "redhorse.n.01", "name": "redhorse"}, - {"id": 1575, "synset": "cyprinodont.n.01", "name": "cyprinodont"}, - {"id": 1576, "synset": "killifish.n.01", "name": "killifish"}, - {"id": 1577, "synset": "mummichog.n.01", "name": "mummichog"}, - {"id": 1578, "synset": "striped_killifish.n.01", "name": "striped_killifish"}, - {"id": 1579, "synset": "rivulus.n.01", "name": "rivulus"}, - {"id": 1580, "synset": "flagfish.n.01", "name": "flagfish"}, - {"id": 1581, "synset": "swordtail.n.01", "name": "swordtail"}, - {"id": 1582, "synset": "guppy.n.01", "name": "guppy"}, - {"id": 1583, "synset": "topminnow.n.01", "name": "topminnow"}, - {"id": 1584, "synset": "mosquitofish.n.01", "name": "mosquitofish"}, - {"id": 1585, "synset": "platy.n.01", "name": "platy"}, - {"id": 1586, "synset": "mollie.n.01", "name": "mollie"}, - {"id": 1587, "synset": "squirrelfish.n.02", "name": "squirrelfish"}, - {"id": 1588, "synset": "reef_squirrelfish.n.01", "name": "reef_squirrelfish"}, - {"id": 1589, "synset": "deepwater_squirrelfish.n.01", "name": "deepwater_squirrelfish"}, - {"id": 1590, "synset": "holocentrus_ascensionis.n.01", "name": "Holocentrus_ascensionis"}, - {"id": 1591, "synset": "soldierfish.n.01", "name": "soldierfish"}, - {"id": 1592, "synset": "anomalops.n.01", "name": "anomalops"}, - {"id": 1593, "synset": "flashlight_fish.n.01", "name": "flashlight_fish"}, - {"id": 1594, "synset": "john_dory.n.01", "name": "John_Dory"}, - {"id": 1595, "synset": "boarfish.n.02", "name": "boarfish"}, - {"id": 1596, "synset": "boarfish.n.01", "name": "boarfish"}, - {"id": 1597, "synset": "cornetfish.n.01", "name": "cornetfish"}, - {"id": 1598, "synset": "stickleback.n.01", "name": "stickleback"}, - {"id": 1599, "synset": "three-spined_stickleback.n.01", "name": "three-spined_stickleback"}, - {"id": 1600, "synset": "ten-spined_stickleback.n.01", "name": "ten-spined_stickleback"}, - {"id": 1601, "synset": "pipefish.n.01", "name": "pipefish"}, - {"id": 1602, "synset": "dwarf_pipefish.n.01", "name": "dwarf_pipefish"}, - {"id": 1603, "synset": "deepwater_pipefish.n.01", "name": "deepwater_pipefish"}, - {"id": 1604, "synset": "snipefish.n.01", "name": "snipefish"}, - {"id": 1605, "synset": "shrimpfish.n.01", "name": "shrimpfish"}, - {"id": 1606, "synset": "trumpetfish.n.01", "name": "trumpetfish"}, - {"id": 1607, "synset": "pellicle.n.01", "name": "pellicle"}, - {"id": 1608, "synset": "embryo.n.02", "name": "embryo"}, - {"id": 1609, "synset": "fetus.n.01", "name": "fetus"}, - {"id": 1610, "synset": "abortus.n.01", "name": "abortus"}, - {"id": 1611, "synset": "spawn.n.01", "name": "spawn"}, - {"id": 1612, "synset": "blastula.n.01", "name": "blastula"}, - {"id": 1613, "synset": "blastocyst.n.01", "name": "blastocyst"}, - {"id": 1614, "synset": "gastrula.n.01", "name": "gastrula"}, - {"id": 1615, "synset": "morula.n.01", "name": "morula"}, - {"id": 1616, "synset": "yolk.n.02", "name": "yolk"}, - {"id": 1617, "synset": "chordate.n.01", "name": "chordate"}, - {"id": 1618, "synset": "cephalochordate.n.01", "name": "cephalochordate"}, - {"id": 1619, "synset": "lancelet.n.01", "name": "lancelet"}, - {"id": 1620, "synset": "tunicate.n.01", "name": "tunicate"}, - {"id": 1621, "synset": "ascidian.n.01", "name": "ascidian"}, - {"id": 1622, "synset": "sea_squirt.n.01", "name": "sea_squirt"}, - {"id": 1623, "synset": "salp.n.01", "name": "salp"}, - {"id": 1624, "synset": "doliolum.n.01", "name": "doliolum"}, - {"id": 1625, "synset": "larvacean.n.01", "name": "larvacean"}, - {"id": 1626, "synset": "appendicularia.n.01", "name": "appendicularia"}, - {"id": 1627, "synset": "ascidian_tadpole.n.01", "name": "ascidian_tadpole"}, - {"id": 1628, "synset": "vertebrate.n.01", "name": "vertebrate"}, - {"id": 1629, "synset": "amniota.n.01", "name": "Amniota"}, - {"id": 1630, "synset": "amniote.n.01", "name": "amniote"}, - {"id": 1631, "synset": "aquatic_vertebrate.n.01", "name": "aquatic_vertebrate"}, - {"id": 1632, "synset": "jawless_vertebrate.n.01", "name": "jawless_vertebrate"}, - {"id": 1633, "synset": "ostracoderm.n.01", "name": "ostracoderm"}, - {"id": 1634, "synset": "heterostracan.n.01", "name": "heterostracan"}, - {"id": 1635, "synset": "anaspid.n.01", "name": "anaspid"}, - {"id": 1636, "synset": "conodont.n.02", "name": "conodont"}, - {"id": 1637, "synset": "cyclostome.n.01", "name": "cyclostome"}, - {"id": 1638, "synset": "lamprey.n.01", "name": "lamprey"}, - {"id": 1639, "synset": "sea_lamprey.n.01", "name": "sea_lamprey"}, - {"id": 1640, "synset": "hagfish.n.01", "name": "hagfish"}, - {"id": 1641, "synset": "myxine_glutinosa.n.01", "name": "Myxine_glutinosa"}, - {"id": 1642, "synset": "eptatretus.n.01", "name": "eptatretus"}, - {"id": 1643, "synset": "gnathostome.n.01", "name": "gnathostome"}, - {"id": 1644, "synset": "placoderm.n.01", "name": "placoderm"}, - {"id": 1645, "synset": "cartilaginous_fish.n.01", "name": "cartilaginous_fish"}, - {"id": 1646, "synset": "holocephalan.n.01", "name": "holocephalan"}, - {"id": 1647, "synset": "chimaera.n.03", "name": "chimaera"}, - {"id": 1648, "synset": "rabbitfish.n.01", "name": "rabbitfish"}, - {"id": 1649, "synset": "elasmobranch.n.01", "name": "elasmobranch"}, - {"id": 1650, "synset": "cow_shark.n.01", "name": "cow_shark"}, - {"id": 1651, "synset": "mackerel_shark.n.01", "name": "mackerel_shark"}, - {"id": 1652, "synset": "porbeagle.n.01", "name": "porbeagle"}, - {"id": 1653, "synset": "mako.n.01", "name": "mako"}, - {"id": 1654, "synset": "shortfin_mako.n.01", "name": "shortfin_mako"}, - {"id": 1655, "synset": "longfin_mako.n.01", "name": "longfin_mako"}, - {"id": 1656, "synset": "bonito_shark.n.01", "name": "bonito_shark"}, - {"id": 1657, "synset": "great_white_shark.n.01", "name": "great_white_shark"}, - {"id": 1658, "synset": "basking_shark.n.01", "name": "basking_shark"}, - {"id": 1659, "synset": "thresher.n.02", "name": "thresher"}, - {"id": 1660, "synset": "carpet_shark.n.01", "name": "carpet_shark"}, - {"id": 1661, "synset": "nurse_shark.n.01", "name": "nurse_shark"}, - {"id": 1662, "synset": "sand_tiger.n.01", "name": "sand_tiger"}, - {"id": 1663, "synset": "whale_shark.n.01", "name": "whale_shark"}, - {"id": 1664, "synset": "requiem_shark.n.01", "name": "requiem_shark"}, - {"id": 1665, "synset": "bull_shark.n.01", "name": "bull_shark"}, - {"id": 1666, "synset": "sandbar_shark.n.02", "name": "sandbar_shark"}, - {"id": 1667, "synset": "blacktip_shark.n.01", "name": "blacktip_shark"}, - {"id": 1668, "synset": "whitetip_shark.n.02", "name": "whitetip_shark"}, - {"id": 1669, "synset": "dusky_shark.n.01", "name": "dusky_shark"}, - {"id": 1670, "synset": "lemon_shark.n.01", "name": "lemon_shark"}, - {"id": 1671, "synset": "blue_shark.n.01", "name": "blue_shark"}, - {"id": 1672, "synset": "tiger_shark.n.01", "name": "tiger_shark"}, - {"id": 1673, "synset": "soupfin_shark.n.01", "name": "soupfin_shark"}, - {"id": 1674, "synset": "dogfish.n.02", "name": "dogfish"}, - {"id": 1675, "synset": "smooth_dogfish.n.01", "name": "smooth_dogfish"}, - {"id": 1676, "synset": "smoothhound.n.01", "name": "smoothhound"}, - {"id": 1677, "synset": "american_smooth_dogfish.n.01", "name": "American_smooth_dogfish"}, - {"id": 1678, "synset": "florida_smoothhound.n.01", "name": "Florida_smoothhound"}, - {"id": 1679, "synset": "whitetip_shark.n.01", "name": "whitetip_shark"}, - {"id": 1680, "synset": "spiny_dogfish.n.01", "name": "spiny_dogfish"}, - {"id": 1681, "synset": "atlantic_spiny_dogfish.n.01", "name": "Atlantic_spiny_dogfish"}, - {"id": 1682, "synset": "pacific_spiny_dogfish.n.01", "name": "Pacific_spiny_dogfish"}, - {"id": 1683, "synset": "hammerhead.n.03", "name": "hammerhead"}, - {"id": 1684, "synset": "smooth_hammerhead.n.01", "name": "smooth_hammerhead"}, - {"id": 1685, "synset": "smalleye_hammerhead.n.01", "name": "smalleye_hammerhead"}, - {"id": 1686, "synset": "shovelhead.n.01", "name": "shovelhead"}, - {"id": 1687, "synset": "angel_shark.n.01", "name": "angel_shark"}, - {"id": 1688, "synset": "ray.n.07", "name": "ray"}, - {"id": 1689, "synset": "electric_ray.n.01", "name": "electric_ray"}, - {"id": 1690, "synset": "sawfish.n.01", "name": "sawfish"}, - {"id": 1691, "synset": "smalltooth_sawfish.n.01", "name": "smalltooth_sawfish"}, - {"id": 1692, "synset": "guitarfish.n.01", "name": "guitarfish"}, - {"id": 1693, "synset": "stingray.n.01", "name": "stingray"}, - {"id": 1694, "synset": "roughtail_stingray.n.01", "name": "roughtail_stingray"}, - {"id": 1695, "synset": "butterfly_ray.n.01", "name": "butterfly_ray"}, - {"id": 1696, "synset": "eagle_ray.n.01", "name": "eagle_ray"}, - {"id": 1697, "synset": "spotted_eagle_ray.n.01", "name": "spotted_eagle_ray"}, - {"id": 1698, "synset": "cownose_ray.n.01", "name": "cownose_ray"}, - {"id": 1699, "synset": "manta.n.02", "name": "manta"}, - {"id": 1700, "synset": "atlantic_manta.n.01", "name": "Atlantic_manta"}, - {"id": 1701, "synset": "devil_ray.n.01", "name": "devil_ray"}, - {"id": 1702, "synset": "skate.n.02", "name": "skate"}, - {"id": 1703, "synset": "grey_skate.n.01", "name": "grey_skate"}, - {"id": 1704, "synset": "little_skate.n.01", "name": "little_skate"}, - {"id": 1705, "synset": "thorny_skate.n.01", "name": "thorny_skate"}, - {"id": 1706, "synset": "barndoor_skate.n.01", "name": "barndoor_skate"}, - {"id": 1707, "synset": "dickeybird.n.01", "name": "dickeybird"}, - {"id": 1708, "synset": "fledgling.n.02", "name": "fledgling"}, - {"id": 1709, "synset": "nestling.n.01", "name": "nestling"}, - {"id": 1710, "synset": "cock.n.05", "name": "cock"}, - {"id": 1711, "synset": "gamecock.n.01", "name": "gamecock"}, - {"id": 1712, "synset": "hen.n.02", "name": "hen"}, - {"id": 1713, "synset": "nester.n.02", "name": "nester"}, - {"id": 1714, "synset": "night_bird.n.01", "name": "night_bird"}, - {"id": 1715, "synset": "night_raven.n.02", "name": "night_raven"}, - {"id": 1716, "synset": "bird_of_passage.n.02", "name": "bird_of_passage"}, - {"id": 1717, "synset": "archaeopteryx.n.01", "name": "archaeopteryx"}, - {"id": 1718, "synset": "archaeornis.n.01", "name": "archaeornis"}, - {"id": 1719, "synset": "ratite.n.01", "name": "ratite"}, - {"id": 1720, "synset": "carinate.n.01", "name": "carinate"}, - {"id": 1721, "synset": "cassowary.n.01", "name": "cassowary"}, - {"id": 1722, "synset": "emu.n.02", "name": "emu"}, - {"id": 1723, "synset": "kiwi.n.04", "name": "kiwi"}, - {"id": 1724, "synset": "rhea.n.03", "name": "rhea"}, - {"id": 1725, "synset": "rhea.n.02", "name": "rhea"}, - {"id": 1726, "synset": "elephant_bird.n.01", "name": "elephant_bird"}, - {"id": 1727, "synset": "moa.n.01", "name": "moa"}, - {"id": 1728, "synset": "passerine.n.01", "name": "passerine"}, - {"id": 1729, "synset": "nonpasserine_bird.n.01", "name": "nonpasserine_bird"}, - {"id": 1730, "synset": "oscine.n.01", "name": "oscine"}, - {"id": 1731, "synset": "songbird.n.01", "name": "songbird"}, - {"id": 1732, "synset": "honey_eater.n.01", "name": "honey_eater"}, - {"id": 1733, "synset": "accentor.n.01", "name": "accentor"}, - {"id": 1734, "synset": "hedge_sparrow.n.01", "name": "hedge_sparrow"}, - {"id": 1735, "synset": "lark.n.03", "name": "lark"}, - {"id": 1736, "synset": "skylark.n.01", "name": "skylark"}, - {"id": 1737, "synset": "wagtail.n.01", "name": "wagtail"}, - {"id": 1738, "synset": "pipit.n.01", "name": "pipit"}, - {"id": 1739, "synset": "meadow_pipit.n.01", "name": "meadow_pipit"}, - {"id": 1740, "synset": "finch.n.01", "name": "finch"}, - {"id": 1741, "synset": "chaffinch.n.01", "name": "chaffinch"}, - {"id": 1742, "synset": "brambling.n.01", "name": "brambling"}, - {"id": 1743, "synset": "goldfinch.n.02", "name": "goldfinch"}, - {"id": 1744, "synset": "linnet.n.02", "name": "linnet"}, - {"id": 1745, "synset": "siskin.n.01", "name": "siskin"}, - {"id": 1746, "synset": "red_siskin.n.01", "name": "red_siskin"}, - {"id": 1747, "synset": "redpoll.n.02", "name": "redpoll"}, - {"id": 1748, "synset": "redpoll.n.01", "name": "redpoll"}, - {"id": 1749, "synset": "new_world_goldfinch.n.01", "name": "New_World_goldfinch"}, - {"id": 1750, "synset": "pine_siskin.n.01", "name": "pine_siskin"}, - {"id": 1751, "synset": "house_finch.n.01", "name": "house_finch"}, - {"id": 1752, "synset": "purple_finch.n.01", "name": "purple_finch"}, - {"id": 1753, "synset": "canary.n.04", "name": "canary"}, - {"id": 1754, "synset": "common_canary.n.01", "name": "common_canary"}, - {"id": 1755, "synset": "serin.n.01", "name": "serin"}, - {"id": 1756, "synset": "crossbill.n.01", "name": "crossbill"}, - {"id": 1757, "synset": "bullfinch.n.02", "name": "bullfinch"}, - {"id": 1758, "synset": "junco.n.01", "name": "junco"}, - {"id": 1759, "synset": "dark-eyed_junco.n.01", "name": "dark-eyed_junco"}, - {"id": 1760, "synset": "new_world_sparrow.n.01", "name": "New_World_sparrow"}, - {"id": 1761, "synset": "vesper_sparrow.n.01", "name": "vesper_sparrow"}, - {"id": 1762, "synset": "white-throated_sparrow.n.01", "name": "white-throated_sparrow"}, - {"id": 1763, "synset": "white-crowned_sparrow.n.01", "name": "white-crowned_sparrow"}, - {"id": 1764, "synset": "chipping_sparrow.n.01", "name": "chipping_sparrow"}, - {"id": 1765, "synset": "field_sparrow.n.01", "name": "field_sparrow"}, - {"id": 1766, "synset": "tree_sparrow.n.02", "name": "tree_sparrow"}, - {"id": 1767, "synset": "song_sparrow.n.01", "name": "song_sparrow"}, - {"id": 1768, "synset": "swamp_sparrow.n.01", "name": "swamp_sparrow"}, - {"id": 1769, "synset": "bunting.n.02", "name": "bunting"}, - {"id": 1770, "synset": "indigo_bunting.n.01", "name": "indigo_bunting"}, - {"id": 1771, "synset": "ortolan.n.01", "name": "ortolan"}, - {"id": 1772, "synset": "reed_bunting.n.01", "name": "reed_bunting"}, - {"id": 1773, "synset": "yellowhammer.n.02", "name": "yellowhammer"}, - {"id": 1774, "synset": "yellow-breasted_bunting.n.01", "name": "yellow-breasted_bunting"}, - {"id": 1775, "synset": "snow_bunting.n.01", "name": "snow_bunting"}, - {"id": 1776, "synset": "honeycreeper.n.02", "name": "honeycreeper"}, - {"id": 1777, "synset": "banana_quit.n.01", "name": "banana_quit"}, - {"id": 1778, "synset": "sparrow.n.01", "name": "sparrow"}, - {"id": 1779, "synset": "english_sparrow.n.01", "name": "English_sparrow"}, - {"id": 1780, "synset": "tree_sparrow.n.01", "name": "tree_sparrow"}, - {"id": 1781, "synset": "grosbeak.n.01", "name": "grosbeak"}, - {"id": 1782, "synset": "evening_grosbeak.n.01", "name": "evening_grosbeak"}, - {"id": 1783, "synset": "hawfinch.n.01", "name": "hawfinch"}, - {"id": 1784, "synset": "pine_grosbeak.n.01", "name": "pine_grosbeak"}, - {"id": 1785, "synset": "cardinal.n.04", "name": "cardinal"}, - {"id": 1786, "synset": "pyrrhuloxia.n.01", "name": "pyrrhuloxia"}, - {"id": 1787, "synset": "towhee.n.01", "name": "towhee"}, - {"id": 1788, "synset": "chewink.n.01", "name": "chewink"}, - {"id": 1789, "synset": "green-tailed_towhee.n.01", "name": "green-tailed_towhee"}, - {"id": 1790, "synset": "weaver.n.02", "name": "weaver"}, - {"id": 1791, "synset": "baya.n.01", "name": "baya"}, - {"id": 1792, "synset": "whydah.n.01", "name": "whydah"}, - {"id": 1793, "synset": "java_sparrow.n.01", "name": "Java_sparrow"}, - {"id": 1794, "synset": "avadavat.n.01", "name": "avadavat"}, - {"id": 1795, "synset": "grassfinch.n.01", "name": "grassfinch"}, - {"id": 1796, "synset": "zebra_finch.n.01", "name": "zebra_finch"}, - {"id": 1797, "synset": "honeycreeper.n.01", "name": "honeycreeper"}, - {"id": 1798, "synset": "lyrebird.n.01", "name": "lyrebird"}, - {"id": 1799, "synset": "scrubbird.n.01", "name": "scrubbird"}, - {"id": 1800, "synset": "broadbill.n.04", "name": "broadbill"}, - {"id": 1801, "synset": "tyrannid.n.01", "name": "tyrannid"}, - {"id": 1802, "synset": "new_world_flycatcher.n.01", "name": "New_World_flycatcher"}, - {"id": 1803, "synset": "kingbird.n.01", "name": "kingbird"}, - {"id": 1804, "synset": "arkansas_kingbird.n.01", "name": "Arkansas_kingbird"}, - {"id": 1805, "synset": "cassin's_kingbird.n.01", "name": "Cassin's_kingbird"}, - {"id": 1806, "synset": "eastern_kingbird.n.01", "name": "eastern_kingbird"}, - {"id": 1807, "synset": "grey_kingbird.n.01", "name": "grey_kingbird"}, - {"id": 1808, "synset": "pewee.n.01", "name": "pewee"}, - {"id": 1809, "synset": "western_wood_pewee.n.01", "name": "western_wood_pewee"}, - {"id": 1810, "synset": "phoebe.n.03", "name": "phoebe"}, - {"id": 1811, "synset": "vermillion_flycatcher.n.01", "name": "vermillion_flycatcher"}, - {"id": 1812, "synset": "cotinga.n.01", "name": "cotinga"}, - {"id": 1813, "synset": "cock_of_the_rock.n.02", "name": "cock_of_the_rock"}, - {"id": 1814, "synset": "cock_of_the_rock.n.01", "name": "cock_of_the_rock"}, - {"id": 1815, "synset": "manakin.n.03", "name": "manakin"}, - {"id": 1816, "synset": "bellbird.n.01", "name": "bellbird"}, - {"id": 1817, "synset": "umbrella_bird.n.01", "name": "umbrella_bird"}, - {"id": 1818, "synset": "ovenbird.n.02", "name": "ovenbird"}, - {"id": 1819, "synset": "antbird.n.01", "name": "antbird"}, - {"id": 1820, "synset": "ant_thrush.n.01", "name": "ant_thrush"}, - {"id": 1821, "synset": "ant_shrike.n.01", "name": "ant_shrike"}, - {"id": 1822, "synset": "spotted_antbird.n.01", "name": "spotted_antbird"}, - {"id": 1823, "synset": "woodhewer.n.01", "name": "woodhewer"}, - {"id": 1824, "synset": "pitta.n.01", "name": "pitta"}, - {"id": 1825, "synset": "scissortail.n.01", "name": "scissortail"}, - {"id": 1826, "synset": "old_world_flycatcher.n.01", "name": "Old_World_flycatcher"}, - {"id": 1827, "synset": "spotted_flycatcher.n.01", "name": "spotted_flycatcher"}, - {"id": 1828, "synset": "thickhead.n.01", "name": "thickhead"}, - {"id": 1829, "synset": "thrush.n.03", "name": "thrush"}, - {"id": 1830, "synset": "missel_thrush.n.01", "name": "missel_thrush"}, - {"id": 1831, "synset": "song_thrush.n.01", "name": "song_thrush"}, - {"id": 1832, "synset": "fieldfare.n.01", "name": "fieldfare"}, - {"id": 1833, "synset": "redwing.n.02", "name": "redwing"}, - {"id": 1834, "synset": "blackbird.n.02", "name": "blackbird"}, - {"id": 1835, "synset": "ring_ouzel.n.01", "name": "ring_ouzel"}, - {"id": 1836, "synset": "robin.n.02", "name": "robin"}, - {"id": 1837, "synset": "clay-colored_robin.n.01", "name": "clay-colored_robin"}, - {"id": 1838, "synset": "hermit_thrush.n.01", "name": "hermit_thrush"}, - {"id": 1839, "synset": "veery.n.01", "name": "veery"}, - {"id": 1840, "synset": "wood_thrush.n.01", "name": "wood_thrush"}, - {"id": 1841, "synset": "nightingale.n.01", "name": "nightingale"}, - {"id": 1842, "synset": "thrush_nightingale.n.01", "name": "thrush_nightingale"}, - {"id": 1843, "synset": "bulbul.n.01", "name": "bulbul"}, - {"id": 1844, "synset": "old_world_chat.n.01", "name": "Old_World_chat"}, - {"id": 1845, "synset": "stonechat.n.01", "name": "stonechat"}, - {"id": 1846, "synset": "whinchat.n.01", "name": "whinchat"}, - {"id": 1847, "synset": "solitaire.n.03", "name": "solitaire"}, - {"id": 1848, "synset": "redstart.n.02", "name": "redstart"}, - {"id": 1849, "synset": "wheatear.n.01", "name": "wheatear"}, - {"id": 1850, "synset": "bluebird.n.02", "name": "bluebird"}, - {"id": 1851, "synset": "robin.n.01", "name": "robin"}, - {"id": 1852, "synset": "bluethroat.n.01", "name": "bluethroat"}, - {"id": 1853, "synset": "warbler.n.02", "name": "warbler"}, - {"id": 1854, "synset": "gnatcatcher.n.01", "name": "gnatcatcher"}, - {"id": 1855, "synset": "kinglet.n.01", "name": "kinglet"}, - {"id": 1856, "synset": "goldcrest.n.01", "name": "goldcrest"}, - {"id": 1857, "synset": "gold-crowned_kinglet.n.01", "name": "gold-crowned_kinglet"}, - {"id": 1858, "synset": "ruby-crowned_kinglet.n.01", "name": "ruby-crowned_kinglet"}, - {"id": 1859, "synset": "old_world_warbler.n.01", "name": "Old_World_warbler"}, - {"id": 1860, "synset": "blackcap.n.04", "name": "blackcap"}, - {"id": 1861, "synset": "greater_whitethroat.n.01", "name": "greater_whitethroat"}, - {"id": 1862, "synset": "lesser_whitethroat.n.01", "name": "lesser_whitethroat"}, - {"id": 1863, "synset": "wood_warbler.n.02", "name": "wood_warbler"}, - {"id": 1864, "synset": "sedge_warbler.n.01", "name": "sedge_warbler"}, - {"id": 1865, "synset": "wren_warbler.n.01", "name": "wren_warbler"}, - {"id": 1866, "synset": "tailorbird.n.01", "name": "tailorbird"}, - {"id": 1867, "synset": "babbler.n.02", "name": "babbler"}, - {"id": 1868, "synset": "new_world_warbler.n.01", "name": "New_World_warbler"}, - {"id": 1869, "synset": "parula_warbler.n.01", "name": "parula_warbler"}, - {"id": 1870, "synset": "wilson's_warbler.n.01", "name": "Wilson's_warbler"}, - {"id": 1871, "synset": "flycatching_warbler.n.01", "name": "flycatching_warbler"}, - {"id": 1872, "synset": "american_redstart.n.01", "name": "American_redstart"}, - {"id": 1873, "synset": "cape_may_warbler.n.01", "name": "Cape_May_warbler"}, - {"id": 1874, "synset": "yellow_warbler.n.01", "name": "yellow_warbler"}, - {"id": 1875, "synset": "blackburn.n.01", "name": "Blackburn"}, - {"id": 1876, "synset": "audubon's_warbler.n.01", "name": "Audubon's_warbler"}, - {"id": 1877, "synset": "myrtle_warbler.n.01", "name": "myrtle_warbler"}, - {"id": 1878, "synset": "blackpoll.n.01", "name": "blackpoll"}, - {"id": 1879, "synset": "new_world_chat.n.01", "name": "New_World_chat"}, - {"id": 1880, "synset": "yellow-breasted_chat.n.01", "name": "yellow-breasted_chat"}, - {"id": 1881, "synset": "ovenbird.n.01", "name": "ovenbird"}, - {"id": 1882, "synset": "water_thrush.n.01", "name": "water_thrush"}, - {"id": 1883, "synset": "yellowthroat.n.01", "name": "yellowthroat"}, - {"id": 1884, "synset": "common_yellowthroat.n.01", "name": "common_yellowthroat"}, - {"id": 1885, "synset": "riflebird.n.01", "name": "riflebird"}, - {"id": 1886, "synset": "new_world_oriole.n.01", "name": "New_World_oriole"}, - {"id": 1887, "synset": "northern_oriole.n.01", "name": "northern_oriole"}, - {"id": 1888, "synset": "baltimore_oriole.n.01", "name": "Baltimore_oriole"}, - {"id": 1889, "synset": "bullock's_oriole.n.01", "name": "Bullock's_oriole"}, - {"id": 1890, "synset": "orchard_oriole.n.01", "name": "orchard_oriole"}, - {"id": 1891, "synset": "meadowlark.n.01", "name": "meadowlark"}, - {"id": 1892, "synset": "eastern_meadowlark.n.01", "name": "eastern_meadowlark"}, - {"id": 1893, "synset": "western_meadowlark.n.01", "name": "western_meadowlark"}, - {"id": 1894, "synset": "cacique.n.01", "name": "cacique"}, - {"id": 1895, "synset": "bobolink.n.01", "name": "bobolink"}, - {"id": 1896, "synset": "new_world_blackbird.n.01", "name": "New_World_blackbird"}, - {"id": 1897, "synset": "grackle.n.02", "name": "grackle"}, - {"id": 1898, "synset": "purple_grackle.n.01", "name": "purple_grackle"}, - {"id": 1899, "synset": "rusty_blackbird.n.01", "name": "rusty_blackbird"}, - {"id": 1900, "synset": "cowbird.n.01", "name": "cowbird"}, - {"id": 1901, "synset": "red-winged_blackbird.n.01", "name": "red-winged_blackbird"}, - {"id": 1902, "synset": "old_world_oriole.n.01", "name": "Old_World_oriole"}, - {"id": 1903, "synset": "golden_oriole.n.01", "name": "golden_oriole"}, - {"id": 1904, "synset": "fig-bird.n.01", "name": "fig-bird"}, - {"id": 1905, "synset": "starling.n.01", "name": "starling"}, - {"id": 1906, "synset": "common_starling.n.01", "name": "common_starling"}, - {"id": 1907, "synset": "rose-colored_starling.n.01", "name": "rose-colored_starling"}, - {"id": 1908, "synset": "myna.n.01", "name": "myna"}, - {"id": 1909, "synset": "crested_myna.n.01", "name": "crested_myna"}, - {"id": 1910, "synset": "hill_myna.n.01", "name": "hill_myna"}, - {"id": 1911, "synset": "corvine_bird.n.01", "name": "corvine_bird"}, - {"id": 1912, "synset": "american_crow.n.01", "name": "American_crow"}, - {"id": 1913, "synset": "raven.n.01", "name": "raven"}, - {"id": 1914, "synset": "rook.n.02", "name": "rook"}, - {"id": 1915, "synset": "jackdaw.n.01", "name": "jackdaw"}, - {"id": 1916, "synset": "chough.n.01", "name": "chough"}, - {"id": 1917, "synset": "jay.n.02", "name": "jay"}, - {"id": 1918, "synset": "old_world_jay.n.01", "name": "Old_World_jay"}, - {"id": 1919, "synset": "common_european_jay.n.01", "name": "common_European_jay"}, - {"id": 1920, "synset": "new_world_jay.n.01", "name": "New_World_jay"}, - {"id": 1921, "synset": "blue_jay.n.01", "name": "blue_jay"}, - {"id": 1922, "synset": "canada_jay.n.01", "name": "Canada_jay"}, - {"id": 1923, "synset": "rocky_mountain_jay.n.01", "name": "Rocky_Mountain_jay"}, - {"id": 1924, "synset": "nutcracker.n.03", "name": "nutcracker"}, - {"id": 1925, "synset": "common_nutcracker.n.01", "name": "common_nutcracker"}, - {"id": 1926, "synset": "clark's_nutcracker.n.01", "name": "Clark's_nutcracker"}, - {"id": 1927, "synset": "magpie.n.01", "name": "magpie"}, - {"id": 1928, "synset": "european_magpie.n.01", "name": "European_magpie"}, - {"id": 1929, "synset": "american_magpie.n.01", "name": "American_magpie"}, - {"id": 1930, "synset": "australian_magpie.n.01", "name": "Australian_magpie"}, - {"id": 1931, "synset": "butcherbird.n.02", "name": "butcherbird"}, - {"id": 1932, "synset": "currawong.n.01", "name": "currawong"}, - {"id": 1933, "synset": "piping_crow.n.01", "name": "piping_crow"}, - {"id": 1934, "synset": "wren.n.02", "name": "wren"}, - {"id": 1935, "synset": "winter_wren.n.01", "name": "winter_wren"}, - {"id": 1936, "synset": "house_wren.n.01", "name": "house_wren"}, - {"id": 1937, "synset": "marsh_wren.n.01", "name": "marsh_wren"}, - {"id": 1938, "synset": "long-billed_marsh_wren.n.01", "name": "long-billed_marsh_wren"}, - {"id": 1939, "synset": "sedge_wren.n.01", "name": "sedge_wren"}, - {"id": 1940, "synset": "rock_wren.n.02", "name": "rock_wren"}, - {"id": 1941, "synset": "carolina_wren.n.01", "name": "Carolina_wren"}, - {"id": 1942, "synset": "cactus_wren.n.01", "name": "cactus_wren"}, - {"id": 1943, "synset": "mockingbird.n.01", "name": "mockingbird"}, - {"id": 1944, "synset": "blue_mockingbird.n.01", "name": "blue_mockingbird"}, - {"id": 1945, "synset": "catbird.n.02", "name": "catbird"}, - {"id": 1946, "synset": "thrasher.n.02", "name": "thrasher"}, - {"id": 1947, "synset": "brown_thrasher.n.01", "name": "brown_thrasher"}, - {"id": 1948, "synset": "new_zealand_wren.n.01", "name": "New_Zealand_wren"}, - {"id": 1949, "synset": "rock_wren.n.01", "name": "rock_wren"}, - {"id": 1950, "synset": "rifleman_bird.n.01", "name": "rifleman_bird"}, - {"id": 1951, "synset": "creeper.n.03", "name": "creeper"}, - {"id": 1952, "synset": "brown_creeper.n.01", "name": "brown_creeper"}, - {"id": 1953, "synset": "european_creeper.n.01", "name": "European_creeper"}, - {"id": 1954, "synset": "wall_creeper.n.01", "name": "wall_creeper"}, - {"id": 1955, "synset": "european_nuthatch.n.01", "name": "European_nuthatch"}, - {"id": 1956, "synset": "red-breasted_nuthatch.n.01", "name": "red-breasted_nuthatch"}, - {"id": 1957, "synset": "white-breasted_nuthatch.n.01", "name": "white-breasted_nuthatch"}, - {"id": 1958, "synset": "titmouse.n.01", "name": "titmouse"}, - {"id": 1959, "synset": "chickadee.n.01", "name": "chickadee"}, - {"id": 1960, "synset": "black-capped_chickadee.n.01", "name": "black-capped_chickadee"}, - {"id": 1961, "synset": "tufted_titmouse.n.01", "name": "tufted_titmouse"}, - {"id": 1962, "synset": "carolina_chickadee.n.01", "name": "Carolina_chickadee"}, - {"id": 1963, "synset": "blue_tit.n.01", "name": "blue_tit"}, - {"id": 1964, "synset": "bushtit.n.01", "name": "bushtit"}, - {"id": 1965, "synset": "wren-tit.n.01", "name": "wren-tit"}, - {"id": 1966, "synset": "verdin.n.01", "name": "verdin"}, - {"id": 1967, "synset": "fairy_bluebird.n.01", "name": "fairy_bluebird"}, - {"id": 1968, "synset": "swallow.n.03", "name": "swallow"}, - {"id": 1969, "synset": "barn_swallow.n.01", "name": "barn_swallow"}, - {"id": 1970, "synset": "cliff_swallow.n.01", "name": "cliff_swallow"}, - {"id": 1971, "synset": "tree_swallow.n.02", "name": "tree_swallow"}, - {"id": 1972, "synset": "white-bellied_swallow.n.01", "name": "white-bellied_swallow"}, - {"id": 1973, "synset": "martin.n.05", "name": "martin"}, - {"id": 1974, "synset": "house_martin.n.01", "name": "house_martin"}, - {"id": 1975, "synset": "bank_martin.n.01", "name": "bank_martin"}, - {"id": 1976, "synset": "purple_martin.n.01", "name": "purple_martin"}, - {"id": 1977, "synset": "wood_swallow.n.01", "name": "wood_swallow"}, - {"id": 1978, "synset": "tanager.n.01", "name": "tanager"}, - {"id": 1979, "synset": "scarlet_tanager.n.01", "name": "scarlet_tanager"}, - {"id": 1980, "synset": "western_tanager.n.01", "name": "western_tanager"}, - {"id": 1981, "synset": "summer_tanager.n.01", "name": "summer_tanager"}, - {"id": 1982, "synset": "hepatic_tanager.n.01", "name": "hepatic_tanager"}, - {"id": 1983, "synset": "shrike.n.01", "name": "shrike"}, - {"id": 1984, "synset": "butcherbird.n.01", "name": "butcherbird"}, - {"id": 1985, "synset": "european_shrike.n.01", "name": "European_shrike"}, - {"id": 1986, "synset": "northern_shrike.n.01", "name": "northern_shrike"}, - {"id": 1987, "synset": "white-rumped_shrike.n.01", "name": "white-rumped_shrike"}, - {"id": 1988, "synset": "loggerhead_shrike.n.01", "name": "loggerhead_shrike"}, - {"id": 1989, "synset": "migrant_shrike.n.01", "name": "migrant_shrike"}, - {"id": 1990, "synset": "bush_shrike.n.01", "name": "bush_shrike"}, - {"id": 1991, "synset": "black-fronted_bush_shrike.n.01", "name": "black-fronted_bush_shrike"}, - {"id": 1992, "synset": "bowerbird.n.01", "name": "bowerbird"}, - {"id": 1993, "synset": "satin_bowerbird.n.01", "name": "satin_bowerbird"}, - {"id": 1994, "synset": "great_bowerbird.n.01", "name": "great_bowerbird"}, - {"id": 1995, "synset": "water_ouzel.n.01", "name": "water_ouzel"}, - {"id": 1996, "synset": "european_water_ouzel.n.01", "name": "European_water_ouzel"}, - {"id": 1997, "synset": "american_water_ouzel.n.01", "name": "American_water_ouzel"}, - {"id": 1998, "synset": "vireo.n.01", "name": "vireo"}, - {"id": 1999, "synset": "red-eyed_vireo.n.01", "name": "red-eyed_vireo"}, - {"id": 2000, "synset": "solitary_vireo.n.01", "name": "solitary_vireo"}, - {"id": 2001, "synset": "blue-headed_vireo.n.01", "name": "blue-headed_vireo"}, - {"id": 2002, "synset": "waxwing.n.01", "name": "waxwing"}, - {"id": 2003, "synset": "cedar_waxwing.n.01", "name": "cedar_waxwing"}, - {"id": 2004, "synset": "bohemian_waxwing.n.01", "name": "Bohemian_waxwing"}, - {"id": 2005, "synset": "bird_of_prey.n.01", "name": "bird_of_prey"}, - {"id": 2006, "synset": "accipitriformes.n.01", "name": "Accipitriformes"}, - {"id": 2007, "synset": "hawk.n.01", "name": "hawk"}, - {"id": 2008, "synset": "eyas.n.01", "name": "eyas"}, - {"id": 2009, "synset": "tiercel.n.01", "name": "tiercel"}, - {"id": 2010, "synset": "goshawk.n.01", "name": "goshawk"}, - {"id": 2011, "synset": "sparrow_hawk.n.02", "name": "sparrow_hawk"}, - {"id": 2012, "synset": "cooper's_hawk.n.01", "name": "Cooper's_hawk"}, - {"id": 2013, "synset": "chicken_hawk.n.01", "name": "chicken_hawk"}, - {"id": 2014, "synset": "buteonine.n.01", "name": "buteonine"}, - {"id": 2015, "synset": "redtail.n.01", "name": "redtail"}, - {"id": 2016, "synset": "rough-legged_hawk.n.01", "name": "rough-legged_hawk"}, - {"id": 2017, "synset": "red-shouldered_hawk.n.01", "name": "red-shouldered_hawk"}, - {"id": 2018, "synset": "buzzard.n.02", "name": "buzzard"}, - {"id": 2019, "synset": "honey_buzzard.n.01", "name": "honey_buzzard"}, - {"id": 2020, "synset": "kite.n.04", "name": "kite"}, - {"id": 2021, "synset": "black_kite.n.01", "name": "black_kite"}, - {"id": 2022, "synset": "swallow-tailed_kite.n.01", "name": "swallow-tailed_kite"}, - {"id": 2023, "synset": "white-tailed_kite.n.01", "name": "white-tailed_kite"}, - {"id": 2024, "synset": "harrier.n.03", "name": "harrier"}, - {"id": 2025, "synset": "marsh_harrier.n.01", "name": "marsh_harrier"}, - {"id": 2026, "synset": "montagu's_harrier.n.01", "name": "Montagu's_harrier"}, - {"id": 2027, "synset": "marsh_hawk.n.01", "name": "marsh_hawk"}, - {"id": 2028, "synset": "harrier_eagle.n.01", "name": "harrier_eagle"}, - {"id": 2029, "synset": "peregrine.n.01", "name": "peregrine"}, - {"id": 2030, "synset": "falcon-gentle.n.01", "name": "falcon-gentle"}, - {"id": 2031, "synset": "gyrfalcon.n.01", "name": "gyrfalcon"}, - {"id": 2032, "synset": "kestrel.n.02", "name": "kestrel"}, - {"id": 2033, "synset": "sparrow_hawk.n.01", "name": "sparrow_hawk"}, - {"id": 2034, "synset": "pigeon_hawk.n.01", "name": "pigeon_hawk"}, - {"id": 2035, "synset": "hobby.n.03", "name": "hobby"}, - {"id": 2036, "synset": "caracara.n.01", "name": "caracara"}, - {"id": 2037, "synset": "audubon's_caracara.n.01", "name": "Audubon's_caracara"}, - {"id": 2038, "synset": "carancha.n.01", "name": "carancha"}, - {"id": 2039, "synset": "young_bird.n.01", "name": "young_bird"}, - {"id": 2040, "synset": "eaglet.n.01", "name": "eaglet"}, - {"id": 2041, "synset": "harpy.n.04", "name": "harpy"}, - {"id": 2042, "synset": "golden_eagle.n.01", "name": "golden_eagle"}, - {"id": 2043, "synset": "tawny_eagle.n.01", "name": "tawny_eagle"}, - {"id": 2044, "synset": "bald_eagle.n.01", "name": "bald_eagle"}, - {"id": 2045, "synset": "sea_eagle.n.02", "name": "sea_eagle"}, - {"id": 2046, "synset": "kamchatkan_sea_eagle.n.01", "name": "Kamchatkan_sea_eagle"}, - {"id": 2047, "synset": "ern.n.01", "name": "ern"}, - {"id": 2048, "synset": "fishing_eagle.n.01", "name": "fishing_eagle"}, - {"id": 2049, "synset": "osprey.n.01", "name": "osprey"}, - {"id": 2050, "synset": "aegypiidae.n.01", "name": "Aegypiidae"}, - {"id": 2051, "synset": "old_world_vulture.n.01", "name": "Old_World_vulture"}, - {"id": 2052, "synset": "griffon_vulture.n.01", "name": "griffon_vulture"}, - {"id": 2053, "synset": "bearded_vulture.n.01", "name": "bearded_vulture"}, - {"id": 2054, "synset": "egyptian_vulture.n.01", "name": "Egyptian_vulture"}, - {"id": 2055, "synset": "black_vulture.n.02", "name": "black_vulture"}, - {"id": 2056, "synset": "secretary_bird.n.01", "name": "secretary_bird"}, - {"id": 2057, "synset": "new_world_vulture.n.01", "name": "New_World_vulture"}, - {"id": 2058, "synset": "buzzard.n.01", "name": "buzzard"}, - {"id": 2059, "synset": "condor.n.01", "name": "condor"}, - {"id": 2060, "synset": "andean_condor.n.01", "name": "Andean_condor"}, - {"id": 2061, "synset": "california_condor.n.01", "name": "California_condor"}, - {"id": 2062, "synset": "black_vulture.n.01", "name": "black_vulture"}, - {"id": 2063, "synset": "king_vulture.n.01", "name": "king_vulture"}, - {"id": 2064, "synset": "owlet.n.01", "name": "owlet"}, - {"id": 2065, "synset": "little_owl.n.01", "name": "little_owl"}, - {"id": 2066, "synset": "horned_owl.n.01", "name": "horned_owl"}, - {"id": 2067, "synset": "great_horned_owl.n.01", "name": "great_horned_owl"}, - {"id": 2068, "synset": "great_grey_owl.n.01", "name": "great_grey_owl"}, - {"id": 2069, "synset": "tawny_owl.n.01", "name": "tawny_owl"}, - {"id": 2070, "synset": "barred_owl.n.01", "name": "barred_owl"}, - {"id": 2071, "synset": "screech_owl.n.02", "name": "screech_owl"}, - {"id": 2072, "synset": "screech_owl.n.01", "name": "screech_owl"}, - {"id": 2073, "synset": "scops_owl.n.01", "name": "scops_owl"}, - {"id": 2074, "synset": "spotted_owl.n.01", "name": "spotted_owl"}, - {"id": 2075, "synset": "old_world_scops_owl.n.01", "name": "Old_World_scops_owl"}, - {"id": 2076, "synset": "oriental_scops_owl.n.01", "name": "Oriental_scops_owl"}, - {"id": 2077, "synset": "hoot_owl.n.01", "name": "hoot_owl"}, - {"id": 2078, "synset": "hawk_owl.n.01", "name": "hawk_owl"}, - {"id": 2079, "synset": "long-eared_owl.n.01", "name": "long-eared_owl"}, - {"id": 2080, "synset": "laughing_owl.n.01", "name": "laughing_owl"}, - {"id": 2081, "synset": "barn_owl.n.01", "name": "barn_owl"}, - {"id": 2082, "synset": "amphibian.n.03", "name": "amphibian"}, - {"id": 2083, "synset": "ichyostega.n.01", "name": "Ichyostega"}, - {"id": 2084, "synset": "urodele.n.01", "name": "urodele"}, - {"id": 2085, "synset": "salamander.n.01", "name": "salamander"}, - {"id": 2086, "synset": "european_fire_salamander.n.01", "name": "European_fire_salamander"}, - {"id": 2087, "synset": "spotted_salamander.n.02", "name": "spotted_salamander"}, - {"id": 2088, "synset": "alpine_salamander.n.01", "name": "alpine_salamander"}, - {"id": 2089, "synset": "newt.n.01", "name": "newt"}, - {"id": 2090, "synset": "common_newt.n.01", "name": "common_newt"}, - {"id": 2091, "synset": "red_eft.n.01", "name": "red_eft"}, - {"id": 2092, "synset": "pacific_newt.n.01", "name": "Pacific_newt"}, - {"id": 2093, "synset": "rough-skinned_newt.n.01", "name": "rough-skinned_newt"}, - {"id": 2094, "synset": "california_newt.n.01", "name": "California_newt"}, - {"id": 2095, "synset": "eft.n.01", "name": "eft"}, - {"id": 2096, "synset": "ambystomid.n.01", "name": "ambystomid"}, - {"id": 2097, "synset": "mole_salamander.n.01", "name": "mole_salamander"}, - {"id": 2098, "synset": "spotted_salamander.n.01", "name": "spotted_salamander"}, - {"id": 2099, "synset": "tiger_salamander.n.01", "name": "tiger_salamander"}, - {"id": 2100, "synset": "axolotl.n.01", "name": "axolotl"}, - {"id": 2101, "synset": "waterdog.n.01", "name": "waterdog"}, - {"id": 2102, "synset": "hellbender.n.01", "name": "hellbender"}, - {"id": 2103, "synset": "giant_salamander.n.01", "name": "giant_salamander"}, - {"id": 2104, "synset": "olm.n.01", "name": "olm"}, - {"id": 2105, "synset": "mud_puppy.n.01", "name": "mud_puppy"}, - {"id": 2106, "synset": "dicamptodon.n.01", "name": "dicamptodon"}, - {"id": 2107, "synset": "pacific_giant_salamander.n.01", "name": "Pacific_giant_salamander"}, - {"id": 2108, "synset": "olympic_salamander.n.01", "name": "olympic_salamander"}, - {"id": 2109, "synset": "lungless_salamander.n.01", "name": "lungless_salamander"}, - { - "id": 2110, - "synset": "eastern_red-backed_salamander.n.01", - "name": "eastern_red-backed_salamander", - }, - { - "id": 2111, - "synset": "western_red-backed_salamander.n.01", - "name": "western_red-backed_salamander", - }, - {"id": 2112, "synset": "dusky_salamander.n.01", "name": "dusky_salamander"}, - {"id": 2113, "synset": "climbing_salamander.n.01", "name": "climbing_salamander"}, - {"id": 2114, "synset": "arboreal_salamander.n.01", "name": "arboreal_salamander"}, - {"id": 2115, "synset": "slender_salamander.n.01", "name": "slender_salamander"}, - {"id": 2116, "synset": "web-toed_salamander.n.01", "name": "web-toed_salamander"}, - {"id": 2117, "synset": "shasta_salamander.n.01", "name": "Shasta_salamander"}, - {"id": 2118, "synset": "limestone_salamander.n.01", "name": "limestone_salamander"}, - {"id": 2119, "synset": "amphiuma.n.01", "name": "amphiuma"}, - {"id": 2120, "synset": "siren.n.05", "name": "siren"}, - {"id": 2121, "synset": "true_frog.n.01", "name": "true_frog"}, - {"id": 2122, "synset": "wood-frog.n.01", "name": "wood-frog"}, - {"id": 2123, "synset": "leopard_frog.n.01", "name": "leopard_frog"}, - {"id": 2124, "synset": "bullfrog.n.01", "name": "bullfrog"}, - {"id": 2125, "synset": "green_frog.n.01", "name": "green_frog"}, - {"id": 2126, "synset": "cascades_frog.n.01", "name": "cascades_frog"}, - {"id": 2127, "synset": "goliath_frog.n.01", "name": "goliath_frog"}, - {"id": 2128, "synset": "pickerel_frog.n.01", "name": "pickerel_frog"}, - {"id": 2129, "synset": "tarahumara_frog.n.01", "name": "tarahumara_frog"}, - {"id": 2130, "synset": "grass_frog.n.01", "name": "grass_frog"}, - {"id": 2131, "synset": "leptodactylid_frog.n.01", "name": "leptodactylid_frog"}, - {"id": 2132, "synset": "robber_frog.n.02", "name": "robber_frog"}, - {"id": 2133, "synset": "barking_frog.n.01", "name": "barking_frog"}, - {"id": 2134, "synset": "crapaud.n.01", "name": "crapaud"}, - {"id": 2135, "synset": "tree_frog.n.02", "name": "tree_frog"}, - {"id": 2136, "synset": "tailed_frog.n.01", "name": "tailed_frog"}, - {"id": 2137, "synset": "liopelma_hamiltoni.n.01", "name": "Liopelma_hamiltoni"}, - {"id": 2138, "synset": "true_toad.n.01", "name": "true_toad"}, - {"id": 2139, "synset": "bufo.n.01", "name": "bufo"}, - {"id": 2140, "synset": "agua.n.01", "name": "agua"}, - {"id": 2141, "synset": "european_toad.n.01", "name": "European_toad"}, - {"id": 2142, "synset": "natterjack.n.01", "name": "natterjack"}, - {"id": 2143, "synset": "american_toad.n.01", "name": "American_toad"}, - {"id": 2144, "synset": "eurasian_green_toad.n.01", "name": "Eurasian_green_toad"}, - {"id": 2145, "synset": "american_green_toad.n.01", "name": "American_green_toad"}, - {"id": 2146, "synset": "yosemite_toad.n.01", "name": "Yosemite_toad"}, - {"id": 2147, "synset": "texas_toad.n.01", "name": "Texas_toad"}, - {"id": 2148, "synset": "southwestern_toad.n.01", "name": "southwestern_toad"}, - {"id": 2149, "synset": "western_toad.n.01", "name": "western_toad"}, - {"id": 2150, "synset": "obstetrical_toad.n.01", "name": "obstetrical_toad"}, - {"id": 2151, "synset": "midwife_toad.n.01", "name": "midwife_toad"}, - {"id": 2152, "synset": "fire-bellied_toad.n.01", "name": "fire-bellied_toad"}, - {"id": 2153, "synset": "spadefoot.n.01", "name": "spadefoot"}, - {"id": 2154, "synset": "western_spadefoot.n.01", "name": "western_spadefoot"}, - {"id": 2155, "synset": "southern_spadefoot.n.01", "name": "southern_spadefoot"}, - {"id": 2156, "synset": "plains_spadefoot.n.01", "name": "plains_spadefoot"}, - {"id": 2157, "synset": "tree_toad.n.01", "name": "tree_toad"}, - {"id": 2158, "synset": "spring_peeper.n.01", "name": "spring_peeper"}, - {"id": 2159, "synset": "pacific_tree_toad.n.01", "name": "Pacific_tree_toad"}, - {"id": 2160, "synset": "canyon_treefrog.n.01", "name": "canyon_treefrog"}, - {"id": 2161, "synset": "chameleon_tree_frog.n.01", "name": "chameleon_tree_frog"}, - {"id": 2162, "synset": "cricket_frog.n.01", "name": "cricket_frog"}, - {"id": 2163, "synset": "northern_cricket_frog.n.01", "name": "northern_cricket_frog"}, - {"id": 2164, "synset": "eastern_cricket_frog.n.01", "name": "eastern_cricket_frog"}, - {"id": 2165, "synset": "chorus_frog.n.01", "name": "chorus_frog"}, - {"id": 2166, "synset": "lowland_burrowing_treefrog.n.01", "name": "lowland_burrowing_treefrog"}, - { - "id": 2167, - "synset": "western_narrow-mouthed_toad.n.01", - "name": "western_narrow-mouthed_toad", - }, - { - "id": 2168, - "synset": "eastern_narrow-mouthed_toad.n.01", - "name": "eastern_narrow-mouthed_toad", - }, - {"id": 2169, "synset": "sheep_frog.n.01", "name": "sheep_frog"}, - {"id": 2170, "synset": "tongueless_frog.n.01", "name": "tongueless_frog"}, - {"id": 2171, "synset": "surinam_toad.n.01", "name": "Surinam_toad"}, - {"id": 2172, "synset": "african_clawed_frog.n.01", "name": "African_clawed_frog"}, - {"id": 2173, "synset": "south_american_poison_toad.n.01", "name": "South_American_poison_toad"}, - {"id": 2174, "synset": "caecilian.n.01", "name": "caecilian"}, - {"id": 2175, "synset": "reptile.n.01", "name": "reptile"}, - {"id": 2176, "synset": "anapsid.n.01", "name": "anapsid"}, - {"id": 2177, "synset": "diapsid.n.01", "name": "diapsid"}, - {"id": 2178, "synset": "diapsida.n.01", "name": "Diapsida"}, - {"id": 2179, "synset": "chelonian.n.01", "name": "chelonian"}, - {"id": 2180, "synset": "sea_turtle.n.01", "name": "sea_turtle"}, - {"id": 2181, "synset": "green_turtle.n.01", "name": "green_turtle"}, - {"id": 2182, "synset": "loggerhead.n.02", "name": "loggerhead"}, - {"id": 2183, "synset": "ridley.n.01", "name": "ridley"}, - {"id": 2184, "synset": "atlantic_ridley.n.01", "name": "Atlantic_ridley"}, - {"id": 2185, "synset": "pacific_ridley.n.01", "name": "Pacific_ridley"}, - {"id": 2186, "synset": "hawksbill_turtle.n.01", "name": "hawksbill_turtle"}, - {"id": 2187, "synset": "leatherback_turtle.n.01", "name": "leatherback_turtle"}, - {"id": 2188, "synset": "snapping_turtle.n.01", "name": "snapping_turtle"}, - {"id": 2189, "synset": "common_snapping_turtle.n.01", "name": "common_snapping_turtle"}, - {"id": 2190, "synset": "alligator_snapping_turtle.n.01", "name": "alligator_snapping_turtle"}, - {"id": 2191, "synset": "mud_turtle.n.01", "name": "mud_turtle"}, - {"id": 2192, "synset": "musk_turtle.n.01", "name": "musk_turtle"}, - {"id": 2193, "synset": "terrapin.n.01", "name": "terrapin"}, - {"id": 2194, "synset": "diamondback_terrapin.n.01", "name": "diamondback_terrapin"}, - {"id": 2195, "synset": "red-bellied_terrapin.n.01", "name": "red-bellied_terrapin"}, - {"id": 2196, "synset": "slider.n.03", "name": "slider"}, - {"id": 2197, "synset": "cooter.n.01", "name": "cooter"}, - {"id": 2198, "synset": "box_turtle.n.01", "name": "box_turtle"}, - {"id": 2199, "synset": "western_box_turtle.n.01", "name": "Western_box_turtle"}, - {"id": 2200, "synset": "painted_turtle.n.01", "name": "painted_turtle"}, - {"id": 2201, "synset": "tortoise.n.01", "name": "tortoise"}, - {"id": 2202, "synset": "european_tortoise.n.01", "name": "European_tortoise"}, - {"id": 2203, "synset": "giant_tortoise.n.01", "name": "giant_tortoise"}, - {"id": 2204, "synset": "gopher_tortoise.n.01", "name": "gopher_tortoise"}, - {"id": 2205, "synset": "desert_tortoise.n.01", "name": "desert_tortoise"}, - {"id": 2206, "synset": "texas_tortoise.n.01", "name": "Texas_tortoise"}, - {"id": 2207, "synset": "soft-shelled_turtle.n.01", "name": "soft-shelled_turtle"}, - {"id": 2208, "synset": "spiny_softshell.n.01", "name": "spiny_softshell"}, - {"id": 2209, "synset": "smooth_softshell.n.01", "name": "smooth_softshell"}, - {"id": 2210, "synset": "tuatara.n.01", "name": "tuatara"}, - {"id": 2211, "synset": "saurian.n.01", "name": "saurian"}, - {"id": 2212, "synset": "gecko.n.01", "name": "gecko"}, - {"id": 2213, "synset": "flying_gecko.n.01", "name": "flying_gecko"}, - {"id": 2214, "synset": "banded_gecko.n.01", "name": "banded_gecko"}, - {"id": 2215, "synset": "iguanid.n.01", "name": "iguanid"}, - {"id": 2216, "synset": "common_iguana.n.01", "name": "common_iguana"}, - {"id": 2217, "synset": "marine_iguana.n.01", "name": "marine_iguana"}, - {"id": 2218, "synset": "desert_iguana.n.01", "name": "desert_iguana"}, - {"id": 2219, "synset": "chuckwalla.n.01", "name": "chuckwalla"}, - {"id": 2220, "synset": "zebra-tailed_lizard.n.01", "name": "zebra-tailed_lizard"}, - {"id": 2221, "synset": "fringe-toed_lizard.n.01", "name": "fringe-toed_lizard"}, - {"id": 2222, "synset": "earless_lizard.n.01", "name": "earless_lizard"}, - {"id": 2223, "synset": "collared_lizard.n.01", "name": "collared_lizard"}, - {"id": 2224, "synset": "leopard_lizard.n.01", "name": "leopard_lizard"}, - {"id": 2225, "synset": "spiny_lizard.n.02", "name": "spiny_lizard"}, - {"id": 2226, "synset": "fence_lizard.n.01", "name": "fence_lizard"}, - {"id": 2227, "synset": "western_fence_lizard.n.01", "name": "western_fence_lizard"}, - {"id": 2228, "synset": "eastern_fence_lizard.n.01", "name": "eastern_fence_lizard"}, - {"id": 2229, "synset": "sagebrush_lizard.n.01", "name": "sagebrush_lizard"}, - {"id": 2230, "synset": "side-blotched_lizard.n.01", "name": "side-blotched_lizard"}, - {"id": 2231, "synset": "tree_lizard.n.01", "name": "tree_lizard"}, - {"id": 2232, "synset": "horned_lizard.n.01", "name": "horned_lizard"}, - {"id": 2233, "synset": "texas_horned_lizard.n.01", "name": "Texas_horned_lizard"}, - {"id": 2234, "synset": "basilisk.n.03", "name": "basilisk"}, - {"id": 2235, "synset": "american_chameleon.n.01", "name": "American_chameleon"}, - {"id": 2236, "synset": "worm_lizard.n.01", "name": "worm_lizard"}, - {"id": 2237, "synset": "night_lizard.n.01", "name": "night_lizard"}, - {"id": 2238, "synset": "skink.n.01", "name": "skink"}, - {"id": 2239, "synset": "western_skink.n.01", "name": "western_skink"}, - {"id": 2240, "synset": "mountain_skink.n.01", "name": "mountain_skink"}, - {"id": 2241, "synset": "teiid_lizard.n.01", "name": "teiid_lizard"}, - {"id": 2242, "synset": "whiptail.n.01", "name": "whiptail"}, - {"id": 2243, "synset": "racerunner.n.01", "name": "racerunner"}, - {"id": 2244, "synset": "plateau_striped_whiptail.n.01", "name": "plateau_striped_whiptail"}, - { - "id": 2245, - "synset": "chihuahuan_spotted_whiptail.n.01", - "name": "Chihuahuan_spotted_whiptail", - }, - {"id": 2246, "synset": "western_whiptail.n.01", "name": "western_whiptail"}, - {"id": 2247, "synset": "checkered_whiptail.n.01", "name": "checkered_whiptail"}, - {"id": 2248, "synset": "teju.n.01", "name": "teju"}, - {"id": 2249, "synset": "caiman_lizard.n.01", "name": "caiman_lizard"}, - {"id": 2250, "synset": "agamid.n.01", "name": "agamid"}, - {"id": 2251, "synset": "agama.n.01", "name": "agama"}, - {"id": 2252, "synset": "frilled_lizard.n.01", "name": "frilled_lizard"}, - {"id": 2253, "synset": "moloch.n.03", "name": "moloch"}, - {"id": 2254, "synset": "mountain_devil.n.02", "name": "mountain_devil"}, - {"id": 2255, "synset": "anguid_lizard.n.01", "name": "anguid_lizard"}, - {"id": 2256, "synset": "alligator_lizard.n.01", "name": "alligator_lizard"}, - {"id": 2257, "synset": "blindworm.n.01", "name": "blindworm"}, - {"id": 2258, "synset": "glass_lizard.n.01", "name": "glass_lizard"}, - {"id": 2259, "synset": "legless_lizard.n.01", "name": "legless_lizard"}, - {"id": 2260, "synset": "lanthanotus_borneensis.n.01", "name": "Lanthanotus_borneensis"}, - {"id": 2261, "synset": "venomous_lizard.n.01", "name": "venomous_lizard"}, - {"id": 2262, "synset": "gila_monster.n.01", "name": "Gila_monster"}, - {"id": 2263, "synset": "beaded_lizard.n.01", "name": "beaded_lizard"}, - {"id": 2264, "synset": "lacertid_lizard.n.01", "name": "lacertid_lizard"}, - {"id": 2265, "synset": "sand_lizard.n.01", "name": "sand_lizard"}, - {"id": 2266, "synset": "green_lizard.n.01", "name": "green_lizard"}, - {"id": 2267, "synset": "chameleon.n.03", "name": "chameleon"}, - {"id": 2268, "synset": "african_chameleon.n.01", "name": "African_chameleon"}, - {"id": 2269, "synset": "horned_chameleon.n.01", "name": "horned_chameleon"}, - {"id": 2270, "synset": "monitor.n.07", "name": "monitor"}, - {"id": 2271, "synset": "african_monitor.n.01", "name": "African_monitor"}, - {"id": 2272, "synset": "komodo_dragon.n.01", "name": "Komodo_dragon"}, - {"id": 2273, "synset": "crocodilian_reptile.n.01", "name": "crocodilian_reptile"}, - {"id": 2274, "synset": "crocodile.n.01", "name": "crocodile"}, - {"id": 2275, "synset": "african_crocodile.n.01", "name": "African_crocodile"}, - {"id": 2276, "synset": "asian_crocodile.n.01", "name": "Asian_crocodile"}, - {"id": 2277, "synset": "morlett's_crocodile.n.01", "name": "Morlett's_crocodile"}, - {"id": 2278, "synset": "false_gavial.n.01", "name": "false_gavial"}, - {"id": 2279, "synset": "american_alligator.n.01", "name": "American_alligator"}, - {"id": 2280, "synset": "chinese_alligator.n.01", "name": "Chinese_alligator"}, - {"id": 2281, "synset": "caiman.n.01", "name": "caiman"}, - {"id": 2282, "synset": "spectacled_caiman.n.01", "name": "spectacled_caiman"}, - {"id": 2283, "synset": "gavial.n.01", "name": "gavial"}, - {"id": 2284, "synset": "armored_dinosaur.n.01", "name": "armored_dinosaur"}, - {"id": 2285, "synset": "stegosaur.n.01", "name": "stegosaur"}, - {"id": 2286, "synset": "ankylosaur.n.01", "name": "ankylosaur"}, - {"id": 2287, "synset": "edmontonia.n.01", "name": "Edmontonia"}, - {"id": 2288, "synset": "bone-headed_dinosaur.n.01", "name": "bone-headed_dinosaur"}, - {"id": 2289, "synset": "pachycephalosaur.n.01", "name": "pachycephalosaur"}, - {"id": 2290, "synset": "ceratopsian.n.01", "name": "ceratopsian"}, - {"id": 2291, "synset": "protoceratops.n.01", "name": "protoceratops"}, - {"id": 2292, "synset": "triceratops.n.01", "name": "triceratops"}, - {"id": 2293, "synset": "styracosaur.n.01", "name": "styracosaur"}, - {"id": 2294, "synset": "psittacosaur.n.01", "name": "psittacosaur"}, - {"id": 2295, "synset": "ornithopod.n.01", "name": "ornithopod"}, - {"id": 2296, "synset": "hadrosaur.n.01", "name": "hadrosaur"}, - {"id": 2297, "synset": "trachodon.n.01", "name": "trachodon"}, - {"id": 2298, "synset": "saurischian.n.01", "name": "saurischian"}, - {"id": 2299, "synset": "sauropod.n.01", "name": "sauropod"}, - {"id": 2300, "synset": "apatosaur.n.01", "name": "apatosaur"}, - {"id": 2301, "synset": "barosaur.n.01", "name": "barosaur"}, - {"id": 2302, "synset": "diplodocus.n.01", "name": "diplodocus"}, - {"id": 2303, "synset": "argentinosaur.n.01", "name": "argentinosaur"}, - {"id": 2304, "synset": "theropod.n.01", "name": "theropod"}, - {"id": 2305, "synset": "ceratosaur.n.01", "name": "ceratosaur"}, - {"id": 2306, "synset": "coelophysis.n.01", "name": "coelophysis"}, - {"id": 2307, "synset": "tyrannosaur.n.01", "name": "tyrannosaur"}, - {"id": 2308, "synset": "allosaur.n.01", "name": "allosaur"}, - {"id": 2309, "synset": "ornithomimid.n.01", "name": "ornithomimid"}, - {"id": 2310, "synset": "maniraptor.n.01", "name": "maniraptor"}, - {"id": 2311, "synset": "oviraptorid.n.01", "name": "oviraptorid"}, - {"id": 2312, "synset": "velociraptor.n.01", "name": "velociraptor"}, - {"id": 2313, "synset": "deinonychus.n.01", "name": "deinonychus"}, - {"id": 2314, "synset": "utahraptor.n.01", "name": "utahraptor"}, - {"id": 2315, "synset": "synapsid.n.01", "name": "synapsid"}, - {"id": 2316, "synset": "dicynodont.n.01", "name": "dicynodont"}, - {"id": 2317, "synset": "pelycosaur.n.01", "name": "pelycosaur"}, - {"id": 2318, "synset": "dimetrodon.n.01", "name": "dimetrodon"}, - {"id": 2319, "synset": "pterosaur.n.01", "name": "pterosaur"}, - {"id": 2320, "synset": "pterodactyl.n.01", "name": "pterodactyl"}, - {"id": 2321, "synset": "ichthyosaur.n.01", "name": "ichthyosaur"}, - {"id": 2322, "synset": "ichthyosaurus.n.01", "name": "ichthyosaurus"}, - {"id": 2323, "synset": "stenopterygius.n.01", "name": "stenopterygius"}, - {"id": 2324, "synset": "plesiosaur.n.01", "name": "plesiosaur"}, - {"id": 2325, "synset": "nothosaur.n.01", "name": "nothosaur"}, - {"id": 2326, "synset": "colubrid_snake.n.01", "name": "colubrid_snake"}, - {"id": 2327, "synset": "hoop_snake.n.01", "name": "hoop_snake"}, - {"id": 2328, "synset": "thunder_snake.n.01", "name": "thunder_snake"}, - {"id": 2329, "synset": "ringneck_snake.n.01", "name": "ringneck_snake"}, - {"id": 2330, "synset": "hognose_snake.n.01", "name": "hognose_snake"}, - {"id": 2331, "synset": "leaf-nosed_snake.n.01", "name": "leaf-nosed_snake"}, - {"id": 2332, "synset": "green_snake.n.02", "name": "green_snake"}, - {"id": 2333, "synset": "smooth_green_snake.n.01", "name": "smooth_green_snake"}, - {"id": 2334, "synset": "rough_green_snake.n.01", "name": "rough_green_snake"}, - {"id": 2335, "synset": "green_snake.n.01", "name": "green_snake"}, - {"id": 2336, "synset": "racer.n.04", "name": "racer"}, - {"id": 2337, "synset": "blacksnake.n.02", "name": "blacksnake"}, - {"id": 2338, "synset": "blue_racer.n.01", "name": "blue_racer"}, - {"id": 2339, "synset": "horseshoe_whipsnake.n.01", "name": "horseshoe_whipsnake"}, - {"id": 2340, "synset": "whip-snake.n.01", "name": "whip-snake"}, - {"id": 2341, "synset": "coachwhip.n.02", "name": "coachwhip"}, - {"id": 2342, "synset": "california_whipsnake.n.01", "name": "California_whipsnake"}, - {"id": 2343, "synset": "sonoran_whipsnake.n.01", "name": "Sonoran_whipsnake"}, - {"id": 2344, "synset": "rat_snake.n.01", "name": "rat_snake"}, - {"id": 2345, "synset": "corn_snake.n.01", "name": "corn_snake"}, - {"id": 2346, "synset": "black_rat_snake.n.01", "name": "black_rat_snake"}, - {"id": 2347, "synset": "chicken_snake.n.01", "name": "chicken_snake"}, - {"id": 2348, "synset": "indian_rat_snake.n.01", "name": "Indian_rat_snake"}, - {"id": 2349, "synset": "glossy_snake.n.01", "name": "glossy_snake"}, - {"id": 2350, "synset": "bull_snake.n.01", "name": "bull_snake"}, - {"id": 2351, "synset": "gopher_snake.n.02", "name": "gopher_snake"}, - {"id": 2352, "synset": "pine_snake.n.01", "name": "pine_snake"}, - {"id": 2353, "synset": "king_snake.n.01", "name": "king_snake"}, - {"id": 2354, "synset": "common_kingsnake.n.01", "name": "common_kingsnake"}, - {"id": 2355, "synset": "milk_snake.n.01", "name": "milk_snake"}, - {"id": 2356, "synset": "garter_snake.n.01", "name": "garter_snake"}, - {"id": 2357, "synset": "common_garter_snake.n.01", "name": "common_garter_snake"}, - {"id": 2358, "synset": "ribbon_snake.n.01", "name": "ribbon_snake"}, - {"id": 2359, "synset": "western_ribbon_snake.n.01", "name": "Western_ribbon_snake"}, - {"id": 2360, "synset": "lined_snake.n.01", "name": "lined_snake"}, - {"id": 2361, "synset": "ground_snake.n.01", "name": "ground_snake"}, - {"id": 2362, "synset": "eastern_ground_snake.n.01", "name": "eastern_ground_snake"}, - {"id": 2363, "synset": "water_snake.n.01", "name": "water_snake"}, - {"id": 2364, "synset": "common_water_snake.n.01", "name": "common_water_snake"}, - {"id": 2365, "synset": "water_moccasin.n.02", "name": "water_moccasin"}, - {"id": 2366, "synset": "grass_snake.n.01", "name": "grass_snake"}, - {"id": 2367, "synset": "viperine_grass_snake.n.01", "name": "viperine_grass_snake"}, - {"id": 2368, "synset": "red-bellied_snake.n.01", "name": "red-bellied_snake"}, - {"id": 2369, "synset": "sand_snake.n.01", "name": "sand_snake"}, - {"id": 2370, "synset": "banded_sand_snake.n.01", "name": "banded_sand_snake"}, - {"id": 2371, "synset": "black-headed_snake.n.01", "name": "black-headed_snake"}, - {"id": 2372, "synset": "vine_snake.n.01", "name": "vine_snake"}, - {"id": 2373, "synset": "lyre_snake.n.01", "name": "lyre_snake"}, - {"id": 2374, "synset": "sonoran_lyre_snake.n.01", "name": "Sonoran_lyre_snake"}, - {"id": 2375, "synset": "night_snake.n.01", "name": "night_snake"}, - {"id": 2376, "synset": "blind_snake.n.01", "name": "blind_snake"}, - {"id": 2377, "synset": "western_blind_snake.n.01", "name": "western_blind_snake"}, - {"id": 2378, "synset": "indigo_snake.n.01", "name": "indigo_snake"}, - {"id": 2379, "synset": "eastern_indigo_snake.n.01", "name": "eastern_indigo_snake"}, - {"id": 2380, "synset": "constrictor.n.01", "name": "constrictor"}, - {"id": 2381, "synset": "boa.n.02", "name": "boa"}, - {"id": 2382, "synset": "boa_constrictor.n.01", "name": "boa_constrictor"}, - {"id": 2383, "synset": "rubber_boa.n.01", "name": "rubber_boa"}, - {"id": 2384, "synset": "rosy_boa.n.01", "name": "rosy_boa"}, - {"id": 2385, "synset": "anaconda.n.01", "name": "anaconda"}, - {"id": 2386, "synset": "python.n.01", "name": "python"}, - {"id": 2387, "synset": "carpet_snake.n.01", "name": "carpet_snake"}, - {"id": 2388, "synset": "reticulated_python.n.01", "name": "reticulated_python"}, - {"id": 2389, "synset": "indian_python.n.01", "name": "Indian_python"}, - {"id": 2390, "synset": "rock_python.n.01", "name": "rock_python"}, - {"id": 2391, "synset": "amethystine_python.n.01", "name": "amethystine_python"}, - {"id": 2392, "synset": "elapid.n.01", "name": "elapid"}, - {"id": 2393, "synset": "coral_snake.n.02", "name": "coral_snake"}, - {"id": 2394, "synset": "eastern_coral_snake.n.01", "name": "eastern_coral_snake"}, - {"id": 2395, "synset": "western_coral_snake.n.01", "name": "western_coral_snake"}, - {"id": 2396, "synset": "coral_snake.n.01", "name": "coral_snake"}, - {"id": 2397, "synset": "african_coral_snake.n.01", "name": "African_coral_snake"}, - {"id": 2398, "synset": "australian_coral_snake.n.01", "name": "Australian_coral_snake"}, - {"id": 2399, "synset": "copperhead.n.02", "name": "copperhead"}, - {"id": 2400, "synset": "cobra.n.01", "name": "cobra"}, - {"id": 2401, "synset": "indian_cobra.n.01", "name": "Indian_cobra"}, - {"id": 2402, "synset": "asp.n.02", "name": "asp"}, - {"id": 2403, "synset": "black-necked_cobra.n.01", "name": "black-necked_cobra"}, - {"id": 2404, "synset": "hamadryad.n.02", "name": "hamadryad"}, - {"id": 2405, "synset": "ringhals.n.01", "name": "ringhals"}, - {"id": 2406, "synset": "mamba.n.01", "name": "mamba"}, - {"id": 2407, "synset": "black_mamba.n.01", "name": "black_mamba"}, - {"id": 2408, "synset": "green_mamba.n.01", "name": "green_mamba"}, - {"id": 2409, "synset": "death_adder.n.01", "name": "death_adder"}, - {"id": 2410, "synset": "tiger_snake.n.01", "name": "tiger_snake"}, - {"id": 2411, "synset": "australian_blacksnake.n.01", "name": "Australian_blacksnake"}, - {"id": 2412, "synset": "krait.n.01", "name": "krait"}, - {"id": 2413, "synset": "banded_krait.n.01", "name": "banded_krait"}, - {"id": 2414, "synset": "taipan.n.01", "name": "taipan"}, - {"id": 2415, "synset": "sea_snake.n.01", "name": "sea_snake"}, - {"id": 2416, "synset": "viper.n.01", "name": "viper"}, - {"id": 2417, "synset": "adder.n.03", "name": "adder"}, - {"id": 2418, "synset": "asp.n.01", "name": "asp"}, - {"id": 2419, "synset": "puff_adder.n.01", "name": "puff_adder"}, - {"id": 2420, "synset": "gaboon_viper.n.01", "name": "gaboon_viper"}, - {"id": 2421, "synset": "horned_viper.n.01", "name": "horned_viper"}, - {"id": 2422, "synset": "pit_viper.n.01", "name": "pit_viper"}, - {"id": 2423, "synset": "copperhead.n.01", "name": "copperhead"}, - {"id": 2424, "synset": "water_moccasin.n.01", "name": "water_moccasin"}, - {"id": 2425, "synset": "rattlesnake.n.01", "name": "rattlesnake"}, - {"id": 2426, "synset": "diamondback.n.01", "name": "diamondback"}, - {"id": 2427, "synset": "timber_rattlesnake.n.01", "name": "timber_rattlesnake"}, - {"id": 2428, "synset": "canebrake_rattlesnake.n.01", "name": "canebrake_rattlesnake"}, - {"id": 2429, "synset": "prairie_rattlesnake.n.01", "name": "prairie_rattlesnake"}, - {"id": 2430, "synset": "sidewinder.n.01", "name": "sidewinder"}, - {"id": 2431, "synset": "western_diamondback.n.01", "name": "Western_diamondback"}, - {"id": 2432, "synset": "rock_rattlesnake.n.01", "name": "rock_rattlesnake"}, - {"id": 2433, "synset": "tiger_rattlesnake.n.01", "name": "tiger_rattlesnake"}, - {"id": 2434, "synset": "mojave_rattlesnake.n.01", "name": "Mojave_rattlesnake"}, - {"id": 2435, "synset": "speckled_rattlesnake.n.01", "name": "speckled_rattlesnake"}, - {"id": 2436, "synset": "massasauga.n.02", "name": "massasauga"}, - {"id": 2437, "synset": "ground_rattler.n.01", "name": "ground_rattler"}, - {"id": 2438, "synset": "fer-de-lance.n.01", "name": "fer-de-lance"}, - {"id": 2439, "synset": "carcase.n.01", "name": "carcase"}, - {"id": 2440, "synset": "carrion.n.01", "name": "carrion"}, - {"id": 2441, "synset": "arthropod.n.01", "name": "arthropod"}, - {"id": 2442, "synset": "trilobite.n.01", "name": "trilobite"}, - {"id": 2443, "synset": "arachnid.n.01", "name": "arachnid"}, - {"id": 2444, "synset": "harvestman.n.01", "name": "harvestman"}, - {"id": 2445, "synset": "scorpion.n.03", "name": "scorpion"}, - {"id": 2446, "synset": "false_scorpion.n.01", "name": "false_scorpion"}, - {"id": 2447, "synset": "book_scorpion.n.01", "name": "book_scorpion"}, - {"id": 2448, "synset": "whip-scorpion.n.01", "name": "whip-scorpion"}, - {"id": 2449, "synset": "vinegarroon.n.01", "name": "vinegarroon"}, - {"id": 2450, "synset": "orb-weaving_spider.n.01", "name": "orb-weaving_spider"}, - { - "id": 2451, - "synset": "black_and_gold_garden_spider.n.01", - "name": "black_and_gold_garden_spider", - }, - {"id": 2452, "synset": "barn_spider.n.01", "name": "barn_spider"}, - {"id": 2453, "synset": "garden_spider.n.01", "name": "garden_spider"}, - {"id": 2454, "synset": "comb-footed_spider.n.01", "name": "comb-footed_spider"}, - {"id": 2455, "synset": "black_widow.n.01", "name": "black_widow"}, - {"id": 2456, "synset": "tarantula.n.02", "name": "tarantula"}, - {"id": 2457, "synset": "wolf_spider.n.01", "name": "wolf_spider"}, - {"id": 2458, "synset": "european_wolf_spider.n.01", "name": "European_wolf_spider"}, - {"id": 2459, "synset": "trap-door_spider.n.01", "name": "trap-door_spider"}, - {"id": 2460, "synset": "acarine.n.01", "name": "acarine"}, - {"id": 2461, "synset": "tick.n.02", "name": "tick"}, - {"id": 2462, "synset": "hard_tick.n.01", "name": "hard_tick"}, - {"id": 2463, "synset": "ixodes_dammini.n.01", "name": "Ixodes_dammini"}, - {"id": 2464, "synset": "ixodes_neotomae.n.01", "name": "Ixodes_neotomae"}, - {"id": 2465, "synset": "ixodes_pacificus.n.01", "name": "Ixodes_pacificus"}, - {"id": 2466, "synset": "ixodes_scapularis.n.01", "name": "Ixodes_scapularis"}, - {"id": 2467, "synset": "sheep-tick.n.02", "name": "sheep-tick"}, - {"id": 2468, "synset": "ixodes_persulcatus.n.01", "name": "Ixodes_persulcatus"}, - {"id": 2469, "synset": "ixodes_dentatus.n.01", "name": "Ixodes_dentatus"}, - {"id": 2470, "synset": "ixodes_spinipalpis.n.01", "name": "Ixodes_spinipalpis"}, - {"id": 2471, "synset": "wood_tick.n.01", "name": "wood_tick"}, - {"id": 2472, "synset": "soft_tick.n.01", "name": "soft_tick"}, - {"id": 2473, "synset": "mite.n.02", "name": "mite"}, - {"id": 2474, "synset": "web-spinning_mite.n.01", "name": "web-spinning_mite"}, - {"id": 2475, "synset": "acarid.n.01", "name": "acarid"}, - {"id": 2476, "synset": "trombidiid.n.01", "name": "trombidiid"}, - {"id": 2477, "synset": "trombiculid.n.01", "name": "trombiculid"}, - {"id": 2478, "synset": "harvest_mite.n.01", "name": "harvest_mite"}, - {"id": 2479, "synset": "acarus.n.01", "name": "acarus"}, - {"id": 2480, "synset": "itch_mite.n.01", "name": "itch_mite"}, - {"id": 2481, "synset": "rust_mite.n.01", "name": "rust_mite"}, - {"id": 2482, "synset": "spider_mite.n.01", "name": "spider_mite"}, - {"id": 2483, "synset": "red_spider.n.01", "name": "red_spider"}, - {"id": 2484, "synset": "myriapod.n.01", "name": "myriapod"}, - {"id": 2485, "synset": "garden_centipede.n.01", "name": "garden_centipede"}, - {"id": 2486, "synset": "tardigrade.n.01", "name": "tardigrade"}, - {"id": 2487, "synset": "centipede.n.01", "name": "centipede"}, - {"id": 2488, "synset": "house_centipede.n.01", "name": "house_centipede"}, - {"id": 2489, "synset": "millipede.n.01", "name": "millipede"}, - {"id": 2490, "synset": "sea_spider.n.01", "name": "sea_spider"}, - {"id": 2491, "synset": "merostomata.n.01", "name": "Merostomata"}, - {"id": 2492, "synset": "horseshoe_crab.n.01", "name": "horseshoe_crab"}, - {"id": 2493, "synset": "asian_horseshoe_crab.n.01", "name": "Asian_horseshoe_crab"}, - {"id": 2494, "synset": "eurypterid.n.01", "name": "eurypterid"}, - {"id": 2495, "synset": "tongue_worm.n.01", "name": "tongue_worm"}, - {"id": 2496, "synset": "gallinaceous_bird.n.01", "name": "gallinaceous_bird"}, - {"id": 2497, "synset": "domestic_fowl.n.01", "name": "domestic_fowl"}, - {"id": 2498, "synset": "dorking.n.01", "name": "Dorking"}, - {"id": 2499, "synset": "plymouth_rock.n.02", "name": "Plymouth_Rock"}, - {"id": 2500, "synset": "cornish.n.02", "name": "Cornish"}, - {"id": 2501, "synset": "rock_cornish.n.01", "name": "Rock_Cornish"}, - {"id": 2502, "synset": "game_fowl.n.01", "name": "game_fowl"}, - {"id": 2503, "synset": "cochin.n.01", "name": "cochin"}, - {"id": 2504, "synset": "jungle_fowl.n.01", "name": "jungle_fowl"}, - {"id": 2505, "synset": "jungle_cock.n.01", "name": "jungle_cock"}, - {"id": 2506, "synset": "jungle_hen.n.01", "name": "jungle_hen"}, - {"id": 2507, "synset": "red_jungle_fowl.n.01", "name": "red_jungle_fowl"}, - {"id": 2508, "synset": "bantam.n.01", "name": "bantam"}, - {"id": 2509, "synset": "chick.n.01", "name": "chick"}, - {"id": 2510, "synset": "cockerel.n.01", "name": "cockerel"}, - {"id": 2511, "synset": "capon.n.02", "name": "capon"}, - {"id": 2512, "synset": "hen.n.01", "name": "hen"}, - {"id": 2513, "synset": "cackler.n.01", "name": "cackler"}, - {"id": 2514, "synset": "brood_hen.n.01", "name": "brood_hen"}, - {"id": 2515, "synset": "mother_hen.n.02", "name": "mother_hen"}, - {"id": 2516, "synset": "layer.n.04", "name": "layer"}, - {"id": 2517, "synset": "pullet.n.02", "name": "pullet"}, - {"id": 2518, "synset": "spring_chicken.n.02", "name": "spring_chicken"}, - {"id": 2519, "synset": "rhode_island_red.n.01", "name": "Rhode_Island_red"}, - {"id": 2520, "synset": "dominique.n.01", "name": "Dominique"}, - {"id": 2521, "synset": "orpington.n.01", "name": "Orpington"}, - {"id": 2522, "synset": "turkey.n.01", "name": "turkey"}, - {"id": 2523, "synset": "turkey_cock.n.01", "name": "turkey_cock"}, - {"id": 2524, "synset": "ocellated_turkey.n.01", "name": "ocellated_turkey"}, - {"id": 2525, "synset": "grouse.n.02", "name": "grouse"}, - {"id": 2526, "synset": "black_grouse.n.01", "name": "black_grouse"}, - {"id": 2527, "synset": "european_black_grouse.n.01", "name": "European_black_grouse"}, - {"id": 2528, "synset": "asian_black_grouse.n.01", "name": "Asian_black_grouse"}, - {"id": 2529, "synset": "blackcock.n.01", "name": "blackcock"}, - {"id": 2530, "synset": "greyhen.n.01", "name": "greyhen"}, - {"id": 2531, "synset": "ptarmigan.n.01", "name": "ptarmigan"}, - {"id": 2532, "synset": "red_grouse.n.01", "name": "red_grouse"}, - {"id": 2533, "synset": "moorhen.n.02", "name": "moorhen"}, - {"id": 2534, "synset": "capercaillie.n.01", "name": "capercaillie"}, - {"id": 2535, "synset": "spruce_grouse.n.01", "name": "spruce_grouse"}, - {"id": 2536, "synset": "sage_grouse.n.01", "name": "sage_grouse"}, - {"id": 2537, "synset": "ruffed_grouse.n.01", "name": "ruffed_grouse"}, - {"id": 2538, "synset": "sharp-tailed_grouse.n.01", "name": "sharp-tailed_grouse"}, - {"id": 2539, "synset": "prairie_chicken.n.01", "name": "prairie_chicken"}, - {"id": 2540, "synset": "greater_prairie_chicken.n.01", "name": "greater_prairie_chicken"}, - {"id": 2541, "synset": "lesser_prairie_chicken.n.01", "name": "lesser_prairie_chicken"}, - {"id": 2542, "synset": "heath_hen.n.01", "name": "heath_hen"}, - {"id": 2543, "synset": "guan.n.01", "name": "guan"}, - {"id": 2544, "synset": "curassow.n.01", "name": "curassow"}, - {"id": 2545, "synset": "piping_guan.n.01", "name": "piping_guan"}, - {"id": 2546, "synset": "chachalaca.n.01", "name": "chachalaca"}, - {"id": 2547, "synset": "texas_chachalaca.n.01", "name": "Texas_chachalaca"}, - {"id": 2548, "synset": "megapode.n.01", "name": "megapode"}, - {"id": 2549, "synset": "mallee_fowl.n.01", "name": "mallee_fowl"}, - {"id": 2550, "synset": "mallee_hen.n.01", "name": "mallee_hen"}, - {"id": 2551, "synset": "brush_turkey.n.01", "name": "brush_turkey"}, - {"id": 2552, "synset": "maleo.n.01", "name": "maleo"}, - {"id": 2553, "synset": "phasianid.n.01", "name": "phasianid"}, - {"id": 2554, "synset": "pheasant.n.01", "name": "pheasant"}, - {"id": 2555, "synset": "ring-necked_pheasant.n.01", "name": "ring-necked_pheasant"}, - {"id": 2556, "synset": "afropavo.n.01", "name": "afropavo"}, - {"id": 2557, "synset": "argus.n.02", "name": "argus"}, - {"id": 2558, "synset": "golden_pheasant.n.01", "name": "golden_pheasant"}, - {"id": 2559, "synset": "bobwhite.n.01", "name": "bobwhite"}, - {"id": 2560, "synset": "northern_bobwhite.n.01", "name": "northern_bobwhite"}, - {"id": 2561, "synset": "old_world_quail.n.01", "name": "Old_World_quail"}, - {"id": 2562, "synset": "migratory_quail.n.01", "name": "migratory_quail"}, - {"id": 2563, "synset": "monal.n.01", "name": "monal"}, - {"id": 2564, "synset": "peafowl.n.01", "name": "peafowl"}, - {"id": 2565, "synset": "peachick.n.01", "name": "peachick"}, - {"id": 2566, "synset": "peacock.n.02", "name": "peacock"}, - {"id": 2567, "synset": "peahen.n.01", "name": "peahen"}, - {"id": 2568, "synset": "blue_peafowl.n.01", "name": "blue_peafowl"}, - {"id": 2569, "synset": "green_peafowl.n.01", "name": "green_peafowl"}, - {"id": 2570, "synset": "quail.n.02", "name": "quail"}, - {"id": 2571, "synset": "california_quail.n.01", "name": "California_quail"}, - {"id": 2572, "synset": "tragopan.n.01", "name": "tragopan"}, - {"id": 2573, "synset": "partridge.n.03", "name": "partridge"}, - {"id": 2574, "synset": "hungarian_partridge.n.01", "name": "Hungarian_partridge"}, - {"id": 2575, "synset": "red-legged_partridge.n.01", "name": "red-legged_partridge"}, - {"id": 2576, "synset": "greek_partridge.n.01", "name": "Greek_partridge"}, - {"id": 2577, "synset": "mountain_quail.n.01", "name": "mountain_quail"}, - {"id": 2578, "synset": "guinea_fowl.n.01", "name": "guinea_fowl"}, - {"id": 2579, "synset": "guinea_hen.n.02", "name": "guinea_hen"}, - {"id": 2580, "synset": "hoatzin.n.01", "name": "hoatzin"}, - {"id": 2581, "synset": "tinamou.n.01", "name": "tinamou"}, - {"id": 2582, "synset": "columbiform_bird.n.01", "name": "columbiform_bird"}, - {"id": 2583, "synset": "dodo.n.02", "name": "dodo"}, - {"id": 2584, "synset": "pouter_pigeon.n.01", "name": "pouter_pigeon"}, - {"id": 2585, "synset": "rock_dove.n.01", "name": "rock_dove"}, - {"id": 2586, "synset": "band-tailed_pigeon.n.01", "name": "band-tailed_pigeon"}, - {"id": 2587, "synset": "wood_pigeon.n.01", "name": "wood_pigeon"}, - {"id": 2588, "synset": "turtledove.n.02", "name": "turtledove"}, - {"id": 2589, "synset": "streptopelia_turtur.n.01", "name": "Streptopelia_turtur"}, - {"id": 2590, "synset": "ringdove.n.01", "name": "ringdove"}, - {"id": 2591, "synset": "australian_turtledove.n.01", "name": "Australian_turtledove"}, - {"id": 2592, "synset": "mourning_dove.n.01", "name": "mourning_dove"}, - {"id": 2593, "synset": "domestic_pigeon.n.01", "name": "domestic_pigeon"}, - {"id": 2594, "synset": "squab.n.03", "name": "squab"}, - {"id": 2595, "synset": "fairy_swallow.n.01", "name": "fairy_swallow"}, - {"id": 2596, "synset": "roller.n.07", "name": "roller"}, - {"id": 2597, "synset": "homing_pigeon.n.01", "name": "homing_pigeon"}, - {"id": 2598, "synset": "carrier_pigeon.n.01", "name": "carrier_pigeon"}, - {"id": 2599, "synset": "passenger_pigeon.n.01", "name": "passenger_pigeon"}, - {"id": 2600, "synset": "sandgrouse.n.01", "name": "sandgrouse"}, - {"id": 2601, "synset": "painted_sandgrouse.n.01", "name": "painted_sandgrouse"}, - {"id": 2602, "synset": "pin-tailed_sandgrouse.n.01", "name": "pin-tailed_sandgrouse"}, - {"id": 2603, "synset": "pallas's_sandgrouse.n.01", "name": "pallas's_sandgrouse"}, - {"id": 2604, "synset": "popinjay.n.02", "name": "popinjay"}, - {"id": 2605, "synset": "poll.n.04", "name": "poll"}, - {"id": 2606, "synset": "african_grey.n.01", "name": "African_grey"}, - {"id": 2607, "synset": "amazon.n.04", "name": "amazon"}, - {"id": 2608, "synset": "macaw.n.01", "name": "macaw"}, - {"id": 2609, "synset": "kea.n.01", "name": "kea"}, - {"id": 2610, "synset": "cockatoo.n.01", "name": "cockatoo"}, - {"id": 2611, "synset": "sulphur-crested_cockatoo.n.01", "name": "sulphur-crested_cockatoo"}, - {"id": 2612, "synset": "pink_cockatoo.n.01", "name": "pink_cockatoo"}, - {"id": 2613, "synset": "cockateel.n.01", "name": "cockateel"}, - {"id": 2614, "synset": "lovebird.n.02", "name": "lovebird"}, - {"id": 2615, "synset": "lory.n.01", "name": "lory"}, - {"id": 2616, "synset": "lorikeet.n.01", "name": "lorikeet"}, - {"id": 2617, "synset": "varied_lorikeet.n.01", "name": "varied_Lorikeet"}, - {"id": 2618, "synset": "rainbow_lorikeet.n.01", "name": "rainbow_lorikeet"}, - {"id": 2619, "synset": "carolina_parakeet.n.01", "name": "Carolina_parakeet"}, - {"id": 2620, "synset": "budgerigar.n.01", "name": "budgerigar"}, - {"id": 2621, "synset": "ring-necked_parakeet.n.01", "name": "ring-necked_parakeet"}, - {"id": 2622, "synset": "cuculiform_bird.n.01", "name": "cuculiform_bird"}, - {"id": 2623, "synset": "cuckoo.n.02", "name": "cuckoo"}, - {"id": 2624, "synset": "european_cuckoo.n.01", "name": "European_cuckoo"}, - {"id": 2625, "synset": "black-billed_cuckoo.n.01", "name": "black-billed_cuckoo"}, - {"id": 2626, "synset": "roadrunner.n.01", "name": "roadrunner"}, - {"id": 2627, "synset": "ani.n.01", "name": "ani"}, - {"id": 2628, "synset": "coucal.n.01", "name": "coucal"}, - {"id": 2629, "synset": "crow_pheasant.n.01", "name": "crow_pheasant"}, - {"id": 2630, "synset": "touraco.n.01", "name": "touraco"}, - {"id": 2631, "synset": "coraciiform_bird.n.01", "name": "coraciiform_bird"}, - {"id": 2632, "synset": "roller.n.06", "name": "roller"}, - {"id": 2633, "synset": "european_roller.n.01", "name": "European_roller"}, - {"id": 2634, "synset": "ground_roller.n.01", "name": "ground_roller"}, - {"id": 2635, "synset": "kingfisher.n.01", "name": "kingfisher"}, - {"id": 2636, "synset": "eurasian_kingfisher.n.01", "name": "Eurasian_kingfisher"}, - {"id": 2637, "synset": "belted_kingfisher.n.01", "name": "belted_kingfisher"}, - {"id": 2638, "synset": "kookaburra.n.01", "name": "kookaburra"}, - {"id": 2639, "synset": "bee_eater.n.01", "name": "bee_eater"}, - {"id": 2640, "synset": "hornbill.n.01", "name": "hornbill"}, - {"id": 2641, "synset": "hoopoe.n.01", "name": "hoopoe"}, - {"id": 2642, "synset": "euopean_hoopoe.n.01", "name": "Euopean_hoopoe"}, - {"id": 2643, "synset": "wood_hoopoe.n.01", "name": "wood_hoopoe"}, - {"id": 2644, "synset": "motmot.n.01", "name": "motmot"}, - {"id": 2645, "synset": "tody.n.01", "name": "tody"}, - {"id": 2646, "synset": "apodiform_bird.n.01", "name": "apodiform_bird"}, - {"id": 2647, "synset": "swift.n.03", "name": "swift"}, - {"id": 2648, "synset": "european_swift.n.01", "name": "European_swift"}, - {"id": 2649, "synset": "chimney_swift.n.01", "name": "chimney_swift"}, - {"id": 2650, "synset": "swiftlet.n.01", "name": "swiftlet"}, - {"id": 2651, "synset": "tree_swift.n.01", "name": "tree_swift"}, - {"id": 2652, "synset": "archilochus_colubris.n.01", "name": "Archilochus_colubris"}, - {"id": 2653, "synset": "thornbill.n.01", "name": "thornbill"}, - {"id": 2654, "synset": "goatsucker.n.01", "name": "goatsucker"}, - {"id": 2655, "synset": "european_goatsucker.n.01", "name": "European_goatsucker"}, - {"id": 2656, "synset": "chuck-will's-widow.n.01", "name": "chuck-will's-widow"}, - {"id": 2657, "synset": "whippoorwill.n.01", "name": "whippoorwill"}, - {"id": 2658, "synset": "poorwill.n.01", "name": "poorwill"}, - {"id": 2659, "synset": "frogmouth.n.01", "name": "frogmouth"}, - {"id": 2660, "synset": "oilbird.n.01", "name": "oilbird"}, - {"id": 2661, "synset": "piciform_bird.n.01", "name": "piciform_bird"}, - {"id": 2662, "synset": "woodpecker.n.01", "name": "woodpecker"}, - {"id": 2663, "synset": "green_woodpecker.n.01", "name": "green_woodpecker"}, - {"id": 2664, "synset": "downy_woodpecker.n.01", "name": "downy_woodpecker"}, - {"id": 2665, "synset": "flicker.n.02", "name": "flicker"}, - {"id": 2666, "synset": "yellow-shafted_flicker.n.01", "name": "yellow-shafted_flicker"}, - {"id": 2667, "synset": "gilded_flicker.n.01", "name": "gilded_flicker"}, - {"id": 2668, "synset": "red-shafted_flicker.n.01", "name": "red-shafted_flicker"}, - {"id": 2669, "synset": "ivorybill.n.01", "name": "ivorybill"}, - {"id": 2670, "synset": "redheaded_woodpecker.n.01", "name": "redheaded_woodpecker"}, - {"id": 2671, "synset": "sapsucker.n.01", "name": "sapsucker"}, - {"id": 2672, "synset": "yellow-bellied_sapsucker.n.01", "name": "yellow-bellied_sapsucker"}, - {"id": 2673, "synset": "red-breasted_sapsucker.n.01", "name": "red-breasted_sapsucker"}, - {"id": 2674, "synset": "wryneck.n.02", "name": "wryneck"}, - {"id": 2675, "synset": "piculet.n.01", "name": "piculet"}, - {"id": 2676, "synset": "barbet.n.01", "name": "barbet"}, - {"id": 2677, "synset": "puffbird.n.01", "name": "puffbird"}, - {"id": 2678, "synset": "honey_guide.n.01", "name": "honey_guide"}, - {"id": 2679, "synset": "jacamar.n.01", "name": "jacamar"}, - {"id": 2680, "synset": "toucan.n.01", "name": "toucan"}, - {"id": 2681, "synset": "toucanet.n.01", "name": "toucanet"}, - {"id": 2682, "synset": "trogon.n.01", "name": "trogon"}, - {"id": 2683, "synset": "quetzal.n.02", "name": "quetzal"}, - {"id": 2684, "synset": "resplendent_quetzel.n.01", "name": "resplendent_quetzel"}, - {"id": 2685, "synset": "aquatic_bird.n.01", "name": "aquatic_bird"}, - {"id": 2686, "synset": "waterfowl.n.01", "name": "waterfowl"}, - {"id": 2687, "synset": "anseriform_bird.n.01", "name": "anseriform_bird"}, - {"id": 2688, "synset": "drake.n.02", "name": "drake"}, - {"id": 2689, "synset": "quack-quack.n.01", "name": "quack-quack"}, - {"id": 2690, "synset": "diving_duck.n.01", "name": "diving_duck"}, - {"id": 2691, "synset": "dabbling_duck.n.01", "name": "dabbling_duck"}, - {"id": 2692, "synset": "black_duck.n.01", "name": "black_duck"}, - {"id": 2693, "synset": "teal.n.02", "name": "teal"}, - {"id": 2694, "synset": "greenwing.n.01", "name": "greenwing"}, - {"id": 2695, "synset": "bluewing.n.01", "name": "bluewing"}, - {"id": 2696, "synset": "garganey.n.01", "name": "garganey"}, - {"id": 2697, "synset": "widgeon.n.01", "name": "widgeon"}, - {"id": 2698, "synset": "american_widgeon.n.01", "name": "American_widgeon"}, - {"id": 2699, "synset": "shoveler.n.02", "name": "shoveler"}, - {"id": 2700, "synset": "pintail.n.01", "name": "pintail"}, - {"id": 2701, "synset": "sheldrake.n.02", "name": "sheldrake"}, - {"id": 2702, "synset": "shelduck.n.01", "name": "shelduck"}, - {"id": 2703, "synset": "ruddy_duck.n.01", "name": "ruddy_duck"}, - {"id": 2704, "synset": "bufflehead.n.01", "name": "bufflehead"}, - {"id": 2705, "synset": "goldeneye.n.02", "name": "goldeneye"}, - {"id": 2706, "synset": "barrow's_goldeneye.n.01", "name": "Barrow's_goldeneye"}, - {"id": 2707, "synset": "canvasback.n.01", "name": "canvasback"}, - {"id": 2708, "synset": "pochard.n.01", "name": "pochard"}, - {"id": 2709, "synset": "redhead.n.02", "name": "redhead"}, - {"id": 2710, "synset": "scaup.n.01", "name": "scaup"}, - {"id": 2711, "synset": "greater_scaup.n.01", "name": "greater_scaup"}, - {"id": 2712, "synset": "lesser_scaup.n.01", "name": "lesser_scaup"}, - {"id": 2713, "synset": "wild_duck.n.01", "name": "wild_duck"}, - {"id": 2714, "synset": "wood_duck.n.01", "name": "wood_duck"}, - {"id": 2715, "synset": "wood_drake.n.01", "name": "wood_drake"}, - {"id": 2716, "synset": "mandarin_duck.n.01", "name": "mandarin_duck"}, - {"id": 2717, "synset": "muscovy_duck.n.01", "name": "muscovy_duck"}, - {"id": 2718, "synset": "sea_duck.n.01", "name": "sea_duck"}, - {"id": 2719, "synset": "eider.n.01", "name": "eider"}, - {"id": 2720, "synset": "scoter.n.01", "name": "scoter"}, - {"id": 2721, "synset": "common_scoter.n.01", "name": "common_scoter"}, - {"id": 2722, "synset": "old_squaw.n.01", "name": "old_squaw"}, - {"id": 2723, "synset": "merganser.n.01", "name": "merganser"}, - {"id": 2724, "synset": "goosander.n.01", "name": "goosander"}, - {"id": 2725, "synset": "american_merganser.n.01", "name": "American_merganser"}, - {"id": 2726, "synset": "red-breasted_merganser.n.01", "name": "red-breasted_merganser"}, - {"id": 2727, "synset": "smew.n.01", "name": "smew"}, - {"id": 2728, "synset": "hooded_merganser.n.01", "name": "hooded_merganser"}, - {"id": 2729, "synset": "gosling.n.01", "name": "gosling"}, - {"id": 2730, "synset": "gander.n.01", "name": "gander"}, - {"id": 2731, "synset": "chinese_goose.n.01", "name": "Chinese_goose"}, - {"id": 2732, "synset": "greylag.n.01", "name": "greylag"}, - {"id": 2733, "synset": "blue_goose.n.01", "name": "blue_goose"}, - {"id": 2734, "synset": "snow_goose.n.01", "name": "snow_goose"}, - {"id": 2735, "synset": "brant.n.01", "name": "brant"}, - {"id": 2736, "synset": "common_brant_goose.n.01", "name": "common_brant_goose"}, - {"id": 2737, "synset": "honker.n.03", "name": "honker"}, - {"id": 2738, "synset": "barnacle_goose.n.01", "name": "barnacle_goose"}, - {"id": 2739, "synset": "coscoroba.n.01", "name": "coscoroba"}, - {"id": 2740, "synset": "swan.n.01", "name": "swan"}, - {"id": 2741, "synset": "cob.n.04", "name": "cob"}, - {"id": 2742, "synset": "pen.n.05", "name": "pen"}, - {"id": 2743, "synset": "cygnet.n.01", "name": "cygnet"}, - {"id": 2744, "synset": "mute_swan.n.01", "name": "mute_swan"}, - {"id": 2745, "synset": "whooper.n.02", "name": "whooper"}, - {"id": 2746, "synset": "tundra_swan.n.01", "name": "tundra_swan"}, - {"id": 2747, "synset": "whistling_swan.n.01", "name": "whistling_swan"}, - {"id": 2748, "synset": "bewick's_swan.n.01", "name": "Bewick's_swan"}, - {"id": 2749, "synset": "trumpeter.n.04", "name": "trumpeter"}, - {"id": 2750, "synset": "black_swan.n.01", "name": "black_swan"}, - {"id": 2751, "synset": "screamer.n.03", "name": "screamer"}, - {"id": 2752, "synset": "horned_screamer.n.01", "name": "horned_screamer"}, - {"id": 2753, "synset": "crested_screamer.n.01", "name": "crested_screamer"}, - {"id": 2754, "synset": "chaja.n.01", "name": "chaja"}, - {"id": 2755, "synset": "mammal.n.01", "name": "mammal"}, - {"id": 2756, "synset": "female_mammal.n.01", "name": "female_mammal"}, - {"id": 2757, "synset": "tusker.n.01", "name": "tusker"}, - {"id": 2758, "synset": "prototherian.n.01", "name": "prototherian"}, - {"id": 2759, "synset": "monotreme.n.01", "name": "monotreme"}, - {"id": 2760, "synset": "echidna.n.02", "name": "echidna"}, - {"id": 2761, "synset": "echidna.n.01", "name": "echidna"}, - {"id": 2762, "synset": "platypus.n.01", "name": "platypus"}, - {"id": 2763, "synset": "marsupial.n.01", "name": "marsupial"}, - {"id": 2764, "synset": "opossum.n.02", "name": "opossum"}, - {"id": 2765, "synset": "common_opossum.n.01", "name": "common_opossum"}, - {"id": 2766, "synset": "crab-eating_opossum.n.01", "name": "crab-eating_opossum"}, - {"id": 2767, "synset": "opossum_rat.n.01", "name": "opossum_rat"}, - {"id": 2768, "synset": "bandicoot.n.01", "name": "bandicoot"}, - {"id": 2769, "synset": "rabbit-eared_bandicoot.n.01", "name": "rabbit-eared_bandicoot"}, - {"id": 2770, "synset": "kangaroo.n.01", "name": "kangaroo"}, - {"id": 2771, "synset": "giant_kangaroo.n.01", "name": "giant_kangaroo"}, - {"id": 2772, "synset": "wallaby.n.01", "name": "wallaby"}, - {"id": 2773, "synset": "common_wallaby.n.01", "name": "common_wallaby"}, - {"id": 2774, "synset": "hare_wallaby.n.01", "name": "hare_wallaby"}, - {"id": 2775, "synset": "nail-tailed_wallaby.n.01", "name": "nail-tailed_wallaby"}, - {"id": 2776, "synset": "rock_wallaby.n.01", "name": "rock_wallaby"}, - {"id": 2777, "synset": "pademelon.n.01", "name": "pademelon"}, - {"id": 2778, "synset": "tree_wallaby.n.01", "name": "tree_wallaby"}, - {"id": 2779, "synset": "musk_kangaroo.n.01", "name": "musk_kangaroo"}, - {"id": 2780, "synset": "rat_kangaroo.n.01", "name": "rat_kangaroo"}, - {"id": 2781, "synset": "potoroo.n.01", "name": "potoroo"}, - {"id": 2782, "synset": "bettong.n.01", "name": "bettong"}, - {"id": 2783, "synset": "jerboa_kangaroo.n.01", "name": "jerboa_kangaroo"}, - {"id": 2784, "synset": "phalanger.n.01", "name": "phalanger"}, - {"id": 2785, "synset": "cuscus.n.01", "name": "cuscus"}, - {"id": 2786, "synset": "brush-tailed_phalanger.n.01", "name": "brush-tailed_phalanger"}, - {"id": 2787, "synset": "flying_phalanger.n.01", "name": "flying_phalanger"}, - {"id": 2788, "synset": "wombat.n.01", "name": "wombat"}, - {"id": 2789, "synset": "dasyurid_marsupial.n.01", "name": "dasyurid_marsupial"}, - {"id": 2790, "synset": "dasyure.n.01", "name": "dasyure"}, - {"id": 2791, "synset": "eastern_dasyure.n.01", "name": "eastern_dasyure"}, - {"id": 2792, "synset": "native_cat.n.01", "name": "native_cat"}, - {"id": 2793, "synset": "thylacine.n.01", "name": "thylacine"}, - {"id": 2794, "synset": "tasmanian_devil.n.01", "name": "Tasmanian_devil"}, - {"id": 2795, "synset": "pouched_mouse.n.01", "name": "pouched_mouse"}, - {"id": 2796, "synset": "numbat.n.01", "name": "numbat"}, - {"id": 2797, "synset": "pouched_mole.n.01", "name": "pouched_mole"}, - {"id": 2798, "synset": "placental.n.01", "name": "placental"}, - {"id": 2799, "synset": "livestock.n.01", "name": "livestock"}, - {"id": 2800, "synset": "cow.n.02", "name": "cow"}, - {"id": 2801, "synset": "calf.n.04", "name": "calf"}, - {"id": 2802, "synset": "yearling.n.03", "name": "yearling"}, - {"id": 2803, "synset": "buck.n.05", "name": "buck"}, - {"id": 2804, "synset": "doe.n.02", "name": "doe"}, - {"id": 2805, "synset": "insectivore.n.01", "name": "insectivore"}, - {"id": 2806, "synset": "mole.n.06", "name": "mole"}, - {"id": 2807, "synset": "starnose_mole.n.01", "name": "starnose_mole"}, - {"id": 2808, "synset": "brewer's_mole.n.01", "name": "brewer's_mole"}, - {"id": 2809, "synset": "golden_mole.n.01", "name": "golden_mole"}, - {"id": 2810, "synset": "shrew_mole.n.01", "name": "shrew_mole"}, - {"id": 2811, "synset": "asiatic_shrew_mole.n.01", "name": "Asiatic_shrew_mole"}, - {"id": 2812, "synset": "american_shrew_mole.n.01", "name": "American_shrew_mole"}, - {"id": 2813, "synset": "shrew.n.02", "name": "shrew"}, - {"id": 2814, "synset": "common_shrew.n.01", "name": "common_shrew"}, - {"id": 2815, "synset": "masked_shrew.n.01", "name": "masked_shrew"}, - {"id": 2816, "synset": "short-tailed_shrew.n.01", "name": "short-tailed_shrew"}, - {"id": 2817, "synset": "water_shrew.n.01", "name": "water_shrew"}, - {"id": 2818, "synset": "american_water_shrew.n.01", "name": "American_water_shrew"}, - {"id": 2819, "synset": "european_water_shrew.n.01", "name": "European_water_shrew"}, - {"id": 2820, "synset": "mediterranean_water_shrew.n.01", "name": "Mediterranean_water_shrew"}, - {"id": 2821, "synset": "least_shrew.n.01", "name": "least_shrew"}, - {"id": 2822, "synset": "hedgehog.n.02", "name": "hedgehog"}, - {"id": 2823, "synset": "tenrec.n.01", "name": "tenrec"}, - {"id": 2824, "synset": "tailless_tenrec.n.01", "name": "tailless_tenrec"}, - {"id": 2825, "synset": "otter_shrew.n.01", "name": "otter_shrew"}, - {"id": 2826, "synset": "eiderdown.n.02", "name": "eiderdown"}, - {"id": 2827, "synset": "aftershaft.n.01", "name": "aftershaft"}, - {"id": 2828, "synset": "sickle_feather.n.01", "name": "sickle_feather"}, - {"id": 2829, "synset": "contour_feather.n.01", "name": "contour_feather"}, - {"id": 2830, "synset": "bastard_wing.n.01", "name": "bastard_wing"}, - {"id": 2831, "synset": "saddle_hackle.n.01", "name": "saddle_hackle"}, - {"id": 2832, "synset": "encolure.n.01", "name": "encolure"}, - {"id": 2833, "synset": "hair.n.06", "name": "hair"}, - {"id": 2834, "synset": "squama.n.01", "name": "squama"}, - {"id": 2835, "synset": "scute.n.01", "name": "scute"}, - {"id": 2836, "synset": "sclerite.n.01", "name": "sclerite"}, - {"id": 2837, "synset": "plastron.n.05", "name": "plastron"}, - {"id": 2838, "synset": "scallop_shell.n.01", "name": "scallop_shell"}, - {"id": 2839, "synset": "oyster_shell.n.01", "name": "oyster_shell"}, - {"id": 2840, "synset": "theca.n.02", "name": "theca"}, - {"id": 2841, "synset": "invertebrate.n.01", "name": "invertebrate"}, - {"id": 2842, "synset": "sponge.n.04", "name": "sponge"}, - {"id": 2843, "synset": "choanocyte.n.01", "name": "choanocyte"}, - {"id": 2844, "synset": "glass_sponge.n.01", "name": "glass_sponge"}, - {"id": 2845, "synset": "venus's_flower_basket.n.01", "name": "Venus's_flower_basket"}, - {"id": 2846, "synset": "metazoan.n.01", "name": "metazoan"}, - {"id": 2847, "synset": "coelenterate.n.01", "name": "coelenterate"}, - {"id": 2848, "synset": "planula.n.01", "name": "planula"}, - {"id": 2849, "synset": "polyp.n.02", "name": "polyp"}, - {"id": 2850, "synset": "medusa.n.02", "name": "medusa"}, - {"id": 2851, "synset": "jellyfish.n.02", "name": "jellyfish"}, - {"id": 2852, "synset": "scyphozoan.n.01", "name": "scyphozoan"}, - {"id": 2853, "synset": "chrysaora_quinquecirrha.n.01", "name": "Chrysaora_quinquecirrha"}, - {"id": 2854, "synset": "hydrozoan.n.01", "name": "hydrozoan"}, - {"id": 2855, "synset": "hydra.n.04", "name": "hydra"}, - {"id": 2856, "synset": "siphonophore.n.01", "name": "siphonophore"}, - {"id": 2857, "synset": "nanomia.n.01", "name": "nanomia"}, - {"id": 2858, "synset": "portuguese_man-of-war.n.01", "name": "Portuguese_man-of-war"}, - {"id": 2859, "synset": "praya.n.01", "name": "praya"}, - {"id": 2860, "synset": "apolemia.n.01", "name": "apolemia"}, - {"id": 2861, "synset": "anthozoan.n.01", "name": "anthozoan"}, - {"id": 2862, "synset": "sea_anemone.n.01", "name": "sea_anemone"}, - {"id": 2863, "synset": "actinia.n.02", "name": "actinia"}, - {"id": 2864, "synset": "sea_pen.n.01", "name": "sea_pen"}, - {"id": 2865, "synset": "coral.n.04", "name": "coral"}, - {"id": 2866, "synset": "gorgonian.n.01", "name": "gorgonian"}, - {"id": 2867, "synset": "sea_feather.n.01", "name": "sea_feather"}, - {"id": 2868, "synset": "sea_fan.n.01", "name": "sea_fan"}, - {"id": 2869, "synset": "red_coral.n.02", "name": "red_coral"}, - {"id": 2870, "synset": "stony_coral.n.01", "name": "stony_coral"}, - {"id": 2871, "synset": "brain_coral.n.01", "name": "brain_coral"}, - {"id": 2872, "synset": "staghorn_coral.n.01", "name": "staghorn_coral"}, - {"id": 2873, "synset": "mushroom_coral.n.01", "name": "mushroom_coral"}, - {"id": 2874, "synset": "ctenophore.n.01", "name": "ctenophore"}, - {"id": 2875, "synset": "beroe.n.01", "name": "beroe"}, - {"id": 2876, "synset": "platyctenean.n.01", "name": "platyctenean"}, - {"id": 2877, "synset": "sea_gooseberry.n.01", "name": "sea_gooseberry"}, - {"id": 2878, "synset": "venus's_girdle.n.01", "name": "Venus's_girdle"}, - {"id": 2879, "synset": "worm.n.01", "name": "worm"}, - {"id": 2880, "synset": "helminth.n.01", "name": "helminth"}, - {"id": 2881, "synset": "woodworm.n.01", "name": "woodworm"}, - {"id": 2882, "synset": "woodborer.n.01", "name": "woodborer"}, - {"id": 2883, "synset": "acanthocephalan.n.01", "name": "acanthocephalan"}, - {"id": 2884, "synset": "arrowworm.n.01", "name": "arrowworm"}, - {"id": 2885, "synset": "bladder_worm.n.01", "name": "bladder_worm"}, - {"id": 2886, "synset": "flatworm.n.01", "name": "flatworm"}, - {"id": 2887, "synset": "planarian.n.01", "name": "planarian"}, - {"id": 2888, "synset": "fluke.n.05", "name": "fluke"}, - {"id": 2889, "synset": "cercaria.n.01", "name": "cercaria"}, - {"id": 2890, "synset": "liver_fluke.n.01", "name": "liver_fluke"}, - {"id": 2891, "synset": "fasciolopsis_buski.n.01", "name": "Fasciolopsis_buski"}, - {"id": 2892, "synset": "schistosome.n.01", "name": "schistosome"}, - {"id": 2893, "synset": "tapeworm.n.01", "name": "tapeworm"}, - {"id": 2894, "synset": "echinococcus.n.01", "name": "echinococcus"}, - {"id": 2895, "synset": "taenia.n.02", "name": "taenia"}, - {"id": 2896, "synset": "ribbon_worm.n.01", "name": "ribbon_worm"}, - {"id": 2897, "synset": "beard_worm.n.01", "name": "beard_worm"}, - {"id": 2898, "synset": "rotifer.n.01", "name": "rotifer"}, - {"id": 2899, "synset": "nematode.n.01", "name": "nematode"}, - {"id": 2900, "synset": "common_roundworm.n.01", "name": "common_roundworm"}, - {"id": 2901, "synset": "chicken_roundworm.n.01", "name": "chicken_roundworm"}, - {"id": 2902, "synset": "pinworm.n.01", "name": "pinworm"}, - {"id": 2903, "synset": "eelworm.n.01", "name": "eelworm"}, - {"id": 2904, "synset": "vinegar_eel.n.01", "name": "vinegar_eel"}, - {"id": 2905, "synset": "trichina.n.01", "name": "trichina"}, - {"id": 2906, "synset": "hookworm.n.01", "name": "hookworm"}, - {"id": 2907, "synset": "filaria.n.02", "name": "filaria"}, - {"id": 2908, "synset": "guinea_worm.n.02", "name": "Guinea_worm"}, - {"id": 2909, "synset": "annelid.n.01", "name": "annelid"}, - {"id": 2910, "synset": "archiannelid.n.01", "name": "archiannelid"}, - {"id": 2911, "synset": "oligochaete.n.01", "name": "oligochaete"}, - {"id": 2912, "synset": "earthworm.n.01", "name": "earthworm"}, - {"id": 2913, "synset": "polychaete.n.01", "name": "polychaete"}, - {"id": 2914, "synset": "lugworm.n.01", "name": "lugworm"}, - {"id": 2915, "synset": "sea_mouse.n.01", "name": "sea_mouse"}, - {"id": 2916, "synset": "bloodworm.n.01", "name": "bloodworm"}, - {"id": 2917, "synset": "leech.n.01", "name": "leech"}, - {"id": 2918, "synset": "medicinal_leech.n.01", "name": "medicinal_leech"}, - {"id": 2919, "synset": "horseleech.n.01", "name": "horseleech"}, - {"id": 2920, "synset": "mollusk.n.01", "name": "mollusk"}, - {"id": 2921, "synset": "scaphopod.n.01", "name": "scaphopod"}, - {"id": 2922, "synset": "tooth_shell.n.01", "name": "tooth_shell"}, - {"id": 2923, "synset": "gastropod.n.01", "name": "gastropod"}, - {"id": 2924, "synset": "abalone.n.01", "name": "abalone"}, - {"id": 2925, "synset": "ormer.n.01", "name": "ormer"}, - {"id": 2926, "synset": "scorpion_shell.n.01", "name": "scorpion_shell"}, - {"id": 2927, "synset": "conch.n.01", "name": "conch"}, - {"id": 2928, "synset": "giant_conch.n.01", "name": "giant_conch"}, - {"id": 2929, "synset": "snail.n.01", "name": "snail"}, - {"id": 2930, "synset": "edible_snail.n.01", "name": "edible_snail"}, - {"id": 2931, "synset": "garden_snail.n.01", "name": "garden_snail"}, - {"id": 2932, "synset": "brown_snail.n.01", "name": "brown_snail"}, - {"id": 2933, "synset": "helix_hortensis.n.01", "name": "Helix_hortensis"}, - {"id": 2934, "synset": "slug.n.07", "name": "slug"}, - {"id": 2935, "synset": "seasnail.n.02", "name": "seasnail"}, - {"id": 2936, "synset": "neritid.n.01", "name": "neritid"}, - {"id": 2937, "synset": "nerita.n.01", "name": "nerita"}, - {"id": 2938, "synset": "bleeding_tooth.n.01", "name": "bleeding_tooth"}, - {"id": 2939, "synset": "neritina.n.01", "name": "neritina"}, - {"id": 2940, "synset": "whelk.n.02", "name": "whelk"}, - {"id": 2941, "synset": "moon_shell.n.01", "name": "moon_shell"}, - {"id": 2942, "synset": "periwinkle.n.04", "name": "periwinkle"}, - {"id": 2943, "synset": "limpet.n.02", "name": "limpet"}, - {"id": 2944, "synset": "common_limpet.n.01", "name": "common_limpet"}, - {"id": 2945, "synset": "keyhole_limpet.n.01", "name": "keyhole_limpet"}, - {"id": 2946, "synset": "river_limpet.n.01", "name": "river_limpet"}, - {"id": 2947, "synset": "sea_slug.n.01", "name": "sea_slug"}, - {"id": 2948, "synset": "sea_hare.n.01", "name": "sea_hare"}, - {"id": 2949, "synset": "hermissenda_crassicornis.n.01", "name": "Hermissenda_crassicornis"}, - {"id": 2950, "synset": "bubble_shell.n.01", "name": "bubble_shell"}, - {"id": 2951, "synset": "physa.n.01", "name": "physa"}, - {"id": 2952, "synset": "cowrie.n.01", "name": "cowrie"}, - {"id": 2953, "synset": "money_cowrie.n.01", "name": "money_cowrie"}, - {"id": 2954, "synset": "tiger_cowrie.n.01", "name": "tiger_cowrie"}, - {"id": 2955, "synset": "solenogaster.n.01", "name": "solenogaster"}, - {"id": 2956, "synset": "chiton.n.02", "name": "chiton"}, - {"id": 2957, "synset": "bivalve.n.01", "name": "bivalve"}, - {"id": 2958, "synset": "spat.n.03", "name": "spat"}, - {"id": 2959, "synset": "clam.n.01", "name": "clam"}, - {"id": 2960, "synset": "soft-shell_clam.n.02", "name": "soft-shell_clam"}, - {"id": 2961, "synset": "quahog.n.02", "name": "quahog"}, - {"id": 2962, "synset": "littleneck.n.02", "name": "littleneck"}, - {"id": 2963, "synset": "cherrystone.n.02", "name": "cherrystone"}, - {"id": 2964, "synset": "geoduck.n.01", "name": "geoduck"}, - {"id": 2965, "synset": "razor_clam.n.01", "name": "razor_clam"}, - {"id": 2966, "synset": "giant_clam.n.01", "name": "giant_clam"}, - {"id": 2967, "synset": "cockle.n.02", "name": "cockle"}, - {"id": 2968, "synset": "edible_cockle.n.01", "name": "edible_cockle"}, - {"id": 2969, "synset": "oyster.n.01", "name": "oyster"}, - {"id": 2970, "synset": "japanese_oyster.n.01", "name": "Japanese_oyster"}, - {"id": 2971, "synset": "virginia_oyster.n.01", "name": "Virginia_oyster"}, - {"id": 2972, "synset": "pearl_oyster.n.01", "name": "pearl_oyster"}, - {"id": 2973, "synset": "saddle_oyster.n.01", "name": "saddle_oyster"}, - {"id": 2974, "synset": "window_oyster.n.01", "name": "window_oyster"}, - {"id": 2975, "synset": "ark_shell.n.01", "name": "ark_shell"}, - {"id": 2976, "synset": "blood_clam.n.01", "name": "blood_clam"}, - {"id": 2977, "synset": "mussel.n.02", "name": "mussel"}, - {"id": 2978, "synset": "marine_mussel.n.01", "name": "marine_mussel"}, - {"id": 2979, "synset": "edible_mussel.n.01", "name": "edible_mussel"}, - {"id": 2980, "synset": "freshwater_mussel.n.01", "name": "freshwater_mussel"}, - {"id": 2981, "synset": "pearly-shelled_mussel.n.01", "name": "pearly-shelled_mussel"}, - {"id": 2982, "synset": "thin-shelled_mussel.n.01", "name": "thin-shelled_mussel"}, - {"id": 2983, "synset": "zebra_mussel.n.01", "name": "zebra_mussel"}, - {"id": 2984, "synset": "scallop.n.04", "name": "scallop"}, - {"id": 2985, "synset": "bay_scallop.n.02", "name": "bay_scallop"}, - {"id": 2986, "synset": "sea_scallop.n.02", "name": "sea_scallop"}, - {"id": 2987, "synset": "shipworm.n.01", "name": "shipworm"}, - {"id": 2988, "synset": "teredo.n.01", "name": "teredo"}, - {"id": 2989, "synset": "piddock.n.01", "name": "piddock"}, - {"id": 2990, "synset": "cephalopod.n.01", "name": "cephalopod"}, - {"id": 2991, "synset": "chambered_nautilus.n.01", "name": "chambered_nautilus"}, - {"id": 2992, "synset": "octopod.n.01", "name": "octopod"}, - {"id": 2993, "synset": "paper_nautilus.n.01", "name": "paper_nautilus"}, - {"id": 2994, "synset": "decapod.n.02", "name": "decapod"}, - {"id": 2995, "synset": "squid.n.02", "name": "squid"}, - {"id": 2996, "synset": "loligo.n.01", "name": "loligo"}, - {"id": 2997, "synset": "ommastrephes.n.01", "name": "ommastrephes"}, - {"id": 2998, "synset": "architeuthis.n.01", "name": "architeuthis"}, - {"id": 2999, "synset": "cuttlefish.n.01", "name": "cuttlefish"}, - {"id": 3000, "synset": "spirula.n.01", "name": "spirula"}, - {"id": 3001, "synset": "crustacean.n.01", "name": "crustacean"}, - {"id": 3002, "synset": "malacostracan_crustacean.n.01", "name": "malacostracan_crustacean"}, - {"id": 3003, "synset": "decapod_crustacean.n.01", "name": "decapod_crustacean"}, - {"id": 3004, "synset": "brachyuran.n.01", "name": "brachyuran"}, - {"id": 3005, "synset": "stone_crab.n.02", "name": "stone_crab"}, - {"id": 3006, "synset": "hard-shell_crab.n.01", "name": "hard-shell_crab"}, - {"id": 3007, "synset": "soft-shell_crab.n.02", "name": "soft-shell_crab"}, - {"id": 3008, "synset": "dungeness_crab.n.02", "name": "Dungeness_crab"}, - {"id": 3009, "synset": "rock_crab.n.01", "name": "rock_crab"}, - {"id": 3010, "synset": "jonah_crab.n.01", "name": "Jonah_crab"}, - {"id": 3011, "synset": "swimming_crab.n.01", "name": "swimming_crab"}, - {"id": 3012, "synset": "english_lady_crab.n.01", "name": "English_lady_crab"}, - {"id": 3013, "synset": "american_lady_crab.n.01", "name": "American_lady_crab"}, - {"id": 3014, "synset": "blue_crab.n.02", "name": "blue_crab"}, - {"id": 3015, "synset": "fiddler_crab.n.01", "name": "fiddler_crab"}, - {"id": 3016, "synset": "pea_crab.n.01", "name": "pea_crab"}, - {"id": 3017, "synset": "king_crab.n.03", "name": "king_crab"}, - {"id": 3018, "synset": "spider_crab.n.01", "name": "spider_crab"}, - {"id": 3019, "synset": "european_spider_crab.n.01", "name": "European_spider_crab"}, - {"id": 3020, "synset": "giant_crab.n.01", "name": "giant_crab"}, - {"id": 3021, "synset": "lobster.n.02", "name": "lobster"}, - {"id": 3022, "synset": "true_lobster.n.01", "name": "true_lobster"}, - {"id": 3023, "synset": "american_lobster.n.02", "name": "American_lobster"}, - {"id": 3024, "synset": "european_lobster.n.02", "name": "European_lobster"}, - {"id": 3025, "synset": "cape_lobster.n.01", "name": "Cape_lobster"}, - {"id": 3026, "synset": "norway_lobster.n.01", "name": "Norway_lobster"}, - {"id": 3027, "synset": "crayfish.n.03", "name": "crayfish"}, - {"id": 3028, "synset": "old_world_crayfish.n.01", "name": "Old_World_crayfish"}, - {"id": 3029, "synset": "american_crayfish.n.01", "name": "American_crayfish"}, - {"id": 3030, "synset": "hermit_crab.n.01", "name": "hermit_crab"}, - {"id": 3031, "synset": "shrimp.n.03", "name": "shrimp"}, - {"id": 3032, "synset": "snapping_shrimp.n.01", "name": "snapping_shrimp"}, - {"id": 3033, "synset": "prawn.n.02", "name": "prawn"}, - {"id": 3034, "synset": "long-clawed_prawn.n.01", "name": "long-clawed_prawn"}, - {"id": 3035, "synset": "tropical_prawn.n.01", "name": "tropical_prawn"}, - {"id": 3036, "synset": "krill.n.01", "name": "krill"}, - {"id": 3037, "synset": "euphausia_pacifica.n.01", "name": "Euphausia_pacifica"}, - {"id": 3038, "synset": "opossum_shrimp.n.01", "name": "opossum_shrimp"}, - {"id": 3039, "synset": "stomatopod.n.01", "name": "stomatopod"}, - {"id": 3040, "synset": "mantis_shrimp.n.01", "name": "mantis_shrimp"}, - {"id": 3041, "synset": "squilla.n.01", "name": "squilla"}, - {"id": 3042, "synset": "isopod.n.01", "name": "isopod"}, - {"id": 3043, "synset": "woodlouse.n.01", "name": "woodlouse"}, - {"id": 3044, "synset": "pill_bug.n.01", "name": "pill_bug"}, - {"id": 3045, "synset": "sow_bug.n.01", "name": "sow_bug"}, - {"id": 3046, "synset": "sea_louse.n.01", "name": "sea_louse"}, - {"id": 3047, "synset": "amphipod.n.01", "name": "amphipod"}, - {"id": 3048, "synset": "skeleton_shrimp.n.01", "name": "skeleton_shrimp"}, - {"id": 3049, "synset": "whale_louse.n.01", "name": "whale_louse"}, - {"id": 3050, "synset": "daphnia.n.01", "name": "daphnia"}, - {"id": 3051, "synset": "fairy_shrimp.n.01", "name": "fairy_shrimp"}, - {"id": 3052, "synset": "brine_shrimp.n.01", "name": "brine_shrimp"}, - {"id": 3053, "synset": "tadpole_shrimp.n.01", "name": "tadpole_shrimp"}, - {"id": 3054, "synset": "copepod.n.01", "name": "copepod"}, - {"id": 3055, "synset": "cyclops.n.02", "name": "cyclops"}, - {"id": 3056, "synset": "seed_shrimp.n.01", "name": "seed_shrimp"}, - {"id": 3057, "synset": "barnacle.n.01", "name": "barnacle"}, - {"id": 3058, "synset": "acorn_barnacle.n.01", "name": "acorn_barnacle"}, - {"id": 3059, "synset": "goose_barnacle.n.01", "name": "goose_barnacle"}, - {"id": 3060, "synset": "onychophoran.n.01", "name": "onychophoran"}, - {"id": 3061, "synset": "wading_bird.n.01", "name": "wading_bird"}, - {"id": 3062, "synset": "stork.n.01", "name": "stork"}, - {"id": 3063, "synset": "white_stork.n.01", "name": "white_stork"}, - {"id": 3064, "synset": "black_stork.n.01", "name": "black_stork"}, - {"id": 3065, "synset": "adjutant_bird.n.01", "name": "adjutant_bird"}, - {"id": 3066, "synset": "marabou.n.01", "name": "marabou"}, - {"id": 3067, "synset": "openbill.n.01", "name": "openbill"}, - {"id": 3068, "synset": "jabiru.n.03", "name": "jabiru"}, - {"id": 3069, "synset": "saddlebill.n.01", "name": "saddlebill"}, - {"id": 3070, "synset": "policeman_bird.n.01", "name": "policeman_bird"}, - {"id": 3071, "synset": "wood_ibis.n.02", "name": "wood_ibis"}, - {"id": 3072, "synset": "shoebill.n.01", "name": "shoebill"}, - {"id": 3073, "synset": "ibis.n.01", "name": "ibis"}, - {"id": 3074, "synset": "wood_ibis.n.01", "name": "wood_ibis"}, - {"id": 3075, "synset": "sacred_ibis.n.01", "name": "sacred_ibis"}, - {"id": 3076, "synset": "spoonbill.n.01", "name": "spoonbill"}, - {"id": 3077, "synset": "common_spoonbill.n.01", "name": "common_spoonbill"}, - {"id": 3078, "synset": "roseate_spoonbill.n.01", "name": "roseate_spoonbill"}, - {"id": 3079, "synset": "great_blue_heron.n.01", "name": "great_blue_heron"}, - {"id": 3080, "synset": "great_white_heron.n.03", "name": "great_white_heron"}, - {"id": 3081, "synset": "egret.n.01", "name": "egret"}, - {"id": 3082, "synset": "little_blue_heron.n.01", "name": "little_blue_heron"}, - {"id": 3083, "synset": "snowy_egret.n.01", "name": "snowy_egret"}, - {"id": 3084, "synset": "little_egret.n.01", "name": "little_egret"}, - {"id": 3085, "synset": "great_white_heron.n.02", "name": "great_white_heron"}, - {"id": 3086, "synset": "american_egret.n.01", "name": "American_egret"}, - {"id": 3087, "synset": "cattle_egret.n.01", "name": "cattle_egret"}, - {"id": 3088, "synset": "night_heron.n.01", "name": "night_heron"}, - {"id": 3089, "synset": "black-crowned_night_heron.n.01", "name": "black-crowned_night_heron"}, - {"id": 3090, "synset": "yellow-crowned_night_heron.n.01", "name": "yellow-crowned_night_heron"}, - {"id": 3091, "synset": "boatbill.n.01", "name": "boatbill"}, - {"id": 3092, "synset": "bittern.n.01", "name": "bittern"}, - {"id": 3093, "synset": "american_bittern.n.01", "name": "American_bittern"}, - {"id": 3094, "synset": "european_bittern.n.01", "name": "European_bittern"}, - {"id": 3095, "synset": "least_bittern.n.01", "name": "least_bittern"}, - {"id": 3096, "synset": "crane.n.05", "name": "crane"}, - {"id": 3097, "synset": "whooping_crane.n.01", "name": "whooping_crane"}, - {"id": 3098, "synset": "courlan.n.01", "name": "courlan"}, - {"id": 3099, "synset": "limpkin.n.01", "name": "limpkin"}, - {"id": 3100, "synset": "crested_cariama.n.01", "name": "crested_cariama"}, - {"id": 3101, "synset": "chunga.n.01", "name": "chunga"}, - {"id": 3102, "synset": "rail.n.05", "name": "rail"}, - {"id": 3103, "synset": "weka.n.01", "name": "weka"}, - {"id": 3104, "synset": "crake.n.01", "name": "crake"}, - {"id": 3105, "synset": "corncrake.n.01", "name": "corncrake"}, - {"id": 3106, "synset": "spotted_crake.n.01", "name": "spotted_crake"}, - {"id": 3107, "synset": "gallinule.n.01", "name": "gallinule"}, - {"id": 3108, "synset": "florida_gallinule.n.01", "name": "Florida_gallinule"}, - {"id": 3109, "synset": "moorhen.n.01", "name": "moorhen"}, - {"id": 3110, "synset": "purple_gallinule.n.01", "name": "purple_gallinule"}, - {"id": 3111, "synset": "european_gallinule.n.01", "name": "European_gallinule"}, - {"id": 3112, "synset": "american_gallinule.n.01", "name": "American_gallinule"}, - {"id": 3113, "synset": "notornis.n.01", "name": "notornis"}, - {"id": 3114, "synset": "coot.n.01", "name": "coot"}, - {"id": 3115, "synset": "american_coot.n.01", "name": "American_coot"}, - {"id": 3116, "synset": "old_world_coot.n.01", "name": "Old_World_coot"}, - {"id": 3117, "synset": "bustard.n.01", "name": "bustard"}, - {"id": 3118, "synset": "great_bustard.n.01", "name": "great_bustard"}, - {"id": 3119, "synset": "plain_turkey.n.01", "name": "plain_turkey"}, - {"id": 3120, "synset": "button_quail.n.01", "name": "button_quail"}, - {"id": 3121, "synset": "striped_button_quail.n.01", "name": "striped_button_quail"}, - {"id": 3122, "synset": "plain_wanderer.n.01", "name": "plain_wanderer"}, - {"id": 3123, "synset": "trumpeter.n.03", "name": "trumpeter"}, - {"id": 3124, "synset": "brazilian_trumpeter.n.01", "name": "Brazilian_trumpeter"}, - {"id": 3125, "synset": "shorebird.n.01", "name": "shorebird"}, - {"id": 3126, "synset": "plover.n.01", "name": "plover"}, - {"id": 3127, "synset": "piping_plover.n.01", "name": "piping_plover"}, - {"id": 3128, "synset": "killdeer.n.01", "name": "killdeer"}, - {"id": 3129, "synset": "dotterel.n.01", "name": "dotterel"}, - {"id": 3130, "synset": "golden_plover.n.01", "name": "golden_plover"}, - {"id": 3131, "synset": "lapwing.n.01", "name": "lapwing"}, - {"id": 3132, "synset": "turnstone.n.01", "name": "turnstone"}, - {"id": 3133, "synset": "ruddy_turnstone.n.01", "name": "ruddy_turnstone"}, - {"id": 3134, "synset": "black_turnstone.n.01", "name": "black_turnstone"}, - {"id": 3135, "synset": "sandpiper.n.01", "name": "sandpiper"}, - {"id": 3136, "synset": "surfbird.n.01", "name": "surfbird"}, - {"id": 3137, "synset": "european_sandpiper.n.01", "name": "European_sandpiper"}, - {"id": 3138, "synset": "spotted_sandpiper.n.01", "name": "spotted_sandpiper"}, - {"id": 3139, "synset": "least_sandpiper.n.01", "name": "least_sandpiper"}, - {"id": 3140, "synset": "red-backed_sandpiper.n.01", "name": "red-backed_sandpiper"}, - {"id": 3141, "synset": "greenshank.n.01", "name": "greenshank"}, - {"id": 3142, "synset": "redshank.n.01", "name": "redshank"}, - {"id": 3143, "synset": "yellowlegs.n.01", "name": "yellowlegs"}, - {"id": 3144, "synset": "greater_yellowlegs.n.01", "name": "greater_yellowlegs"}, - {"id": 3145, "synset": "lesser_yellowlegs.n.01", "name": "lesser_yellowlegs"}, - {"id": 3146, "synset": "pectoral_sandpiper.n.01", "name": "pectoral_sandpiper"}, - {"id": 3147, "synset": "knot.n.07", "name": "knot"}, - {"id": 3148, "synset": "curlew_sandpiper.n.01", "name": "curlew_sandpiper"}, - {"id": 3149, "synset": "sanderling.n.01", "name": "sanderling"}, - {"id": 3150, "synset": "upland_sandpiper.n.01", "name": "upland_sandpiper"}, - {"id": 3151, "synset": "ruff.n.03", "name": "ruff"}, - {"id": 3152, "synset": "reeve.n.01", "name": "reeve"}, - {"id": 3153, "synset": "tattler.n.02", "name": "tattler"}, - {"id": 3154, "synset": "polynesian_tattler.n.01", "name": "Polynesian_tattler"}, - {"id": 3155, "synset": "willet.n.01", "name": "willet"}, - {"id": 3156, "synset": "woodcock.n.01", "name": "woodcock"}, - {"id": 3157, "synset": "eurasian_woodcock.n.01", "name": "Eurasian_woodcock"}, - {"id": 3158, "synset": "american_woodcock.n.01", "name": "American_woodcock"}, - {"id": 3159, "synset": "snipe.n.01", "name": "snipe"}, - {"id": 3160, "synset": "whole_snipe.n.01", "name": "whole_snipe"}, - {"id": 3161, "synset": "wilson's_snipe.n.01", "name": "Wilson's_snipe"}, - {"id": 3162, "synset": "great_snipe.n.01", "name": "great_snipe"}, - {"id": 3163, "synset": "jacksnipe.n.01", "name": "jacksnipe"}, - {"id": 3164, "synset": "dowitcher.n.01", "name": "dowitcher"}, - {"id": 3165, "synset": "greyback.n.02", "name": "greyback"}, - {"id": 3166, "synset": "red-breasted_snipe.n.01", "name": "red-breasted_snipe"}, - {"id": 3167, "synset": "curlew.n.01", "name": "curlew"}, - {"id": 3168, "synset": "european_curlew.n.01", "name": "European_curlew"}, - {"id": 3169, "synset": "eskimo_curlew.n.01", "name": "Eskimo_curlew"}, - {"id": 3170, "synset": "godwit.n.01", "name": "godwit"}, - {"id": 3171, "synset": "hudsonian_godwit.n.01", "name": "Hudsonian_godwit"}, - {"id": 3172, "synset": "stilt.n.04", "name": "stilt"}, - {"id": 3173, "synset": "black-necked_stilt.n.01", "name": "black-necked_stilt"}, - {"id": 3174, "synset": "black-winged_stilt.n.01", "name": "black-winged_stilt"}, - {"id": 3175, "synset": "white-headed_stilt.n.01", "name": "white-headed_stilt"}, - {"id": 3176, "synset": "kaki.n.02", "name": "kaki"}, - {"id": 3177, "synset": "stilt.n.03", "name": "stilt"}, - {"id": 3178, "synset": "banded_stilt.n.01", "name": "banded_stilt"}, - {"id": 3179, "synset": "avocet.n.01", "name": "avocet"}, - {"id": 3180, "synset": "oystercatcher.n.01", "name": "oystercatcher"}, - {"id": 3181, "synset": "phalarope.n.01", "name": "phalarope"}, - {"id": 3182, "synset": "red_phalarope.n.01", "name": "red_phalarope"}, - {"id": 3183, "synset": "northern_phalarope.n.01", "name": "northern_phalarope"}, - {"id": 3184, "synset": "wilson's_phalarope.n.01", "name": "Wilson's_phalarope"}, - {"id": 3185, "synset": "pratincole.n.01", "name": "pratincole"}, - {"id": 3186, "synset": "courser.n.04", "name": "courser"}, - {"id": 3187, "synset": "cream-colored_courser.n.01", "name": "cream-colored_courser"}, - {"id": 3188, "synset": "crocodile_bird.n.01", "name": "crocodile_bird"}, - {"id": 3189, "synset": "stone_curlew.n.01", "name": "stone_curlew"}, - {"id": 3190, "synset": "coastal_diving_bird.n.01", "name": "coastal_diving_bird"}, - {"id": 3191, "synset": "larid.n.01", "name": "larid"}, - {"id": 3192, "synset": "mew.n.02", "name": "mew"}, - {"id": 3193, "synset": "black-backed_gull.n.01", "name": "black-backed_gull"}, - {"id": 3194, "synset": "herring_gull.n.01", "name": "herring_gull"}, - {"id": 3195, "synset": "laughing_gull.n.01", "name": "laughing_gull"}, - {"id": 3196, "synset": "ivory_gull.n.01", "name": "ivory_gull"}, - {"id": 3197, "synset": "kittiwake.n.01", "name": "kittiwake"}, - {"id": 3198, "synset": "tern.n.01", "name": "tern"}, - {"id": 3199, "synset": "sea_swallow.n.01", "name": "sea_swallow"}, - {"id": 3200, "synset": "skimmer.n.04", "name": "skimmer"}, - {"id": 3201, "synset": "jaeger.n.01", "name": "jaeger"}, - {"id": 3202, "synset": "parasitic_jaeger.n.01", "name": "parasitic_jaeger"}, - {"id": 3203, "synset": "skua.n.01", "name": "skua"}, - {"id": 3204, "synset": "great_skua.n.01", "name": "great_skua"}, - {"id": 3205, "synset": "auk.n.01", "name": "auk"}, - {"id": 3206, "synset": "auklet.n.01", "name": "auklet"}, - {"id": 3207, "synset": "razorbill.n.01", "name": "razorbill"}, - {"id": 3208, "synset": "little_auk.n.01", "name": "little_auk"}, - {"id": 3209, "synset": "guillemot.n.01", "name": "guillemot"}, - {"id": 3210, "synset": "black_guillemot.n.01", "name": "black_guillemot"}, - {"id": 3211, "synset": "pigeon_guillemot.n.01", "name": "pigeon_guillemot"}, - {"id": 3212, "synset": "murre.n.01", "name": "murre"}, - {"id": 3213, "synset": "common_murre.n.01", "name": "common_murre"}, - {"id": 3214, "synset": "thick-billed_murre.n.01", "name": "thick-billed_murre"}, - {"id": 3215, "synset": "atlantic_puffin.n.01", "name": "Atlantic_puffin"}, - {"id": 3216, "synset": "horned_puffin.n.01", "name": "horned_puffin"}, - {"id": 3217, "synset": "tufted_puffin.n.01", "name": "tufted_puffin"}, - {"id": 3218, "synset": "gaviiform_seabird.n.01", "name": "gaviiform_seabird"}, - {"id": 3219, "synset": "loon.n.02", "name": "loon"}, - {"id": 3220, "synset": "podicipitiform_seabird.n.01", "name": "podicipitiform_seabird"}, - {"id": 3221, "synset": "grebe.n.01", "name": "grebe"}, - {"id": 3222, "synset": "great_crested_grebe.n.01", "name": "great_crested_grebe"}, - {"id": 3223, "synset": "red-necked_grebe.n.01", "name": "red-necked_grebe"}, - {"id": 3224, "synset": "black-necked_grebe.n.01", "name": "black-necked_grebe"}, - {"id": 3225, "synset": "dabchick.n.01", "name": "dabchick"}, - {"id": 3226, "synset": "pied-billed_grebe.n.01", "name": "pied-billed_grebe"}, - {"id": 3227, "synset": "pelecaniform_seabird.n.01", "name": "pelecaniform_seabird"}, - {"id": 3228, "synset": "white_pelican.n.01", "name": "white_pelican"}, - {"id": 3229, "synset": "old_world_white_pelican.n.01", "name": "Old_world_white_pelican"}, - {"id": 3230, "synset": "frigate_bird.n.01", "name": "frigate_bird"}, - {"id": 3231, "synset": "gannet.n.01", "name": "gannet"}, - {"id": 3232, "synset": "solan.n.01", "name": "solan"}, - {"id": 3233, "synset": "booby.n.02", "name": "booby"}, - {"id": 3234, "synset": "cormorant.n.01", "name": "cormorant"}, - {"id": 3235, "synset": "snakebird.n.01", "name": "snakebird"}, - {"id": 3236, "synset": "water_turkey.n.01", "name": "water_turkey"}, - {"id": 3237, "synset": "tropic_bird.n.01", "name": "tropic_bird"}, - {"id": 3238, "synset": "sphenisciform_seabird.n.01", "name": "sphenisciform_seabird"}, - {"id": 3239, "synset": "adelie.n.01", "name": "Adelie"}, - {"id": 3240, "synset": "king_penguin.n.01", "name": "king_penguin"}, - {"id": 3241, "synset": "emperor_penguin.n.01", "name": "emperor_penguin"}, - {"id": 3242, "synset": "jackass_penguin.n.01", "name": "jackass_penguin"}, - {"id": 3243, "synset": "rock_hopper.n.01", "name": "rock_hopper"}, - {"id": 3244, "synset": "pelagic_bird.n.01", "name": "pelagic_bird"}, - {"id": 3245, "synset": "procellariiform_seabird.n.01", "name": "procellariiform_seabird"}, - {"id": 3246, "synset": "albatross.n.02", "name": "albatross"}, - {"id": 3247, "synset": "wandering_albatross.n.01", "name": "wandering_albatross"}, - {"id": 3248, "synset": "black-footed_albatross.n.01", "name": "black-footed_albatross"}, - {"id": 3249, "synset": "petrel.n.01", "name": "petrel"}, - {"id": 3250, "synset": "white-chinned_petrel.n.01", "name": "white-chinned_petrel"}, - {"id": 3251, "synset": "giant_petrel.n.01", "name": "giant_petrel"}, - {"id": 3252, "synset": "fulmar.n.01", "name": "fulmar"}, - {"id": 3253, "synset": "shearwater.n.01", "name": "shearwater"}, - {"id": 3254, "synset": "manx_shearwater.n.01", "name": "Manx_shearwater"}, - {"id": 3255, "synset": "storm_petrel.n.01", "name": "storm_petrel"}, - {"id": 3256, "synset": "stormy_petrel.n.01", "name": "stormy_petrel"}, - {"id": 3257, "synset": "mother_carey's_chicken.n.01", "name": "Mother_Carey's_chicken"}, - {"id": 3258, "synset": "diving_petrel.n.01", "name": "diving_petrel"}, - {"id": 3259, "synset": "aquatic_mammal.n.01", "name": "aquatic_mammal"}, - {"id": 3260, "synset": "cetacean.n.01", "name": "cetacean"}, - {"id": 3261, "synset": "whale.n.02", "name": "whale"}, - {"id": 3262, "synset": "baleen_whale.n.01", "name": "baleen_whale"}, - {"id": 3263, "synset": "right_whale.n.01", "name": "right_whale"}, - {"id": 3264, "synset": "bowhead.n.01", "name": "bowhead"}, - {"id": 3265, "synset": "rorqual.n.01", "name": "rorqual"}, - {"id": 3266, "synset": "blue_whale.n.01", "name": "blue_whale"}, - {"id": 3267, "synset": "finback.n.01", "name": "finback"}, - {"id": 3268, "synset": "sei_whale.n.01", "name": "sei_whale"}, - {"id": 3269, "synset": "lesser_rorqual.n.01", "name": "lesser_rorqual"}, - {"id": 3270, "synset": "humpback.n.03", "name": "humpback"}, - {"id": 3271, "synset": "grey_whale.n.01", "name": "grey_whale"}, - {"id": 3272, "synset": "toothed_whale.n.01", "name": "toothed_whale"}, - {"id": 3273, "synset": "sperm_whale.n.01", "name": "sperm_whale"}, - {"id": 3274, "synset": "pygmy_sperm_whale.n.01", "name": "pygmy_sperm_whale"}, - {"id": 3275, "synset": "dwarf_sperm_whale.n.01", "name": "dwarf_sperm_whale"}, - {"id": 3276, "synset": "beaked_whale.n.01", "name": "beaked_whale"}, - {"id": 3277, "synset": "bottle-nosed_whale.n.01", "name": "bottle-nosed_whale"}, - {"id": 3278, "synset": "common_dolphin.n.01", "name": "common_dolphin"}, - {"id": 3279, "synset": "bottlenose_dolphin.n.01", "name": "bottlenose_dolphin"}, - { - "id": 3280, - "synset": "atlantic_bottlenose_dolphin.n.01", - "name": "Atlantic_bottlenose_dolphin", - }, - {"id": 3281, "synset": "pacific_bottlenose_dolphin.n.01", "name": "Pacific_bottlenose_dolphin"}, - {"id": 3282, "synset": "porpoise.n.01", "name": "porpoise"}, - {"id": 3283, "synset": "harbor_porpoise.n.01", "name": "harbor_porpoise"}, - {"id": 3284, "synset": "vaquita.n.01", "name": "vaquita"}, - {"id": 3285, "synset": "grampus.n.02", "name": "grampus"}, - {"id": 3286, "synset": "killer_whale.n.01", "name": "killer_whale"}, - {"id": 3287, "synset": "pilot_whale.n.01", "name": "pilot_whale"}, - {"id": 3288, "synset": "river_dolphin.n.01", "name": "river_dolphin"}, - {"id": 3289, "synset": "narwhal.n.01", "name": "narwhal"}, - {"id": 3290, "synset": "white_whale.n.01", "name": "white_whale"}, - {"id": 3291, "synset": "sea_cow.n.01", "name": "sea_cow"}, - {"id": 3292, "synset": "dugong.n.01", "name": "dugong"}, - {"id": 3293, "synset": "steller's_sea_cow.n.01", "name": "Steller's_sea_cow"}, - {"id": 3294, "synset": "carnivore.n.01", "name": "carnivore"}, - {"id": 3295, "synset": "omnivore.n.02", "name": "omnivore"}, - {"id": 3296, "synset": "pinniped_mammal.n.01", "name": "pinniped_mammal"}, - {"id": 3297, "synset": "seal.n.09", "name": "seal"}, - {"id": 3298, "synset": "crabeater_seal.n.01", "name": "crabeater_seal"}, - {"id": 3299, "synset": "eared_seal.n.01", "name": "eared_seal"}, - {"id": 3300, "synset": "fur_seal.n.02", "name": "fur_seal"}, - {"id": 3301, "synset": "guadalupe_fur_seal.n.01", "name": "guadalupe_fur_seal"}, - {"id": 3302, "synset": "fur_seal.n.01", "name": "fur_seal"}, - {"id": 3303, "synset": "alaska_fur_seal.n.01", "name": "Alaska_fur_seal"}, - {"id": 3304, "synset": "sea_lion.n.01", "name": "sea_lion"}, - {"id": 3305, "synset": "south_american_sea_lion.n.01", "name": "South_American_sea_lion"}, - {"id": 3306, "synset": "california_sea_lion.n.01", "name": "California_sea_lion"}, - {"id": 3307, "synset": "australian_sea_lion.n.01", "name": "Australian_sea_lion"}, - {"id": 3308, "synset": "steller_sea_lion.n.01", "name": "Steller_sea_lion"}, - {"id": 3309, "synset": "earless_seal.n.01", "name": "earless_seal"}, - {"id": 3310, "synset": "harbor_seal.n.01", "name": "harbor_seal"}, - {"id": 3311, "synset": "harp_seal.n.01", "name": "harp_seal"}, - {"id": 3312, "synset": "elephant_seal.n.01", "name": "elephant_seal"}, - {"id": 3313, "synset": "bearded_seal.n.01", "name": "bearded_seal"}, - {"id": 3314, "synset": "hooded_seal.n.01", "name": "hooded_seal"}, - {"id": 3315, "synset": "atlantic_walrus.n.01", "name": "Atlantic_walrus"}, - {"id": 3316, "synset": "pacific_walrus.n.01", "name": "Pacific_walrus"}, - {"id": 3317, "synset": "fissipedia.n.01", "name": "Fissipedia"}, - {"id": 3318, "synset": "fissiped_mammal.n.01", "name": "fissiped_mammal"}, - {"id": 3319, "synset": "aardvark.n.01", "name": "aardvark"}, - {"id": 3320, "synset": "canine.n.02", "name": "canine"}, - {"id": 3321, "synset": "bitch.n.04", "name": "bitch"}, - {"id": 3322, "synset": "brood_bitch.n.01", "name": "brood_bitch"}, - {"id": 3323, "synset": "pooch.n.01", "name": "pooch"}, - {"id": 3324, "synset": "cur.n.01", "name": "cur"}, - {"id": 3325, "synset": "feist.n.01", "name": "feist"}, - {"id": 3326, "synset": "pariah_dog.n.01", "name": "pariah_dog"}, - {"id": 3327, "synset": "lapdog.n.01", "name": "lapdog"}, - {"id": 3328, "synset": "toy_dog.n.01", "name": "toy_dog"}, - {"id": 3329, "synset": "chihuahua.n.03", "name": "Chihuahua"}, - {"id": 3330, "synset": "japanese_spaniel.n.01", "name": "Japanese_spaniel"}, - {"id": 3331, "synset": "maltese_dog.n.01", "name": "Maltese_dog"}, - {"id": 3332, "synset": "pekinese.n.01", "name": "Pekinese"}, - {"id": 3333, "synset": "shih-tzu.n.01", "name": "Shih-Tzu"}, - {"id": 3334, "synset": "toy_spaniel.n.01", "name": "toy_spaniel"}, - {"id": 3335, "synset": "english_toy_spaniel.n.01", "name": "English_toy_spaniel"}, - {"id": 3336, "synset": "blenheim_spaniel.n.01", "name": "Blenheim_spaniel"}, - {"id": 3337, "synset": "king_charles_spaniel.n.01", "name": "King_Charles_spaniel"}, - {"id": 3338, "synset": "papillon.n.01", "name": "papillon"}, - {"id": 3339, "synset": "toy_terrier.n.01", "name": "toy_terrier"}, - {"id": 3340, "synset": "hunting_dog.n.01", "name": "hunting_dog"}, - {"id": 3341, "synset": "courser.n.03", "name": "courser"}, - {"id": 3342, "synset": "rhodesian_ridgeback.n.01", "name": "Rhodesian_ridgeback"}, - {"id": 3343, "synset": "hound.n.01", "name": "hound"}, - {"id": 3344, "synset": "afghan_hound.n.01", "name": "Afghan_hound"}, - {"id": 3345, "synset": "basset.n.01", "name": "basset"}, - {"id": 3346, "synset": "beagle.n.01", "name": "beagle"}, - {"id": 3347, "synset": "bloodhound.n.01", "name": "bloodhound"}, - {"id": 3348, "synset": "bluetick.n.01", "name": "bluetick"}, - {"id": 3349, "synset": "boarhound.n.01", "name": "boarhound"}, - {"id": 3350, "synset": "coonhound.n.01", "name": "coonhound"}, - {"id": 3351, "synset": "coondog.n.01", "name": "coondog"}, - {"id": 3352, "synset": "black-and-tan_coonhound.n.01", "name": "black-and-tan_coonhound"}, - {"id": 3353, "synset": "dachshund.n.01", "name": "dachshund"}, - {"id": 3354, "synset": "sausage_dog.n.01", "name": "sausage_dog"}, - {"id": 3355, "synset": "foxhound.n.01", "name": "foxhound"}, - {"id": 3356, "synset": "american_foxhound.n.01", "name": "American_foxhound"}, - {"id": 3357, "synset": "walker_hound.n.01", "name": "Walker_hound"}, - {"id": 3358, "synset": "english_foxhound.n.01", "name": "English_foxhound"}, - {"id": 3359, "synset": "harrier.n.02", "name": "harrier"}, - {"id": 3360, "synset": "plott_hound.n.01", "name": "Plott_hound"}, - {"id": 3361, "synset": "redbone.n.01", "name": "redbone"}, - {"id": 3362, "synset": "wolfhound.n.01", "name": "wolfhound"}, - {"id": 3363, "synset": "borzoi.n.01", "name": "borzoi"}, - {"id": 3364, "synset": "irish_wolfhound.n.01", "name": "Irish_wolfhound"}, - {"id": 3365, "synset": "greyhound.n.01", "name": "greyhound"}, - {"id": 3366, "synset": "italian_greyhound.n.01", "name": "Italian_greyhound"}, - {"id": 3367, "synset": "whippet.n.01", "name": "whippet"}, - {"id": 3368, "synset": "ibizan_hound.n.01", "name": "Ibizan_hound"}, - {"id": 3369, "synset": "norwegian_elkhound.n.01", "name": "Norwegian_elkhound"}, - {"id": 3370, "synset": "otterhound.n.01", "name": "otterhound"}, - {"id": 3371, "synset": "saluki.n.01", "name": "Saluki"}, - {"id": 3372, "synset": "scottish_deerhound.n.01", "name": "Scottish_deerhound"}, - {"id": 3373, "synset": "staghound.n.01", "name": "staghound"}, - {"id": 3374, "synset": "weimaraner.n.01", "name": "Weimaraner"}, - {"id": 3375, "synset": "terrier.n.01", "name": "terrier"}, - {"id": 3376, "synset": "bullterrier.n.01", "name": "bullterrier"}, - {"id": 3377, "synset": "staffordshire_bullterrier.n.01", "name": "Staffordshire_bullterrier"}, - { - "id": 3378, - "synset": "american_staffordshire_terrier.n.01", - "name": "American_Staffordshire_terrier", - }, - {"id": 3379, "synset": "bedlington_terrier.n.01", "name": "Bedlington_terrier"}, - {"id": 3380, "synset": "border_terrier.n.01", "name": "Border_terrier"}, - {"id": 3381, "synset": "kerry_blue_terrier.n.01", "name": "Kerry_blue_terrier"}, - {"id": 3382, "synset": "irish_terrier.n.01", "name": "Irish_terrier"}, - {"id": 3383, "synset": "norfolk_terrier.n.01", "name": "Norfolk_terrier"}, - {"id": 3384, "synset": "norwich_terrier.n.01", "name": "Norwich_terrier"}, - {"id": 3385, "synset": "yorkshire_terrier.n.01", "name": "Yorkshire_terrier"}, - {"id": 3386, "synset": "rat_terrier.n.01", "name": "rat_terrier"}, - {"id": 3387, "synset": "manchester_terrier.n.01", "name": "Manchester_terrier"}, - {"id": 3388, "synset": "toy_manchester.n.01", "name": "toy_Manchester"}, - {"id": 3389, "synset": "fox_terrier.n.01", "name": "fox_terrier"}, - {"id": 3390, "synset": "smooth-haired_fox_terrier.n.01", "name": "smooth-haired_fox_terrier"}, - {"id": 3391, "synset": "wire-haired_fox_terrier.n.01", "name": "wire-haired_fox_terrier"}, - {"id": 3392, "synset": "wirehair.n.01", "name": "wirehair"}, - {"id": 3393, "synset": "lakeland_terrier.n.01", "name": "Lakeland_terrier"}, - {"id": 3394, "synset": "welsh_terrier.n.01", "name": "Welsh_terrier"}, - {"id": 3395, "synset": "sealyham_terrier.n.01", "name": "Sealyham_terrier"}, - {"id": 3396, "synset": "airedale.n.01", "name": "Airedale"}, - {"id": 3397, "synset": "cairn.n.02", "name": "cairn"}, - {"id": 3398, "synset": "australian_terrier.n.01", "name": "Australian_terrier"}, - {"id": 3399, "synset": "dandie_dinmont.n.01", "name": "Dandie_Dinmont"}, - {"id": 3400, "synset": "boston_bull.n.01", "name": "Boston_bull"}, - {"id": 3401, "synset": "schnauzer.n.01", "name": "schnauzer"}, - {"id": 3402, "synset": "miniature_schnauzer.n.01", "name": "miniature_schnauzer"}, - {"id": 3403, "synset": "giant_schnauzer.n.01", "name": "giant_schnauzer"}, - {"id": 3404, "synset": "standard_schnauzer.n.01", "name": "standard_schnauzer"}, - {"id": 3405, "synset": "scotch_terrier.n.01", "name": "Scotch_terrier"}, - {"id": 3406, "synset": "tibetan_terrier.n.01", "name": "Tibetan_terrier"}, - {"id": 3407, "synset": "silky_terrier.n.01", "name": "silky_terrier"}, - {"id": 3408, "synset": "skye_terrier.n.01", "name": "Skye_terrier"}, - {"id": 3409, "synset": "clydesdale_terrier.n.01", "name": "Clydesdale_terrier"}, - { - "id": 3410, - "synset": "soft-coated_wheaten_terrier.n.01", - "name": "soft-coated_wheaten_terrier", - }, - { - "id": 3411, - "synset": "west_highland_white_terrier.n.01", - "name": "West_Highland_white_terrier", - }, - {"id": 3412, "synset": "lhasa.n.02", "name": "Lhasa"}, - {"id": 3413, "synset": "sporting_dog.n.01", "name": "sporting_dog"}, - {"id": 3414, "synset": "bird_dog.n.01", "name": "bird_dog"}, - {"id": 3415, "synset": "water_dog.n.02", "name": "water_dog"}, - {"id": 3416, "synset": "retriever.n.01", "name": "retriever"}, - {"id": 3417, "synset": "flat-coated_retriever.n.01", "name": "flat-coated_retriever"}, - {"id": 3418, "synset": "curly-coated_retriever.n.01", "name": "curly-coated_retriever"}, - {"id": 3419, "synset": "golden_retriever.n.01", "name": "golden_retriever"}, - {"id": 3420, "synset": "labrador_retriever.n.01", "name": "Labrador_retriever"}, - {"id": 3421, "synset": "chesapeake_bay_retriever.n.01", "name": "Chesapeake_Bay_retriever"}, - {"id": 3422, "synset": "pointer.n.04", "name": "pointer"}, - { - "id": 3423, - "synset": "german_short-haired_pointer.n.01", - "name": "German_short-haired_pointer", - }, - {"id": 3424, "synset": "setter.n.02", "name": "setter"}, - {"id": 3425, "synset": "vizsla.n.01", "name": "vizsla"}, - {"id": 3426, "synset": "english_setter.n.01", "name": "English_setter"}, - {"id": 3427, "synset": "irish_setter.n.01", "name": "Irish_setter"}, - {"id": 3428, "synset": "gordon_setter.n.01", "name": "Gordon_setter"}, - {"id": 3429, "synset": "spaniel.n.01", "name": "spaniel"}, - {"id": 3430, "synset": "brittany_spaniel.n.01", "name": "Brittany_spaniel"}, - {"id": 3431, "synset": "clumber.n.01", "name": "clumber"}, - {"id": 3432, "synset": "field_spaniel.n.01", "name": "field_spaniel"}, - {"id": 3433, "synset": "springer_spaniel.n.01", "name": "springer_spaniel"}, - {"id": 3434, "synset": "english_springer.n.01", "name": "English_springer"}, - {"id": 3435, "synset": "welsh_springer_spaniel.n.01", "name": "Welsh_springer_spaniel"}, - {"id": 3436, "synset": "cocker_spaniel.n.01", "name": "cocker_spaniel"}, - {"id": 3437, "synset": "sussex_spaniel.n.01", "name": "Sussex_spaniel"}, - {"id": 3438, "synset": "water_spaniel.n.01", "name": "water_spaniel"}, - {"id": 3439, "synset": "american_water_spaniel.n.01", "name": "American_water_spaniel"}, - {"id": 3440, "synset": "irish_water_spaniel.n.01", "name": "Irish_water_spaniel"}, - {"id": 3441, "synset": "griffon.n.03", "name": "griffon"}, - {"id": 3442, "synset": "working_dog.n.01", "name": "working_dog"}, - {"id": 3443, "synset": "watchdog.n.02", "name": "watchdog"}, - {"id": 3444, "synset": "kuvasz.n.01", "name": "kuvasz"}, - {"id": 3445, "synset": "attack_dog.n.01", "name": "attack_dog"}, - {"id": 3446, "synset": "housedog.n.01", "name": "housedog"}, - {"id": 3447, "synset": "schipperke.n.01", "name": "schipperke"}, - {"id": 3448, "synset": "belgian_sheepdog.n.01", "name": "Belgian_sheepdog"}, - {"id": 3449, "synset": "groenendael.n.01", "name": "groenendael"}, - {"id": 3450, "synset": "malinois.n.01", "name": "malinois"}, - {"id": 3451, "synset": "briard.n.01", "name": "briard"}, - {"id": 3452, "synset": "kelpie.n.02", "name": "kelpie"}, - {"id": 3453, "synset": "komondor.n.01", "name": "komondor"}, - {"id": 3454, "synset": "old_english_sheepdog.n.01", "name": "Old_English_sheepdog"}, - {"id": 3455, "synset": "shetland_sheepdog.n.01", "name": "Shetland_sheepdog"}, - {"id": 3456, "synset": "collie.n.01", "name": "collie"}, - {"id": 3457, "synset": "border_collie.n.01", "name": "Border_collie"}, - {"id": 3458, "synset": "bouvier_des_flandres.n.01", "name": "Bouvier_des_Flandres"}, - {"id": 3459, "synset": "rottweiler.n.01", "name": "Rottweiler"}, - {"id": 3460, "synset": "german_shepherd.n.01", "name": "German_shepherd"}, - {"id": 3461, "synset": "police_dog.n.01", "name": "police_dog"}, - {"id": 3462, "synset": "pinscher.n.01", "name": "pinscher"}, - {"id": 3463, "synset": "doberman.n.01", "name": "Doberman"}, - {"id": 3464, "synset": "miniature_pinscher.n.01", "name": "miniature_pinscher"}, - {"id": 3465, "synset": "sennenhunde.n.01", "name": "Sennenhunde"}, - {"id": 3466, "synset": "greater_swiss_mountain_dog.n.01", "name": "Greater_Swiss_Mountain_dog"}, - {"id": 3467, "synset": "bernese_mountain_dog.n.01", "name": "Bernese_mountain_dog"}, - {"id": 3468, "synset": "appenzeller.n.01", "name": "Appenzeller"}, - {"id": 3469, "synset": "entlebucher.n.01", "name": "EntleBucher"}, - {"id": 3470, "synset": "boxer.n.04", "name": "boxer"}, - {"id": 3471, "synset": "mastiff.n.01", "name": "mastiff"}, - {"id": 3472, "synset": "bull_mastiff.n.01", "name": "bull_mastiff"}, - {"id": 3473, "synset": "tibetan_mastiff.n.01", "name": "Tibetan_mastiff"}, - {"id": 3474, "synset": "french_bulldog.n.01", "name": "French_bulldog"}, - {"id": 3475, "synset": "great_dane.n.01", "name": "Great_Dane"}, - {"id": 3476, "synset": "guide_dog.n.01", "name": "guide_dog"}, - {"id": 3477, "synset": "seeing_eye_dog.n.01", "name": "Seeing_Eye_dog"}, - {"id": 3478, "synset": "hearing_dog.n.01", "name": "hearing_dog"}, - {"id": 3479, "synset": "saint_bernard.n.01", "name": "Saint_Bernard"}, - {"id": 3480, "synset": "seizure-alert_dog.n.01", "name": "seizure-alert_dog"}, - {"id": 3481, "synset": "sled_dog.n.01", "name": "sled_dog"}, - {"id": 3482, "synset": "eskimo_dog.n.01", "name": "Eskimo_dog"}, - {"id": 3483, "synset": "malamute.n.01", "name": "malamute"}, - {"id": 3484, "synset": "siberian_husky.n.01", "name": "Siberian_husky"}, - {"id": 3485, "synset": "liver-spotted_dalmatian.n.01", "name": "liver-spotted_dalmatian"}, - {"id": 3486, "synset": "affenpinscher.n.01", "name": "affenpinscher"}, - {"id": 3487, "synset": "basenji.n.01", "name": "basenji"}, - {"id": 3488, "synset": "leonberg.n.01", "name": "Leonberg"}, - {"id": 3489, "synset": "newfoundland.n.01", "name": "Newfoundland"}, - {"id": 3490, "synset": "great_pyrenees.n.01", "name": "Great_Pyrenees"}, - {"id": 3491, "synset": "spitz.n.01", "name": "spitz"}, - {"id": 3492, "synset": "samoyed.n.03", "name": "Samoyed"}, - {"id": 3493, "synset": "pomeranian.n.01", "name": "Pomeranian"}, - {"id": 3494, "synset": "chow.n.03", "name": "chow"}, - {"id": 3495, "synset": "keeshond.n.01", "name": "keeshond"}, - {"id": 3496, "synset": "griffon.n.02", "name": "griffon"}, - {"id": 3497, "synset": "brabancon_griffon.n.01", "name": "Brabancon_griffon"}, - {"id": 3498, "synset": "corgi.n.01", "name": "corgi"}, - {"id": 3499, "synset": "pembroke.n.01", "name": "Pembroke"}, - {"id": 3500, "synset": "cardigan.n.02", "name": "Cardigan"}, - {"id": 3501, "synset": "poodle.n.01", "name": "poodle"}, - {"id": 3502, "synset": "toy_poodle.n.01", "name": "toy_poodle"}, - {"id": 3503, "synset": "miniature_poodle.n.01", "name": "miniature_poodle"}, - {"id": 3504, "synset": "standard_poodle.n.01", "name": "standard_poodle"}, - {"id": 3505, "synset": "large_poodle.n.01", "name": "large_poodle"}, - {"id": 3506, "synset": "mexican_hairless.n.01", "name": "Mexican_hairless"}, - {"id": 3507, "synset": "timber_wolf.n.01", "name": "timber_wolf"}, - {"id": 3508, "synset": "white_wolf.n.01", "name": "white_wolf"}, - {"id": 3509, "synset": "red_wolf.n.01", "name": "red_wolf"}, - {"id": 3510, "synset": "coyote.n.01", "name": "coyote"}, - {"id": 3511, "synset": "coydog.n.01", "name": "coydog"}, - {"id": 3512, "synset": "jackal.n.01", "name": "jackal"}, - {"id": 3513, "synset": "wild_dog.n.01", "name": "wild_dog"}, - {"id": 3514, "synset": "dingo.n.01", "name": "dingo"}, - {"id": 3515, "synset": "dhole.n.01", "name": "dhole"}, - {"id": 3516, "synset": "crab-eating_dog.n.01", "name": "crab-eating_dog"}, - {"id": 3517, "synset": "raccoon_dog.n.01", "name": "raccoon_dog"}, - {"id": 3518, "synset": "african_hunting_dog.n.01", "name": "African_hunting_dog"}, - {"id": 3519, "synset": "hyena.n.01", "name": "hyena"}, - {"id": 3520, "synset": "striped_hyena.n.01", "name": "striped_hyena"}, - {"id": 3521, "synset": "brown_hyena.n.01", "name": "brown_hyena"}, - {"id": 3522, "synset": "spotted_hyena.n.01", "name": "spotted_hyena"}, - {"id": 3523, "synset": "aardwolf.n.01", "name": "aardwolf"}, - {"id": 3524, "synset": "fox.n.01", "name": "fox"}, - {"id": 3525, "synset": "vixen.n.02", "name": "vixen"}, - {"id": 3526, "synset": "reynard.n.01", "name": "Reynard"}, - {"id": 3527, "synset": "red_fox.n.03", "name": "red_fox"}, - {"id": 3528, "synset": "black_fox.n.01", "name": "black_fox"}, - {"id": 3529, "synset": "silver_fox.n.01", "name": "silver_fox"}, - {"id": 3530, "synset": "red_fox.n.02", "name": "red_fox"}, - {"id": 3531, "synset": "kit_fox.n.02", "name": "kit_fox"}, - {"id": 3532, "synset": "kit_fox.n.01", "name": "kit_fox"}, - {"id": 3533, "synset": "arctic_fox.n.01", "name": "Arctic_fox"}, - {"id": 3534, "synset": "blue_fox.n.01", "name": "blue_fox"}, - {"id": 3535, "synset": "grey_fox.n.01", "name": "grey_fox"}, - {"id": 3536, "synset": "feline.n.01", "name": "feline"}, - {"id": 3537, "synset": "domestic_cat.n.01", "name": "domestic_cat"}, - {"id": 3538, "synset": "kitty.n.04", "name": "kitty"}, - {"id": 3539, "synset": "mouser.n.01", "name": "mouser"}, - {"id": 3540, "synset": "alley_cat.n.01", "name": "alley_cat"}, - {"id": 3541, "synset": "stray.n.01", "name": "stray"}, - {"id": 3542, "synset": "tom.n.02", "name": "tom"}, - {"id": 3543, "synset": "gib.n.02", "name": "gib"}, - {"id": 3544, "synset": "tabby.n.02", "name": "tabby"}, - {"id": 3545, "synset": "tabby.n.01", "name": "tabby"}, - {"id": 3546, "synset": "tiger_cat.n.02", "name": "tiger_cat"}, - {"id": 3547, "synset": "tortoiseshell.n.03", "name": "tortoiseshell"}, - {"id": 3548, "synset": "persian_cat.n.01", "name": "Persian_cat"}, - {"id": 3549, "synset": "angora.n.04", "name": "Angora"}, - {"id": 3550, "synset": "siamese_cat.n.01", "name": "Siamese_cat"}, - {"id": 3551, "synset": "blue_point_siamese.n.01", "name": "blue_point_Siamese"}, - {"id": 3552, "synset": "burmese_cat.n.01", "name": "Burmese_cat"}, - {"id": 3553, "synset": "egyptian_cat.n.01", "name": "Egyptian_cat"}, - {"id": 3554, "synset": "maltese.n.03", "name": "Maltese"}, - {"id": 3555, "synset": "abyssinian.n.01", "name": "Abyssinian"}, - {"id": 3556, "synset": "manx.n.02", "name": "Manx"}, - {"id": 3557, "synset": "wildcat.n.03", "name": "wildcat"}, - {"id": 3558, "synset": "sand_cat.n.01", "name": "sand_cat"}, - {"id": 3559, "synset": "european_wildcat.n.01", "name": "European_wildcat"}, - {"id": 3560, "synset": "ocelot.n.01", "name": "ocelot"}, - {"id": 3561, "synset": "jaguarundi.n.01", "name": "jaguarundi"}, - {"id": 3562, "synset": "kaffir_cat.n.01", "name": "kaffir_cat"}, - {"id": 3563, "synset": "jungle_cat.n.01", "name": "jungle_cat"}, - {"id": 3564, "synset": "serval.n.01", "name": "serval"}, - {"id": 3565, "synset": "leopard_cat.n.01", "name": "leopard_cat"}, - {"id": 3566, "synset": "margay.n.01", "name": "margay"}, - {"id": 3567, "synset": "manul.n.01", "name": "manul"}, - {"id": 3568, "synset": "lynx.n.02", "name": "lynx"}, - {"id": 3569, "synset": "common_lynx.n.01", "name": "common_lynx"}, - {"id": 3570, "synset": "canada_lynx.n.01", "name": "Canada_lynx"}, - {"id": 3571, "synset": "bobcat.n.01", "name": "bobcat"}, - {"id": 3572, "synset": "spotted_lynx.n.01", "name": "spotted_lynx"}, - {"id": 3573, "synset": "caracal.n.01", "name": "caracal"}, - {"id": 3574, "synset": "big_cat.n.01", "name": "big_cat"}, - {"id": 3575, "synset": "leopard.n.02", "name": "leopard"}, - {"id": 3576, "synset": "leopardess.n.01", "name": "leopardess"}, - {"id": 3577, "synset": "panther.n.02", "name": "panther"}, - {"id": 3578, "synset": "snow_leopard.n.01", "name": "snow_leopard"}, - {"id": 3579, "synset": "jaguar.n.01", "name": "jaguar"}, - {"id": 3580, "synset": "lioness.n.01", "name": "lioness"}, - {"id": 3581, "synset": "lionet.n.01", "name": "lionet"}, - {"id": 3582, "synset": "bengal_tiger.n.01", "name": "Bengal_tiger"}, - {"id": 3583, "synset": "tigress.n.01", "name": "tigress"}, - {"id": 3584, "synset": "liger.n.01", "name": "liger"}, - {"id": 3585, "synset": "tiglon.n.01", "name": "tiglon"}, - {"id": 3586, "synset": "cheetah.n.01", "name": "cheetah"}, - {"id": 3587, "synset": "saber-toothed_tiger.n.01", "name": "saber-toothed_tiger"}, - {"id": 3588, "synset": "smiledon_californicus.n.01", "name": "Smiledon_californicus"}, - {"id": 3589, "synset": "brown_bear.n.01", "name": "brown_bear"}, - {"id": 3590, "synset": "bruin.n.01", "name": "bruin"}, - {"id": 3591, "synset": "syrian_bear.n.01", "name": "Syrian_bear"}, - {"id": 3592, "synset": "alaskan_brown_bear.n.01", "name": "Alaskan_brown_bear"}, - {"id": 3593, "synset": "american_black_bear.n.01", "name": "American_black_bear"}, - {"id": 3594, "synset": "cinnamon_bear.n.01", "name": "cinnamon_bear"}, - {"id": 3595, "synset": "asiatic_black_bear.n.01", "name": "Asiatic_black_bear"}, - {"id": 3596, "synset": "sloth_bear.n.01", "name": "sloth_bear"}, - {"id": 3597, "synset": "viverrine.n.01", "name": "viverrine"}, - {"id": 3598, "synset": "civet.n.01", "name": "civet"}, - {"id": 3599, "synset": "large_civet.n.01", "name": "large_civet"}, - {"id": 3600, "synset": "small_civet.n.01", "name": "small_civet"}, - {"id": 3601, "synset": "binturong.n.01", "name": "binturong"}, - {"id": 3602, "synset": "cryptoprocta.n.01", "name": "Cryptoprocta"}, - {"id": 3603, "synset": "fossa.n.03", "name": "fossa"}, - {"id": 3604, "synset": "fanaloka.n.01", "name": "fanaloka"}, - {"id": 3605, "synset": "genet.n.03", "name": "genet"}, - {"id": 3606, "synset": "banded_palm_civet.n.01", "name": "banded_palm_civet"}, - {"id": 3607, "synset": "mongoose.n.01", "name": "mongoose"}, - {"id": 3608, "synset": "indian_mongoose.n.01", "name": "Indian_mongoose"}, - {"id": 3609, "synset": "ichneumon.n.01", "name": "ichneumon"}, - {"id": 3610, "synset": "palm_cat.n.01", "name": "palm_cat"}, - {"id": 3611, "synset": "meerkat.n.01", "name": "meerkat"}, - {"id": 3612, "synset": "slender-tailed_meerkat.n.01", "name": "slender-tailed_meerkat"}, - {"id": 3613, "synset": "suricate.n.01", "name": "suricate"}, - {"id": 3614, "synset": "fruit_bat.n.01", "name": "fruit_bat"}, - {"id": 3615, "synset": "flying_fox.n.01", "name": "flying_fox"}, - {"id": 3616, "synset": "pteropus_capestratus.n.01", "name": "Pteropus_capestratus"}, - {"id": 3617, "synset": "pteropus_hypomelanus.n.01", "name": "Pteropus_hypomelanus"}, - {"id": 3618, "synset": "harpy.n.03", "name": "harpy"}, - {"id": 3619, "synset": "cynopterus_sphinx.n.01", "name": "Cynopterus_sphinx"}, - {"id": 3620, "synset": "carnivorous_bat.n.01", "name": "carnivorous_bat"}, - {"id": 3621, "synset": "mouse-eared_bat.n.01", "name": "mouse-eared_bat"}, - {"id": 3622, "synset": "leafnose_bat.n.01", "name": "leafnose_bat"}, - {"id": 3623, "synset": "macrotus.n.01", "name": "macrotus"}, - {"id": 3624, "synset": "spearnose_bat.n.01", "name": "spearnose_bat"}, - {"id": 3625, "synset": "phyllostomus_hastatus.n.01", "name": "Phyllostomus_hastatus"}, - {"id": 3626, "synset": "hognose_bat.n.01", "name": "hognose_bat"}, - {"id": 3627, "synset": "horseshoe_bat.n.02", "name": "horseshoe_bat"}, - {"id": 3628, "synset": "horseshoe_bat.n.01", "name": "horseshoe_bat"}, - {"id": 3629, "synset": "orange_bat.n.01", "name": "orange_bat"}, - {"id": 3630, "synset": "false_vampire.n.01", "name": "false_vampire"}, - {"id": 3631, "synset": "big-eared_bat.n.01", "name": "big-eared_bat"}, - {"id": 3632, "synset": "vespertilian_bat.n.01", "name": "vespertilian_bat"}, - {"id": 3633, "synset": "frosted_bat.n.01", "name": "frosted_bat"}, - {"id": 3634, "synset": "red_bat.n.01", "name": "red_bat"}, - {"id": 3635, "synset": "brown_bat.n.01", "name": "brown_bat"}, - {"id": 3636, "synset": "little_brown_bat.n.01", "name": "little_brown_bat"}, - {"id": 3637, "synset": "cave_myotis.n.01", "name": "cave_myotis"}, - {"id": 3638, "synset": "big_brown_bat.n.01", "name": "big_brown_bat"}, - {"id": 3639, "synset": "serotine.n.01", "name": "serotine"}, - {"id": 3640, "synset": "pallid_bat.n.01", "name": "pallid_bat"}, - {"id": 3641, "synset": "pipistrelle.n.01", "name": "pipistrelle"}, - {"id": 3642, "synset": "eastern_pipistrel.n.01", "name": "eastern_pipistrel"}, - {"id": 3643, "synset": "jackass_bat.n.01", "name": "jackass_bat"}, - {"id": 3644, "synset": "long-eared_bat.n.01", "name": "long-eared_bat"}, - {"id": 3645, "synset": "western_big-eared_bat.n.01", "name": "western_big-eared_bat"}, - {"id": 3646, "synset": "freetail.n.01", "name": "freetail"}, - {"id": 3647, "synset": "guano_bat.n.01", "name": "guano_bat"}, - {"id": 3648, "synset": "pocketed_bat.n.01", "name": "pocketed_bat"}, - {"id": 3649, "synset": "mastiff_bat.n.01", "name": "mastiff_bat"}, - {"id": 3650, "synset": "vampire_bat.n.01", "name": "vampire_bat"}, - {"id": 3651, "synset": "desmodus_rotundus.n.01", "name": "Desmodus_rotundus"}, - {"id": 3652, "synset": "hairy-legged_vampire_bat.n.01", "name": "hairy-legged_vampire_bat"}, - {"id": 3653, "synset": "predator.n.02", "name": "predator"}, - {"id": 3654, "synset": "prey.n.02", "name": "prey"}, - {"id": 3655, "synset": "game.n.04", "name": "game"}, - {"id": 3656, "synset": "big_game.n.01", "name": "big_game"}, - {"id": 3657, "synset": "game_bird.n.01", "name": "game_bird"}, - {"id": 3658, "synset": "fossorial_mammal.n.01", "name": "fossorial_mammal"}, - {"id": 3659, "synset": "tetrapod.n.01", "name": "tetrapod"}, - {"id": 3660, "synset": "quadruped.n.01", "name": "quadruped"}, - {"id": 3661, "synset": "hexapod.n.01", "name": "hexapod"}, - {"id": 3662, "synset": "biped.n.01", "name": "biped"}, - {"id": 3663, "synset": "insect.n.01", "name": "insect"}, - {"id": 3664, "synset": "social_insect.n.01", "name": "social_insect"}, - {"id": 3665, "synset": "holometabola.n.01", "name": "holometabola"}, - {"id": 3666, "synset": "defoliator.n.01", "name": "defoliator"}, - {"id": 3667, "synset": "pollinator.n.01", "name": "pollinator"}, - {"id": 3668, "synset": "gallfly.n.03", "name": "gallfly"}, - {"id": 3669, "synset": "scorpion_fly.n.01", "name": "scorpion_fly"}, - {"id": 3670, "synset": "hanging_fly.n.01", "name": "hanging_fly"}, - {"id": 3671, "synset": "collembolan.n.01", "name": "collembolan"}, - {"id": 3672, "synset": "tiger_beetle.n.01", "name": "tiger_beetle"}, - {"id": 3673, "synset": "two-spotted_ladybug.n.01", "name": "two-spotted_ladybug"}, - {"id": 3674, "synset": "mexican_bean_beetle.n.01", "name": "Mexican_bean_beetle"}, - {"id": 3675, "synset": "hippodamia_convergens.n.01", "name": "Hippodamia_convergens"}, - {"id": 3676, "synset": "vedalia.n.01", "name": "vedalia"}, - {"id": 3677, "synset": "ground_beetle.n.01", "name": "ground_beetle"}, - {"id": 3678, "synset": "bombardier_beetle.n.01", "name": "bombardier_beetle"}, - {"id": 3679, "synset": "calosoma.n.01", "name": "calosoma"}, - {"id": 3680, "synset": "searcher.n.03", "name": "searcher"}, - {"id": 3681, "synset": "firefly.n.02", "name": "firefly"}, - {"id": 3682, "synset": "glowworm.n.01", "name": "glowworm"}, - {"id": 3683, "synset": "long-horned_beetle.n.01", "name": "long-horned_beetle"}, - {"id": 3684, "synset": "sawyer.n.02", "name": "sawyer"}, - {"id": 3685, "synset": "pine_sawyer.n.01", "name": "pine_sawyer"}, - {"id": 3686, "synset": "leaf_beetle.n.01", "name": "leaf_beetle"}, - {"id": 3687, "synset": "flea_beetle.n.01", "name": "flea_beetle"}, - {"id": 3688, "synset": "colorado_potato_beetle.n.01", "name": "Colorado_potato_beetle"}, - {"id": 3689, "synset": "carpet_beetle.n.01", "name": "carpet_beetle"}, - {"id": 3690, "synset": "buffalo_carpet_beetle.n.01", "name": "buffalo_carpet_beetle"}, - {"id": 3691, "synset": "black_carpet_beetle.n.01", "name": "black_carpet_beetle"}, - {"id": 3692, "synset": "clerid_beetle.n.01", "name": "clerid_beetle"}, - {"id": 3693, "synset": "bee_beetle.n.01", "name": "bee_beetle"}, - {"id": 3694, "synset": "lamellicorn_beetle.n.01", "name": "lamellicorn_beetle"}, - {"id": 3695, "synset": "scarabaeid_beetle.n.01", "name": "scarabaeid_beetle"}, - {"id": 3696, "synset": "dung_beetle.n.01", "name": "dung_beetle"}, - {"id": 3697, "synset": "scarab.n.01", "name": "scarab"}, - {"id": 3698, "synset": "tumblebug.n.01", "name": "tumblebug"}, - {"id": 3699, "synset": "dorbeetle.n.01", "name": "dorbeetle"}, - {"id": 3700, "synset": "june_beetle.n.01", "name": "June_beetle"}, - {"id": 3701, "synset": "green_june_beetle.n.01", "name": "green_June_beetle"}, - {"id": 3702, "synset": "japanese_beetle.n.01", "name": "Japanese_beetle"}, - {"id": 3703, "synset": "oriental_beetle.n.01", "name": "Oriental_beetle"}, - {"id": 3704, "synset": "rhinoceros_beetle.n.01", "name": "rhinoceros_beetle"}, - {"id": 3705, "synset": "melolonthid_beetle.n.01", "name": "melolonthid_beetle"}, - {"id": 3706, "synset": "cockchafer.n.01", "name": "cockchafer"}, - {"id": 3707, "synset": "rose_chafer.n.02", "name": "rose_chafer"}, - {"id": 3708, "synset": "rose_chafer.n.01", "name": "rose_chafer"}, - {"id": 3709, "synset": "stag_beetle.n.01", "name": "stag_beetle"}, - {"id": 3710, "synset": "elaterid_beetle.n.01", "name": "elaterid_beetle"}, - {"id": 3711, "synset": "click_beetle.n.01", "name": "click_beetle"}, - {"id": 3712, "synset": "firefly.n.01", "name": "firefly"}, - {"id": 3713, "synset": "wireworm.n.01", "name": "wireworm"}, - {"id": 3714, "synset": "water_beetle.n.01", "name": "water_beetle"}, - {"id": 3715, "synset": "whirligig_beetle.n.01", "name": "whirligig_beetle"}, - {"id": 3716, "synset": "deathwatch_beetle.n.01", "name": "deathwatch_beetle"}, - {"id": 3717, "synset": "weevil.n.01", "name": "weevil"}, - {"id": 3718, "synset": "snout_beetle.n.01", "name": "snout_beetle"}, - {"id": 3719, "synset": "boll_weevil.n.01", "name": "boll_weevil"}, - {"id": 3720, "synset": "blister_beetle.n.01", "name": "blister_beetle"}, - {"id": 3721, "synset": "oil_beetle.n.01", "name": "oil_beetle"}, - {"id": 3722, "synset": "spanish_fly.n.01", "name": "Spanish_fly"}, - {"id": 3723, "synset": "dutch-elm_beetle.n.01", "name": "Dutch-elm_beetle"}, - {"id": 3724, "synset": "bark_beetle.n.01", "name": "bark_beetle"}, - {"id": 3725, "synset": "spruce_bark_beetle.n.01", "name": "spruce_bark_beetle"}, - {"id": 3726, "synset": "rove_beetle.n.01", "name": "rove_beetle"}, - {"id": 3727, "synset": "darkling_beetle.n.01", "name": "darkling_beetle"}, - {"id": 3728, "synset": "mealworm.n.01", "name": "mealworm"}, - {"id": 3729, "synset": "flour_beetle.n.01", "name": "flour_beetle"}, - {"id": 3730, "synset": "seed_beetle.n.01", "name": "seed_beetle"}, - {"id": 3731, "synset": "pea_weevil.n.01", "name": "pea_weevil"}, - {"id": 3732, "synset": "bean_weevil.n.01", "name": "bean_weevil"}, - {"id": 3733, "synset": "rice_weevil.n.01", "name": "rice_weevil"}, - {"id": 3734, "synset": "asian_longhorned_beetle.n.01", "name": "Asian_longhorned_beetle"}, - {"id": 3735, "synset": "web_spinner.n.01", "name": "web_spinner"}, - {"id": 3736, "synset": "louse.n.01", "name": "louse"}, - {"id": 3737, "synset": "common_louse.n.01", "name": "common_louse"}, - {"id": 3738, "synset": "head_louse.n.01", "name": "head_louse"}, - {"id": 3739, "synset": "body_louse.n.01", "name": "body_louse"}, - {"id": 3740, "synset": "crab_louse.n.01", "name": "crab_louse"}, - {"id": 3741, "synset": "bird_louse.n.01", "name": "bird_louse"}, - {"id": 3742, "synset": "flea.n.01", "name": "flea"}, - {"id": 3743, "synset": "pulex_irritans.n.01", "name": "Pulex_irritans"}, - {"id": 3744, "synset": "dog_flea.n.01", "name": "dog_flea"}, - {"id": 3745, "synset": "cat_flea.n.01", "name": "cat_flea"}, - {"id": 3746, "synset": "chigoe.n.01", "name": "chigoe"}, - {"id": 3747, "synset": "sticktight.n.02", "name": "sticktight"}, - {"id": 3748, "synset": "dipterous_insect.n.01", "name": "dipterous_insect"}, - {"id": 3749, "synset": "gall_midge.n.01", "name": "gall_midge"}, - {"id": 3750, "synset": "hessian_fly.n.01", "name": "Hessian_fly"}, - {"id": 3751, "synset": "fly.n.01", "name": "fly"}, - {"id": 3752, "synset": "housefly.n.01", "name": "housefly"}, - {"id": 3753, "synset": "tsetse_fly.n.01", "name": "tsetse_fly"}, - {"id": 3754, "synset": "blowfly.n.01", "name": "blowfly"}, - {"id": 3755, "synset": "bluebottle.n.02", "name": "bluebottle"}, - {"id": 3756, "synset": "greenbottle.n.01", "name": "greenbottle"}, - {"id": 3757, "synset": "flesh_fly.n.01", "name": "flesh_fly"}, - {"id": 3758, "synset": "tachina_fly.n.01", "name": "tachina_fly"}, - {"id": 3759, "synset": "gadfly.n.02", "name": "gadfly"}, - {"id": 3760, "synset": "botfly.n.01", "name": "botfly"}, - {"id": 3761, "synset": "human_botfly.n.01", "name": "human_botfly"}, - {"id": 3762, "synset": "sheep_botfly.n.01", "name": "sheep_botfly"}, - {"id": 3763, "synset": "warble_fly.n.01", "name": "warble_fly"}, - {"id": 3764, "synset": "horsefly.n.02", "name": "horsefly"}, - {"id": 3765, "synset": "bee_fly.n.01", "name": "bee_fly"}, - {"id": 3766, "synset": "robber_fly.n.01", "name": "robber_fly"}, - {"id": 3767, "synset": "fruit_fly.n.01", "name": "fruit_fly"}, - {"id": 3768, "synset": "apple_maggot.n.01", "name": "apple_maggot"}, - {"id": 3769, "synset": "mediterranean_fruit_fly.n.01", "name": "Mediterranean_fruit_fly"}, - {"id": 3770, "synset": "drosophila.n.01", "name": "drosophila"}, - {"id": 3771, "synset": "vinegar_fly.n.01", "name": "vinegar_fly"}, - {"id": 3772, "synset": "leaf_miner.n.01", "name": "leaf_miner"}, - {"id": 3773, "synset": "louse_fly.n.01", "name": "louse_fly"}, - {"id": 3774, "synset": "horse_tick.n.01", "name": "horse_tick"}, - {"id": 3775, "synset": "sheep_ked.n.01", "name": "sheep_ked"}, - {"id": 3776, "synset": "horn_fly.n.01", "name": "horn_fly"}, - {"id": 3777, "synset": "mosquito.n.01", "name": "mosquito"}, - {"id": 3778, "synset": "wiggler.n.02", "name": "wiggler"}, - {"id": 3779, "synset": "gnat.n.02", "name": "gnat"}, - {"id": 3780, "synset": "yellow-fever_mosquito.n.01", "name": "yellow-fever_mosquito"}, - {"id": 3781, "synset": "asian_tiger_mosquito.n.01", "name": "Asian_tiger_mosquito"}, - {"id": 3782, "synset": "anopheline.n.01", "name": "anopheline"}, - {"id": 3783, "synset": "malarial_mosquito.n.01", "name": "malarial_mosquito"}, - {"id": 3784, "synset": "common_mosquito.n.01", "name": "common_mosquito"}, - {"id": 3785, "synset": "culex_quinquefasciatus.n.01", "name": "Culex_quinquefasciatus"}, - {"id": 3786, "synset": "gnat.n.01", "name": "gnat"}, - {"id": 3787, "synset": "punkie.n.01", "name": "punkie"}, - {"id": 3788, "synset": "midge.n.01", "name": "midge"}, - {"id": 3789, "synset": "fungus_gnat.n.02", "name": "fungus_gnat"}, - {"id": 3790, "synset": "psychodid.n.01", "name": "psychodid"}, - {"id": 3791, "synset": "sand_fly.n.01", "name": "sand_fly"}, - {"id": 3792, "synset": "fungus_gnat.n.01", "name": "fungus_gnat"}, - {"id": 3793, "synset": "armyworm.n.03", "name": "armyworm"}, - {"id": 3794, "synset": "crane_fly.n.01", "name": "crane_fly"}, - {"id": 3795, "synset": "blackfly.n.02", "name": "blackfly"}, - {"id": 3796, "synset": "hymenopterous_insect.n.01", "name": "hymenopterous_insect"}, - {"id": 3797, "synset": "bee.n.01", "name": "bee"}, - {"id": 3798, "synset": "drone.n.01", "name": "drone"}, - {"id": 3799, "synset": "queen_bee.n.01", "name": "queen_bee"}, - {"id": 3800, "synset": "worker.n.03", "name": "worker"}, - {"id": 3801, "synset": "soldier.n.02", "name": "soldier"}, - {"id": 3802, "synset": "worker_bee.n.01", "name": "worker_bee"}, - {"id": 3803, "synset": "honeybee.n.01", "name": "honeybee"}, - {"id": 3804, "synset": "africanized_bee.n.01", "name": "Africanized_bee"}, - {"id": 3805, "synset": "black_bee.n.01", "name": "black_bee"}, - {"id": 3806, "synset": "carniolan_bee.n.01", "name": "Carniolan_bee"}, - {"id": 3807, "synset": "italian_bee.n.01", "name": "Italian_bee"}, - {"id": 3808, "synset": "carpenter_bee.n.01", "name": "carpenter_bee"}, - {"id": 3809, "synset": "bumblebee.n.01", "name": "bumblebee"}, - {"id": 3810, "synset": "cuckoo-bumblebee.n.01", "name": "cuckoo-bumblebee"}, - {"id": 3811, "synset": "andrena.n.01", "name": "andrena"}, - {"id": 3812, "synset": "nomia_melanderi.n.01", "name": "Nomia_melanderi"}, - {"id": 3813, "synset": "leaf-cutting_bee.n.01", "name": "leaf-cutting_bee"}, - {"id": 3814, "synset": "mason_bee.n.01", "name": "mason_bee"}, - {"id": 3815, "synset": "potter_bee.n.01", "name": "potter_bee"}, - {"id": 3816, "synset": "wasp.n.02", "name": "wasp"}, - {"id": 3817, "synset": "vespid.n.01", "name": "vespid"}, - {"id": 3818, "synset": "paper_wasp.n.01", "name": "paper_wasp"}, - {"id": 3819, "synset": "giant_hornet.n.01", "name": "giant_hornet"}, - {"id": 3820, "synset": "common_wasp.n.01", "name": "common_wasp"}, - {"id": 3821, "synset": "bald-faced_hornet.n.01", "name": "bald-faced_hornet"}, - {"id": 3822, "synset": "yellow_jacket.n.02", "name": "yellow_jacket"}, - {"id": 3823, "synset": "polistes_annularis.n.01", "name": "Polistes_annularis"}, - {"id": 3824, "synset": "mason_wasp.n.02", "name": "mason_wasp"}, - {"id": 3825, "synset": "potter_wasp.n.01", "name": "potter_wasp"}, - {"id": 3826, "synset": "mutillidae.n.01", "name": "Mutillidae"}, - {"id": 3827, "synset": "velvet_ant.n.01", "name": "velvet_ant"}, - {"id": 3828, "synset": "sphecoid_wasp.n.01", "name": "sphecoid_wasp"}, - {"id": 3829, "synset": "mason_wasp.n.01", "name": "mason_wasp"}, - {"id": 3830, "synset": "digger_wasp.n.01", "name": "digger_wasp"}, - {"id": 3831, "synset": "cicada_killer.n.01", "name": "cicada_killer"}, - {"id": 3832, "synset": "mud_dauber.n.01", "name": "mud_dauber"}, - {"id": 3833, "synset": "gall_wasp.n.01", "name": "gall_wasp"}, - {"id": 3834, "synset": "chalcid_fly.n.01", "name": "chalcid_fly"}, - {"id": 3835, "synset": "strawworm.n.02", "name": "strawworm"}, - {"id": 3836, "synset": "chalcis_fly.n.01", "name": "chalcis_fly"}, - {"id": 3837, "synset": "ichneumon_fly.n.01", "name": "ichneumon_fly"}, - {"id": 3838, "synset": "sawfly.n.01", "name": "sawfly"}, - {"id": 3839, "synset": "birch_leaf_miner.n.01", "name": "birch_leaf_miner"}, - {"id": 3840, "synset": "ant.n.01", "name": "ant"}, - {"id": 3841, "synset": "pharaoh_ant.n.01", "name": "pharaoh_ant"}, - {"id": 3842, "synset": "little_black_ant.n.01", "name": "little_black_ant"}, - {"id": 3843, "synset": "army_ant.n.01", "name": "army_ant"}, - {"id": 3844, "synset": "carpenter_ant.n.01", "name": "carpenter_ant"}, - {"id": 3845, "synset": "fire_ant.n.01", "name": "fire_ant"}, - {"id": 3846, "synset": "wood_ant.n.01", "name": "wood_ant"}, - {"id": 3847, "synset": "slave_ant.n.01", "name": "slave_ant"}, - {"id": 3848, "synset": "formica_fusca.n.01", "name": "Formica_fusca"}, - {"id": 3849, "synset": "slave-making_ant.n.01", "name": "slave-making_ant"}, - {"id": 3850, "synset": "sanguinary_ant.n.01", "name": "sanguinary_ant"}, - {"id": 3851, "synset": "bulldog_ant.n.01", "name": "bulldog_ant"}, - {"id": 3852, "synset": "amazon_ant.n.01", "name": "Amazon_ant"}, - {"id": 3853, "synset": "termite.n.01", "name": "termite"}, - {"id": 3854, "synset": "dry-wood_termite.n.01", "name": "dry-wood_termite"}, - {"id": 3855, "synset": "reticulitermes_lucifugus.n.01", "name": "Reticulitermes_lucifugus"}, - {"id": 3856, "synset": "mastotermes_darwiniensis.n.01", "name": "Mastotermes_darwiniensis"}, - { - "id": 3857, - "synset": "mastotermes_electrodominicus.n.01", - "name": "Mastotermes_electrodominicus", - }, - {"id": 3858, "synset": "powder-post_termite.n.01", "name": "powder-post_termite"}, - {"id": 3859, "synset": "orthopterous_insect.n.01", "name": "orthopterous_insect"}, - {"id": 3860, "synset": "grasshopper.n.01", "name": "grasshopper"}, - {"id": 3861, "synset": "short-horned_grasshopper.n.01", "name": "short-horned_grasshopper"}, - {"id": 3862, "synset": "locust.n.01", "name": "locust"}, - {"id": 3863, "synset": "migratory_locust.n.01", "name": "migratory_locust"}, - {"id": 3864, "synset": "migratory_grasshopper.n.01", "name": "migratory_grasshopper"}, - {"id": 3865, "synset": "long-horned_grasshopper.n.01", "name": "long-horned_grasshopper"}, - {"id": 3866, "synset": "katydid.n.01", "name": "katydid"}, - {"id": 3867, "synset": "mormon_cricket.n.01", "name": "mormon_cricket"}, - {"id": 3868, "synset": "sand_cricket.n.01", "name": "sand_cricket"}, - {"id": 3869, "synset": "cricket.n.01", "name": "cricket"}, - {"id": 3870, "synset": "mole_cricket.n.01", "name": "mole_cricket"}, - {"id": 3871, "synset": "european_house_cricket.n.01", "name": "European_house_cricket"}, - {"id": 3872, "synset": "field_cricket.n.01", "name": "field_cricket"}, - {"id": 3873, "synset": "tree_cricket.n.01", "name": "tree_cricket"}, - {"id": 3874, "synset": "snowy_tree_cricket.n.01", "name": "snowy_tree_cricket"}, - {"id": 3875, "synset": "phasmid.n.01", "name": "phasmid"}, - {"id": 3876, "synset": "walking_stick.n.02", "name": "walking_stick"}, - {"id": 3877, "synset": "diapheromera.n.01", "name": "diapheromera"}, - {"id": 3878, "synset": "walking_leaf.n.02", "name": "walking_leaf"}, - {"id": 3879, "synset": "oriental_cockroach.n.01", "name": "oriental_cockroach"}, - {"id": 3880, "synset": "american_cockroach.n.01", "name": "American_cockroach"}, - {"id": 3881, "synset": "australian_cockroach.n.01", "name": "Australian_cockroach"}, - {"id": 3882, "synset": "german_cockroach.n.01", "name": "German_cockroach"}, - {"id": 3883, "synset": "giant_cockroach.n.01", "name": "giant_cockroach"}, - {"id": 3884, "synset": "mantis.n.01", "name": "mantis"}, - {"id": 3885, "synset": "praying_mantis.n.01", "name": "praying_mantis"}, - {"id": 3886, "synset": "bug.n.01", "name": "bug"}, - {"id": 3887, "synset": "hemipterous_insect.n.01", "name": "hemipterous_insect"}, - {"id": 3888, "synset": "leaf_bug.n.01", "name": "leaf_bug"}, - {"id": 3889, "synset": "mirid_bug.n.01", "name": "mirid_bug"}, - {"id": 3890, "synset": "four-lined_plant_bug.n.01", "name": "four-lined_plant_bug"}, - {"id": 3891, "synset": "lygus_bug.n.01", "name": "lygus_bug"}, - {"id": 3892, "synset": "tarnished_plant_bug.n.01", "name": "tarnished_plant_bug"}, - {"id": 3893, "synset": "lace_bug.n.01", "name": "lace_bug"}, - {"id": 3894, "synset": "lygaeid.n.01", "name": "lygaeid"}, - {"id": 3895, "synset": "chinch_bug.n.01", "name": "chinch_bug"}, - {"id": 3896, "synset": "coreid_bug.n.01", "name": "coreid_bug"}, - {"id": 3897, "synset": "squash_bug.n.01", "name": "squash_bug"}, - {"id": 3898, "synset": "leaf-footed_bug.n.01", "name": "leaf-footed_bug"}, - {"id": 3899, "synset": "bedbug.n.01", "name": "bedbug"}, - {"id": 3900, "synset": "backswimmer.n.01", "name": "backswimmer"}, - {"id": 3901, "synset": "true_bug.n.01", "name": "true_bug"}, - {"id": 3902, "synset": "heteropterous_insect.n.01", "name": "heteropterous_insect"}, - {"id": 3903, "synset": "water_bug.n.01", "name": "water_bug"}, - {"id": 3904, "synset": "giant_water_bug.n.01", "name": "giant_water_bug"}, - {"id": 3905, "synset": "water_scorpion.n.01", "name": "water_scorpion"}, - {"id": 3906, "synset": "water_boatman.n.01", "name": "water_boatman"}, - {"id": 3907, "synset": "water_strider.n.01", "name": "water_strider"}, - {"id": 3908, "synset": "common_pond-skater.n.01", "name": "common_pond-skater"}, - {"id": 3909, "synset": "assassin_bug.n.01", "name": "assassin_bug"}, - {"id": 3910, "synset": "conenose.n.01", "name": "conenose"}, - {"id": 3911, "synset": "wheel_bug.n.01", "name": "wheel_bug"}, - {"id": 3912, "synset": "firebug.n.02", "name": "firebug"}, - {"id": 3913, "synset": "cotton_stainer.n.01", "name": "cotton_stainer"}, - {"id": 3914, "synset": "homopterous_insect.n.01", "name": "homopterous_insect"}, - {"id": 3915, "synset": "whitefly.n.01", "name": "whitefly"}, - {"id": 3916, "synset": "citrus_whitefly.n.01", "name": "citrus_whitefly"}, - {"id": 3917, "synset": "greenhouse_whitefly.n.01", "name": "greenhouse_whitefly"}, - {"id": 3918, "synset": "sweet-potato_whitefly.n.01", "name": "sweet-potato_whitefly"}, - {"id": 3919, "synset": "superbug.n.02", "name": "superbug"}, - {"id": 3920, "synset": "cotton_strain.n.01", "name": "cotton_strain"}, - {"id": 3921, "synset": "coccid_insect.n.01", "name": "coccid_insect"}, - {"id": 3922, "synset": "scale_insect.n.01", "name": "scale_insect"}, - {"id": 3923, "synset": "soft_scale.n.01", "name": "soft_scale"}, - {"id": 3924, "synset": "brown_soft_scale.n.01", "name": "brown_soft_scale"}, - {"id": 3925, "synset": "armored_scale.n.01", "name": "armored_scale"}, - {"id": 3926, "synset": "san_jose_scale.n.01", "name": "San_Jose_scale"}, - {"id": 3927, "synset": "cochineal_insect.n.01", "name": "cochineal_insect"}, - {"id": 3928, "synset": "mealybug.n.01", "name": "mealybug"}, - {"id": 3929, "synset": "citrophilous_mealybug.n.01", "name": "citrophilous_mealybug"}, - {"id": 3930, "synset": "comstock_mealybug.n.01", "name": "Comstock_mealybug"}, - {"id": 3931, "synset": "citrus_mealybug.n.01", "name": "citrus_mealybug"}, - {"id": 3932, "synset": "plant_louse.n.01", "name": "plant_louse"}, - {"id": 3933, "synset": "aphid.n.01", "name": "aphid"}, - {"id": 3934, "synset": "apple_aphid.n.01", "name": "apple_aphid"}, - {"id": 3935, "synset": "blackfly.n.01", "name": "blackfly"}, - {"id": 3936, "synset": "greenfly.n.01", "name": "greenfly"}, - {"id": 3937, "synset": "green_peach_aphid.n.01", "name": "green_peach_aphid"}, - {"id": 3938, "synset": "ant_cow.n.01", "name": "ant_cow"}, - {"id": 3939, "synset": "woolly_aphid.n.01", "name": "woolly_aphid"}, - {"id": 3940, "synset": "woolly_apple_aphid.n.01", "name": "woolly_apple_aphid"}, - {"id": 3941, "synset": "woolly_alder_aphid.n.01", "name": "woolly_alder_aphid"}, - {"id": 3942, "synset": "adelgid.n.01", "name": "adelgid"}, - {"id": 3943, "synset": "balsam_woolly_aphid.n.01", "name": "balsam_woolly_aphid"}, - {"id": 3944, "synset": "spruce_gall_aphid.n.01", "name": "spruce_gall_aphid"}, - {"id": 3945, "synset": "woolly_adelgid.n.01", "name": "woolly_adelgid"}, - {"id": 3946, "synset": "jumping_plant_louse.n.01", "name": "jumping_plant_louse"}, - {"id": 3947, "synset": "cicada.n.01", "name": "cicada"}, - {"id": 3948, "synset": "dog-day_cicada.n.01", "name": "dog-day_cicada"}, - {"id": 3949, "synset": "seventeen-year_locust.n.01", "name": "seventeen-year_locust"}, - {"id": 3950, "synset": "spittle_insect.n.01", "name": "spittle_insect"}, - {"id": 3951, "synset": "froghopper.n.01", "name": "froghopper"}, - {"id": 3952, "synset": "meadow_spittlebug.n.01", "name": "meadow_spittlebug"}, - {"id": 3953, "synset": "pine_spittlebug.n.01", "name": "pine_spittlebug"}, - {"id": 3954, "synset": "saratoga_spittlebug.n.01", "name": "Saratoga_spittlebug"}, - {"id": 3955, "synset": "leafhopper.n.01", "name": "leafhopper"}, - {"id": 3956, "synset": "plant_hopper.n.01", "name": "plant_hopper"}, - {"id": 3957, "synset": "treehopper.n.01", "name": "treehopper"}, - {"id": 3958, "synset": "lantern_fly.n.01", "name": "lantern_fly"}, - {"id": 3959, "synset": "psocopterous_insect.n.01", "name": "psocopterous_insect"}, - {"id": 3960, "synset": "psocid.n.01", "name": "psocid"}, - {"id": 3961, "synset": "bark-louse.n.01", "name": "bark-louse"}, - {"id": 3962, "synset": "booklouse.n.01", "name": "booklouse"}, - {"id": 3963, "synset": "common_booklouse.n.01", "name": "common_booklouse"}, - {"id": 3964, "synset": "ephemerid.n.01", "name": "ephemerid"}, - {"id": 3965, "synset": "mayfly.n.01", "name": "mayfly"}, - {"id": 3966, "synset": "stonefly.n.01", "name": "stonefly"}, - {"id": 3967, "synset": "neuropteron.n.01", "name": "neuropteron"}, - {"id": 3968, "synset": "ant_lion.n.02", "name": "ant_lion"}, - {"id": 3969, "synset": "doodlebug.n.03", "name": "doodlebug"}, - {"id": 3970, "synset": "lacewing.n.01", "name": "lacewing"}, - {"id": 3971, "synset": "aphid_lion.n.01", "name": "aphid_lion"}, - {"id": 3972, "synset": "green_lacewing.n.01", "name": "green_lacewing"}, - {"id": 3973, "synset": "brown_lacewing.n.01", "name": "brown_lacewing"}, - {"id": 3974, "synset": "dobson.n.02", "name": "dobson"}, - {"id": 3975, "synset": "hellgrammiate.n.01", "name": "hellgrammiate"}, - {"id": 3976, "synset": "fish_fly.n.01", "name": "fish_fly"}, - {"id": 3977, "synset": "alderfly.n.01", "name": "alderfly"}, - {"id": 3978, "synset": "snakefly.n.01", "name": "snakefly"}, - {"id": 3979, "synset": "mantispid.n.01", "name": "mantispid"}, - {"id": 3980, "synset": "odonate.n.01", "name": "odonate"}, - {"id": 3981, "synset": "damselfly.n.01", "name": "damselfly"}, - {"id": 3982, "synset": "trichopterous_insect.n.01", "name": "trichopterous_insect"}, - {"id": 3983, "synset": "caddis_fly.n.01", "name": "caddis_fly"}, - {"id": 3984, "synset": "caseworm.n.01", "name": "caseworm"}, - {"id": 3985, "synset": "caddisworm.n.01", "name": "caddisworm"}, - {"id": 3986, "synset": "thysanuran_insect.n.01", "name": "thysanuran_insect"}, - {"id": 3987, "synset": "bristletail.n.01", "name": "bristletail"}, - {"id": 3988, "synset": "silverfish.n.01", "name": "silverfish"}, - {"id": 3989, "synset": "firebrat.n.01", "name": "firebrat"}, - {"id": 3990, "synset": "jumping_bristletail.n.01", "name": "jumping_bristletail"}, - {"id": 3991, "synset": "thysanopter.n.01", "name": "thysanopter"}, - {"id": 3992, "synset": "thrips.n.01", "name": "thrips"}, - {"id": 3993, "synset": "tobacco_thrips.n.01", "name": "tobacco_thrips"}, - {"id": 3994, "synset": "onion_thrips.n.01", "name": "onion_thrips"}, - {"id": 3995, "synset": "earwig.n.01", "name": "earwig"}, - {"id": 3996, "synset": "common_european_earwig.n.01", "name": "common_European_earwig"}, - {"id": 3997, "synset": "lepidopterous_insect.n.01", "name": "lepidopterous_insect"}, - {"id": 3998, "synset": "nymphalid.n.01", "name": "nymphalid"}, - {"id": 3999, "synset": "mourning_cloak.n.01", "name": "mourning_cloak"}, - {"id": 4000, "synset": "tortoiseshell.n.02", "name": "tortoiseshell"}, - {"id": 4001, "synset": "painted_beauty.n.01", "name": "painted_beauty"}, - {"id": 4002, "synset": "admiral.n.02", "name": "admiral"}, - {"id": 4003, "synset": "red_admiral.n.01", "name": "red_admiral"}, - {"id": 4004, "synset": "white_admiral.n.02", "name": "white_admiral"}, - {"id": 4005, "synset": "banded_purple.n.01", "name": "banded_purple"}, - {"id": 4006, "synset": "red-spotted_purple.n.01", "name": "red-spotted_purple"}, - {"id": 4007, "synset": "viceroy.n.02", "name": "viceroy"}, - {"id": 4008, "synset": "anglewing.n.01", "name": "anglewing"}, - {"id": 4009, "synset": "ringlet.n.04", "name": "ringlet"}, - {"id": 4010, "synset": "comma.n.02", "name": "comma"}, - {"id": 4011, "synset": "fritillary.n.02", "name": "fritillary"}, - {"id": 4012, "synset": "silverspot.n.01", "name": "silverspot"}, - {"id": 4013, "synset": "emperor_butterfly.n.01", "name": "emperor_butterfly"}, - {"id": 4014, "synset": "purple_emperor.n.01", "name": "purple_emperor"}, - {"id": 4015, "synset": "peacock.n.01", "name": "peacock"}, - {"id": 4016, "synset": "danaid.n.01", "name": "danaid"}, - {"id": 4017, "synset": "monarch.n.02", "name": "monarch"}, - {"id": 4018, "synset": "pierid.n.01", "name": "pierid"}, - {"id": 4019, "synset": "cabbage_butterfly.n.01", "name": "cabbage_butterfly"}, - {"id": 4020, "synset": "small_white.n.01", "name": "small_white"}, - {"id": 4021, "synset": "large_white.n.01", "name": "large_white"}, - {"id": 4022, "synset": "southern_cabbage_butterfly.n.01", "name": "southern_cabbage_butterfly"}, - {"id": 4023, "synset": "sulphur_butterfly.n.01", "name": "sulphur_butterfly"}, - {"id": 4024, "synset": "lycaenid.n.01", "name": "lycaenid"}, - {"id": 4025, "synset": "blue.n.07", "name": "blue"}, - {"id": 4026, "synset": "copper.n.05", "name": "copper"}, - {"id": 4027, "synset": "american_copper.n.01", "name": "American_copper"}, - {"id": 4028, "synset": "hairstreak.n.01", "name": "hairstreak"}, - {"id": 4029, "synset": "strymon_melinus.n.01", "name": "Strymon_melinus"}, - {"id": 4030, "synset": "moth.n.01", "name": "moth"}, - {"id": 4031, "synset": "moth_miller.n.01", "name": "moth_miller"}, - {"id": 4032, "synset": "tortricid.n.01", "name": "tortricid"}, - {"id": 4033, "synset": "leaf_roller.n.01", "name": "leaf_roller"}, - {"id": 4034, "synset": "tea_tortrix.n.01", "name": "tea_tortrix"}, - {"id": 4035, "synset": "orange_tortrix.n.01", "name": "orange_tortrix"}, - {"id": 4036, "synset": "codling_moth.n.01", "name": "codling_moth"}, - {"id": 4037, "synset": "lymantriid.n.01", "name": "lymantriid"}, - {"id": 4038, "synset": "tussock_caterpillar.n.01", "name": "tussock_caterpillar"}, - {"id": 4039, "synset": "gypsy_moth.n.01", "name": "gypsy_moth"}, - {"id": 4040, "synset": "browntail.n.01", "name": "browntail"}, - {"id": 4041, "synset": "gold-tail_moth.n.01", "name": "gold-tail_moth"}, - {"id": 4042, "synset": "geometrid.n.01", "name": "geometrid"}, - {"id": 4043, "synset": "paleacrita_vernata.n.01", "name": "Paleacrita_vernata"}, - {"id": 4044, "synset": "alsophila_pometaria.n.01", "name": "Alsophila_pometaria"}, - {"id": 4045, "synset": "cankerworm.n.01", "name": "cankerworm"}, - {"id": 4046, "synset": "spring_cankerworm.n.01", "name": "spring_cankerworm"}, - {"id": 4047, "synset": "fall_cankerworm.n.01", "name": "fall_cankerworm"}, - {"id": 4048, "synset": "measuring_worm.n.01", "name": "measuring_worm"}, - {"id": 4049, "synset": "pyralid.n.01", "name": "pyralid"}, - {"id": 4050, "synset": "bee_moth.n.01", "name": "bee_moth"}, - {"id": 4051, "synset": "corn_borer.n.02", "name": "corn_borer"}, - {"id": 4052, "synset": "mediterranean_flour_moth.n.01", "name": "Mediterranean_flour_moth"}, - {"id": 4053, "synset": "tobacco_moth.n.01", "name": "tobacco_moth"}, - {"id": 4054, "synset": "almond_moth.n.01", "name": "almond_moth"}, - {"id": 4055, "synset": "raisin_moth.n.01", "name": "raisin_moth"}, - {"id": 4056, "synset": "tineoid.n.01", "name": "tineoid"}, - {"id": 4057, "synset": "tineid.n.01", "name": "tineid"}, - {"id": 4058, "synset": "clothes_moth.n.01", "name": "clothes_moth"}, - {"id": 4059, "synset": "casemaking_clothes_moth.n.01", "name": "casemaking_clothes_moth"}, - {"id": 4060, "synset": "webbing_clothes_moth.n.01", "name": "webbing_clothes_moth"}, - {"id": 4061, "synset": "carpet_moth.n.01", "name": "carpet_moth"}, - {"id": 4062, "synset": "gelechiid.n.01", "name": "gelechiid"}, - {"id": 4063, "synset": "grain_moth.n.01", "name": "grain_moth"}, - {"id": 4064, "synset": "angoumois_moth.n.01", "name": "angoumois_moth"}, - {"id": 4065, "synset": "potato_moth.n.01", "name": "potato_moth"}, - {"id": 4066, "synset": "potato_tuberworm.n.01", "name": "potato_tuberworm"}, - {"id": 4067, "synset": "noctuid_moth.n.01", "name": "noctuid_moth"}, - {"id": 4068, "synset": "cutworm.n.01", "name": "cutworm"}, - {"id": 4069, "synset": "underwing.n.01", "name": "underwing"}, - {"id": 4070, "synset": "red_underwing.n.01", "name": "red_underwing"}, - {"id": 4071, "synset": "antler_moth.n.01", "name": "antler_moth"}, - {"id": 4072, "synset": "heliothis_moth.n.01", "name": "heliothis_moth"}, - {"id": 4073, "synset": "army_cutworm.n.01", "name": "army_cutworm"}, - {"id": 4074, "synset": "armyworm.n.02", "name": "armyworm"}, - {"id": 4075, "synset": "armyworm.n.01", "name": "armyworm"}, - {"id": 4076, "synset": "spodoptera_exigua.n.02", "name": "Spodoptera_exigua"}, - {"id": 4077, "synset": "beet_armyworm.n.01", "name": "beet_armyworm"}, - {"id": 4078, "synset": "spodoptera_frugiperda.n.02", "name": "Spodoptera_frugiperda"}, - {"id": 4079, "synset": "fall_armyworm.n.01", "name": "fall_armyworm"}, - {"id": 4080, "synset": "hawkmoth.n.01", "name": "hawkmoth"}, - {"id": 4081, "synset": "manduca_sexta.n.02", "name": "Manduca_sexta"}, - {"id": 4082, "synset": "tobacco_hornworm.n.01", "name": "tobacco_hornworm"}, - {"id": 4083, "synset": "manduca_quinquemaculata.n.02", "name": "Manduca_quinquemaculata"}, - {"id": 4084, "synset": "tomato_hornworm.n.01", "name": "tomato_hornworm"}, - {"id": 4085, "synset": "death's-head_moth.n.01", "name": "death's-head_moth"}, - {"id": 4086, "synset": "bombycid.n.01", "name": "bombycid"}, - {"id": 4087, "synset": "domestic_silkworm_moth.n.01", "name": "domestic_silkworm_moth"}, - {"id": 4088, "synset": "silkworm.n.01", "name": "silkworm"}, - {"id": 4089, "synset": "saturniid.n.01", "name": "saturniid"}, - {"id": 4090, "synset": "emperor.n.03", "name": "emperor"}, - {"id": 4091, "synset": "imperial_moth.n.01", "name": "imperial_moth"}, - {"id": 4092, "synset": "giant_silkworm_moth.n.01", "name": "giant_silkworm_moth"}, - {"id": 4093, "synset": "silkworm.n.02", "name": "silkworm"}, - {"id": 4094, "synset": "luna_moth.n.01", "name": "luna_moth"}, - {"id": 4095, "synset": "cecropia.n.02", "name": "cecropia"}, - {"id": 4096, "synset": "cynthia_moth.n.01", "name": "cynthia_moth"}, - {"id": 4097, "synset": "ailanthus_silkworm.n.01", "name": "ailanthus_silkworm"}, - {"id": 4098, "synset": "io_moth.n.01", "name": "io_moth"}, - {"id": 4099, "synset": "polyphemus_moth.n.01", "name": "polyphemus_moth"}, - {"id": 4100, "synset": "pernyi_moth.n.01", "name": "pernyi_moth"}, - {"id": 4101, "synset": "tussah.n.01", "name": "tussah"}, - {"id": 4102, "synset": "atlas_moth.n.01", "name": "atlas_moth"}, - {"id": 4103, "synset": "arctiid.n.01", "name": "arctiid"}, - {"id": 4104, "synset": "tiger_moth.n.01", "name": "tiger_moth"}, - {"id": 4105, "synset": "cinnabar.n.02", "name": "cinnabar"}, - {"id": 4106, "synset": "lasiocampid.n.01", "name": "lasiocampid"}, - {"id": 4107, "synset": "eggar.n.01", "name": "eggar"}, - {"id": 4108, "synset": "tent-caterpillar_moth.n.02", "name": "tent-caterpillar_moth"}, - {"id": 4109, "synset": "tent_caterpillar.n.01", "name": "tent_caterpillar"}, - {"id": 4110, "synset": "tent-caterpillar_moth.n.01", "name": "tent-caterpillar_moth"}, - {"id": 4111, "synset": "forest_tent_caterpillar.n.01", "name": "forest_tent_caterpillar"}, - {"id": 4112, "synset": "lappet.n.03", "name": "lappet"}, - {"id": 4113, "synset": "lappet_caterpillar.n.01", "name": "lappet_caterpillar"}, - {"id": 4114, "synset": "webworm.n.01", "name": "webworm"}, - {"id": 4115, "synset": "webworm_moth.n.01", "name": "webworm_moth"}, - {"id": 4116, "synset": "hyphantria_cunea.n.02", "name": "Hyphantria_cunea"}, - {"id": 4117, "synset": "fall_webworm.n.01", "name": "fall_webworm"}, - {"id": 4118, "synset": "garden_webworm.n.01", "name": "garden_webworm"}, - {"id": 4119, "synset": "instar.n.01", "name": "instar"}, - {"id": 4120, "synset": "caterpillar.n.01", "name": "caterpillar"}, - {"id": 4121, "synset": "corn_borer.n.01", "name": "corn_borer"}, - {"id": 4122, "synset": "bollworm.n.01", "name": "bollworm"}, - {"id": 4123, "synset": "pink_bollworm.n.01", "name": "pink_bollworm"}, - {"id": 4124, "synset": "corn_earworm.n.01", "name": "corn_earworm"}, - {"id": 4125, "synset": "cabbageworm.n.01", "name": "cabbageworm"}, - {"id": 4126, "synset": "woolly_bear.n.01", "name": "woolly_bear"}, - {"id": 4127, "synset": "woolly_bear_moth.n.01", "name": "woolly_bear_moth"}, - {"id": 4128, "synset": "larva.n.01", "name": "larva"}, - {"id": 4129, "synset": "nymph.n.02", "name": "nymph"}, - {"id": 4130, "synset": "leptocephalus.n.01", "name": "leptocephalus"}, - {"id": 4131, "synset": "grub.n.02", "name": "grub"}, - {"id": 4132, "synset": "maggot.n.01", "name": "maggot"}, - {"id": 4133, "synset": "leatherjacket.n.03", "name": "leatherjacket"}, - {"id": 4134, "synset": "pupa.n.01", "name": "pupa"}, - {"id": 4135, "synset": "chrysalis.n.01", "name": "chrysalis"}, - {"id": 4136, "synset": "imago.n.02", "name": "imago"}, - {"id": 4137, "synset": "queen.n.01", "name": "queen"}, - {"id": 4138, "synset": "phoronid.n.01", "name": "phoronid"}, - {"id": 4139, "synset": "bryozoan.n.01", "name": "bryozoan"}, - {"id": 4140, "synset": "brachiopod.n.01", "name": "brachiopod"}, - {"id": 4141, "synset": "peanut_worm.n.01", "name": "peanut_worm"}, - {"id": 4142, "synset": "echinoderm.n.01", "name": "echinoderm"}, - {"id": 4143, "synset": "brittle_star.n.01", "name": "brittle_star"}, - {"id": 4144, "synset": "basket_star.n.01", "name": "basket_star"}, - {"id": 4145, "synset": "astrophyton_muricatum.n.01", "name": "Astrophyton_muricatum"}, - {"id": 4146, "synset": "sea_urchin.n.01", "name": "sea_urchin"}, - {"id": 4147, "synset": "edible_sea_urchin.n.01", "name": "edible_sea_urchin"}, - {"id": 4148, "synset": "sand_dollar.n.01", "name": "sand_dollar"}, - {"id": 4149, "synset": "heart_urchin.n.01", "name": "heart_urchin"}, - {"id": 4150, "synset": "crinoid.n.01", "name": "crinoid"}, - {"id": 4151, "synset": "sea_lily.n.01", "name": "sea_lily"}, - {"id": 4152, "synset": "feather_star.n.01", "name": "feather_star"}, - {"id": 4153, "synset": "sea_cucumber.n.01", "name": "sea_cucumber"}, - {"id": 4154, "synset": "trepang.n.01", "name": "trepang"}, - {"id": 4155, "synset": "duplicidentata.n.01", "name": "Duplicidentata"}, - {"id": 4156, "synset": "lagomorph.n.01", "name": "lagomorph"}, - {"id": 4157, "synset": "leporid.n.01", "name": "leporid"}, - {"id": 4158, "synset": "rabbit_ears.n.02", "name": "rabbit_ears"}, - {"id": 4159, "synset": "lapin.n.02", "name": "lapin"}, - {"id": 4160, "synset": "bunny.n.02", "name": "bunny"}, - {"id": 4161, "synset": "european_rabbit.n.01", "name": "European_rabbit"}, - {"id": 4162, "synset": "wood_rabbit.n.01", "name": "wood_rabbit"}, - {"id": 4163, "synset": "eastern_cottontail.n.01", "name": "eastern_cottontail"}, - {"id": 4164, "synset": "swamp_rabbit.n.02", "name": "swamp_rabbit"}, - {"id": 4165, "synset": "marsh_hare.n.01", "name": "marsh_hare"}, - {"id": 4166, "synset": "hare.n.01", "name": "hare"}, - {"id": 4167, "synset": "leveret.n.01", "name": "leveret"}, - {"id": 4168, "synset": "european_hare.n.01", "name": "European_hare"}, - {"id": 4169, "synset": "jackrabbit.n.01", "name": "jackrabbit"}, - {"id": 4170, "synset": "white-tailed_jackrabbit.n.01", "name": "white-tailed_jackrabbit"}, - {"id": 4171, "synset": "blacktail_jackrabbit.n.01", "name": "blacktail_jackrabbit"}, - {"id": 4172, "synset": "polar_hare.n.01", "name": "polar_hare"}, - {"id": 4173, "synset": "snowshoe_hare.n.01", "name": "snowshoe_hare"}, - {"id": 4174, "synset": "belgian_hare.n.01", "name": "Belgian_hare"}, - {"id": 4175, "synset": "angora.n.03", "name": "Angora"}, - {"id": 4176, "synset": "pika.n.01", "name": "pika"}, - {"id": 4177, "synset": "little_chief_hare.n.01", "name": "little_chief_hare"}, - {"id": 4178, "synset": "collared_pika.n.01", "name": "collared_pika"}, - {"id": 4179, "synset": "mouse.n.01", "name": "mouse"}, - {"id": 4180, "synset": "pocket_rat.n.01", "name": "pocket_rat"}, - {"id": 4181, "synset": "murine.n.01", "name": "murine"}, - {"id": 4182, "synset": "house_mouse.n.01", "name": "house_mouse"}, - {"id": 4183, "synset": "harvest_mouse.n.02", "name": "harvest_mouse"}, - {"id": 4184, "synset": "field_mouse.n.02", "name": "field_mouse"}, - {"id": 4185, "synset": "nude_mouse.n.01", "name": "nude_mouse"}, - {"id": 4186, "synset": "european_wood_mouse.n.01", "name": "European_wood_mouse"}, - {"id": 4187, "synset": "brown_rat.n.01", "name": "brown_rat"}, - {"id": 4188, "synset": "wharf_rat.n.02", "name": "wharf_rat"}, - {"id": 4189, "synset": "sewer_rat.n.01", "name": "sewer_rat"}, - {"id": 4190, "synset": "black_rat.n.01", "name": "black_rat"}, - {"id": 4191, "synset": "bandicoot_rat.n.01", "name": "bandicoot_rat"}, - {"id": 4192, "synset": "jerboa_rat.n.01", "name": "jerboa_rat"}, - {"id": 4193, "synset": "kangaroo_mouse.n.02", "name": "kangaroo_mouse"}, - {"id": 4194, "synset": "water_rat.n.03", "name": "water_rat"}, - {"id": 4195, "synset": "beaver_rat.n.01", "name": "beaver_rat"}, - {"id": 4196, "synset": "new_world_mouse.n.01", "name": "New_World_mouse"}, - {"id": 4197, "synset": "american_harvest_mouse.n.01", "name": "American_harvest_mouse"}, - {"id": 4198, "synset": "wood_mouse.n.01", "name": "wood_mouse"}, - {"id": 4199, "synset": "white-footed_mouse.n.01", "name": "white-footed_mouse"}, - {"id": 4200, "synset": "deer_mouse.n.01", "name": "deer_mouse"}, - {"id": 4201, "synset": "cactus_mouse.n.01", "name": "cactus_mouse"}, - {"id": 4202, "synset": "cotton_mouse.n.01", "name": "cotton_mouse"}, - {"id": 4203, "synset": "pygmy_mouse.n.01", "name": "pygmy_mouse"}, - {"id": 4204, "synset": "grasshopper_mouse.n.01", "name": "grasshopper_mouse"}, - {"id": 4205, "synset": "muskrat.n.02", "name": "muskrat"}, - {"id": 4206, "synset": "round-tailed_muskrat.n.01", "name": "round-tailed_muskrat"}, - {"id": 4207, "synset": "cotton_rat.n.01", "name": "cotton_rat"}, - {"id": 4208, "synset": "wood_rat.n.01", "name": "wood_rat"}, - {"id": 4209, "synset": "dusky-footed_wood_rat.n.01", "name": "dusky-footed_wood_rat"}, - {"id": 4210, "synset": "vole.n.01", "name": "vole"}, - {"id": 4211, "synset": "packrat.n.02", "name": "packrat"}, - {"id": 4212, "synset": "dusky-footed_woodrat.n.01", "name": "dusky-footed_woodrat"}, - {"id": 4213, "synset": "eastern_woodrat.n.01", "name": "eastern_woodrat"}, - {"id": 4214, "synset": "rice_rat.n.01", "name": "rice_rat"}, - {"id": 4215, "synset": "pine_vole.n.01", "name": "pine_vole"}, - {"id": 4216, "synset": "meadow_vole.n.01", "name": "meadow_vole"}, - {"id": 4217, "synset": "water_vole.n.02", "name": "water_vole"}, - {"id": 4218, "synset": "prairie_vole.n.01", "name": "prairie_vole"}, - {"id": 4219, "synset": "water_vole.n.01", "name": "water_vole"}, - {"id": 4220, "synset": "red-backed_mouse.n.01", "name": "red-backed_mouse"}, - {"id": 4221, "synset": "phenacomys.n.01", "name": "phenacomys"}, - {"id": 4222, "synset": "eurasian_hamster.n.01", "name": "Eurasian_hamster"}, - {"id": 4223, "synset": "golden_hamster.n.01", "name": "golden_hamster"}, - {"id": 4224, "synset": "gerbil.n.01", "name": "gerbil"}, - {"id": 4225, "synset": "jird.n.01", "name": "jird"}, - {"id": 4226, "synset": "tamarisk_gerbil.n.01", "name": "tamarisk_gerbil"}, - {"id": 4227, "synset": "sand_rat.n.02", "name": "sand_rat"}, - {"id": 4228, "synset": "lemming.n.01", "name": "lemming"}, - {"id": 4229, "synset": "european_lemming.n.01", "name": "European_lemming"}, - {"id": 4230, "synset": "brown_lemming.n.01", "name": "brown_lemming"}, - {"id": 4231, "synset": "grey_lemming.n.01", "name": "grey_lemming"}, - {"id": 4232, "synset": "pied_lemming.n.01", "name": "pied_lemming"}, - { - "id": 4233, - "synset": "hudson_bay_collared_lemming.n.01", - "name": "Hudson_bay_collared_lemming", - }, - {"id": 4234, "synset": "southern_bog_lemming.n.01", "name": "southern_bog_lemming"}, - {"id": 4235, "synset": "northern_bog_lemming.n.01", "name": "northern_bog_lemming"}, - {"id": 4236, "synset": "porcupine.n.01", "name": "porcupine"}, - {"id": 4237, "synset": "old_world_porcupine.n.01", "name": "Old_World_porcupine"}, - {"id": 4238, "synset": "brush-tailed_porcupine.n.01", "name": "brush-tailed_porcupine"}, - {"id": 4239, "synset": "long-tailed_porcupine.n.01", "name": "long-tailed_porcupine"}, - {"id": 4240, "synset": "new_world_porcupine.n.01", "name": "New_World_porcupine"}, - {"id": 4241, "synset": "canada_porcupine.n.01", "name": "Canada_porcupine"}, - {"id": 4242, "synset": "pocket_mouse.n.01", "name": "pocket_mouse"}, - {"id": 4243, "synset": "silky_pocket_mouse.n.01", "name": "silky_pocket_mouse"}, - {"id": 4244, "synset": "plains_pocket_mouse.n.01", "name": "plains_pocket_mouse"}, - {"id": 4245, "synset": "hispid_pocket_mouse.n.01", "name": "hispid_pocket_mouse"}, - {"id": 4246, "synset": "mexican_pocket_mouse.n.01", "name": "Mexican_pocket_mouse"}, - {"id": 4247, "synset": "kangaroo_rat.n.01", "name": "kangaroo_rat"}, - {"id": 4248, "synset": "ord_kangaroo_rat.n.01", "name": "Ord_kangaroo_rat"}, - {"id": 4249, "synset": "kangaroo_mouse.n.01", "name": "kangaroo_mouse"}, - {"id": 4250, "synset": "jumping_mouse.n.01", "name": "jumping_mouse"}, - {"id": 4251, "synset": "meadow_jumping_mouse.n.01", "name": "meadow_jumping_mouse"}, - {"id": 4252, "synset": "jerboa.n.01", "name": "jerboa"}, - {"id": 4253, "synset": "typical_jerboa.n.01", "name": "typical_jerboa"}, - {"id": 4254, "synset": "jaculus_jaculus.n.01", "name": "Jaculus_jaculus"}, - {"id": 4255, "synset": "dormouse.n.01", "name": "dormouse"}, - {"id": 4256, "synset": "loir.n.01", "name": "loir"}, - {"id": 4257, "synset": "hazel_mouse.n.01", "name": "hazel_mouse"}, - {"id": 4258, "synset": "lerot.n.01", "name": "lerot"}, - {"id": 4259, "synset": "gopher.n.04", "name": "gopher"}, - {"id": 4260, "synset": "plains_pocket_gopher.n.01", "name": "plains_pocket_gopher"}, - {"id": 4261, "synset": "southeastern_pocket_gopher.n.01", "name": "southeastern_pocket_gopher"}, - {"id": 4262, "synset": "valley_pocket_gopher.n.01", "name": "valley_pocket_gopher"}, - {"id": 4263, "synset": "northern_pocket_gopher.n.01", "name": "northern_pocket_gopher"}, - {"id": 4264, "synset": "tree_squirrel.n.01", "name": "tree_squirrel"}, - {"id": 4265, "synset": "eastern_grey_squirrel.n.01", "name": "eastern_grey_squirrel"}, - {"id": 4266, "synset": "western_grey_squirrel.n.01", "name": "western_grey_squirrel"}, - {"id": 4267, "synset": "fox_squirrel.n.01", "name": "fox_squirrel"}, - {"id": 4268, "synset": "black_squirrel.n.01", "name": "black_squirrel"}, - {"id": 4269, "synset": "red_squirrel.n.02", "name": "red_squirrel"}, - {"id": 4270, "synset": "american_red_squirrel.n.01", "name": "American_red_squirrel"}, - {"id": 4271, "synset": "chickeree.n.01", "name": "chickeree"}, - {"id": 4272, "synset": "antelope_squirrel.n.01", "name": "antelope_squirrel"}, - {"id": 4273, "synset": "ground_squirrel.n.02", "name": "ground_squirrel"}, - {"id": 4274, "synset": "mantled_ground_squirrel.n.01", "name": "mantled_ground_squirrel"}, - {"id": 4275, "synset": "suslik.n.01", "name": "suslik"}, - {"id": 4276, "synset": "flickertail.n.01", "name": "flickertail"}, - {"id": 4277, "synset": "rock_squirrel.n.01", "name": "rock_squirrel"}, - {"id": 4278, "synset": "arctic_ground_squirrel.n.01", "name": "Arctic_ground_squirrel"}, - {"id": 4279, "synset": "prairie_dog.n.01", "name": "prairie_dog"}, - {"id": 4280, "synset": "blacktail_prairie_dog.n.01", "name": "blacktail_prairie_dog"}, - {"id": 4281, "synset": "whitetail_prairie_dog.n.01", "name": "whitetail_prairie_dog"}, - {"id": 4282, "synset": "eastern_chipmunk.n.01", "name": "eastern_chipmunk"}, - {"id": 4283, "synset": "chipmunk.n.01", "name": "chipmunk"}, - {"id": 4284, "synset": "baronduki.n.01", "name": "baronduki"}, - {"id": 4285, "synset": "american_flying_squirrel.n.01", "name": "American_flying_squirrel"}, - {"id": 4286, "synset": "southern_flying_squirrel.n.01", "name": "southern_flying_squirrel"}, - {"id": 4287, "synset": "northern_flying_squirrel.n.01", "name": "northern_flying_squirrel"}, - {"id": 4288, "synset": "marmot.n.01", "name": "marmot"}, - {"id": 4289, "synset": "groundhog.n.01", "name": "groundhog"}, - {"id": 4290, "synset": "hoary_marmot.n.01", "name": "hoary_marmot"}, - {"id": 4291, "synset": "yellowbelly_marmot.n.01", "name": "yellowbelly_marmot"}, - {"id": 4292, "synset": "asiatic_flying_squirrel.n.01", "name": "Asiatic_flying_squirrel"}, - {"id": 4293, "synset": "beaver.n.07", "name": "beaver"}, - {"id": 4294, "synset": "old_world_beaver.n.01", "name": "Old_World_beaver"}, - {"id": 4295, "synset": "new_world_beaver.n.01", "name": "New_World_beaver"}, - {"id": 4296, "synset": "mountain_beaver.n.01", "name": "mountain_beaver"}, - {"id": 4297, "synset": "cavy.n.01", "name": "cavy"}, - {"id": 4298, "synset": "guinea_pig.n.02", "name": "guinea_pig"}, - {"id": 4299, "synset": "aperea.n.01", "name": "aperea"}, - {"id": 4300, "synset": "mara.n.02", "name": "mara"}, - {"id": 4301, "synset": "capybara.n.01", "name": "capybara"}, - {"id": 4302, "synset": "agouti.n.01", "name": "agouti"}, - {"id": 4303, "synset": "paca.n.01", "name": "paca"}, - {"id": 4304, "synset": "mountain_paca.n.01", "name": "mountain_paca"}, - {"id": 4305, "synset": "coypu.n.01", "name": "coypu"}, - {"id": 4306, "synset": "chinchilla.n.03", "name": "chinchilla"}, - {"id": 4307, "synset": "mountain_chinchilla.n.01", "name": "mountain_chinchilla"}, - {"id": 4308, "synset": "viscacha.n.01", "name": "viscacha"}, - {"id": 4309, "synset": "abrocome.n.01", "name": "abrocome"}, - {"id": 4310, "synset": "mole_rat.n.02", "name": "mole_rat"}, - {"id": 4311, "synset": "mole_rat.n.01", "name": "mole_rat"}, - {"id": 4312, "synset": "sand_rat.n.01", "name": "sand_rat"}, - {"id": 4313, "synset": "naked_mole_rat.n.01", "name": "naked_mole_rat"}, - {"id": 4314, "synset": "queen.n.09", "name": "queen"}, - {"id": 4315, "synset": "damaraland_mole_rat.n.01", "name": "Damaraland_mole_rat"}, - {"id": 4316, "synset": "ungulata.n.01", "name": "Ungulata"}, - {"id": 4317, "synset": "ungulate.n.01", "name": "ungulate"}, - {"id": 4318, "synset": "unguiculate.n.01", "name": "unguiculate"}, - {"id": 4319, "synset": "dinoceras.n.01", "name": "dinoceras"}, - {"id": 4320, "synset": "hyrax.n.01", "name": "hyrax"}, - {"id": 4321, "synset": "rock_hyrax.n.01", "name": "rock_hyrax"}, - {"id": 4322, "synset": "odd-toed_ungulate.n.01", "name": "odd-toed_ungulate"}, - {"id": 4323, "synset": "equine.n.01", "name": "equine"}, - {"id": 4324, "synset": "roan.n.02", "name": "roan"}, - {"id": 4325, "synset": "stablemate.n.01", "name": "stablemate"}, - {"id": 4326, "synset": "gee-gee.n.01", "name": "gee-gee"}, - {"id": 4327, "synset": "eohippus.n.01", "name": "eohippus"}, - {"id": 4328, "synset": "filly.n.01", "name": "filly"}, - {"id": 4329, "synset": "colt.n.01", "name": "colt"}, - {"id": 4330, "synset": "male_horse.n.01", "name": "male_horse"}, - {"id": 4331, "synset": "ridgeling.n.01", "name": "ridgeling"}, - {"id": 4332, "synset": "stallion.n.01", "name": "stallion"}, - {"id": 4333, "synset": "stud.n.04", "name": "stud"}, - {"id": 4334, "synset": "gelding.n.01", "name": "gelding"}, - {"id": 4335, "synset": "mare.n.01", "name": "mare"}, - {"id": 4336, "synset": "broodmare.n.01", "name": "broodmare"}, - {"id": 4337, "synset": "saddle_horse.n.01", "name": "saddle_horse"}, - {"id": 4338, "synset": "remount.n.01", "name": "remount"}, - {"id": 4339, "synset": "palfrey.n.01", "name": "palfrey"}, - {"id": 4340, "synset": "warhorse.n.03", "name": "warhorse"}, - {"id": 4341, "synset": "cavalry_horse.n.01", "name": "cavalry_horse"}, - {"id": 4342, "synset": "charger.n.01", "name": "charger"}, - {"id": 4343, "synset": "steed.n.01", "name": "steed"}, - {"id": 4344, "synset": "prancer.n.01", "name": "prancer"}, - {"id": 4345, "synset": "hack.n.08", "name": "hack"}, - {"id": 4346, "synset": "cow_pony.n.01", "name": "cow_pony"}, - {"id": 4347, "synset": "quarter_horse.n.01", "name": "quarter_horse"}, - {"id": 4348, "synset": "morgan.n.06", "name": "Morgan"}, - {"id": 4349, "synset": "tennessee_walker.n.01", "name": "Tennessee_walker"}, - {"id": 4350, "synset": "american_saddle_horse.n.01", "name": "American_saddle_horse"}, - {"id": 4351, "synset": "appaloosa.n.01", "name": "Appaloosa"}, - {"id": 4352, "synset": "arabian.n.02", "name": "Arabian"}, - {"id": 4353, "synset": "lippizan.n.01", "name": "Lippizan"}, - {"id": 4354, "synset": "pony.n.01", "name": "pony"}, - {"id": 4355, "synset": "polo_pony.n.01", "name": "polo_pony"}, - {"id": 4356, "synset": "mustang.n.01", "name": "mustang"}, - {"id": 4357, "synset": "bronco.n.01", "name": "bronco"}, - {"id": 4358, "synset": "bucking_bronco.n.01", "name": "bucking_bronco"}, - {"id": 4359, "synset": "buckskin.n.01", "name": "buckskin"}, - {"id": 4360, "synset": "crowbait.n.01", "name": "crowbait"}, - {"id": 4361, "synset": "dun.n.01", "name": "dun"}, - {"id": 4362, "synset": "grey.n.07", "name": "grey"}, - {"id": 4363, "synset": "wild_horse.n.01", "name": "wild_horse"}, - {"id": 4364, "synset": "tarpan.n.01", "name": "tarpan"}, - {"id": 4365, "synset": "przewalski's_horse.n.01", "name": "Przewalski's_horse"}, - {"id": 4366, "synset": "cayuse.n.01", "name": "cayuse"}, - {"id": 4367, "synset": "hack.n.07", "name": "hack"}, - {"id": 4368, "synset": "hack.n.06", "name": "hack"}, - {"id": 4369, "synset": "plow_horse.n.01", "name": "plow_horse"}, - {"id": 4370, "synset": "shetland_pony.n.01", "name": "Shetland_pony"}, - {"id": 4371, "synset": "welsh_pony.n.01", "name": "Welsh_pony"}, - {"id": 4372, "synset": "exmoor.n.02", "name": "Exmoor"}, - {"id": 4373, "synset": "racehorse.n.01", "name": "racehorse"}, - {"id": 4374, "synset": "thoroughbred.n.02", "name": "thoroughbred"}, - {"id": 4375, "synset": "steeplechaser.n.01", "name": "steeplechaser"}, - {"id": 4376, "synset": "racer.n.03", "name": "racer"}, - {"id": 4377, "synset": "finisher.n.06", "name": "finisher"}, - {"id": 4378, "synset": "pony.n.02", "name": "pony"}, - {"id": 4379, "synset": "yearling.n.02", "name": "yearling"}, - {"id": 4380, "synset": "dark_horse.n.02", "name": "dark_horse"}, - {"id": 4381, "synset": "mudder.n.01", "name": "mudder"}, - {"id": 4382, "synset": "nonstarter.n.02", "name": "nonstarter"}, - {"id": 4383, "synset": "stalking-horse.n.04", "name": "stalking-horse"}, - {"id": 4384, "synset": "harness_horse.n.01", "name": "harness_horse"}, - {"id": 4385, "synset": "cob.n.02", "name": "cob"}, - {"id": 4386, "synset": "hackney.n.02", "name": "hackney"}, - {"id": 4387, "synset": "workhorse.n.02", "name": "workhorse"}, - {"id": 4388, "synset": "draft_horse.n.01", "name": "draft_horse"}, - {"id": 4389, "synset": "packhorse.n.01", "name": "packhorse"}, - {"id": 4390, "synset": "carthorse.n.01", "name": "carthorse"}, - {"id": 4391, "synset": "clydesdale.n.01", "name": "Clydesdale"}, - {"id": 4392, "synset": "percheron.n.01", "name": "Percheron"}, - {"id": 4393, "synset": "farm_horse.n.01", "name": "farm_horse"}, - {"id": 4394, "synset": "shire.n.02", "name": "shire"}, - {"id": 4395, "synset": "pole_horse.n.02", "name": "pole_horse"}, - {"id": 4396, "synset": "post_horse.n.01", "name": "post_horse"}, - {"id": 4397, "synset": "coach_horse.n.01", "name": "coach_horse"}, - {"id": 4398, "synset": "pacer.n.02", "name": "pacer"}, - {"id": 4399, "synset": "pacer.n.01", "name": "pacer"}, - {"id": 4400, "synset": "trotting_horse.n.01", "name": "trotting_horse"}, - {"id": 4401, "synset": "pole_horse.n.01", "name": "pole_horse"}, - {"id": 4402, "synset": "stepper.n.03", "name": "stepper"}, - {"id": 4403, "synset": "chestnut.n.06", "name": "chestnut"}, - {"id": 4404, "synset": "liver_chestnut.n.01", "name": "liver_chestnut"}, - {"id": 4405, "synset": "bay.n.07", "name": "bay"}, - {"id": 4406, "synset": "sorrel.n.05", "name": "sorrel"}, - {"id": 4407, "synset": "palomino.n.01", "name": "palomino"}, - {"id": 4408, "synset": "pinto.n.01", "name": "pinto"}, - {"id": 4409, "synset": "ass.n.03", "name": "ass"}, - {"id": 4410, "synset": "burro.n.01", "name": "burro"}, - {"id": 4411, "synset": "moke.n.01", "name": "moke"}, - {"id": 4412, "synset": "jack.n.12", "name": "jack"}, - {"id": 4413, "synset": "jennet.n.01", "name": "jennet"}, - {"id": 4414, "synset": "mule.n.01", "name": "mule"}, - {"id": 4415, "synset": "hinny.n.01", "name": "hinny"}, - {"id": 4416, "synset": "wild_ass.n.01", "name": "wild_ass"}, - {"id": 4417, "synset": "african_wild_ass.n.01", "name": "African_wild_ass"}, - {"id": 4418, "synset": "kiang.n.01", "name": "kiang"}, - {"id": 4419, "synset": "onager.n.02", "name": "onager"}, - {"id": 4420, "synset": "chigetai.n.01", "name": "chigetai"}, - {"id": 4421, "synset": "common_zebra.n.01", "name": "common_zebra"}, - {"id": 4422, "synset": "mountain_zebra.n.01", "name": "mountain_zebra"}, - {"id": 4423, "synset": "grevy's_zebra.n.01", "name": "grevy's_zebra"}, - {"id": 4424, "synset": "quagga.n.01", "name": "quagga"}, - {"id": 4425, "synset": "indian_rhinoceros.n.01", "name": "Indian_rhinoceros"}, - {"id": 4426, "synset": "woolly_rhinoceros.n.01", "name": "woolly_rhinoceros"}, - {"id": 4427, "synset": "white_rhinoceros.n.01", "name": "white_rhinoceros"}, - {"id": 4428, "synset": "black_rhinoceros.n.01", "name": "black_rhinoceros"}, - {"id": 4429, "synset": "tapir.n.01", "name": "tapir"}, - {"id": 4430, "synset": "new_world_tapir.n.01", "name": "New_World_tapir"}, - {"id": 4431, "synset": "malayan_tapir.n.01", "name": "Malayan_tapir"}, - {"id": 4432, "synset": "even-toed_ungulate.n.01", "name": "even-toed_ungulate"}, - {"id": 4433, "synset": "swine.n.01", "name": "swine"}, - {"id": 4434, "synset": "piglet.n.01", "name": "piglet"}, - {"id": 4435, "synset": "sucking_pig.n.01", "name": "sucking_pig"}, - {"id": 4436, "synset": "porker.n.01", "name": "porker"}, - {"id": 4437, "synset": "boar.n.02", "name": "boar"}, - {"id": 4438, "synset": "sow.n.01", "name": "sow"}, - {"id": 4439, "synset": "razorback.n.01", "name": "razorback"}, - {"id": 4440, "synset": "wild_boar.n.01", "name": "wild_boar"}, - {"id": 4441, "synset": "babirusa.n.01", "name": "babirusa"}, - {"id": 4442, "synset": "warthog.n.01", "name": "warthog"}, - {"id": 4443, "synset": "peccary.n.01", "name": "peccary"}, - {"id": 4444, "synset": "collared_peccary.n.01", "name": "collared_peccary"}, - {"id": 4445, "synset": "white-lipped_peccary.n.01", "name": "white-lipped_peccary"}, - {"id": 4446, "synset": "ruminant.n.01", "name": "ruminant"}, - {"id": 4447, "synset": "bovid.n.01", "name": "bovid"}, - {"id": 4448, "synset": "bovine.n.01", "name": "bovine"}, - {"id": 4449, "synset": "ox.n.02", "name": "ox"}, - {"id": 4450, "synset": "cattle.n.01", "name": "cattle"}, - {"id": 4451, "synset": "ox.n.01", "name": "ox"}, - {"id": 4452, "synset": "stirk.n.01", "name": "stirk"}, - {"id": 4453, "synset": "bullock.n.02", "name": "bullock"}, - {"id": 4454, "synset": "bull.n.01", "name": "bull"}, - {"id": 4455, "synset": "cow.n.01", "name": "cow"}, - {"id": 4456, "synset": "heifer.n.01", "name": "heifer"}, - {"id": 4457, "synset": "bullock.n.01", "name": "bullock"}, - {"id": 4458, "synset": "dogie.n.01", "name": "dogie"}, - {"id": 4459, "synset": "maverick.n.02", "name": "maverick"}, - {"id": 4460, "synset": "longhorn.n.01", "name": "longhorn"}, - {"id": 4461, "synset": "brahman.n.04", "name": "Brahman"}, - {"id": 4462, "synset": "zebu.n.01", "name": "zebu"}, - {"id": 4463, "synset": "aurochs.n.02", "name": "aurochs"}, - {"id": 4464, "synset": "yak.n.02", "name": "yak"}, - {"id": 4465, "synset": "banteng.n.01", "name": "banteng"}, - {"id": 4466, "synset": "welsh.n.03", "name": "Welsh"}, - {"id": 4467, "synset": "red_poll.n.01", "name": "red_poll"}, - {"id": 4468, "synset": "santa_gertrudis.n.01", "name": "Santa_Gertrudis"}, - {"id": 4469, "synset": "aberdeen_angus.n.01", "name": "Aberdeen_Angus"}, - {"id": 4470, "synset": "africander.n.01", "name": "Africander"}, - {"id": 4471, "synset": "dairy_cattle.n.01", "name": "dairy_cattle"}, - {"id": 4472, "synset": "ayrshire.n.01", "name": "Ayrshire"}, - {"id": 4473, "synset": "brown_swiss.n.01", "name": "Brown_Swiss"}, - {"id": 4474, "synset": "charolais.n.01", "name": "Charolais"}, - {"id": 4475, "synset": "jersey.n.05", "name": "Jersey"}, - {"id": 4476, "synset": "devon.n.02", "name": "Devon"}, - {"id": 4477, "synset": "grade.n.09", "name": "grade"}, - {"id": 4478, "synset": "durham.n.02", "name": "Durham"}, - {"id": 4479, "synset": "milking_shorthorn.n.01", "name": "milking_shorthorn"}, - {"id": 4480, "synset": "galloway.n.02", "name": "Galloway"}, - {"id": 4481, "synset": "friesian.n.01", "name": "Friesian"}, - {"id": 4482, "synset": "guernsey.n.02", "name": "Guernsey"}, - {"id": 4483, "synset": "hereford.n.01", "name": "Hereford"}, - {"id": 4484, "synset": "cattalo.n.01", "name": "cattalo"}, - {"id": 4485, "synset": "old_world_buffalo.n.01", "name": "Old_World_buffalo"}, - {"id": 4486, "synset": "water_buffalo.n.01", "name": "water_buffalo"}, - {"id": 4487, "synset": "indian_buffalo.n.01", "name": "Indian_buffalo"}, - {"id": 4488, "synset": "carabao.n.01", "name": "carabao"}, - {"id": 4489, "synset": "anoa.n.01", "name": "anoa"}, - {"id": 4490, "synset": "tamarau.n.01", "name": "tamarau"}, - {"id": 4491, "synset": "cape_buffalo.n.01", "name": "Cape_buffalo"}, - {"id": 4492, "synset": "asian_wild_ox.n.01", "name": "Asian_wild_ox"}, - {"id": 4493, "synset": "gaur.n.01", "name": "gaur"}, - {"id": 4494, "synset": "gayal.n.01", "name": "gayal"}, - {"id": 4495, "synset": "bison.n.01", "name": "bison"}, - {"id": 4496, "synset": "american_bison.n.01", "name": "American_bison"}, - {"id": 4497, "synset": "wisent.n.01", "name": "wisent"}, - {"id": 4498, "synset": "musk_ox.n.01", "name": "musk_ox"}, - {"id": 4499, "synset": "ewe.n.03", "name": "ewe"}, - {"id": 4500, "synset": "wether.n.01", "name": "wether"}, - {"id": 4501, "synset": "lambkin.n.01", "name": "lambkin"}, - {"id": 4502, "synset": "baa-lamb.n.01", "name": "baa-lamb"}, - {"id": 4503, "synset": "hog.n.02", "name": "hog"}, - {"id": 4504, "synset": "teg.n.01", "name": "teg"}, - {"id": 4505, "synset": "persian_lamb.n.02", "name": "Persian_lamb"}, - {"id": 4506, "synset": "domestic_sheep.n.01", "name": "domestic_sheep"}, - {"id": 4507, "synset": "cotswold.n.01", "name": "Cotswold"}, - {"id": 4508, "synset": "hampshire.n.02", "name": "Hampshire"}, - {"id": 4509, "synset": "lincoln.n.03", "name": "Lincoln"}, - {"id": 4510, "synset": "exmoor.n.01", "name": "Exmoor"}, - {"id": 4511, "synset": "cheviot.n.01", "name": "Cheviot"}, - {"id": 4512, "synset": "broadtail.n.02", "name": "broadtail"}, - {"id": 4513, "synset": "longwool.n.01", "name": "longwool"}, - {"id": 4514, "synset": "merino.n.01", "name": "merino"}, - {"id": 4515, "synset": "rambouillet.n.01", "name": "Rambouillet"}, - {"id": 4516, "synset": "wild_sheep.n.01", "name": "wild_sheep"}, - {"id": 4517, "synset": "argali.n.01", "name": "argali"}, - {"id": 4518, "synset": "marco_polo_sheep.n.01", "name": "Marco_Polo_sheep"}, - {"id": 4519, "synset": "urial.n.01", "name": "urial"}, - {"id": 4520, "synset": "dall_sheep.n.01", "name": "Dall_sheep"}, - {"id": 4521, "synset": "mountain_sheep.n.01", "name": "mountain_sheep"}, - {"id": 4522, "synset": "bighorn.n.02", "name": "bighorn"}, - {"id": 4523, "synset": "mouflon.n.01", "name": "mouflon"}, - {"id": 4524, "synset": "aoudad.n.01", "name": "aoudad"}, - {"id": 4525, "synset": "kid.n.05", "name": "kid"}, - {"id": 4526, "synset": "billy.n.02", "name": "billy"}, - {"id": 4527, "synset": "nanny.n.02", "name": "nanny"}, - {"id": 4528, "synset": "domestic_goat.n.01", "name": "domestic_goat"}, - {"id": 4529, "synset": "cashmere_goat.n.01", "name": "Cashmere_goat"}, - {"id": 4530, "synset": "angora.n.02", "name": "Angora"}, - {"id": 4531, "synset": "wild_goat.n.01", "name": "wild_goat"}, - {"id": 4532, "synset": "bezoar_goat.n.01", "name": "bezoar_goat"}, - {"id": 4533, "synset": "markhor.n.01", "name": "markhor"}, - {"id": 4534, "synset": "ibex.n.01", "name": "ibex"}, - {"id": 4535, "synset": "goat_antelope.n.01", "name": "goat_antelope"}, - {"id": 4536, "synset": "mountain_goat.n.01", "name": "mountain_goat"}, - {"id": 4537, "synset": "goral.n.01", "name": "goral"}, - {"id": 4538, "synset": "serow.n.01", "name": "serow"}, - {"id": 4539, "synset": "chamois.n.02", "name": "chamois"}, - {"id": 4540, "synset": "takin.n.01", "name": "takin"}, - {"id": 4541, "synset": "antelope.n.01", "name": "antelope"}, - {"id": 4542, "synset": "blackbuck.n.01", "name": "blackbuck"}, - {"id": 4543, "synset": "gerenuk.n.01", "name": "gerenuk"}, - {"id": 4544, "synset": "addax.n.01", "name": "addax"}, - {"id": 4545, "synset": "gnu.n.01", "name": "gnu"}, - {"id": 4546, "synset": "dik-dik.n.01", "name": "dik-dik"}, - {"id": 4547, "synset": "hartebeest.n.01", "name": "hartebeest"}, - {"id": 4548, "synset": "sassaby.n.01", "name": "sassaby"}, - {"id": 4549, "synset": "impala.n.01", "name": "impala"}, - {"id": 4550, "synset": "thomson's_gazelle.n.01", "name": "Thomson's_gazelle"}, - {"id": 4551, "synset": "gazella_subgutturosa.n.01", "name": "Gazella_subgutturosa"}, - {"id": 4552, "synset": "springbok.n.01", "name": "springbok"}, - {"id": 4553, "synset": "bongo.n.02", "name": "bongo"}, - {"id": 4554, "synset": "kudu.n.01", "name": "kudu"}, - {"id": 4555, "synset": "greater_kudu.n.01", "name": "greater_kudu"}, - {"id": 4556, "synset": "lesser_kudu.n.01", "name": "lesser_kudu"}, - {"id": 4557, "synset": "harnessed_antelope.n.01", "name": "harnessed_antelope"}, - {"id": 4558, "synset": "nyala.n.02", "name": "nyala"}, - {"id": 4559, "synset": "mountain_nyala.n.01", "name": "mountain_nyala"}, - {"id": 4560, "synset": "bushbuck.n.01", "name": "bushbuck"}, - {"id": 4561, "synset": "nilgai.n.01", "name": "nilgai"}, - {"id": 4562, "synset": "sable_antelope.n.01", "name": "sable_antelope"}, - {"id": 4563, "synset": "saiga.n.01", "name": "saiga"}, - {"id": 4564, "synset": "steenbok.n.01", "name": "steenbok"}, - {"id": 4565, "synset": "eland.n.01", "name": "eland"}, - {"id": 4566, "synset": "common_eland.n.01", "name": "common_eland"}, - {"id": 4567, "synset": "giant_eland.n.01", "name": "giant_eland"}, - {"id": 4568, "synset": "kob.n.01", "name": "kob"}, - {"id": 4569, "synset": "lechwe.n.01", "name": "lechwe"}, - {"id": 4570, "synset": "waterbuck.n.01", "name": "waterbuck"}, - {"id": 4571, "synset": "puku.n.01", "name": "puku"}, - {"id": 4572, "synset": "oryx.n.01", "name": "oryx"}, - {"id": 4573, "synset": "gemsbok.n.01", "name": "gemsbok"}, - {"id": 4574, "synset": "forest_goat.n.01", "name": "forest_goat"}, - {"id": 4575, "synset": "pronghorn.n.01", "name": "pronghorn"}, - {"id": 4576, "synset": "stag.n.02", "name": "stag"}, - {"id": 4577, "synset": "royal.n.02", "name": "royal"}, - {"id": 4578, "synset": "pricket.n.02", "name": "pricket"}, - {"id": 4579, "synset": "fawn.n.02", "name": "fawn"}, - {"id": 4580, "synset": "red_deer.n.01", "name": "red_deer"}, - {"id": 4581, "synset": "hart.n.03", "name": "hart"}, - {"id": 4582, "synset": "hind.n.02", "name": "hind"}, - {"id": 4583, "synset": "brocket.n.02", "name": "brocket"}, - {"id": 4584, "synset": "sambar.n.01", "name": "sambar"}, - {"id": 4585, "synset": "wapiti.n.01", "name": "wapiti"}, - {"id": 4586, "synset": "japanese_deer.n.01", "name": "Japanese_deer"}, - {"id": 4587, "synset": "virginia_deer.n.01", "name": "Virginia_deer"}, - {"id": 4588, "synset": "mule_deer.n.01", "name": "mule_deer"}, - {"id": 4589, "synset": "black-tailed_deer.n.01", "name": "black-tailed_deer"}, - {"id": 4590, "synset": "fallow_deer.n.01", "name": "fallow_deer"}, - {"id": 4591, "synset": "roe_deer.n.01", "name": "roe_deer"}, - {"id": 4592, "synset": "roebuck.n.01", "name": "roebuck"}, - {"id": 4593, "synset": "caribou.n.01", "name": "caribou"}, - {"id": 4594, "synset": "woodland_caribou.n.01", "name": "woodland_caribou"}, - {"id": 4595, "synset": "barren_ground_caribou.n.01", "name": "barren_ground_caribou"}, - {"id": 4596, "synset": "brocket.n.01", "name": "brocket"}, - {"id": 4597, "synset": "muntjac.n.01", "name": "muntjac"}, - {"id": 4598, "synset": "musk_deer.n.01", "name": "musk_deer"}, - {"id": 4599, "synset": "pere_david's_deer.n.01", "name": "pere_david's_deer"}, - {"id": 4600, "synset": "chevrotain.n.01", "name": "chevrotain"}, - {"id": 4601, "synset": "kanchil.n.01", "name": "kanchil"}, - {"id": 4602, "synset": "napu.n.01", "name": "napu"}, - {"id": 4603, "synset": "water_chevrotain.n.01", "name": "water_chevrotain"}, - {"id": 4604, "synset": "arabian_camel.n.01", "name": "Arabian_camel"}, - {"id": 4605, "synset": "bactrian_camel.n.01", "name": "Bactrian_camel"}, - {"id": 4606, "synset": "llama.n.01", "name": "llama"}, - {"id": 4607, "synset": "domestic_llama.n.01", "name": "domestic_llama"}, - {"id": 4608, "synset": "guanaco.n.01", "name": "guanaco"}, - {"id": 4609, "synset": "alpaca.n.03", "name": "alpaca"}, - {"id": 4610, "synset": "vicuna.n.03", "name": "vicuna"}, - {"id": 4611, "synset": "okapi.n.01", "name": "okapi"}, - {"id": 4612, "synset": "musteline_mammal.n.01", "name": "musteline_mammal"}, - {"id": 4613, "synset": "weasel.n.02", "name": "weasel"}, - {"id": 4614, "synset": "ermine.n.02", "name": "ermine"}, - {"id": 4615, "synset": "stoat.n.01", "name": "stoat"}, - {"id": 4616, "synset": "new_world_least_weasel.n.01", "name": "New_World_least_weasel"}, - {"id": 4617, "synset": "old_world_least_weasel.n.01", "name": "Old_World_least_weasel"}, - {"id": 4618, "synset": "longtail_weasel.n.01", "name": "longtail_weasel"}, - {"id": 4619, "synset": "mink.n.03", "name": "mink"}, - {"id": 4620, "synset": "american_mink.n.01", "name": "American_mink"}, - {"id": 4621, "synset": "polecat.n.02", "name": "polecat"}, - {"id": 4622, "synset": "black-footed_ferret.n.01", "name": "black-footed_ferret"}, - {"id": 4623, "synset": "muishond.n.01", "name": "muishond"}, - {"id": 4624, "synset": "snake_muishond.n.01", "name": "snake_muishond"}, - {"id": 4625, "synset": "striped_muishond.n.01", "name": "striped_muishond"}, - {"id": 4626, "synset": "otter.n.02", "name": "otter"}, - {"id": 4627, "synset": "river_otter.n.01", "name": "river_otter"}, - {"id": 4628, "synset": "eurasian_otter.n.01", "name": "Eurasian_otter"}, - {"id": 4629, "synset": "sea_otter.n.01", "name": "sea_otter"}, - {"id": 4630, "synset": "skunk.n.04", "name": "skunk"}, - {"id": 4631, "synset": "striped_skunk.n.01", "name": "striped_skunk"}, - {"id": 4632, "synset": "hooded_skunk.n.01", "name": "hooded_skunk"}, - {"id": 4633, "synset": "hog-nosed_skunk.n.01", "name": "hog-nosed_skunk"}, - {"id": 4634, "synset": "spotted_skunk.n.01", "name": "spotted_skunk"}, - {"id": 4635, "synset": "badger.n.02", "name": "badger"}, - {"id": 4636, "synset": "american_badger.n.01", "name": "American_badger"}, - {"id": 4637, "synset": "eurasian_badger.n.01", "name": "Eurasian_badger"}, - {"id": 4638, "synset": "ratel.n.01", "name": "ratel"}, - {"id": 4639, "synset": "ferret_badger.n.01", "name": "ferret_badger"}, - {"id": 4640, "synset": "hog_badger.n.01", "name": "hog_badger"}, - {"id": 4641, "synset": "wolverine.n.03", "name": "wolverine"}, - {"id": 4642, "synset": "glutton.n.02", "name": "glutton"}, - {"id": 4643, "synset": "grison.n.01", "name": "grison"}, - {"id": 4644, "synset": "marten.n.01", "name": "marten"}, - {"id": 4645, "synset": "pine_marten.n.01", "name": "pine_marten"}, - {"id": 4646, "synset": "sable.n.05", "name": "sable"}, - {"id": 4647, "synset": "american_marten.n.01", "name": "American_marten"}, - {"id": 4648, "synset": "stone_marten.n.01", "name": "stone_marten"}, - {"id": 4649, "synset": "fisher.n.02", "name": "fisher"}, - {"id": 4650, "synset": "yellow-throated_marten.n.01", "name": "yellow-throated_marten"}, - {"id": 4651, "synset": "tayra.n.01", "name": "tayra"}, - {"id": 4652, "synset": "fictional_animal.n.01", "name": "fictional_animal"}, - {"id": 4653, "synset": "pachyderm.n.01", "name": "pachyderm"}, - {"id": 4654, "synset": "edentate.n.01", "name": "edentate"}, - {"id": 4655, "synset": "armadillo.n.01", "name": "armadillo"}, - {"id": 4656, "synset": "peba.n.01", "name": "peba"}, - {"id": 4657, "synset": "apar.n.01", "name": "apar"}, - {"id": 4658, "synset": "tatouay.n.01", "name": "tatouay"}, - {"id": 4659, "synset": "peludo.n.01", "name": "peludo"}, - {"id": 4660, "synset": "giant_armadillo.n.01", "name": "giant_armadillo"}, - {"id": 4661, "synset": "pichiciago.n.01", "name": "pichiciago"}, - {"id": 4662, "synset": "sloth.n.02", "name": "sloth"}, - {"id": 4663, "synset": "three-toed_sloth.n.01", "name": "three-toed_sloth"}, - {"id": 4664, "synset": "two-toed_sloth.n.02", "name": "two-toed_sloth"}, - {"id": 4665, "synset": "two-toed_sloth.n.01", "name": "two-toed_sloth"}, - {"id": 4666, "synset": "megatherian.n.01", "name": "megatherian"}, - {"id": 4667, "synset": "mylodontid.n.01", "name": "mylodontid"}, - {"id": 4668, "synset": "anteater.n.02", "name": "anteater"}, - {"id": 4669, "synset": "ant_bear.n.01", "name": "ant_bear"}, - {"id": 4670, "synset": "silky_anteater.n.01", "name": "silky_anteater"}, - {"id": 4671, "synset": "tamandua.n.01", "name": "tamandua"}, - {"id": 4672, "synset": "pangolin.n.01", "name": "pangolin"}, - {"id": 4673, "synset": "coronet.n.02", "name": "coronet"}, - {"id": 4674, "synset": "scapular.n.01", "name": "scapular"}, - {"id": 4675, "synset": "tadpole.n.01", "name": "tadpole"}, - {"id": 4676, "synset": "primate.n.02", "name": "primate"}, - {"id": 4677, "synset": "simian.n.01", "name": "simian"}, - {"id": 4678, "synset": "ape.n.01", "name": "ape"}, - {"id": 4679, "synset": "anthropoid.n.02", "name": "anthropoid"}, - {"id": 4680, "synset": "anthropoid_ape.n.01", "name": "anthropoid_ape"}, - {"id": 4681, "synset": "hominoid.n.01", "name": "hominoid"}, - {"id": 4682, "synset": "hominid.n.01", "name": "hominid"}, - {"id": 4683, "synset": "homo.n.02", "name": "homo"}, - {"id": 4684, "synset": "world.n.08", "name": "world"}, - {"id": 4685, "synset": "homo_erectus.n.01", "name": "Homo_erectus"}, - {"id": 4686, "synset": "pithecanthropus.n.01", "name": "Pithecanthropus"}, - {"id": 4687, "synset": "java_man.n.01", "name": "Java_man"}, - {"id": 4688, "synset": "peking_man.n.01", "name": "Peking_man"}, - {"id": 4689, "synset": "sinanthropus.n.01", "name": "Sinanthropus"}, - {"id": 4690, "synset": "homo_soloensis.n.01", "name": "Homo_soloensis"}, - {"id": 4691, "synset": "javanthropus.n.01", "name": "Javanthropus"}, - {"id": 4692, "synset": "homo_habilis.n.01", "name": "Homo_habilis"}, - {"id": 4693, "synset": "homo_sapiens.n.01", "name": "Homo_sapiens"}, - {"id": 4694, "synset": "neandertal_man.n.01", "name": "Neandertal_man"}, - {"id": 4695, "synset": "cro-magnon.n.01", "name": "Cro-magnon"}, - {"id": 4696, "synset": "homo_sapiens_sapiens.n.01", "name": "Homo_sapiens_sapiens"}, - {"id": 4697, "synset": "australopithecine.n.01", "name": "australopithecine"}, - {"id": 4698, "synset": "australopithecus_afarensis.n.01", "name": "Australopithecus_afarensis"}, - {"id": 4699, "synset": "australopithecus_africanus.n.01", "name": "Australopithecus_africanus"}, - {"id": 4700, "synset": "australopithecus_boisei.n.01", "name": "Australopithecus_boisei"}, - {"id": 4701, "synset": "zinjanthropus.n.01", "name": "Zinjanthropus"}, - {"id": 4702, "synset": "australopithecus_robustus.n.01", "name": "Australopithecus_robustus"}, - {"id": 4703, "synset": "paranthropus.n.01", "name": "Paranthropus"}, - {"id": 4704, "synset": "sivapithecus.n.01", "name": "Sivapithecus"}, - {"id": 4705, "synset": "rudapithecus.n.01", "name": "rudapithecus"}, - {"id": 4706, "synset": "proconsul.n.03", "name": "proconsul"}, - {"id": 4707, "synset": "aegyptopithecus.n.01", "name": "Aegyptopithecus"}, - {"id": 4708, "synset": "great_ape.n.01", "name": "great_ape"}, - {"id": 4709, "synset": "orangutan.n.01", "name": "orangutan"}, - {"id": 4710, "synset": "western_lowland_gorilla.n.01", "name": "western_lowland_gorilla"}, - {"id": 4711, "synset": "eastern_lowland_gorilla.n.01", "name": "eastern_lowland_gorilla"}, - {"id": 4712, "synset": "mountain_gorilla.n.01", "name": "mountain_gorilla"}, - {"id": 4713, "synset": "silverback.n.01", "name": "silverback"}, - {"id": 4714, "synset": "chimpanzee.n.01", "name": "chimpanzee"}, - {"id": 4715, "synset": "western_chimpanzee.n.01", "name": "western_chimpanzee"}, - {"id": 4716, "synset": "eastern_chimpanzee.n.01", "name": "eastern_chimpanzee"}, - {"id": 4717, "synset": "central_chimpanzee.n.01", "name": "central_chimpanzee"}, - {"id": 4718, "synset": "pygmy_chimpanzee.n.01", "name": "pygmy_chimpanzee"}, - {"id": 4719, "synset": "lesser_ape.n.01", "name": "lesser_ape"}, - {"id": 4720, "synset": "gibbon.n.02", "name": "gibbon"}, - {"id": 4721, "synset": "siamang.n.01", "name": "siamang"}, - {"id": 4722, "synset": "old_world_monkey.n.01", "name": "Old_World_monkey"}, - {"id": 4723, "synset": "guenon.n.01", "name": "guenon"}, - {"id": 4724, "synset": "talapoin.n.01", "name": "talapoin"}, - {"id": 4725, "synset": "grivet.n.01", "name": "grivet"}, - {"id": 4726, "synset": "vervet.n.01", "name": "vervet"}, - {"id": 4727, "synset": "green_monkey.n.01", "name": "green_monkey"}, - {"id": 4728, "synset": "mangabey.n.01", "name": "mangabey"}, - {"id": 4729, "synset": "patas.n.01", "name": "patas"}, - {"id": 4730, "synset": "chacma.n.01", "name": "chacma"}, - {"id": 4731, "synset": "mandrill.n.01", "name": "mandrill"}, - {"id": 4732, "synset": "drill.n.02", "name": "drill"}, - {"id": 4733, "synset": "macaque.n.01", "name": "macaque"}, - {"id": 4734, "synset": "rhesus.n.01", "name": "rhesus"}, - {"id": 4735, "synset": "bonnet_macaque.n.01", "name": "bonnet_macaque"}, - {"id": 4736, "synset": "barbary_ape.n.01", "name": "Barbary_ape"}, - {"id": 4737, "synset": "crab-eating_macaque.n.01", "name": "crab-eating_macaque"}, - {"id": 4738, "synset": "langur.n.01", "name": "langur"}, - {"id": 4739, "synset": "entellus.n.01", "name": "entellus"}, - {"id": 4740, "synset": "colobus.n.01", "name": "colobus"}, - {"id": 4741, "synset": "guereza.n.01", "name": "guereza"}, - {"id": 4742, "synset": "proboscis_monkey.n.01", "name": "proboscis_monkey"}, - {"id": 4743, "synset": "new_world_monkey.n.01", "name": "New_World_monkey"}, - {"id": 4744, "synset": "marmoset.n.01", "name": "marmoset"}, - {"id": 4745, "synset": "true_marmoset.n.01", "name": "true_marmoset"}, - {"id": 4746, "synset": "pygmy_marmoset.n.01", "name": "pygmy_marmoset"}, - {"id": 4747, "synset": "tamarin.n.01", "name": "tamarin"}, - {"id": 4748, "synset": "silky_tamarin.n.01", "name": "silky_tamarin"}, - {"id": 4749, "synset": "pinche.n.01", "name": "pinche"}, - {"id": 4750, "synset": "capuchin.n.02", "name": "capuchin"}, - {"id": 4751, "synset": "douroucouli.n.01", "name": "douroucouli"}, - {"id": 4752, "synset": "howler_monkey.n.01", "name": "howler_monkey"}, - {"id": 4753, "synset": "saki.n.03", "name": "saki"}, - {"id": 4754, "synset": "uakari.n.01", "name": "uakari"}, - {"id": 4755, "synset": "titi.n.03", "name": "titi"}, - {"id": 4756, "synset": "spider_monkey.n.01", "name": "spider_monkey"}, - {"id": 4757, "synset": "squirrel_monkey.n.01", "name": "squirrel_monkey"}, - {"id": 4758, "synset": "woolly_monkey.n.01", "name": "woolly_monkey"}, - {"id": 4759, "synset": "tree_shrew.n.01", "name": "tree_shrew"}, - {"id": 4760, "synset": "prosimian.n.01", "name": "prosimian"}, - {"id": 4761, "synset": "lemur.n.01", "name": "lemur"}, - {"id": 4762, "synset": "madagascar_cat.n.01", "name": "Madagascar_cat"}, - {"id": 4763, "synset": "aye-aye.n.01", "name": "aye-aye"}, - {"id": 4764, "synset": "slender_loris.n.01", "name": "slender_loris"}, - {"id": 4765, "synset": "slow_loris.n.01", "name": "slow_loris"}, - {"id": 4766, "synset": "potto.n.02", "name": "potto"}, - {"id": 4767, "synset": "angwantibo.n.01", "name": "angwantibo"}, - {"id": 4768, "synset": "galago.n.01", "name": "galago"}, - {"id": 4769, "synset": "indri.n.01", "name": "indri"}, - {"id": 4770, "synset": "woolly_indris.n.01", "name": "woolly_indris"}, - {"id": 4771, "synset": "tarsier.n.01", "name": "tarsier"}, - {"id": 4772, "synset": "tarsius_syrichta.n.01", "name": "Tarsius_syrichta"}, - {"id": 4773, "synset": "tarsius_glis.n.01", "name": "Tarsius_glis"}, - {"id": 4774, "synset": "flying_lemur.n.01", "name": "flying_lemur"}, - {"id": 4775, "synset": "cynocephalus_variegatus.n.01", "name": "Cynocephalus_variegatus"}, - {"id": 4776, "synset": "proboscidean.n.01", "name": "proboscidean"}, - {"id": 4777, "synset": "rogue_elephant.n.01", "name": "rogue_elephant"}, - {"id": 4778, "synset": "indian_elephant.n.01", "name": "Indian_elephant"}, - {"id": 4779, "synset": "african_elephant.n.01", "name": "African_elephant"}, - {"id": 4780, "synset": "woolly_mammoth.n.01", "name": "woolly_mammoth"}, - {"id": 4781, "synset": "columbian_mammoth.n.01", "name": "columbian_mammoth"}, - {"id": 4782, "synset": "imperial_mammoth.n.01", "name": "imperial_mammoth"}, - {"id": 4783, "synset": "mastodon.n.01", "name": "mastodon"}, - {"id": 4784, "synset": "plantigrade_mammal.n.01", "name": "plantigrade_mammal"}, - {"id": 4785, "synset": "digitigrade_mammal.n.01", "name": "digitigrade_mammal"}, - {"id": 4786, "synset": "procyonid.n.01", "name": "procyonid"}, - {"id": 4787, "synset": "raccoon.n.02", "name": "raccoon"}, - {"id": 4788, "synset": "common_raccoon.n.01", "name": "common_raccoon"}, - {"id": 4789, "synset": "crab-eating_raccoon.n.01", "name": "crab-eating_raccoon"}, - {"id": 4790, "synset": "bassarisk.n.01", "name": "bassarisk"}, - {"id": 4791, "synset": "kinkajou.n.01", "name": "kinkajou"}, - {"id": 4792, "synset": "coati.n.01", "name": "coati"}, - {"id": 4793, "synset": "lesser_panda.n.01", "name": "lesser_panda"}, - {"id": 4794, "synset": "twitterer.n.01", "name": "twitterer"}, - {"id": 4795, "synset": "fingerling.n.01", "name": "fingerling"}, - {"id": 4796, "synset": "game_fish.n.01", "name": "game_fish"}, - {"id": 4797, "synset": "food_fish.n.01", "name": "food_fish"}, - {"id": 4798, "synset": "rough_fish.n.01", "name": "rough_fish"}, - {"id": 4799, "synset": "groundfish.n.01", "name": "groundfish"}, - {"id": 4800, "synset": "young_fish.n.01", "name": "young_fish"}, - {"id": 4801, "synset": "parr.n.03", "name": "parr"}, - {"id": 4802, "synset": "mouthbreeder.n.01", "name": "mouthbreeder"}, - {"id": 4803, "synset": "spawner.n.01", "name": "spawner"}, - {"id": 4804, "synset": "barracouta.n.01", "name": "barracouta"}, - {"id": 4805, "synset": "crossopterygian.n.01", "name": "crossopterygian"}, - {"id": 4806, "synset": "coelacanth.n.01", "name": "coelacanth"}, - {"id": 4807, "synset": "lungfish.n.01", "name": "lungfish"}, - {"id": 4808, "synset": "ceratodus.n.01", "name": "ceratodus"}, - {"id": 4809, "synset": "catfish.n.03", "name": "catfish"}, - {"id": 4810, "synset": "silurid.n.01", "name": "silurid"}, - {"id": 4811, "synset": "european_catfish.n.01", "name": "European_catfish"}, - {"id": 4812, "synset": "electric_catfish.n.01", "name": "electric_catfish"}, - {"id": 4813, "synset": "bullhead.n.02", "name": "bullhead"}, - {"id": 4814, "synset": "horned_pout.n.01", "name": "horned_pout"}, - {"id": 4815, "synset": "brown_bullhead.n.01", "name": "brown_bullhead"}, - {"id": 4816, "synset": "channel_catfish.n.01", "name": "channel_catfish"}, - {"id": 4817, "synset": "blue_catfish.n.01", "name": "blue_catfish"}, - {"id": 4818, "synset": "flathead_catfish.n.01", "name": "flathead_catfish"}, - {"id": 4819, "synset": "armored_catfish.n.01", "name": "armored_catfish"}, - {"id": 4820, "synset": "sea_catfish.n.01", "name": "sea_catfish"}, - {"id": 4821, "synset": "gadoid.n.01", "name": "gadoid"}, - {"id": 4822, "synset": "cod.n.03", "name": "cod"}, - {"id": 4823, "synset": "codling.n.01", "name": "codling"}, - {"id": 4824, "synset": "atlantic_cod.n.01", "name": "Atlantic_cod"}, - {"id": 4825, "synset": "pacific_cod.n.01", "name": "Pacific_cod"}, - {"id": 4826, "synset": "whiting.n.06", "name": "whiting"}, - {"id": 4827, "synset": "burbot.n.01", "name": "burbot"}, - {"id": 4828, "synset": "haddock.n.02", "name": "haddock"}, - {"id": 4829, "synset": "pollack.n.03", "name": "pollack"}, - {"id": 4830, "synset": "hake.n.02", "name": "hake"}, - {"id": 4831, "synset": "silver_hake.n.01", "name": "silver_hake"}, - {"id": 4832, "synset": "ling.n.04", "name": "ling"}, - {"id": 4833, "synset": "cusk.n.02", "name": "cusk"}, - {"id": 4834, "synset": "grenadier.n.02", "name": "grenadier"}, - {"id": 4835, "synset": "eel.n.02", "name": "eel"}, - {"id": 4836, "synset": "elver.n.02", "name": "elver"}, - {"id": 4837, "synset": "common_eel.n.01", "name": "common_eel"}, - {"id": 4838, "synset": "tuna.n.04", "name": "tuna"}, - {"id": 4839, "synset": "moray.n.01", "name": "moray"}, - {"id": 4840, "synset": "conger.n.01", "name": "conger"}, - {"id": 4841, "synset": "teleost_fish.n.01", "name": "teleost_fish"}, - {"id": 4842, "synset": "beaked_salmon.n.01", "name": "beaked_salmon"}, - {"id": 4843, "synset": "clupeid_fish.n.01", "name": "clupeid_fish"}, - {"id": 4844, "synset": "whitebait.n.02", "name": "whitebait"}, - {"id": 4845, "synset": "brit.n.02", "name": "brit"}, - {"id": 4846, "synset": "shad.n.02", "name": "shad"}, - {"id": 4847, "synset": "common_american_shad.n.01", "name": "common_American_shad"}, - {"id": 4848, "synset": "river_shad.n.01", "name": "river_shad"}, - {"id": 4849, "synset": "allice_shad.n.01", "name": "allice_shad"}, - {"id": 4850, "synset": "alewife.n.02", "name": "alewife"}, - {"id": 4851, "synset": "menhaden.n.01", "name": "menhaden"}, - {"id": 4852, "synset": "herring.n.02", "name": "herring"}, - {"id": 4853, "synset": "atlantic_herring.n.01", "name": "Atlantic_herring"}, - {"id": 4854, "synset": "pacific_herring.n.01", "name": "Pacific_herring"}, - {"id": 4855, "synset": "sardine.n.02", "name": "sardine"}, - {"id": 4856, "synset": "sild.n.01", "name": "sild"}, - {"id": 4857, "synset": "brisling.n.02", "name": "brisling"}, - {"id": 4858, "synset": "pilchard.n.02", "name": "pilchard"}, - {"id": 4859, "synset": "pacific_sardine.n.01", "name": "Pacific_sardine"}, - {"id": 4860, "synset": "anchovy.n.02", "name": "anchovy"}, - {"id": 4861, "synset": "mediterranean_anchovy.n.01", "name": "mediterranean_anchovy"}, - {"id": 4862, "synset": "salmonid.n.01", "name": "salmonid"}, - {"id": 4863, "synset": "parr.n.02", "name": "parr"}, - {"id": 4864, "synset": "blackfish.n.02", "name": "blackfish"}, - {"id": 4865, "synset": "redfish.n.03", "name": "redfish"}, - {"id": 4866, "synset": "atlantic_salmon.n.02", "name": "Atlantic_salmon"}, - {"id": 4867, "synset": "landlocked_salmon.n.01", "name": "landlocked_salmon"}, - {"id": 4868, "synset": "sockeye.n.02", "name": "sockeye"}, - {"id": 4869, "synset": "chinook.n.05", "name": "chinook"}, - {"id": 4870, "synset": "coho.n.02", "name": "coho"}, - {"id": 4871, "synset": "trout.n.02", "name": "trout"}, - {"id": 4872, "synset": "brown_trout.n.01", "name": "brown_trout"}, - {"id": 4873, "synset": "rainbow_trout.n.02", "name": "rainbow_trout"}, - {"id": 4874, "synset": "sea_trout.n.03", "name": "sea_trout"}, - {"id": 4875, "synset": "lake_trout.n.02", "name": "lake_trout"}, - {"id": 4876, "synset": "brook_trout.n.02", "name": "brook_trout"}, - {"id": 4877, "synset": "char.n.03", "name": "char"}, - {"id": 4878, "synset": "arctic_char.n.01", "name": "Arctic_char"}, - {"id": 4879, "synset": "whitefish.n.03", "name": "whitefish"}, - {"id": 4880, "synset": "lake_whitefish.n.01", "name": "lake_whitefish"}, - {"id": 4881, "synset": "cisco.n.02", "name": "cisco"}, - {"id": 4882, "synset": "round_whitefish.n.01", "name": "round_whitefish"}, - {"id": 4883, "synset": "smelt.n.02", "name": "smelt"}, - {"id": 4884, "synset": "sparling.n.02", "name": "sparling"}, - {"id": 4885, "synset": "capelin.n.01", "name": "capelin"}, - {"id": 4886, "synset": "tarpon.n.01", "name": "tarpon"}, - {"id": 4887, "synset": "ladyfish.n.01", "name": "ladyfish"}, - {"id": 4888, "synset": "bonefish.n.01", "name": "bonefish"}, - {"id": 4889, "synset": "argentine.n.01", "name": "argentine"}, - {"id": 4890, "synset": "lanternfish.n.01", "name": "lanternfish"}, - {"id": 4891, "synset": "lizardfish.n.01", "name": "lizardfish"}, - {"id": 4892, "synset": "lancetfish.n.01", "name": "lancetfish"}, - {"id": 4893, "synset": "opah.n.01", "name": "opah"}, - {"id": 4894, "synset": "new_world_opah.n.01", "name": "New_World_opah"}, - {"id": 4895, "synset": "ribbonfish.n.02", "name": "ribbonfish"}, - {"id": 4896, "synset": "dealfish.n.01", "name": "dealfish"}, - {"id": 4897, "synset": "oarfish.n.01", "name": "oarfish"}, - {"id": 4898, "synset": "batfish.n.01", "name": "batfish"}, - {"id": 4899, "synset": "goosefish.n.01", "name": "goosefish"}, - {"id": 4900, "synset": "toadfish.n.01", "name": "toadfish"}, - {"id": 4901, "synset": "oyster_fish.n.01", "name": "oyster_fish"}, - {"id": 4902, "synset": "frogfish.n.01", "name": "frogfish"}, - {"id": 4903, "synset": "sargassum_fish.n.01", "name": "sargassum_fish"}, - {"id": 4904, "synset": "needlefish.n.01", "name": "needlefish"}, - {"id": 4905, "synset": "timucu.n.01", "name": "timucu"}, - {"id": 4906, "synset": "flying_fish.n.01", "name": "flying_fish"}, - {"id": 4907, "synset": "monoplane_flying_fish.n.01", "name": "monoplane_flying_fish"}, - {"id": 4908, "synset": "halfbeak.n.01", "name": "halfbeak"}, - {"id": 4909, "synset": "saury.n.01", "name": "saury"}, - {"id": 4910, "synset": "spiny-finned_fish.n.01", "name": "spiny-finned_fish"}, - {"id": 4911, "synset": "lingcod.n.02", "name": "lingcod"}, - {"id": 4912, "synset": "percoid_fish.n.01", "name": "percoid_fish"}, - {"id": 4913, "synset": "perch.n.07", "name": "perch"}, - {"id": 4914, "synset": "climbing_perch.n.01", "name": "climbing_perch"}, - {"id": 4915, "synset": "perch.n.06", "name": "perch"}, - {"id": 4916, "synset": "yellow_perch.n.01", "name": "yellow_perch"}, - {"id": 4917, "synset": "european_perch.n.01", "name": "European_perch"}, - {"id": 4918, "synset": "pike-perch.n.01", "name": "pike-perch"}, - {"id": 4919, "synset": "walleye.n.02", "name": "walleye"}, - {"id": 4920, "synset": "blue_pike.n.01", "name": "blue_pike"}, - {"id": 4921, "synset": "snail_darter.n.01", "name": "snail_darter"}, - {"id": 4922, "synset": "cusk-eel.n.01", "name": "cusk-eel"}, - {"id": 4923, "synset": "brotula.n.01", "name": "brotula"}, - {"id": 4924, "synset": "pearlfish.n.01", "name": "pearlfish"}, - {"id": 4925, "synset": "robalo.n.01", "name": "robalo"}, - {"id": 4926, "synset": "snook.n.01", "name": "snook"}, - {"id": 4927, "synset": "pike.n.05", "name": "pike"}, - {"id": 4928, "synset": "northern_pike.n.01", "name": "northern_pike"}, - {"id": 4929, "synset": "muskellunge.n.02", "name": "muskellunge"}, - {"id": 4930, "synset": "pickerel.n.02", "name": "pickerel"}, - {"id": 4931, "synset": "chain_pickerel.n.01", "name": "chain_pickerel"}, - {"id": 4932, "synset": "redfin_pickerel.n.01", "name": "redfin_pickerel"}, - {"id": 4933, "synset": "sunfish.n.03", "name": "sunfish"}, - {"id": 4934, "synset": "crappie.n.02", "name": "crappie"}, - {"id": 4935, "synset": "black_crappie.n.01", "name": "black_crappie"}, - {"id": 4936, "synset": "white_crappie.n.01", "name": "white_crappie"}, - {"id": 4937, "synset": "freshwater_bream.n.02", "name": "freshwater_bream"}, - {"id": 4938, "synset": "pumpkinseed.n.01", "name": "pumpkinseed"}, - {"id": 4939, "synset": "bluegill.n.01", "name": "bluegill"}, - {"id": 4940, "synset": "spotted_sunfish.n.01", "name": "spotted_sunfish"}, - {"id": 4941, "synset": "freshwater_bass.n.02", "name": "freshwater_bass"}, - {"id": 4942, "synset": "rock_bass.n.02", "name": "rock_bass"}, - {"id": 4943, "synset": "black_bass.n.02", "name": "black_bass"}, - {"id": 4944, "synset": "kentucky_black_bass.n.01", "name": "Kentucky_black_bass"}, - {"id": 4945, "synset": "smallmouth.n.01", "name": "smallmouth"}, - {"id": 4946, "synset": "largemouth.n.01", "name": "largemouth"}, - {"id": 4947, "synset": "bass.n.08", "name": "bass"}, - {"id": 4948, "synset": "serranid_fish.n.01", "name": "serranid_fish"}, - {"id": 4949, "synset": "white_perch.n.01", "name": "white_perch"}, - {"id": 4950, "synset": "yellow_bass.n.01", "name": "yellow_bass"}, - {"id": 4951, "synset": "blackmouth_bass.n.01", "name": "blackmouth_bass"}, - {"id": 4952, "synset": "rock_sea_bass.n.01", "name": "rock_sea_bass"}, - {"id": 4953, "synset": "striped_bass.n.02", "name": "striped_bass"}, - {"id": 4954, "synset": "stone_bass.n.01", "name": "stone_bass"}, - {"id": 4955, "synset": "grouper.n.02", "name": "grouper"}, - {"id": 4956, "synset": "hind.n.01", "name": "hind"}, - {"id": 4957, "synset": "rock_hind.n.01", "name": "rock_hind"}, - {"id": 4958, "synset": "creole-fish.n.01", "name": "creole-fish"}, - {"id": 4959, "synset": "jewfish.n.02", "name": "jewfish"}, - {"id": 4960, "synset": "soapfish.n.01", "name": "soapfish"}, - {"id": 4961, "synset": "surfperch.n.01", "name": "surfperch"}, - {"id": 4962, "synset": "rainbow_seaperch.n.01", "name": "rainbow_seaperch"}, - {"id": 4963, "synset": "bigeye.n.01", "name": "bigeye"}, - {"id": 4964, "synset": "catalufa.n.01", "name": "catalufa"}, - {"id": 4965, "synset": "cardinalfish.n.01", "name": "cardinalfish"}, - {"id": 4966, "synset": "flame_fish.n.01", "name": "flame_fish"}, - {"id": 4967, "synset": "tilefish.n.02", "name": "tilefish"}, - {"id": 4968, "synset": "bluefish.n.01", "name": "bluefish"}, - {"id": 4969, "synset": "cobia.n.01", "name": "cobia"}, - {"id": 4970, "synset": "remora.n.01", "name": "remora"}, - {"id": 4971, "synset": "sharksucker.n.01", "name": "sharksucker"}, - {"id": 4972, "synset": "whale_sucker.n.01", "name": "whale_sucker"}, - {"id": 4973, "synset": "carangid_fish.n.01", "name": "carangid_fish"}, - {"id": 4974, "synset": "jack.n.11", "name": "jack"}, - {"id": 4975, "synset": "crevalle_jack.n.01", "name": "crevalle_jack"}, - {"id": 4976, "synset": "yellow_jack.n.03", "name": "yellow_jack"}, - {"id": 4977, "synset": "runner.n.10", "name": "runner"}, - {"id": 4978, "synset": "rainbow_runner.n.01", "name": "rainbow_runner"}, - {"id": 4979, "synset": "leatherjacket.n.02", "name": "leatherjacket"}, - {"id": 4980, "synset": "threadfish.n.01", "name": "threadfish"}, - {"id": 4981, "synset": "moonfish.n.01", "name": "moonfish"}, - {"id": 4982, "synset": "lookdown.n.01", "name": "lookdown"}, - {"id": 4983, "synset": "amberjack.n.01", "name": "amberjack"}, - {"id": 4984, "synset": "yellowtail.n.02", "name": "yellowtail"}, - {"id": 4985, "synset": "kingfish.n.05", "name": "kingfish"}, - {"id": 4986, "synset": "pompano.n.02", "name": "pompano"}, - {"id": 4987, "synset": "florida_pompano.n.01", "name": "Florida_pompano"}, - {"id": 4988, "synset": "permit.n.03", "name": "permit"}, - {"id": 4989, "synset": "scad.n.01", "name": "scad"}, - {"id": 4990, "synset": "horse_mackerel.n.03", "name": "horse_mackerel"}, - {"id": 4991, "synset": "horse_mackerel.n.02", "name": "horse_mackerel"}, - {"id": 4992, "synset": "bigeye_scad.n.01", "name": "bigeye_scad"}, - {"id": 4993, "synset": "mackerel_scad.n.01", "name": "mackerel_scad"}, - {"id": 4994, "synset": "round_scad.n.01", "name": "round_scad"}, - {"id": 4995, "synset": "dolphinfish.n.02", "name": "dolphinfish"}, - {"id": 4996, "synset": "coryphaena_hippurus.n.01", "name": "Coryphaena_hippurus"}, - {"id": 4997, "synset": "coryphaena_equisetis.n.01", "name": "Coryphaena_equisetis"}, - {"id": 4998, "synset": "pomfret.n.01", "name": "pomfret"}, - {"id": 4999, "synset": "characin.n.01", "name": "characin"}, - {"id": 5000, "synset": "tetra.n.01", "name": "tetra"}, - {"id": 5001, "synset": "cardinal_tetra.n.01", "name": "cardinal_tetra"}, - {"id": 5002, "synset": "piranha.n.02", "name": "piranha"}, - {"id": 5003, "synset": "cichlid.n.01", "name": "cichlid"}, - {"id": 5004, "synset": "bolti.n.01", "name": "bolti"}, - {"id": 5005, "synset": "snapper.n.05", "name": "snapper"}, - {"id": 5006, "synset": "red_snapper.n.02", "name": "red_snapper"}, - {"id": 5007, "synset": "grey_snapper.n.01", "name": "grey_snapper"}, - {"id": 5008, "synset": "mutton_snapper.n.01", "name": "mutton_snapper"}, - {"id": 5009, "synset": "schoolmaster.n.03", "name": "schoolmaster"}, - {"id": 5010, "synset": "yellowtail.n.01", "name": "yellowtail"}, - {"id": 5011, "synset": "grunt.n.03", "name": "grunt"}, - {"id": 5012, "synset": "margate.n.01", "name": "margate"}, - {"id": 5013, "synset": "spanish_grunt.n.01", "name": "Spanish_grunt"}, - {"id": 5014, "synset": "tomtate.n.01", "name": "tomtate"}, - {"id": 5015, "synset": "cottonwick.n.01", "name": "cottonwick"}, - {"id": 5016, "synset": "sailor's-choice.n.02", "name": "sailor's-choice"}, - {"id": 5017, "synset": "porkfish.n.01", "name": "porkfish"}, - {"id": 5018, "synset": "pompon.n.02", "name": "pompon"}, - {"id": 5019, "synset": "pigfish.n.02", "name": "pigfish"}, - {"id": 5020, "synset": "sparid.n.01", "name": "sparid"}, - {"id": 5021, "synset": "sea_bream.n.02", "name": "sea_bream"}, - {"id": 5022, "synset": "porgy.n.02", "name": "porgy"}, - {"id": 5023, "synset": "red_porgy.n.01", "name": "red_porgy"}, - {"id": 5024, "synset": "european_sea_bream.n.01", "name": "European_sea_bream"}, - {"id": 5025, "synset": "atlantic_sea_bream.n.01", "name": "Atlantic_sea_bream"}, - {"id": 5026, "synset": "sheepshead.n.01", "name": "sheepshead"}, - {"id": 5027, "synset": "pinfish.n.01", "name": "pinfish"}, - {"id": 5028, "synset": "sheepshead_porgy.n.01", "name": "sheepshead_porgy"}, - {"id": 5029, "synset": "snapper.n.04", "name": "snapper"}, - {"id": 5030, "synset": "black_bream.n.01", "name": "black_bream"}, - {"id": 5031, "synset": "scup.n.04", "name": "scup"}, - {"id": 5032, "synset": "scup.n.03", "name": "scup"}, - {"id": 5033, "synset": "sciaenid_fish.n.01", "name": "sciaenid_fish"}, - {"id": 5034, "synset": "striped_drum.n.01", "name": "striped_drum"}, - {"id": 5035, "synset": "jackknife-fish.n.01", "name": "jackknife-fish"}, - {"id": 5036, "synset": "silver_perch.n.01", "name": "silver_perch"}, - {"id": 5037, "synset": "red_drum.n.01", "name": "red_drum"}, - {"id": 5038, "synset": "mulloway.n.01", "name": "mulloway"}, - {"id": 5039, "synset": "maigre.n.01", "name": "maigre"}, - {"id": 5040, "synset": "croaker.n.02", "name": "croaker"}, - {"id": 5041, "synset": "atlantic_croaker.n.01", "name": "Atlantic_croaker"}, - {"id": 5042, "synset": "yellowfin_croaker.n.01", "name": "yellowfin_croaker"}, - {"id": 5043, "synset": "whiting.n.04", "name": "whiting"}, - {"id": 5044, "synset": "kingfish.n.04", "name": "kingfish"}, - {"id": 5045, "synset": "king_whiting.n.01", "name": "king_whiting"}, - {"id": 5046, "synset": "northern_whiting.n.01", "name": "northern_whiting"}, - {"id": 5047, "synset": "corbina.n.01", "name": "corbina"}, - {"id": 5048, "synset": "white_croaker.n.02", "name": "white_croaker"}, - {"id": 5049, "synset": "white_croaker.n.01", "name": "white_croaker"}, - {"id": 5050, "synset": "sea_trout.n.02", "name": "sea_trout"}, - {"id": 5051, "synset": "weakfish.n.02", "name": "weakfish"}, - {"id": 5052, "synset": "spotted_weakfish.n.01", "name": "spotted_weakfish"}, - {"id": 5053, "synset": "mullet.n.03", "name": "mullet"}, - {"id": 5054, "synset": "goatfish.n.01", "name": "goatfish"}, - {"id": 5055, "synset": "red_goatfish.n.01", "name": "red_goatfish"}, - {"id": 5056, "synset": "yellow_goatfish.n.01", "name": "yellow_goatfish"}, - {"id": 5057, "synset": "mullet.n.02", "name": "mullet"}, - {"id": 5058, "synset": "striped_mullet.n.01", "name": "striped_mullet"}, - {"id": 5059, "synset": "white_mullet.n.01", "name": "white_mullet"}, - {"id": 5060, "synset": "liza.n.01", "name": "liza"}, - {"id": 5061, "synset": "silversides.n.01", "name": "silversides"}, - {"id": 5062, "synset": "jacksmelt.n.01", "name": "jacksmelt"}, - {"id": 5063, "synset": "barracuda.n.01", "name": "barracuda"}, - {"id": 5064, "synset": "great_barracuda.n.01", "name": "great_barracuda"}, - {"id": 5065, "synset": "sweeper.n.03", "name": "sweeper"}, - {"id": 5066, "synset": "sea_chub.n.01", "name": "sea_chub"}, - {"id": 5067, "synset": "bermuda_chub.n.01", "name": "Bermuda_chub"}, - {"id": 5068, "synset": "spadefish.n.01", "name": "spadefish"}, - {"id": 5069, "synset": "butterfly_fish.n.01", "name": "butterfly_fish"}, - {"id": 5070, "synset": "chaetodon.n.01", "name": "chaetodon"}, - {"id": 5071, "synset": "angelfish.n.01", "name": "angelfish"}, - {"id": 5072, "synset": "rock_beauty.n.01", "name": "rock_beauty"}, - {"id": 5073, "synset": "damselfish.n.01", "name": "damselfish"}, - {"id": 5074, "synset": "beaugregory.n.01", "name": "beaugregory"}, - {"id": 5075, "synset": "anemone_fish.n.01", "name": "anemone_fish"}, - {"id": 5076, "synset": "clown_anemone_fish.n.01", "name": "clown_anemone_fish"}, - {"id": 5077, "synset": "sergeant_major.n.02", "name": "sergeant_major"}, - {"id": 5078, "synset": "wrasse.n.01", "name": "wrasse"}, - {"id": 5079, "synset": "pigfish.n.01", "name": "pigfish"}, - {"id": 5080, "synset": "hogfish.n.01", "name": "hogfish"}, - {"id": 5081, "synset": "slippery_dick.n.01", "name": "slippery_dick"}, - {"id": 5082, "synset": "puddingwife.n.01", "name": "puddingwife"}, - {"id": 5083, "synset": "bluehead.n.01", "name": "bluehead"}, - {"id": 5084, "synset": "pearly_razorfish.n.01", "name": "pearly_razorfish"}, - {"id": 5085, "synset": "tautog.n.01", "name": "tautog"}, - {"id": 5086, "synset": "cunner.n.01", "name": "cunner"}, - {"id": 5087, "synset": "parrotfish.n.01", "name": "parrotfish"}, - {"id": 5088, "synset": "threadfin.n.01", "name": "threadfin"}, - {"id": 5089, "synset": "jawfish.n.01", "name": "jawfish"}, - {"id": 5090, "synset": "stargazer.n.03", "name": "stargazer"}, - {"id": 5091, "synset": "sand_stargazer.n.01", "name": "sand_stargazer"}, - {"id": 5092, "synset": "blenny.n.01", "name": "blenny"}, - {"id": 5093, "synset": "shanny.n.01", "name": "shanny"}, - {"id": 5094, "synset": "molly_miller.n.01", "name": "Molly_Miller"}, - {"id": 5095, "synset": "clinid.n.01", "name": "clinid"}, - {"id": 5096, "synset": "pikeblenny.n.01", "name": "pikeblenny"}, - {"id": 5097, "synset": "bluethroat_pikeblenny.n.01", "name": "bluethroat_pikeblenny"}, - {"id": 5098, "synset": "gunnel.n.02", "name": "gunnel"}, - {"id": 5099, "synset": "rock_gunnel.n.01", "name": "rock_gunnel"}, - {"id": 5100, "synset": "eelblenny.n.01", "name": "eelblenny"}, - {"id": 5101, "synset": "wrymouth.n.01", "name": "wrymouth"}, - {"id": 5102, "synset": "wolffish.n.01", "name": "wolffish"}, - {"id": 5103, "synset": "viviparous_eelpout.n.01", "name": "viviparous_eelpout"}, - {"id": 5104, "synset": "ocean_pout.n.01", "name": "ocean_pout"}, - {"id": 5105, "synset": "sand_lance.n.01", "name": "sand_lance"}, - {"id": 5106, "synset": "dragonet.n.01", "name": "dragonet"}, - {"id": 5107, "synset": "goby.n.01", "name": "goby"}, - {"id": 5108, "synset": "mudskipper.n.01", "name": "mudskipper"}, - {"id": 5109, "synset": "sleeper.n.08", "name": "sleeper"}, - {"id": 5110, "synset": "flathead.n.02", "name": "flathead"}, - {"id": 5111, "synset": "archerfish.n.01", "name": "archerfish"}, - {"id": 5112, "synset": "surgeonfish.n.01", "name": "surgeonfish"}, - {"id": 5113, "synset": "gempylid.n.01", "name": "gempylid"}, - {"id": 5114, "synset": "snake_mackerel.n.01", "name": "snake_mackerel"}, - {"id": 5115, "synset": "escolar.n.01", "name": "escolar"}, - {"id": 5116, "synset": "oilfish.n.01", "name": "oilfish"}, - {"id": 5117, "synset": "cutlassfish.n.01", "name": "cutlassfish"}, - {"id": 5118, "synset": "scombroid.n.01", "name": "scombroid"}, - {"id": 5119, "synset": "mackerel.n.02", "name": "mackerel"}, - {"id": 5120, "synset": "common_mackerel.n.01", "name": "common_mackerel"}, - {"id": 5121, "synset": "spanish_mackerel.n.03", "name": "Spanish_mackerel"}, - {"id": 5122, "synset": "chub_mackerel.n.01", "name": "chub_mackerel"}, - {"id": 5123, "synset": "wahoo.n.03", "name": "wahoo"}, - {"id": 5124, "synset": "spanish_mackerel.n.02", "name": "Spanish_mackerel"}, - {"id": 5125, "synset": "king_mackerel.n.01", "name": "king_mackerel"}, - {"id": 5126, "synset": "scomberomorus_maculatus.n.01", "name": "Scomberomorus_maculatus"}, - {"id": 5127, "synset": "cero.n.01", "name": "cero"}, - {"id": 5128, "synset": "sierra.n.02", "name": "sierra"}, - {"id": 5129, "synset": "tuna.n.03", "name": "tuna"}, - {"id": 5130, "synset": "albacore.n.02", "name": "albacore"}, - {"id": 5131, "synset": "bluefin.n.02", "name": "bluefin"}, - {"id": 5132, "synset": "yellowfin.n.01", "name": "yellowfin"}, - {"id": 5133, "synset": "bonito.n.03", "name": "bonito"}, - {"id": 5134, "synset": "skipjack.n.02", "name": "skipjack"}, - {"id": 5135, "synset": "chile_bonito.n.01", "name": "Chile_bonito"}, - {"id": 5136, "synset": "skipjack.n.01", "name": "skipjack"}, - {"id": 5137, "synset": "bonito.n.02", "name": "bonito"}, - {"id": 5138, "synset": "swordfish.n.02", "name": "swordfish"}, - {"id": 5139, "synset": "sailfish.n.02", "name": "sailfish"}, - {"id": 5140, "synset": "atlantic_sailfish.n.01", "name": "Atlantic_sailfish"}, - {"id": 5141, "synset": "billfish.n.02", "name": "billfish"}, - {"id": 5142, "synset": "marlin.n.01", "name": "marlin"}, - {"id": 5143, "synset": "blue_marlin.n.01", "name": "blue_marlin"}, - {"id": 5144, "synset": "black_marlin.n.01", "name": "black_marlin"}, - {"id": 5145, "synset": "striped_marlin.n.01", "name": "striped_marlin"}, - {"id": 5146, "synset": "white_marlin.n.01", "name": "white_marlin"}, - {"id": 5147, "synset": "spearfish.n.01", "name": "spearfish"}, - {"id": 5148, "synset": "louvar.n.01", "name": "louvar"}, - {"id": 5149, "synset": "dollarfish.n.01", "name": "dollarfish"}, - {"id": 5150, "synset": "palometa.n.01", "name": "palometa"}, - {"id": 5151, "synset": "harvestfish.n.01", "name": "harvestfish"}, - {"id": 5152, "synset": "driftfish.n.01", "name": "driftfish"}, - {"id": 5153, "synset": "barrelfish.n.01", "name": "barrelfish"}, - {"id": 5154, "synset": "clingfish.n.01", "name": "clingfish"}, - {"id": 5155, "synset": "tripletail.n.01", "name": "tripletail"}, - {"id": 5156, "synset": "atlantic_tripletail.n.01", "name": "Atlantic_tripletail"}, - {"id": 5157, "synset": "pacific_tripletail.n.01", "name": "Pacific_tripletail"}, - {"id": 5158, "synset": "mojarra.n.01", "name": "mojarra"}, - {"id": 5159, "synset": "yellowfin_mojarra.n.01", "name": "yellowfin_mojarra"}, - {"id": 5160, "synset": "silver_jenny.n.01", "name": "silver_jenny"}, - {"id": 5161, "synset": "whiting.n.03", "name": "whiting"}, - {"id": 5162, "synset": "ganoid.n.01", "name": "ganoid"}, - {"id": 5163, "synset": "bowfin.n.01", "name": "bowfin"}, - {"id": 5164, "synset": "paddlefish.n.01", "name": "paddlefish"}, - {"id": 5165, "synset": "chinese_paddlefish.n.01", "name": "Chinese_paddlefish"}, - {"id": 5166, "synset": "sturgeon.n.01", "name": "sturgeon"}, - {"id": 5167, "synset": "pacific_sturgeon.n.01", "name": "Pacific_sturgeon"}, - {"id": 5168, "synset": "beluga.n.01", "name": "beluga"}, - {"id": 5169, "synset": "gar.n.01", "name": "gar"}, - {"id": 5170, "synset": "scorpaenoid.n.01", "name": "scorpaenoid"}, - {"id": 5171, "synset": "scorpaenid.n.01", "name": "scorpaenid"}, - {"id": 5172, "synset": "scorpionfish.n.01", "name": "scorpionfish"}, - {"id": 5173, "synset": "plumed_scorpionfish.n.01", "name": "plumed_scorpionfish"}, - {"id": 5174, "synset": "lionfish.n.01", "name": "lionfish"}, - {"id": 5175, "synset": "stonefish.n.01", "name": "stonefish"}, - {"id": 5176, "synset": "rockfish.n.02", "name": "rockfish"}, - {"id": 5177, "synset": "copper_rockfish.n.01", "name": "copper_rockfish"}, - {"id": 5178, "synset": "vermillion_rockfish.n.01", "name": "vermillion_rockfish"}, - {"id": 5179, "synset": "red_rockfish.n.02", "name": "red_rockfish"}, - {"id": 5180, "synset": "rosefish.n.02", "name": "rosefish"}, - {"id": 5181, "synset": "bullhead.n.01", "name": "bullhead"}, - {"id": 5182, "synset": "miller's-thumb.n.01", "name": "miller's-thumb"}, - {"id": 5183, "synset": "sea_raven.n.01", "name": "sea_raven"}, - {"id": 5184, "synset": "lumpfish.n.01", "name": "lumpfish"}, - {"id": 5185, "synset": "lumpsucker.n.01", "name": "lumpsucker"}, - {"id": 5186, "synset": "pogge.n.01", "name": "pogge"}, - {"id": 5187, "synset": "greenling.n.01", "name": "greenling"}, - {"id": 5188, "synset": "kelp_greenling.n.01", "name": "kelp_greenling"}, - {"id": 5189, "synset": "painted_greenling.n.01", "name": "painted_greenling"}, - {"id": 5190, "synset": "flathead.n.01", "name": "flathead"}, - {"id": 5191, "synset": "gurnard.n.01", "name": "gurnard"}, - {"id": 5192, "synset": "tub_gurnard.n.01", "name": "tub_gurnard"}, - {"id": 5193, "synset": "sea_robin.n.01", "name": "sea_robin"}, - {"id": 5194, "synset": "northern_sea_robin.n.01", "name": "northern_sea_robin"}, - {"id": 5195, "synset": "flying_gurnard.n.01", "name": "flying_gurnard"}, - {"id": 5196, "synset": "plectognath.n.01", "name": "plectognath"}, - {"id": 5197, "synset": "triggerfish.n.01", "name": "triggerfish"}, - {"id": 5198, "synset": "queen_triggerfish.n.01", "name": "queen_triggerfish"}, - {"id": 5199, "synset": "filefish.n.01", "name": "filefish"}, - {"id": 5200, "synset": "leatherjacket.n.01", "name": "leatherjacket"}, - {"id": 5201, "synset": "boxfish.n.01", "name": "boxfish"}, - {"id": 5202, "synset": "cowfish.n.01", "name": "cowfish"}, - {"id": 5203, "synset": "spiny_puffer.n.01", "name": "spiny_puffer"}, - {"id": 5204, "synset": "porcupinefish.n.01", "name": "porcupinefish"}, - {"id": 5205, "synset": "balloonfish.n.01", "name": "balloonfish"}, - {"id": 5206, "synset": "burrfish.n.01", "name": "burrfish"}, - {"id": 5207, "synset": "ocean_sunfish.n.01", "name": "ocean_sunfish"}, - {"id": 5208, "synset": "sharptail_mola.n.01", "name": "sharptail_mola"}, - {"id": 5209, "synset": "flatfish.n.02", "name": "flatfish"}, - {"id": 5210, "synset": "flounder.n.02", "name": "flounder"}, - {"id": 5211, "synset": "righteye_flounder.n.01", "name": "righteye_flounder"}, - {"id": 5212, "synset": "plaice.n.02", "name": "plaice"}, - {"id": 5213, "synset": "european_flatfish.n.01", "name": "European_flatfish"}, - {"id": 5214, "synset": "yellowtail_flounder.n.02", "name": "yellowtail_flounder"}, - {"id": 5215, "synset": "winter_flounder.n.02", "name": "winter_flounder"}, - {"id": 5216, "synset": "lemon_sole.n.05", "name": "lemon_sole"}, - {"id": 5217, "synset": "american_plaice.n.01", "name": "American_plaice"}, - {"id": 5218, "synset": "halibut.n.02", "name": "halibut"}, - {"id": 5219, "synset": "atlantic_halibut.n.01", "name": "Atlantic_halibut"}, - {"id": 5220, "synset": "pacific_halibut.n.01", "name": "Pacific_halibut"}, - {"id": 5221, "synset": "lefteye_flounder.n.01", "name": "lefteye_flounder"}, - {"id": 5222, "synset": "southern_flounder.n.01", "name": "southern_flounder"}, - {"id": 5223, "synset": "summer_flounder.n.01", "name": "summer_flounder"}, - {"id": 5224, "synset": "whiff.n.02", "name": "whiff"}, - {"id": 5225, "synset": "horned_whiff.n.01", "name": "horned_whiff"}, - {"id": 5226, "synset": "sand_dab.n.02", "name": "sand_dab"}, - {"id": 5227, "synset": "windowpane.n.02", "name": "windowpane"}, - {"id": 5228, "synset": "brill.n.01", "name": "brill"}, - {"id": 5229, "synset": "turbot.n.02", "name": "turbot"}, - {"id": 5230, "synset": "tonguefish.n.01", "name": "tonguefish"}, - {"id": 5231, "synset": "sole.n.04", "name": "sole"}, - {"id": 5232, "synset": "european_sole.n.01", "name": "European_sole"}, - {"id": 5233, "synset": "english_sole.n.02", "name": "English_sole"}, - {"id": 5234, "synset": "hogchoker.n.01", "name": "hogchoker"}, - {"id": 5235, "synset": "aba.n.02", "name": "aba"}, - {"id": 5236, "synset": "abacus.n.02", "name": "abacus"}, - {"id": 5237, "synset": "abandoned_ship.n.01", "name": "abandoned_ship"}, - {"id": 5238, "synset": "a_battery.n.01", "name": "A_battery"}, - {"id": 5239, "synset": "abattoir.n.01", "name": "abattoir"}, - {"id": 5240, "synset": "abaya.n.01", "name": "abaya"}, - {"id": 5241, "synset": "abbe_condenser.n.01", "name": "Abbe_condenser"}, - {"id": 5242, "synset": "abbey.n.03", "name": "abbey"}, - {"id": 5243, "synset": "abbey.n.02", "name": "abbey"}, - {"id": 5244, "synset": "abbey.n.01", "name": "abbey"}, - {"id": 5245, "synset": "abney_level.n.01", "name": "Abney_level"}, - {"id": 5246, "synset": "abrader.n.01", "name": "abrader"}, - {"id": 5247, "synset": "abrading_stone.n.01", "name": "abrading_stone"}, - {"id": 5248, "synset": "abutment.n.02", "name": "abutment"}, - {"id": 5249, "synset": "abutment_arch.n.01", "name": "abutment_arch"}, - {"id": 5250, "synset": "academic_costume.n.01", "name": "academic_costume"}, - {"id": 5251, "synset": "academic_gown.n.01", "name": "academic_gown"}, - {"id": 5252, "synset": "accelerator.n.02", "name": "accelerator"}, - {"id": 5253, "synset": "accelerator.n.04", "name": "accelerator"}, - {"id": 5254, "synset": "accelerator.n.01", "name": "accelerator"}, - {"id": 5255, "synset": "accelerometer.n.01", "name": "accelerometer"}, - {"id": 5256, "synset": "accessory.n.01", "name": "accessory"}, - {"id": 5257, "synset": "accommodating_lens_implant.n.01", "name": "accommodating_lens_implant"}, - {"id": 5258, "synset": "accommodation.n.04", "name": "accommodation"}, - {"id": 5259, "synset": "accordion.n.01", "name": "accordion"}, - {"id": 5260, "synset": "acetate_disk.n.01", "name": "acetate_disk"}, - {"id": 5261, "synset": "acetate_rayon.n.01", "name": "acetate_rayon"}, - {"id": 5262, "synset": "achromatic_lens.n.01", "name": "achromatic_lens"}, - {"id": 5263, "synset": "acoustic_delay_line.n.01", "name": "acoustic_delay_line"}, - {"id": 5264, "synset": "acoustic_device.n.01", "name": "acoustic_device"}, - {"id": 5265, "synset": "acoustic_guitar.n.01", "name": "acoustic_guitar"}, - {"id": 5266, "synset": "acoustic_modem.n.01", "name": "acoustic_modem"}, - {"id": 5267, "synset": "acropolis.n.01", "name": "acropolis"}, - {"id": 5268, "synset": "acrylic.n.04", "name": "acrylic"}, - {"id": 5269, "synset": "acrylic.n.03", "name": "acrylic"}, - {"id": 5270, "synset": "actinometer.n.01", "name": "actinometer"}, - {"id": 5271, "synset": "action.n.07", "name": "action"}, - {"id": 5272, "synset": "active_matrix_screen.n.01", "name": "active_matrix_screen"}, - {"id": 5273, "synset": "actuator.n.01", "name": "actuator"}, - {"id": 5274, "synset": "adapter.n.02", "name": "adapter"}, - {"id": 5275, "synset": "adder.n.02", "name": "adder"}, - {"id": 5276, "synset": "adding_machine.n.01", "name": "adding_machine"}, - {"id": 5277, "synset": "addressing_machine.n.01", "name": "addressing_machine"}, - {"id": 5278, "synset": "adhesive_bandage.n.01", "name": "adhesive_bandage"}, - {"id": 5279, "synset": "adit.n.01", "name": "adit"}, - {"id": 5280, "synset": "adjoining_room.n.01", "name": "adjoining_room"}, - {"id": 5281, "synset": "adjustable_wrench.n.01", "name": "adjustable_wrench"}, - {"id": 5282, "synset": "adobe.n.02", "name": "adobe"}, - {"id": 5283, "synset": "adz.n.01", "name": "adz"}, - {"id": 5284, "synset": "aeolian_harp.n.01", "name": "aeolian_harp"}, - {"id": 5285, "synset": "aerator.n.01", "name": "aerator"}, - {"id": 5286, "synset": "aerial_torpedo.n.01", "name": "aerial_torpedo"}, - {"id": 5287, "synset": "aertex.n.01", "name": "Aertex"}, - {"id": 5288, "synset": "afghan.n.01", "name": "afghan"}, - {"id": 5289, "synset": "afro-wig.n.01", "name": "Afro-wig"}, - {"id": 5290, "synset": "afterburner.n.01", "name": "afterburner"}, - {"id": 5291, "synset": "after-shave.n.01", "name": "after-shave"}, - {"id": 5292, "synset": "agateware.n.01", "name": "agateware"}, - {"id": 5293, "synset": "agglomerator.n.01", "name": "agglomerator"}, - {"id": 5294, "synset": "aglet.n.02", "name": "aglet"}, - {"id": 5295, "synset": "aglet.n.01", "name": "aglet"}, - {"id": 5296, "synset": "agora.n.03", "name": "agora"}, - {"id": 5297, "synset": "aigrette.n.01", "name": "aigrette"}, - {"id": 5298, "synset": "aileron.n.01", "name": "aileron"}, - {"id": 5299, "synset": "air_bag.n.01", "name": "air_bag"}, - {"id": 5300, "synset": "airbrake.n.02", "name": "airbrake"}, - {"id": 5301, "synset": "airbrush.n.01", "name": "airbrush"}, - {"id": 5302, "synset": "airbus.n.01", "name": "airbus"}, - {"id": 5303, "synset": "air_compressor.n.01", "name": "air_compressor"}, - {"id": 5304, "synset": "aircraft.n.01", "name": "aircraft"}, - {"id": 5305, "synset": "aircraft_carrier.n.01", "name": "aircraft_carrier"}, - {"id": 5306, "synset": "aircraft_engine.n.01", "name": "aircraft_engine"}, - {"id": 5307, "synset": "air_cushion.n.02", "name": "air_cushion"}, - {"id": 5308, "synset": "airdock.n.01", "name": "airdock"}, - {"id": 5309, "synset": "airfield.n.01", "name": "airfield"}, - {"id": 5310, "synset": "air_filter.n.01", "name": "air_filter"}, - {"id": 5311, "synset": "airfoil.n.01", "name": "airfoil"}, - {"id": 5312, "synset": "airframe.n.01", "name": "airframe"}, - {"id": 5313, "synset": "air_gun.n.01", "name": "air_gun"}, - {"id": 5314, "synset": "air_hammer.n.01", "name": "air_hammer"}, - {"id": 5315, "synset": "air_horn.n.01", "name": "air_horn"}, - {"id": 5316, "synset": "airing_cupboard.n.01", "name": "airing_cupboard"}, - {"id": 5317, "synset": "airliner.n.01", "name": "airliner"}, - {"id": 5318, "synset": "airmailer.n.01", "name": "airmailer"}, - {"id": 5319, "synset": "airplane_propeller.n.01", "name": "airplane_propeller"}, - {"id": 5320, "synset": "airport.n.01", "name": "airport"}, - {"id": 5321, "synset": "air_pump.n.01", "name": "air_pump"}, - {"id": 5322, "synset": "air_search_radar.n.01", "name": "air_search_radar"}, - {"id": 5323, "synset": "airship.n.01", "name": "airship"}, - {"id": 5324, "synset": "air_terminal.n.01", "name": "air_terminal"}, - {"id": 5325, "synset": "air-to-air_missile.n.01", "name": "air-to-air_missile"}, - {"id": 5326, "synset": "air-to-ground_missile.n.01", "name": "air-to-ground_missile"}, - {"id": 5327, "synset": "aisle.n.03", "name": "aisle"}, - {"id": 5328, "synset": "aladdin's_lamp.n.01", "name": "Aladdin's_lamp"}, - {"id": 5329, "synset": "alarm.n.02", "name": "alarm"}, - {"id": 5330, "synset": "alb.n.01", "name": "alb"}, - {"id": 5331, "synset": "alcazar.n.01", "name": "alcazar"}, - {"id": 5332, "synset": "alcohol_thermometer.n.01", "name": "alcohol_thermometer"}, - {"id": 5333, "synset": "alehouse.n.01", "name": "alehouse"}, - {"id": 5334, "synset": "alembic.n.01", "name": "alembic"}, - {"id": 5335, "synset": "algometer.n.01", "name": "algometer"}, - {"id": 5336, "synset": "alidade.n.02", "name": "alidade"}, - {"id": 5337, "synset": "alidade.n.01", "name": "alidade"}, - {"id": 5338, "synset": "a-line.n.01", "name": "A-line"}, - {"id": 5339, "synset": "allen_screw.n.01", "name": "Allen_screw"}, - {"id": 5340, "synset": "allen_wrench.n.01", "name": "Allen_wrench"}, - {"id": 5341, "synset": "alligator_wrench.n.01", "name": "alligator_wrench"}, - {"id": 5342, "synset": "alms_dish.n.01", "name": "alms_dish"}, - {"id": 5343, "synset": "alpaca.n.02", "name": "alpaca"}, - {"id": 5344, "synset": "alpenstock.n.01", "name": "alpenstock"}, - {"id": 5345, "synset": "altar.n.02", "name": "altar"}, - {"id": 5346, "synset": "altar.n.01", "name": "altar"}, - {"id": 5347, "synset": "altarpiece.n.01", "name": "altarpiece"}, - {"id": 5348, "synset": "altazimuth.n.01", "name": "altazimuth"}, - {"id": 5349, "synset": "alternator.n.01", "name": "alternator"}, - {"id": 5350, "synset": "altimeter.n.01", "name": "altimeter"}, - {"id": 5351, "synset": "amati.n.02", "name": "Amati"}, - {"id": 5352, "synset": "amen_corner.n.01", "name": "amen_corner"}, - {"id": 5353, "synset": "american_organ.n.01", "name": "American_organ"}, - {"id": 5354, "synset": "ammeter.n.01", "name": "ammeter"}, - {"id": 5355, "synset": "ammonia_clock.n.01", "name": "ammonia_clock"}, - {"id": 5356, "synset": "ammunition.n.01", "name": "ammunition"}, - {"id": 5357, "synset": "amphibian.n.02", "name": "amphibian"}, - {"id": 5358, "synset": "amphibian.n.01", "name": "amphibian"}, - {"id": 5359, "synset": "amphitheater.n.02", "name": "amphitheater"}, - {"id": 5360, "synset": "amphitheater.n.01", "name": "amphitheater"}, - {"id": 5361, "synset": "amphora.n.01", "name": "amphora"}, - {"id": 5362, "synset": "ampulla.n.02", "name": "ampulla"}, - {"id": 5363, "synset": "amusement_arcade.n.01", "name": "amusement_arcade"}, - {"id": 5364, "synset": "analog_clock.n.01", "name": "analog_clock"}, - {"id": 5365, "synset": "analog_computer.n.01", "name": "analog_computer"}, - {"id": 5366, "synset": "analog_watch.n.01", "name": "analog_watch"}, - {"id": 5367, "synset": "analytical_balance.n.01", "name": "analytical_balance"}, - {"id": 5368, "synset": "analyzer.n.01", "name": "analyzer"}, - {"id": 5369, "synset": "anamorphosis.n.02", "name": "anamorphosis"}, - {"id": 5370, "synset": "anastigmat.n.01", "name": "anastigmat"}, - {"id": 5371, "synset": "anchor.n.01", "name": "anchor"}, - {"id": 5372, "synset": "anchor_chain.n.01", "name": "anchor_chain"}, - {"id": 5373, "synset": "anchor_light.n.01", "name": "anchor_light"}, - {"id": 5374, "synset": "and_circuit.n.01", "name": "AND_circuit"}, - {"id": 5375, "synset": "andiron.n.01", "name": "andiron"}, - {"id": 5376, "synset": "android.n.01", "name": "android"}, - {"id": 5377, "synset": "anechoic_chamber.n.01", "name": "anechoic_chamber"}, - {"id": 5378, "synset": "anemometer.n.01", "name": "anemometer"}, - {"id": 5379, "synset": "aneroid_barometer.n.01", "name": "aneroid_barometer"}, - {"id": 5380, "synset": "angiocardiogram.n.01", "name": "angiocardiogram"}, - {"id": 5381, "synset": "angioscope.n.01", "name": "angioscope"}, - {"id": 5382, "synset": "angle_bracket.n.02", "name": "angle_bracket"}, - {"id": 5383, "synset": "angledozer.n.01", "name": "angledozer"}, - {"id": 5384, "synset": "ankle_brace.n.01", "name": "ankle_brace"}, - {"id": 5385, "synset": "anklet.n.02", "name": "anklet"}, - {"id": 5386, "synset": "anklet.n.01", "name": "anklet"}, - {"id": 5387, "synset": "ankus.n.01", "name": "ankus"}, - {"id": 5388, "synset": "anode.n.01", "name": "anode"}, - {"id": 5389, "synset": "anode.n.02", "name": "anode"}, - {"id": 5390, "synset": "answering_machine.n.01", "name": "answering_machine"}, - {"id": 5391, "synset": "anteroom.n.01", "name": "anteroom"}, - {"id": 5392, "synset": "antiaircraft.n.01", "name": "antiaircraft"}, - {"id": 5393, "synset": "antiballistic_missile.n.01", "name": "antiballistic_missile"}, - {"id": 5394, "synset": "antifouling_paint.n.01", "name": "antifouling_paint"}, - {"id": 5395, "synset": "anti-g_suit.n.01", "name": "anti-G_suit"}, - {"id": 5396, "synset": "antimacassar.n.01", "name": "antimacassar"}, - {"id": 5397, "synset": "antiperspirant.n.01", "name": "antiperspirant"}, - {"id": 5398, "synset": "anti-submarine_rocket.n.01", "name": "anti-submarine_rocket"}, - {"id": 5399, "synset": "anvil.n.01", "name": "anvil"}, - {"id": 5400, "synset": "ao_dai.n.01", "name": "ao_dai"}, - {"id": 5401, "synset": "apadana.n.01", "name": "apadana"}, - {"id": 5402, "synset": "apartment.n.01", "name": "apartment"}, - {"id": 5403, "synset": "apartment_building.n.01", "name": "apartment_building"}, - {"id": 5404, "synset": "aperture.n.03", "name": "aperture"}, - {"id": 5405, "synset": "aperture.n.01", "name": "aperture"}, - {"id": 5406, "synset": "apiary.n.01", "name": "apiary"}, - {"id": 5407, "synset": "apparatus.n.01", "name": "apparatus"}, - {"id": 5408, "synset": "apparel.n.01", "name": "apparel"}, - {"id": 5409, "synset": "applecart.n.02", "name": "applecart"}, - {"id": 5410, "synset": "appliance.n.02", "name": "appliance"}, - {"id": 5411, "synset": "appliance.n.01", "name": "appliance"}, - {"id": 5412, "synset": "applicator.n.01", "name": "applicator"}, - {"id": 5413, "synset": "appointment.n.03", "name": "appointment"}, - {"id": 5414, "synset": "apron_string.n.01", "name": "apron_string"}, - {"id": 5415, "synset": "apse.n.01", "name": "apse"}, - {"id": 5416, "synset": "aqualung.n.01", "name": "aqualung"}, - {"id": 5417, "synset": "aquaplane.n.01", "name": "aquaplane"}, - {"id": 5418, "synset": "arabesque.n.02", "name": "arabesque"}, - {"id": 5419, "synset": "arbor.n.03", "name": "arbor"}, - {"id": 5420, "synset": "arcade.n.02", "name": "arcade"}, - {"id": 5421, "synset": "arch.n.04", "name": "arch"}, - {"id": 5422, "synset": "architecture.n.01", "name": "architecture"}, - {"id": 5423, "synset": "architrave.n.02", "name": "architrave"}, - {"id": 5424, "synset": "arch_support.n.01", "name": "arch_support"}, - {"id": 5425, "synset": "arc_lamp.n.01", "name": "arc_lamp"}, - {"id": 5426, "synset": "area.n.05", "name": "area"}, - {"id": 5427, "synset": "areaway.n.01", "name": "areaway"}, - {"id": 5428, "synset": "argyle.n.03", "name": "argyle"}, - {"id": 5429, "synset": "ark.n.02", "name": "ark"}, - {"id": 5430, "synset": "arm.n.04", "name": "arm"}, - {"id": 5431, "synset": "armament.n.01", "name": "armament"}, - {"id": 5432, "synset": "armature.n.01", "name": "armature"}, - {"id": 5433, "synset": "armet.n.01", "name": "armet"}, - {"id": 5434, "synset": "arm_guard.n.01", "name": "arm_guard"}, - {"id": 5435, "synset": "armhole.n.01", "name": "armhole"}, - {"id": 5436, "synset": "armilla.n.02", "name": "armilla"}, - {"id": 5437, "synset": "armlet.n.01", "name": "armlet"}, - {"id": 5438, "synset": "armored_car.n.02", "name": "armored_car"}, - {"id": 5439, "synset": "armored_car.n.01", "name": "armored_car"}, - {"id": 5440, "synset": "armored_personnel_carrier.n.01", "name": "armored_personnel_carrier"}, - {"id": 5441, "synset": "armored_vehicle.n.01", "name": "armored_vehicle"}, - {"id": 5442, "synset": "armor_plate.n.01", "name": "armor_plate"}, - {"id": 5443, "synset": "armory.n.04", "name": "armory"}, - {"id": 5444, "synset": "armrest.n.01", "name": "armrest"}, - {"id": 5445, "synset": "arquebus.n.01", "name": "arquebus"}, - {"id": 5446, "synset": "array.n.04", "name": "array"}, - {"id": 5447, "synset": "array.n.03", "name": "array"}, - {"id": 5448, "synset": "arrester.n.01", "name": "arrester"}, - {"id": 5449, "synset": "arrow.n.02", "name": "arrow"}, - {"id": 5450, "synset": "arsenal.n.01", "name": "arsenal"}, - {"id": 5451, "synset": "arterial_road.n.01", "name": "arterial_road"}, - {"id": 5452, "synset": "arthrogram.n.01", "name": "arthrogram"}, - {"id": 5453, "synset": "arthroscope.n.01", "name": "arthroscope"}, - {"id": 5454, "synset": "artificial_heart.n.01", "name": "artificial_heart"}, - {"id": 5455, "synset": "artificial_horizon.n.01", "name": "artificial_horizon"}, - {"id": 5456, "synset": "artificial_joint.n.01", "name": "artificial_joint"}, - {"id": 5457, "synset": "artificial_kidney.n.01", "name": "artificial_kidney"}, - {"id": 5458, "synset": "artificial_skin.n.01", "name": "artificial_skin"}, - {"id": 5459, "synset": "artillery.n.01", "name": "artillery"}, - {"id": 5460, "synset": "artillery_shell.n.01", "name": "artillery_shell"}, - {"id": 5461, "synset": "artist's_loft.n.01", "name": "artist's_loft"}, - {"id": 5462, "synset": "art_school.n.01", "name": "art_school"}, - {"id": 5463, "synset": "ascot.n.01", "name": "ascot"}, - {"id": 5464, "synset": "ash-pan.n.01", "name": "ash-pan"}, - {"id": 5465, "synset": "aspergill.n.01", "name": "aspergill"}, - {"id": 5466, "synset": "aspersorium.n.01", "name": "aspersorium"}, - {"id": 5467, "synset": "aspirator.n.01", "name": "aspirator"}, - {"id": 5468, "synset": "aspirin_powder.n.01", "name": "aspirin_powder"}, - {"id": 5469, "synset": "assault_gun.n.02", "name": "assault_gun"}, - {"id": 5470, "synset": "assault_rifle.n.01", "name": "assault_rifle"}, - {"id": 5471, "synset": "assegai.n.01", "name": "assegai"}, - {"id": 5472, "synset": "assembly.n.01", "name": "assembly"}, - {"id": 5473, "synset": "assembly.n.05", "name": "assembly"}, - {"id": 5474, "synset": "assembly_hall.n.01", "name": "assembly_hall"}, - {"id": 5475, "synset": "assembly_plant.n.01", "name": "assembly_plant"}, - {"id": 5476, "synset": "astatic_coils.n.01", "name": "astatic_coils"}, - {"id": 5477, "synset": "astatic_galvanometer.n.01", "name": "astatic_galvanometer"}, - {"id": 5478, "synset": "astrodome.n.01", "name": "astrodome"}, - {"id": 5479, "synset": "astrolabe.n.01", "name": "astrolabe"}, - {"id": 5480, "synset": "astronomical_telescope.n.01", "name": "astronomical_telescope"}, - {"id": 5481, "synset": "astronomy_satellite.n.01", "name": "astronomy_satellite"}, - {"id": 5482, "synset": "athenaeum.n.02", "name": "athenaeum"}, - {"id": 5483, "synset": "athletic_sock.n.01", "name": "athletic_sock"}, - {"id": 5484, "synset": "athletic_supporter.n.01", "name": "athletic_supporter"}, - {"id": 5485, "synset": "atlas.n.04", "name": "atlas"}, - {"id": 5486, "synset": "atmometer.n.01", "name": "atmometer"}, - {"id": 5487, "synset": "atom_bomb.n.01", "name": "atom_bomb"}, - {"id": 5488, "synset": "atomic_clock.n.01", "name": "atomic_clock"}, - {"id": 5489, "synset": "atomic_pile.n.01", "name": "atomic_pile"}, - {"id": 5490, "synset": "atrium.n.02", "name": "atrium"}, - {"id": 5491, "synset": "attache_case.n.01", "name": "attache_case"}, - {"id": 5492, "synset": "attachment.n.04", "name": "attachment"}, - {"id": 5493, "synset": "attack_submarine.n.01", "name": "attack_submarine"}, - {"id": 5494, "synset": "attenuator.n.01", "name": "attenuator"}, - {"id": 5495, "synset": "attic.n.04", "name": "attic"}, - {"id": 5496, "synset": "attic_fan.n.01", "name": "attic_fan"}, - {"id": 5497, "synset": "attire.n.01", "name": "attire"}, - {"id": 5498, "synset": "audio_amplifier.n.01", "name": "audio_amplifier"}, - {"id": 5499, "synset": "audiocassette.n.01", "name": "audiocassette"}, - {"id": 5500, "synset": "audio_cd.n.01", "name": "audio_CD"}, - {"id": 5501, "synset": "audiometer.n.01", "name": "audiometer"}, - {"id": 5502, "synset": "audio_system.n.01", "name": "audio_system"}, - {"id": 5503, "synset": "audiotape.n.02", "name": "audiotape"}, - {"id": 5504, "synset": "audiotape.n.01", "name": "audiotape"}, - {"id": 5505, "synset": "audiovisual.n.01", "name": "audiovisual"}, - {"id": 5506, "synset": "auditorium.n.01", "name": "auditorium"}, - {"id": 5507, "synset": "auger.n.02", "name": "auger"}, - {"id": 5508, "synset": "autobahn.n.01", "name": "autobahn"}, - {"id": 5509, "synset": "autoclave.n.01", "name": "autoclave"}, - {"id": 5510, "synset": "autofocus.n.01", "name": "autofocus"}, - {"id": 5511, "synset": "autogiro.n.01", "name": "autogiro"}, - {"id": 5512, "synset": "autoinjector.n.01", "name": "autoinjector"}, - {"id": 5513, "synset": "autoloader.n.01", "name": "autoloader"}, - {"id": 5514, "synset": "automat.n.02", "name": "automat"}, - {"id": 5515, "synset": "automat.n.01", "name": "automat"}, - {"id": 5516, "synset": "automatic_choke.n.01", "name": "automatic_choke"}, - {"id": 5517, "synset": "automatic_firearm.n.01", "name": "automatic_firearm"}, - {"id": 5518, "synset": "automatic_pistol.n.01", "name": "automatic_pistol"}, - {"id": 5519, "synset": "automatic_rifle.n.01", "name": "automatic_rifle"}, - {"id": 5520, "synset": "automatic_transmission.n.01", "name": "automatic_transmission"}, - {"id": 5521, "synset": "automation.n.03", "name": "automation"}, - {"id": 5522, "synset": "automaton.n.02", "name": "automaton"}, - {"id": 5523, "synset": "automobile_engine.n.01", "name": "automobile_engine"}, - {"id": 5524, "synset": "automobile_factory.n.01", "name": "automobile_factory"}, - {"id": 5525, "synset": "automobile_horn.n.01", "name": "automobile_horn"}, - {"id": 5526, "synset": "autopilot.n.02", "name": "autopilot"}, - {"id": 5527, "synset": "autoradiograph.n.01", "name": "autoradiograph"}, - {"id": 5528, "synset": "autostrada.n.01", "name": "autostrada"}, - {"id": 5529, "synset": "auxiliary_boiler.n.01", "name": "auxiliary_boiler"}, - {"id": 5530, "synset": "auxiliary_engine.n.01", "name": "auxiliary_engine"}, - {"id": 5531, "synset": "auxiliary_pump.n.01", "name": "auxiliary_pump"}, - { - "id": 5532, - "synset": "auxiliary_research_submarine.n.01", - "name": "auxiliary_research_submarine", - }, - {"id": 5533, "synset": "auxiliary_storage.n.01", "name": "auxiliary_storage"}, - {"id": 5534, "synset": "aviary.n.01", "name": "aviary"}, - {"id": 5535, "synset": "awl.n.01", "name": "awl"}, - {"id": 5536, "synset": "ax_handle.n.01", "name": "ax_handle"}, - {"id": 5537, "synset": "ax_head.n.01", "name": "ax_head"}, - {"id": 5538, "synset": "axis.n.06", "name": "axis"}, - {"id": 5539, "synset": "axle.n.01", "name": "axle"}, - {"id": 5540, "synset": "axle_bar.n.01", "name": "axle_bar"}, - {"id": 5541, "synset": "axletree.n.01", "name": "axletree"}, - {"id": 5542, "synset": "babushka.n.01", "name": "babushka"}, - {"id": 5543, "synset": "baby_bed.n.01", "name": "baby_bed"}, - {"id": 5544, "synset": "baby_grand.n.01", "name": "baby_grand"}, - {"id": 5545, "synset": "baby_powder.n.01", "name": "baby_powder"}, - {"id": 5546, "synset": "baby_shoe.n.01", "name": "baby_shoe"}, - {"id": 5547, "synset": "back.n.08", "name": "back"}, - {"id": 5548, "synset": "back.n.07", "name": "back"}, - {"id": 5549, "synset": "backbench.n.01", "name": "backbench"}, - {"id": 5550, "synset": "backboard.n.02", "name": "backboard"}, - {"id": 5551, "synset": "backbone.n.05", "name": "backbone"}, - {"id": 5552, "synset": "back_brace.n.01", "name": "back_brace"}, - {"id": 5553, "synset": "backgammon_board.n.01", "name": "backgammon_board"}, - {"id": 5554, "synset": "background.n.07", "name": "background"}, - {"id": 5555, "synset": "backhoe.n.01", "name": "backhoe"}, - {"id": 5556, "synset": "backlighting.n.01", "name": "backlighting"}, - {"id": 5557, "synset": "backpacking_tent.n.01", "name": "backpacking_tent"}, - {"id": 5558, "synset": "backplate.n.01", "name": "backplate"}, - {"id": 5559, "synset": "back_porch.n.01", "name": "back_porch"}, - {"id": 5560, "synset": "backsaw.n.01", "name": "backsaw"}, - {"id": 5561, "synset": "backscratcher.n.02", "name": "backscratcher"}, - {"id": 5562, "synset": "backseat.n.02", "name": "backseat"}, - {"id": 5563, "synset": "backspace_key.n.01", "name": "backspace_key"}, - {"id": 5564, "synset": "backstairs.n.01", "name": "backstairs"}, - {"id": 5565, "synset": "backstay.n.01", "name": "backstay"}, - {"id": 5566, "synset": "backstop.n.02", "name": "backstop"}, - {"id": 5567, "synset": "backsword.n.02", "name": "backsword"}, - {"id": 5568, "synset": "backup_system.n.01", "name": "backup_system"}, - {"id": 5569, "synset": "badminton_court.n.01", "name": "badminton_court"}, - {"id": 5570, "synset": "badminton_equipment.n.01", "name": "badminton_equipment"}, - {"id": 5571, "synset": "badminton_racket.n.01", "name": "badminton_racket"}, - {"id": 5572, "synset": "bag.n.01", "name": "bag"}, - {"id": 5573, "synset": "baggage.n.01", "name": "baggage"}, - {"id": 5574, "synset": "baggage.n.03", "name": "baggage"}, - {"id": 5575, "synset": "baggage_car.n.01", "name": "baggage_car"}, - {"id": 5576, "synset": "baggage_claim.n.01", "name": "baggage_claim"}, - {"id": 5577, "synset": "bailey.n.04", "name": "bailey"}, - {"id": 5578, "synset": "bailey.n.03", "name": "bailey"}, - {"id": 5579, "synset": "bailey_bridge.n.01", "name": "Bailey_bridge"}, - {"id": 5580, "synset": "bain-marie.n.01", "name": "bain-marie"}, - {"id": 5581, "synset": "baize.n.01", "name": "baize"}, - {"id": 5582, "synset": "bakery.n.01", "name": "bakery"}, - {"id": 5583, "synset": "balaclava.n.01", "name": "balaclava"}, - {"id": 5584, "synset": "balalaika.n.01", "name": "balalaika"}, - {"id": 5585, "synset": "balance.n.12", "name": "balance"}, - {"id": 5586, "synset": "balance_beam.n.01", "name": "balance_beam"}, - {"id": 5587, "synset": "balance_wheel.n.01", "name": "balance_wheel"}, - {"id": 5588, "synset": "balbriggan.n.01", "name": "balbriggan"}, - {"id": 5589, "synset": "balcony.n.02", "name": "balcony"}, - {"id": 5590, "synset": "balcony.n.01", "name": "balcony"}, - {"id": 5591, "synset": "baldachin.n.01", "name": "baldachin"}, - {"id": 5592, "synset": "baldric.n.01", "name": "baldric"}, - {"id": 5593, "synset": "bale.n.01", "name": "bale"}, - {"id": 5594, "synset": "baling_wire.n.01", "name": "baling_wire"}, - {"id": 5595, "synset": "ball.n.01", "name": "ball"}, - {"id": 5596, "synset": "ball_and_chain.n.01", "name": "ball_and_chain"}, - {"id": 5597, "synset": "ball-and-socket_joint.n.02", "name": "ball-and-socket_joint"}, - {"id": 5598, "synset": "ballast.n.05", "name": "ballast"}, - {"id": 5599, "synset": "ball_bearing.n.01", "name": "ball_bearing"}, - {"id": 5600, "synset": "ball_cartridge.n.01", "name": "ball_cartridge"}, - {"id": 5601, "synset": "ballcock.n.01", "name": "ballcock"}, - {"id": 5602, "synset": "balldress.n.01", "name": "balldress"}, - {"id": 5603, "synset": "ball_gown.n.01", "name": "ball_gown"}, - {"id": 5604, "synset": "ballistic_galvanometer.n.01", "name": "ballistic_galvanometer"}, - {"id": 5605, "synset": "ballistic_missile.n.01", "name": "ballistic_missile"}, - {"id": 5606, "synset": "ballistic_pendulum.n.01", "name": "ballistic_pendulum"}, - {"id": 5607, "synset": "ballistocardiograph.n.01", "name": "ballistocardiograph"}, - {"id": 5608, "synset": "balloon_bomb.n.01", "name": "balloon_bomb"}, - {"id": 5609, "synset": "balloon_sail.n.01", "name": "balloon_sail"}, - {"id": 5610, "synset": "ballot_box.n.01", "name": "ballot_box"}, - {"id": 5611, "synset": "ballpark.n.01", "name": "ballpark"}, - {"id": 5612, "synset": "ball-peen_hammer.n.01", "name": "ball-peen_hammer"}, - {"id": 5613, "synset": "ballpoint.n.01", "name": "ballpoint"}, - {"id": 5614, "synset": "ballroom.n.01", "name": "ballroom"}, - {"id": 5615, "synset": "ball_valve.n.01", "name": "ball_valve"}, - {"id": 5616, "synset": "balsa_raft.n.01", "name": "balsa_raft"}, - {"id": 5617, "synset": "baluster.n.01", "name": "baluster"}, - {"id": 5618, "synset": "banana_boat.n.01", "name": "banana_boat"}, - {"id": 5619, "synset": "band.n.13", "name": "band"}, - {"id": 5620, "synset": "bandbox.n.01", "name": "bandbox"}, - {"id": 5621, "synset": "banderilla.n.01", "name": "banderilla"}, - {"id": 5622, "synset": "bandoleer.n.01", "name": "bandoleer"}, - {"id": 5623, "synset": "bandoneon.n.01", "name": "bandoneon"}, - {"id": 5624, "synset": "bandsaw.n.01", "name": "bandsaw"}, - {"id": 5625, "synset": "bandwagon.n.02", "name": "bandwagon"}, - {"id": 5626, "synset": "bangalore_torpedo.n.01", "name": "bangalore_torpedo"}, - {"id": 5627, "synset": "bangle.n.02", "name": "bangle"}, - {"id": 5628, "synset": "bannister.n.02", "name": "bannister"}, - {"id": 5629, "synset": "banquette.n.01", "name": "banquette"}, - {"id": 5630, "synset": "banyan.n.02", "name": "banyan"}, - {"id": 5631, "synset": "baptismal_font.n.01", "name": "baptismal_font"}, - {"id": 5632, "synset": "bar.n.03", "name": "bar"}, - {"id": 5633, "synset": "bar.n.02", "name": "bar"}, - {"id": 5634, "synset": "barbecue.n.03", "name": "barbecue"}, - {"id": 5635, "synset": "barbed_wire.n.01", "name": "barbed_wire"}, - {"id": 5636, "synset": "barber_chair.n.01", "name": "barber_chair"}, - {"id": 5637, "synset": "barbershop.n.01", "name": "barbershop"}, - {"id": 5638, "synset": "barbette_carriage.n.01", "name": "barbette_carriage"}, - {"id": 5639, "synset": "barbican.n.01", "name": "barbican"}, - {"id": 5640, "synset": "bar_bit.n.01", "name": "bar_bit"}, - {"id": 5641, "synset": "bareboat.n.01", "name": "bareboat"}, - {"id": 5642, "synset": "barge_pole.n.01", "name": "barge_pole"}, - {"id": 5643, "synset": "baritone.n.03", "name": "baritone"}, - {"id": 5644, "synset": "bark.n.03", "name": "bark"}, - {"id": 5645, "synset": "bar_magnet.n.01", "name": "bar_magnet"}, - {"id": 5646, "synset": "bar_mask.n.01", "name": "bar_mask"}, - {"id": 5647, "synset": "barn.n.01", "name": "barn"}, - {"id": 5648, "synset": "barndoor.n.01", "name": "barndoor"}, - {"id": 5649, "synset": "barn_door.n.01", "name": "barn_door"}, - {"id": 5650, "synset": "barnyard.n.01", "name": "barnyard"}, - {"id": 5651, "synset": "barograph.n.01", "name": "barograph"}, - {"id": 5652, "synset": "barometer.n.01", "name": "barometer"}, - {"id": 5653, "synset": "barong.n.01", "name": "barong"}, - {"id": 5654, "synset": "barouche.n.01", "name": "barouche"}, - {"id": 5655, "synset": "bar_printer.n.01", "name": "bar_printer"}, - {"id": 5656, "synset": "barrack.n.01", "name": "barrack"}, - {"id": 5657, "synset": "barrage_balloon.n.01", "name": "barrage_balloon"}, - {"id": 5658, "synset": "barrel.n.01", "name": "barrel"}, - {"id": 5659, "synset": "barrelhouse.n.01", "name": "barrelhouse"}, - {"id": 5660, "synset": "barrel_knot.n.01", "name": "barrel_knot"}, - {"id": 5661, "synset": "barrel_organ.n.01", "name": "barrel_organ"}, - {"id": 5662, "synset": "barrel_vault.n.01", "name": "barrel_vault"}, - {"id": 5663, "synset": "barricade.n.02", "name": "barricade"}, - {"id": 5664, "synset": "barrier.n.01", "name": "barrier"}, - {"id": 5665, "synset": "barroom.n.01", "name": "barroom"}, - {"id": 5666, "synset": "bascule.n.01", "name": "bascule"}, - {"id": 5667, "synset": "base.n.08", "name": "base"}, - {"id": 5668, "synset": "baseball_equipment.n.01", "name": "baseball_equipment"}, - {"id": 5669, "synset": "basement.n.01", "name": "basement"}, - {"id": 5670, "synset": "basement.n.02", "name": "basement"}, - { - "id": 5671, - "synset": "basic_point_defense_missile_system.n.01", - "name": "basic_point_defense_missile_system", - }, - {"id": 5672, "synset": "basilica.n.02", "name": "basilica"}, - {"id": 5673, "synset": "basilica.n.01", "name": "basilica"}, - {"id": 5674, "synset": "basilisk.n.02", "name": "basilisk"}, - {"id": 5675, "synset": "basin.n.01", "name": "basin"}, - {"id": 5676, "synset": "basinet.n.01", "name": "basinet"}, - {"id": 5677, "synset": "basket.n.03", "name": "basket"}, - {"id": 5678, "synset": "basketball_court.n.01", "name": "basketball_court"}, - {"id": 5679, "synset": "basketball_equipment.n.01", "name": "basketball_equipment"}, - {"id": 5680, "synset": "basket_weave.n.01", "name": "basket_weave"}, - {"id": 5681, "synset": "bass.n.07", "name": "bass"}, - {"id": 5682, "synset": "bass_clarinet.n.01", "name": "bass_clarinet"}, - {"id": 5683, "synset": "bass_drum.n.01", "name": "bass_drum"}, - {"id": 5684, "synset": "basset_horn.n.01", "name": "basset_horn"}, - {"id": 5685, "synset": "bass_fiddle.n.01", "name": "bass_fiddle"}, - {"id": 5686, "synset": "bass_guitar.n.01", "name": "bass_guitar"}, - {"id": 5687, "synset": "bassinet.n.01", "name": "bassinet"}, - {"id": 5688, "synset": "bassinet.n.02", "name": "bassinet"}, - {"id": 5689, "synset": "bassoon.n.01", "name": "bassoon"}, - {"id": 5690, "synset": "baster.n.03", "name": "baster"}, - {"id": 5691, "synset": "bastinado.n.01", "name": "bastinado"}, - {"id": 5692, "synset": "bastion.n.03", "name": "bastion"}, - {"id": 5693, "synset": "bastion.n.02", "name": "bastion"}, - {"id": 5694, "synset": "bat.n.05", "name": "bat"}, - {"id": 5695, "synset": "bath.n.01", "name": "bath"}, - {"id": 5696, "synset": "bath_chair.n.01", "name": "bath_chair"}, - {"id": 5697, "synset": "bathhouse.n.02", "name": "bathhouse"}, - {"id": 5698, "synset": "bathhouse.n.01", "name": "bathhouse"}, - {"id": 5699, "synset": "bathing_cap.n.01", "name": "bathing_cap"}, - {"id": 5700, "synset": "bath_oil.n.01", "name": "bath_oil"}, - {"id": 5701, "synset": "bathroom.n.01", "name": "bathroom"}, - {"id": 5702, "synset": "bath_salts.n.01", "name": "bath_salts"}, - {"id": 5703, "synset": "bathyscaphe.n.01", "name": "bathyscaphe"}, - {"id": 5704, "synset": "bathysphere.n.01", "name": "bathysphere"}, - {"id": 5705, "synset": "batik.n.01", "name": "batik"}, - {"id": 5706, "synset": "batiste.n.01", "name": "batiste"}, - {"id": 5707, "synset": "baton.n.01", "name": "baton"}, - {"id": 5708, "synset": "baton.n.05", "name": "baton"}, - {"id": 5709, "synset": "baton.n.04", "name": "baton"}, - {"id": 5710, "synset": "baton.n.03", "name": "baton"}, - {"id": 5711, "synset": "battering_ram.n.01", "name": "battering_ram"}, - {"id": 5712, "synset": "batter's_box.n.01", "name": "batter's_box"}, - {"id": 5713, "synset": "battery.n.05", "name": "battery"}, - {"id": 5714, "synset": "batting_cage.n.01", "name": "batting_cage"}, - {"id": 5715, "synset": "batting_glove.n.01", "name": "batting_glove"}, - {"id": 5716, "synset": "batting_helmet.n.01", "name": "batting_helmet"}, - {"id": 5717, "synset": "battle-ax.n.01", "name": "battle-ax"}, - {"id": 5718, "synset": "battle_cruiser.n.01", "name": "battle_cruiser"}, - {"id": 5719, "synset": "battle_dress.n.01", "name": "battle_dress"}, - {"id": 5720, "synset": "battlement.n.01", "name": "battlement"}, - {"id": 5721, "synset": "battleship.n.01", "name": "battleship"}, - {"id": 5722, "synset": "battle_sight.n.01", "name": "battle_sight"}, - {"id": 5723, "synset": "bay.n.05", "name": "bay"}, - {"id": 5724, "synset": "bay.n.04", "name": "bay"}, - {"id": 5725, "synset": "bayonet.n.01", "name": "bayonet"}, - {"id": 5726, "synset": "bay_rum.n.01", "name": "bay_rum"}, - {"id": 5727, "synset": "bay_window.n.02", "name": "bay_window"}, - {"id": 5728, "synset": "bazaar.n.01", "name": "bazaar"}, - {"id": 5729, "synset": "bazaar.n.02", "name": "bazaar"}, - {"id": 5730, "synset": "bazooka.n.01", "name": "bazooka"}, - {"id": 5731, "synset": "b_battery.n.01", "name": "B_battery"}, - {"id": 5732, "synset": "bb_gun.n.01", "name": "BB_gun"}, - {"id": 5733, "synset": "beach_house.n.01", "name": "beach_house"}, - {"id": 5734, "synset": "beach_towel.n.01", "name": "beach_towel"}, - {"id": 5735, "synset": "beach_wagon.n.01", "name": "beach_wagon"}, - {"id": 5736, "synset": "beachwear.n.01", "name": "beachwear"}, - {"id": 5737, "synset": "beacon.n.03", "name": "beacon"}, - {"id": 5738, "synset": "beading_plane.n.01", "name": "beading_plane"}, - {"id": 5739, "synset": "beaker.n.02", "name": "beaker"}, - {"id": 5740, "synset": "beaker.n.01", "name": "beaker"}, - {"id": 5741, "synset": "beam.n.02", "name": "beam"}, - {"id": 5742, "synset": "beam_balance.n.01", "name": "beam_balance"}, - {"id": 5743, "synset": "bearing.n.06", "name": "bearing"}, - {"id": 5744, "synset": "bearing_rein.n.01", "name": "bearing_rein"}, - {"id": 5745, "synset": "bearing_wall.n.01", "name": "bearing_wall"}, - {"id": 5746, "synset": "bearskin.n.02", "name": "bearskin"}, - {"id": 5747, "synset": "beater.n.02", "name": "beater"}, - {"id": 5748, "synset": "beating-reed_instrument.n.01", "name": "beating-reed_instrument"}, - {"id": 5749, "synset": "beaver.n.06", "name": "beaver"}, - {"id": 5750, "synset": "beaver.n.05", "name": "beaver"}, - {"id": 5751, "synset": "beckman_thermometer.n.01", "name": "Beckman_thermometer"}, - {"id": 5752, "synset": "bed.n.08", "name": "bed"}, - {"id": 5753, "synset": "bed_and_breakfast.n.01", "name": "bed_and_breakfast"}, - {"id": 5754, "synset": "bedclothes.n.01", "name": "bedclothes"}, - {"id": 5755, "synset": "bedford_cord.n.01", "name": "Bedford_cord"}, - {"id": 5756, "synset": "bed_jacket.n.01", "name": "bed_jacket"}, - {"id": 5757, "synset": "bedpost.n.01", "name": "bedpost"}, - {"id": 5758, "synset": "bedroll.n.01", "name": "bedroll"}, - {"id": 5759, "synset": "bedroom.n.01", "name": "bedroom"}, - {"id": 5760, "synset": "bedroom_furniture.n.01", "name": "bedroom_furniture"}, - {"id": 5761, "synset": "bedsitting_room.n.01", "name": "bedsitting_room"}, - {"id": 5762, "synset": "bedspring.n.01", "name": "bedspring"}, - {"id": 5763, "synset": "bedstead.n.01", "name": "bedstead"}, - {"id": 5764, "synset": "beefcake.n.01", "name": "beefcake"}, - {"id": 5765, "synset": "beehive.n.04", "name": "beehive"}, - {"id": 5766, "synset": "beer_barrel.n.01", "name": "beer_barrel"}, - {"id": 5767, "synset": "beer_garden.n.01", "name": "beer_garden"}, - {"id": 5768, "synset": "beer_glass.n.01", "name": "beer_glass"}, - {"id": 5769, "synset": "beer_hall.n.01", "name": "beer_hall"}, - {"id": 5770, "synset": "beer_mat.n.01", "name": "beer_mat"}, - {"id": 5771, "synset": "beer_mug.n.01", "name": "beer_mug"}, - {"id": 5772, "synset": "belaying_pin.n.01", "name": "belaying_pin"}, - {"id": 5773, "synset": "belfry.n.02", "name": "belfry"}, - {"id": 5774, "synset": "bell_arch.n.01", "name": "bell_arch"}, - {"id": 5775, "synset": "bellarmine.n.02", "name": "bellarmine"}, - {"id": 5776, "synset": "bellbottom_trousers.n.01", "name": "bellbottom_trousers"}, - {"id": 5777, "synset": "bell_cote.n.01", "name": "bell_cote"}, - {"id": 5778, "synset": "bell_foundry.n.01", "name": "bell_foundry"}, - {"id": 5779, "synset": "bell_gable.n.01", "name": "bell_gable"}, - {"id": 5780, "synset": "bell_jar.n.01", "name": "bell_jar"}, - {"id": 5781, "synset": "bellows.n.01", "name": "bellows"}, - {"id": 5782, "synset": "bellpull.n.01", "name": "bellpull"}, - {"id": 5783, "synset": "bell_push.n.01", "name": "bell_push"}, - {"id": 5784, "synset": "bell_seat.n.01", "name": "bell_seat"}, - {"id": 5785, "synset": "bell_tent.n.01", "name": "bell_tent"}, - {"id": 5786, "synset": "bell_tower.n.01", "name": "bell_tower"}, - {"id": 5787, "synset": "bellyband.n.01", "name": "bellyband"}, - {"id": 5788, "synset": "belt.n.06", "name": "belt"}, - {"id": 5789, "synset": "belting.n.01", "name": "belting"}, - {"id": 5790, "synset": "bench_clamp.n.01", "name": "bench_clamp"}, - {"id": 5791, "synset": "bench_hook.n.01", "name": "bench_hook"}, - {"id": 5792, "synset": "bench_lathe.n.01", "name": "bench_lathe"}, - {"id": 5793, "synset": "bench_press.n.02", "name": "bench_press"}, - {"id": 5794, "synset": "bender.n.01", "name": "bender"}, - {"id": 5795, "synset": "berlin.n.03", "name": "berlin"}, - {"id": 5796, "synset": "bermuda_shorts.n.01", "name": "Bermuda_shorts"}, - {"id": 5797, "synset": "berth.n.03", "name": "berth"}, - {"id": 5798, "synset": "besom.n.01", "name": "besom"}, - {"id": 5799, "synset": "bessemer_converter.n.01", "name": "Bessemer_converter"}, - {"id": 5800, "synset": "bethel.n.01", "name": "bethel"}, - {"id": 5801, "synset": "betting_shop.n.01", "name": "betting_shop"}, - {"id": 5802, "synset": "bevatron.n.01", "name": "bevatron"}, - {"id": 5803, "synset": "bevel.n.02", "name": "bevel"}, - {"id": 5804, "synset": "bevel_gear.n.01", "name": "bevel_gear"}, - {"id": 5805, "synset": "b-flat_clarinet.n.01", "name": "B-flat_clarinet"}, - {"id": 5806, "synset": "bib.n.01", "name": "bib"}, - {"id": 5807, "synset": "bib-and-tucker.n.01", "name": "bib-and-tucker"}, - {"id": 5808, "synset": "bicorn.n.01", "name": "bicorn"}, - {"id": 5809, "synset": "bicycle-built-for-two.n.01", "name": "bicycle-built-for-two"}, - {"id": 5810, "synset": "bicycle_chain.n.01", "name": "bicycle_chain"}, - {"id": 5811, "synset": "bicycle_clip.n.01", "name": "bicycle_clip"}, - {"id": 5812, "synset": "bicycle_pump.n.01", "name": "bicycle_pump"}, - {"id": 5813, "synset": "bicycle_rack.n.01", "name": "bicycle_rack"}, - {"id": 5814, "synset": "bicycle_seat.n.01", "name": "bicycle_seat"}, - {"id": 5815, "synset": "bicycle_wheel.n.01", "name": "bicycle_wheel"}, - {"id": 5816, "synset": "bidet.n.01", "name": "bidet"}, - {"id": 5817, "synset": "bier.n.02", "name": "bier"}, - {"id": 5818, "synset": "bier.n.01", "name": "bier"}, - {"id": 5819, "synset": "bi-fold_door.n.01", "name": "bi-fold_door"}, - {"id": 5820, "synset": "bifocals.n.01", "name": "bifocals"}, - {"id": 5821, "synset": "big_blue.n.01", "name": "Big_Blue"}, - {"id": 5822, "synset": "big_board.n.02", "name": "big_board"}, - {"id": 5823, "synset": "bight.n.04", "name": "bight"}, - {"id": 5824, "synset": "bikini.n.02", "name": "bikini"}, - {"id": 5825, "synset": "bikini_pants.n.01", "name": "bikini_pants"}, - {"id": 5826, "synset": "bilge.n.02", "name": "bilge"}, - {"id": 5827, "synset": "bilge_keel.n.01", "name": "bilge_keel"}, - {"id": 5828, "synset": "bilge_pump.n.01", "name": "bilge_pump"}, - {"id": 5829, "synset": "bilge_well.n.01", "name": "bilge_well"}, - {"id": 5830, "synset": "bill.n.08", "name": "bill"}, - {"id": 5831, "synset": "billiard_ball.n.01", "name": "billiard_ball"}, - {"id": 5832, "synset": "billiard_room.n.01", "name": "billiard_room"}, - {"id": 5833, "synset": "bin.n.01", "name": "bin"}, - {"id": 5834, "synset": "binder.n.04", "name": "binder"}, - {"id": 5835, "synset": "bindery.n.01", "name": "bindery"}, - {"id": 5836, "synset": "binding.n.05", "name": "binding"}, - {"id": 5837, "synset": "bin_liner.n.01", "name": "bin_liner"}, - {"id": 5838, "synset": "binnacle.n.01", "name": "binnacle"}, - {"id": 5839, "synset": "binocular_microscope.n.01", "name": "binocular_microscope"}, - {"id": 5840, "synset": "biochip.n.01", "name": "biochip"}, - {"id": 5841, "synset": "biohazard_suit.n.01", "name": "biohazard_suit"}, - {"id": 5842, "synset": "bioscope.n.02", "name": "bioscope"}, - {"id": 5843, "synset": "biplane.n.01", "name": "biplane"}, - {"id": 5844, "synset": "birch.n.03", "name": "birch"}, - {"id": 5845, "synset": "birchbark_canoe.n.01", "name": "birchbark_canoe"}, - {"id": 5846, "synset": "birdcall.n.02", "name": "birdcall"}, - {"id": 5847, "synset": "bird_shot.n.01", "name": "bird_shot"}, - {"id": 5848, "synset": "biretta.n.01", "name": "biretta"}, - {"id": 5849, "synset": "bishop.n.03", "name": "bishop"}, - {"id": 5850, "synset": "bistro.n.01", "name": "bistro"}, - {"id": 5851, "synset": "bit.n.11", "name": "bit"}, - {"id": 5852, "synset": "bit.n.05", "name": "bit"}, - {"id": 5853, "synset": "bite_plate.n.01", "name": "bite_plate"}, - {"id": 5854, "synset": "bitewing.n.01", "name": "bitewing"}, - {"id": 5855, "synset": "bitumastic.n.01", "name": "bitumastic"}, - {"id": 5856, "synset": "black.n.07", "name": "black"}, - {"id": 5857, "synset": "black.n.06", "name": "black"}, - {"id": 5858, "synset": "blackboard_eraser.n.01", "name": "blackboard_eraser"}, - {"id": 5859, "synset": "black_box.n.01", "name": "black_box"}, - {"id": 5860, "synset": "blackface.n.01", "name": "blackface"}, - {"id": 5861, "synset": "blackjack.n.02", "name": "blackjack"}, - {"id": 5862, "synset": "black_tie.n.02", "name": "black_tie"}, - {"id": 5863, "synset": "blackwash.n.03", "name": "blackwash"}, - {"id": 5864, "synset": "bladder.n.02", "name": "bladder"}, - {"id": 5865, "synset": "blade.n.09", "name": "blade"}, - {"id": 5866, "synset": "blade.n.08", "name": "blade"}, - {"id": 5867, "synset": "blade.n.07", "name": "blade"}, - {"id": 5868, "synset": "blank.n.04", "name": "blank"}, - {"id": 5869, "synset": "blast_furnace.n.01", "name": "blast_furnace"}, - {"id": 5870, "synset": "blasting_cap.n.01", "name": "blasting_cap"}, - {"id": 5871, "synset": "blind.n.03", "name": "blind"}, - {"id": 5872, "synset": "blind_curve.n.01", "name": "blind_curve"}, - {"id": 5873, "synset": "blindfold.n.01", "name": "blindfold"}, - {"id": 5874, "synset": "bling.n.01", "name": "bling"}, - {"id": 5875, "synset": "blister_pack.n.01", "name": "blister_pack"}, - {"id": 5876, "synset": "block.n.05", "name": "block"}, - {"id": 5877, "synset": "blockade.n.02", "name": "blockade"}, - {"id": 5878, "synset": "blockade-runner.n.01", "name": "blockade-runner"}, - {"id": 5879, "synset": "block_and_tackle.n.01", "name": "block_and_tackle"}, - {"id": 5880, "synset": "blockbuster.n.01", "name": "blockbuster"}, - {"id": 5881, "synset": "blockhouse.n.01", "name": "blockhouse"}, - {"id": 5882, "synset": "block_plane.n.01", "name": "block_plane"}, - {"id": 5883, "synset": "bloodmobile.n.01", "name": "bloodmobile"}, - {"id": 5884, "synset": "bloomers.n.01", "name": "bloomers"}, - {"id": 5885, "synset": "blower.n.01", "name": "blower"}, - {"id": 5886, "synset": "blowtorch.n.01", "name": "blowtorch"}, - {"id": 5887, "synset": "blucher.n.02", "name": "blucher"}, - {"id": 5888, "synset": "bludgeon.n.01", "name": "bludgeon"}, - {"id": 5889, "synset": "blue.n.02", "name": "blue"}, - {"id": 5890, "synset": "blue_chip.n.02", "name": "blue_chip"}, - {"id": 5891, "synset": "blunderbuss.n.01", "name": "blunderbuss"}, - {"id": 5892, "synset": "blunt_file.n.01", "name": "blunt_file"}, - {"id": 5893, "synset": "boarding.n.02", "name": "boarding"}, - {"id": 5894, "synset": "boarding_house.n.01", "name": "boarding_house"}, - {"id": 5895, "synset": "boardroom.n.01", "name": "boardroom"}, - {"id": 5896, "synset": "boards.n.02", "name": "boards"}, - {"id": 5897, "synset": "boater.n.01", "name": "boater"}, - {"id": 5898, "synset": "boat_hook.n.01", "name": "boat_hook"}, - {"id": 5899, "synset": "boathouse.n.01", "name": "boathouse"}, - {"id": 5900, "synset": "boatswain's_chair.n.01", "name": "boatswain's_chair"}, - {"id": 5901, "synset": "boat_train.n.01", "name": "boat_train"}, - {"id": 5902, "synset": "boatyard.n.01", "name": "boatyard"}, - {"id": 5903, "synset": "bobsled.n.02", "name": "bobsled"}, - {"id": 5904, "synset": "bobsled.n.01", "name": "bobsled"}, - {"id": 5905, "synset": "bocce_ball.n.01", "name": "bocce_ball"}, - {"id": 5906, "synset": "bodega.n.01", "name": "bodega"}, - {"id": 5907, "synset": "bodice.n.01", "name": "bodice"}, - {"id": 5908, "synset": "bodkin.n.04", "name": "bodkin"}, - {"id": 5909, "synset": "bodkin.n.03", "name": "bodkin"}, - {"id": 5910, "synset": "bodkin.n.02", "name": "bodkin"}, - {"id": 5911, "synset": "body.n.11", "name": "body"}, - {"id": 5912, "synset": "body_armor.n.01", "name": "body_armor"}, - {"id": 5913, "synset": "body_lotion.n.01", "name": "body_lotion"}, - {"id": 5914, "synset": "body_stocking.n.01", "name": "body_stocking"}, - {"id": 5915, "synset": "body_plethysmograph.n.01", "name": "body_plethysmograph"}, - {"id": 5916, "synset": "body_pad.n.01", "name": "body_pad"}, - {"id": 5917, "synset": "bodywork.n.01", "name": "bodywork"}, - {"id": 5918, "synset": "bofors_gun.n.01", "name": "Bofors_gun"}, - {"id": 5919, "synset": "bogy.n.01", "name": "bogy"}, - {"id": 5920, "synset": "boiler.n.01", "name": "boiler"}, - {"id": 5921, "synset": "boiling_water_reactor.n.01", "name": "boiling_water_reactor"}, - {"id": 5922, "synset": "bolero.n.02", "name": "bolero"}, - {"id": 5923, "synset": "bollard.n.01", "name": "bollard"}, - {"id": 5924, "synset": "bolo.n.02", "name": "bolo"}, - {"id": 5925, "synset": "bolt.n.02", "name": "bolt"}, - {"id": 5926, "synset": "bolt_cutter.n.01", "name": "bolt_cutter"}, - {"id": 5927, "synset": "bomb.n.01", "name": "bomb"}, - {"id": 5928, "synset": "bombazine.n.01", "name": "bombazine"}, - {"id": 5929, "synset": "bomb_calorimeter.n.01", "name": "bomb_calorimeter"}, - {"id": 5930, "synset": "bomber.n.01", "name": "bomber"}, - {"id": 5931, "synset": "bomber_jacket.n.01", "name": "bomber_jacket"}, - {"id": 5932, "synset": "bomblet.n.01", "name": "bomblet"}, - {"id": 5933, "synset": "bomb_rack.n.01", "name": "bomb_rack"}, - {"id": 5934, "synset": "bombshell.n.03", "name": "bombshell"}, - {"id": 5935, "synset": "bomb_shelter.n.01", "name": "bomb_shelter"}, - {"id": 5936, "synset": "bone-ash_cup.n.01", "name": "bone-ash_cup"}, - {"id": 5937, "synset": "bone_china.n.01", "name": "bone_china"}, - {"id": 5938, "synset": "bones.n.01", "name": "bones"}, - {"id": 5939, "synset": "boneshaker.n.01", "name": "boneshaker"}, - {"id": 5940, "synset": "bongo.n.01", "name": "bongo"}, - {"id": 5941, "synset": "book.n.11", "name": "book"}, - {"id": 5942, "synset": "book_bag.n.01", "name": "book_bag"}, - {"id": 5943, "synset": "bookbindery.n.01", "name": "bookbindery"}, - {"id": 5944, "synset": "bookend.n.01", "name": "bookend"}, - {"id": 5945, "synset": "bookmobile.n.01", "name": "bookmobile"}, - {"id": 5946, "synset": "bookshelf.n.01", "name": "bookshelf"}, - {"id": 5947, "synset": "bookshop.n.01", "name": "bookshop"}, - {"id": 5948, "synset": "boom.n.05", "name": "boom"}, - {"id": 5949, "synset": "boomerang.n.01", "name": "boomerang"}, - {"id": 5950, "synset": "booster.n.05", "name": "booster"}, - {"id": 5951, "synset": "booster.n.04", "name": "booster"}, - {"id": 5952, "synset": "boot.n.04", "name": "boot"}, - {"id": 5953, "synset": "boot_camp.n.01", "name": "boot_camp"}, - {"id": 5954, "synset": "bootee.n.01", "name": "bootee"}, - {"id": 5955, "synset": "booth.n.02", "name": "booth"}, - {"id": 5956, "synset": "booth.n.04", "name": "booth"}, - {"id": 5957, "synset": "booth.n.01", "name": "booth"}, - {"id": 5958, "synset": "boothose.n.01", "name": "boothose"}, - {"id": 5959, "synset": "bootjack.n.01", "name": "bootjack"}, - {"id": 5960, "synset": "bootlace.n.01", "name": "bootlace"}, - {"id": 5961, "synset": "bootleg.n.02", "name": "bootleg"}, - {"id": 5962, "synset": "bootstrap.n.01", "name": "bootstrap"}, - {"id": 5963, "synset": "bore_bit.n.01", "name": "bore_bit"}, - {"id": 5964, "synset": "boron_chamber.n.01", "name": "boron_chamber"}, - {"id": 5965, "synset": "borstal.n.01", "name": "borstal"}, - {"id": 5966, "synset": "bosom.n.03", "name": "bosom"}, - {"id": 5967, "synset": "boston_rocker.n.01", "name": "Boston_rocker"}, - {"id": 5968, "synset": "bota.n.01", "name": "bota"}, - {"id": 5969, "synset": "bottle.n.03", "name": "bottle"}, - {"id": 5970, "synset": "bottle_bank.n.01", "name": "bottle_bank"}, - {"id": 5971, "synset": "bottlebrush.n.01", "name": "bottlebrush"}, - {"id": 5972, "synset": "bottlecap.n.01", "name": "bottlecap"}, - {"id": 5973, "synset": "bottling_plant.n.01", "name": "bottling_plant"}, - {"id": 5974, "synset": "bottom.n.07", "name": "bottom"}, - {"id": 5975, "synset": "boucle.n.01", "name": "boucle"}, - {"id": 5976, "synset": "boudoir.n.01", "name": "boudoir"}, - {"id": 5977, "synset": "boulle.n.01", "name": "boulle"}, - {"id": 5978, "synset": "bouncing_betty.n.01", "name": "bouncing_betty"}, - {"id": 5979, "synset": "boutique.n.01", "name": "boutique"}, - {"id": 5980, "synset": "boutonniere.n.01", "name": "boutonniere"}, - {"id": 5981, "synset": "bow.n.02", "name": "bow"}, - {"id": 5982, "synset": "bow.n.01", "name": "bow"}, - {"id": 5983, "synset": "bow_and_arrow.n.01", "name": "bow_and_arrow"}, - {"id": 5984, "synset": "bowed_stringed_instrument.n.01", "name": "bowed_stringed_instrument"}, - {"id": 5985, "synset": "bowie_knife.n.01", "name": "Bowie_knife"}, - {"id": 5986, "synset": "bowl.n.01", "name": "bowl"}, - {"id": 5987, "synset": "bowl.n.07", "name": "bowl"}, - {"id": 5988, "synset": "bowline.n.01", "name": "bowline"}, - {"id": 5989, "synset": "bowling_alley.n.01", "name": "bowling_alley"}, - {"id": 5990, "synset": "bowling_equipment.n.01", "name": "bowling_equipment"}, - {"id": 5991, "synset": "bowling_pin.n.01", "name": "bowling_pin"}, - {"id": 5992, "synset": "bowling_shoe.n.01", "name": "bowling_shoe"}, - {"id": 5993, "synset": "bowsprit.n.01", "name": "bowsprit"}, - {"id": 5994, "synset": "bowstring.n.01", "name": "bowstring"}, - {"id": 5995, "synset": "box.n.02", "name": "box"}, - {"id": 5996, "synset": "box.n.08", "name": "box"}, - {"id": 5997, "synset": "box_beam.n.01", "name": "box_beam"}, - {"id": 5998, "synset": "box_camera.n.01", "name": "box_camera"}, - {"id": 5999, "synset": "boxcar.n.01", "name": "boxcar"}, - {"id": 6000, "synset": "box_coat.n.01", "name": "box_coat"}, - {"id": 6001, "synset": "boxing_equipment.n.01", "name": "boxing_equipment"}, - {"id": 6002, "synset": "box_office.n.02", "name": "box_office"}, - {"id": 6003, "synset": "box_spring.n.01", "name": "box_spring"}, - {"id": 6004, "synset": "box_wrench.n.01", "name": "box_wrench"}, - {"id": 6005, "synset": "brace.n.09", "name": "brace"}, - {"id": 6006, "synset": "brace.n.07", "name": "brace"}, - {"id": 6007, "synset": "brace.n.01", "name": "brace"}, - {"id": 6008, "synset": "brace_and_bit.n.01", "name": "brace_and_bit"}, - {"id": 6009, "synset": "bracer.n.01", "name": "bracer"}, - {"id": 6010, "synset": "brace_wrench.n.01", "name": "brace_wrench"}, - {"id": 6011, "synset": "bracket.n.04", "name": "bracket"}, - {"id": 6012, "synset": "bradawl.n.01", "name": "bradawl"}, - {"id": 6013, "synset": "brake.n.01", "name": "brake"}, - {"id": 6014, "synset": "brake.n.05", "name": "brake"}, - {"id": 6015, "synset": "brake_band.n.01", "name": "brake_band"}, - {"id": 6016, "synset": "brake_cylinder.n.01", "name": "brake_cylinder"}, - {"id": 6017, "synset": "brake_disk.n.01", "name": "brake_disk"}, - {"id": 6018, "synset": "brake_drum.n.01", "name": "brake_drum"}, - {"id": 6019, "synset": "brake_lining.n.01", "name": "brake_lining"}, - {"id": 6020, "synset": "brake_pad.n.01", "name": "brake_pad"}, - {"id": 6021, "synset": "brake_pedal.n.01", "name": "brake_pedal"}, - {"id": 6022, "synset": "brake_shoe.n.01", "name": "brake_shoe"}, - {"id": 6023, "synset": "brake_system.n.01", "name": "brake_system"}, - {"id": 6024, "synset": "brass.n.02", "name": "brass"}, - {"id": 6025, "synset": "brass.n.05", "name": "brass"}, - {"id": 6026, "synset": "brassard.n.01", "name": "brassard"}, - {"id": 6027, "synset": "brasserie.n.01", "name": "brasserie"}, - {"id": 6028, "synset": "brassie.n.01", "name": "brassie"}, - {"id": 6029, "synset": "brass_knucks.n.01", "name": "brass_knucks"}, - {"id": 6030, "synset": "brattice.n.01", "name": "brattice"}, - {"id": 6031, "synset": "brazier.n.01", "name": "brazier"}, - {"id": 6032, "synset": "breadbasket.n.03", "name": "breadbasket"}, - {"id": 6033, "synset": "bread_knife.n.01", "name": "bread_knife"}, - {"id": 6034, "synset": "breakable.n.01", "name": "breakable"}, - {"id": 6035, "synset": "breakfast_area.n.01", "name": "breakfast_area"}, - {"id": 6036, "synset": "breakfast_table.n.01", "name": "breakfast_table"}, - {"id": 6037, "synset": "breakwater.n.01", "name": "breakwater"}, - {"id": 6038, "synset": "breast_drill.n.01", "name": "breast_drill"}, - {"id": 6039, "synset": "breast_implant.n.01", "name": "breast_implant"}, - {"id": 6040, "synset": "breastplate.n.01", "name": "breastplate"}, - {"id": 6041, "synset": "breast_pocket.n.01", "name": "breast_pocket"}, - {"id": 6042, "synset": "breathalyzer.n.01", "name": "breathalyzer"}, - {"id": 6043, "synset": "breechblock.n.01", "name": "breechblock"}, - {"id": 6044, "synset": "breeches.n.01", "name": "breeches"}, - {"id": 6045, "synset": "breeches_buoy.n.01", "name": "breeches_buoy"}, - {"id": 6046, "synset": "breechloader.n.01", "name": "breechloader"}, - {"id": 6047, "synset": "breeder_reactor.n.01", "name": "breeder_reactor"}, - {"id": 6048, "synset": "bren.n.01", "name": "Bren"}, - {"id": 6049, "synset": "brewpub.n.01", "name": "brewpub"}, - {"id": 6050, "synset": "brick.n.01", "name": "brick"}, - {"id": 6051, "synset": "brickkiln.n.01", "name": "brickkiln"}, - {"id": 6052, "synset": "bricklayer's_hammer.n.01", "name": "bricklayer's_hammer"}, - {"id": 6053, "synset": "brick_trowel.n.01", "name": "brick_trowel"}, - {"id": 6054, "synset": "brickwork.n.01", "name": "brickwork"}, - {"id": 6055, "synset": "bridge.n.01", "name": "bridge"}, - {"id": 6056, "synset": "bridge.n.08", "name": "bridge"}, - {"id": 6057, "synset": "bridle.n.01", "name": "bridle"}, - {"id": 6058, "synset": "bridle_path.n.01", "name": "bridle_path"}, - {"id": 6059, "synset": "bridoon.n.01", "name": "bridoon"}, - {"id": 6060, "synset": "briefcase_bomb.n.01", "name": "briefcase_bomb"}, - {"id": 6061, "synset": "briefcase_computer.n.01", "name": "briefcase_computer"}, - {"id": 6062, "synset": "briefs.n.01", "name": "briefs"}, - {"id": 6063, "synset": "brig.n.02", "name": "brig"}, - {"id": 6064, "synset": "brig.n.01", "name": "brig"}, - {"id": 6065, "synset": "brigandine.n.01", "name": "brigandine"}, - {"id": 6066, "synset": "brigantine.n.01", "name": "brigantine"}, - {"id": 6067, "synset": "brilliantine.n.01", "name": "brilliantine"}, - {"id": 6068, "synset": "brilliant_pebble.n.01", "name": "brilliant_pebble"}, - {"id": 6069, "synset": "brim.n.02", "name": "brim"}, - {"id": 6070, "synset": "bristle_brush.n.01", "name": "bristle_brush"}, - {"id": 6071, "synset": "britches.n.01", "name": "britches"}, - {"id": 6072, "synset": "broad_arrow.n.03", "name": "broad_arrow"}, - {"id": 6073, "synset": "broadax.n.01", "name": "broadax"}, - {"id": 6074, "synset": "brochette.n.01", "name": "brochette"}, - {"id": 6075, "synset": "broadcaster.n.02", "name": "broadcaster"}, - {"id": 6076, "synset": "broadcloth.n.02", "name": "broadcloth"}, - {"id": 6077, "synset": "broadcloth.n.01", "name": "broadcloth"}, - {"id": 6078, "synset": "broad_hatchet.n.01", "name": "broad_hatchet"}, - {"id": 6079, "synset": "broadloom.n.01", "name": "broadloom"}, - {"id": 6080, "synset": "broadside.n.03", "name": "broadside"}, - {"id": 6081, "synset": "broadsword.n.01", "name": "broadsword"}, - {"id": 6082, "synset": "brocade.n.01", "name": "brocade"}, - {"id": 6083, "synset": "brogan.n.01", "name": "brogan"}, - {"id": 6084, "synset": "broiler.n.01", "name": "broiler"}, - {"id": 6085, "synset": "broken_arch.n.01", "name": "broken_arch"}, - {"id": 6086, "synset": "bronchoscope.n.01", "name": "bronchoscope"}, - {"id": 6087, "synset": "broom_closet.n.01", "name": "broom_closet"}, - {"id": 6088, "synset": "broomstick.n.01", "name": "broomstick"}, - {"id": 6089, "synset": "brougham.n.01", "name": "brougham"}, - {"id": 6090, "synset": "browning_automatic_rifle.n.01", "name": "Browning_automatic_rifle"}, - {"id": 6091, "synset": "browning_machine_gun.n.01", "name": "Browning_machine_gun"}, - {"id": 6092, "synset": "brownstone.n.02", "name": "brownstone"}, - {"id": 6093, "synset": "brunch_coat.n.01", "name": "brunch_coat"}, - {"id": 6094, "synset": "brush.n.02", "name": "brush"}, - {"id": 6095, "synset": "brussels_carpet.n.01", "name": "Brussels_carpet"}, - {"id": 6096, "synset": "brussels_lace.n.01", "name": "Brussels_lace"}, - {"id": 6097, "synset": "bubble.n.04", "name": "bubble"}, - {"id": 6098, "synset": "bubble_chamber.n.01", "name": "bubble_chamber"}, - {"id": 6099, "synset": "bubble_jet_printer.n.01", "name": "bubble_jet_printer"}, - {"id": 6100, "synset": "buckboard.n.01", "name": "buckboard"}, - {"id": 6101, "synset": "bucket_seat.n.01", "name": "bucket_seat"}, - {"id": 6102, "synset": "bucket_shop.n.02", "name": "bucket_shop"}, - {"id": 6103, "synset": "buckle.n.01", "name": "buckle"}, - {"id": 6104, "synset": "buckram.n.01", "name": "buckram"}, - {"id": 6105, "synset": "bucksaw.n.01", "name": "bucksaw"}, - {"id": 6106, "synset": "buckskins.n.01", "name": "buckskins"}, - {"id": 6107, "synset": "buff.n.05", "name": "buff"}, - {"id": 6108, "synset": "buffer.n.05", "name": "buffer"}, - {"id": 6109, "synset": "buffer.n.04", "name": "buffer"}, - {"id": 6110, "synset": "buffet.n.01", "name": "buffet"}, - {"id": 6111, "synset": "buffing_wheel.n.01", "name": "buffing_wheel"}, - {"id": 6112, "synset": "bugle.n.01", "name": "bugle"}, - {"id": 6113, "synset": "building.n.01", "name": "building"}, - {"id": 6114, "synset": "building_complex.n.01", "name": "building_complex"}, - {"id": 6115, "synset": "bulldog_clip.n.01", "name": "bulldog_clip"}, - {"id": 6116, "synset": "bulldog_wrench.n.01", "name": "bulldog_wrench"}, - {"id": 6117, "synset": "bullet.n.01", "name": "bullet"}, - {"id": 6118, "synset": "bullion.n.02", "name": "bullion"}, - {"id": 6119, "synset": "bullnose.n.01", "name": "bullnose"}, - {"id": 6120, "synset": "bullpen.n.02", "name": "bullpen"}, - {"id": 6121, "synset": "bullpen.n.01", "name": "bullpen"}, - {"id": 6122, "synset": "bullring.n.01", "name": "bullring"}, - {"id": 6123, "synset": "bulwark.n.02", "name": "bulwark"}, - {"id": 6124, "synset": "bumboat.n.01", "name": "bumboat"}, - {"id": 6125, "synset": "bumper.n.02", "name": "bumper"}, - {"id": 6126, "synset": "bumper.n.01", "name": "bumper"}, - {"id": 6127, "synset": "bumper_car.n.01", "name": "bumper_car"}, - {"id": 6128, "synset": "bumper_guard.n.01", "name": "bumper_guard"}, - {"id": 6129, "synset": "bumper_jack.n.01", "name": "bumper_jack"}, - {"id": 6130, "synset": "bundle.n.02", "name": "bundle"}, - {"id": 6131, "synset": "bung.n.01", "name": "bung"}, - {"id": 6132, "synset": "bungalow.n.01", "name": "bungalow"}, - {"id": 6133, "synset": "bungee.n.01", "name": "bungee"}, - {"id": 6134, "synset": "bunghole.n.02", "name": "bunghole"}, - {"id": 6135, "synset": "bunk.n.03", "name": "bunk"}, - {"id": 6136, "synset": "bunk.n.01", "name": "bunk"}, - {"id": 6137, "synset": "bunker.n.01", "name": "bunker"}, - {"id": 6138, "synset": "bunker.n.03", "name": "bunker"}, - {"id": 6139, "synset": "bunker.n.02", "name": "bunker"}, - {"id": 6140, "synset": "bunsen_burner.n.01", "name": "bunsen_burner"}, - {"id": 6141, "synset": "bunting.n.01", "name": "bunting"}, - {"id": 6142, "synset": "bur.n.02", "name": "bur"}, - {"id": 6143, "synset": "burberry.n.01", "name": "Burberry"}, - {"id": 6144, "synset": "burette.n.01", "name": "burette"}, - {"id": 6145, "synset": "burglar_alarm.n.02", "name": "burglar_alarm"}, - {"id": 6146, "synset": "burial_chamber.n.01", "name": "burial_chamber"}, - {"id": 6147, "synset": "burial_garment.n.01", "name": "burial_garment"}, - {"id": 6148, "synset": "burial_mound.n.01", "name": "burial_mound"}, - {"id": 6149, "synset": "burin.n.01", "name": "burin"}, - {"id": 6150, "synset": "burqa.n.01", "name": "burqa"}, - {"id": 6151, "synset": "burlap.n.01", "name": "burlap"}, - {"id": 6152, "synset": "burn_bag.n.01", "name": "burn_bag"}, - {"id": 6153, "synset": "burner.n.01", "name": "burner"}, - {"id": 6154, "synset": "burnous.n.01", "name": "burnous"}, - {"id": 6155, "synset": "burp_gun.n.01", "name": "burp_gun"}, - {"id": 6156, "synset": "burr.n.04", "name": "burr"}, - {"id": 6157, "synset": "bushel_basket.n.01", "name": "bushel_basket"}, - {"id": 6158, "synset": "bushing.n.02", "name": "bushing"}, - {"id": 6159, "synset": "bush_jacket.n.01", "name": "bush_jacket"}, - {"id": 6160, "synset": "business_suit.n.01", "name": "business_suit"}, - {"id": 6161, "synset": "buskin.n.01", "name": "buskin"}, - {"id": 6162, "synset": "bustier.n.01", "name": "bustier"}, - {"id": 6163, "synset": "bustle.n.02", "name": "bustle"}, - {"id": 6164, "synset": "butcher_knife.n.01", "name": "butcher_knife"}, - {"id": 6165, "synset": "butcher_shop.n.01", "name": "butcher_shop"}, - {"id": 6166, "synset": "butter_dish.n.01", "name": "butter_dish"}, - {"id": 6167, "synset": "butterfly_valve.n.01", "name": "butterfly_valve"}, - {"id": 6168, "synset": "butter_knife.n.01", "name": "butter_knife"}, - {"id": 6169, "synset": "butt_hinge.n.01", "name": "butt_hinge"}, - {"id": 6170, "synset": "butt_joint.n.01", "name": "butt_joint"}, - {"id": 6171, "synset": "buttonhook.n.01", "name": "buttonhook"}, - {"id": 6172, "synset": "buttress.n.01", "name": "buttress"}, - {"id": 6173, "synset": "butt_shaft.n.01", "name": "butt_shaft"}, - {"id": 6174, "synset": "butt_weld.n.01", "name": "butt_weld"}, - {"id": 6175, "synset": "buzz_bomb.n.01", "name": "buzz_bomb"}, - {"id": 6176, "synset": "buzzer.n.02", "name": "buzzer"}, - {"id": 6177, "synset": "bvd.n.01", "name": "BVD"}, - {"id": 6178, "synset": "bypass_condenser.n.01", "name": "bypass_condenser"}, - {"id": 6179, "synset": "byway.n.01", "name": "byway"}, - {"id": 6180, "synset": "cab.n.02", "name": "cab"}, - {"id": 6181, "synset": "cab.n.01", "name": "cab"}, - {"id": 6182, "synset": "cabaret.n.01", "name": "cabaret"}, - {"id": 6183, "synset": "caber.n.01", "name": "caber"}, - {"id": 6184, "synset": "cabin.n.03", "name": "cabin"}, - {"id": 6185, "synset": "cabin.n.02", "name": "cabin"}, - {"id": 6186, "synset": "cabin_class.n.01", "name": "cabin_class"}, - {"id": 6187, "synset": "cabin_cruiser.n.01", "name": "cabin_cruiser"}, - {"id": 6188, "synset": "cabinet.n.04", "name": "cabinet"}, - {"id": 6189, "synset": "cabinetwork.n.01", "name": "cabinetwork"}, - {"id": 6190, "synset": "cabin_liner.n.01", "name": "cabin_liner"}, - {"id": 6191, "synset": "cable.n.06", "name": "cable"}, - {"id": 6192, "synset": "cable.n.02", "name": "cable"}, - {"id": 6193, "synset": "cable_car.n.01", "name": "cable_car"}, - {"id": 6194, "synset": "cache.n.03", "name": "cache"}, - {"id": 6195, "synset": "caddy.n.01", "name": "caddy"}, - {"id": 6196, "synset": "caesium_clock.n.01", "name": "caesium_clock"}, - {"id": 6197, "synset": "cafe.n.01", "name": "cafe"}, - {"id": 6198, "synset": "cafeteria.n.01", "name": "cafeteria"}, - {"id": 6199, "synset": "cafeteria_tray.n.01", "name": "cafeteria_tray"}, - {"id": 6200, "synset": "caff.n.01", "name": "caff"}, - {"id": 6201, "synset": "caftan.n.02", "name": "caftan"}, - {"id": 6202, "synset": "caftan.n.01", "name": "caftan"}, - {"id": 6203, "synset": "cage.n.01", "name": "cage"}, - {"id": 6204, "synset": "cage.n.04", "name": "cage"}, - {"id": 6205, "synset": "cagoule.n.01", "name": "cagoule"}, - {"id": 6206, "synset": "caisson.n.02", "name": "caisson"}, - {"id": 6207, "synset": "calash.n.02", "name": "calash"}, - {"id": 6208, "synset": "calceus.n.01", "name": "calceus"}, - {"id": 6209, "synset": "calcimine.n.01", "name": "calcimine"}, - {"id": 6210, "synset": "caldron.n.01", "name": "caldron"}, - {"id": 6211, "synset": "calico.n.01", "name": "calico"}, - {"id": 6212, "synset": "caliper.n.01", "name": "caliper"}, - {"id": 6213, "synset": "call-board.n.01", "name": "call-board"}, - {"id": 6214, "synset": "call_center.n.01", "name": "call_center"}, - {"id": 6215, "synset": "caller_id.n.01", "name": "caller_ID"}, - {"id": 6216, "synset": "calliope.n.02", "name": "calliope"}, - {"id": 6217, "synset": "calorimeter.n.01", "name": "calorimeter"}, - {"id": 6218, "synset": "calpac.n.01", "name": "calpac"}, - {"id": 6219, "synset": "camail.n.01", "name": "camail"}, - {"id": 6220, "synset": "camber_arch.n.01", "name": "camber_arch"}, - {"id": 6221, "synset": "cambric.n.01", "name": "cambric"}, - {"id": 6222, "synset": "camel's_hair.n.01", "name": "camel's_hair"}, - {"id": 6223, "synset": "camera_lucida.n.01", "name": "camera_lucida"}, - {"id": 6224, "synset": "camera_obscura.n.01", "name": "camera_obscura"}, - {"id": 6225, "synset": "camera_tripod.n.01", "name": "camera_tripod"}, - {"id": 6226, "synset": "camise.n.01", "name": "camise"}, - {"id": 6227, "synset": "camisole.n.02", "name": "camisole"}, - {"id": 6228, "synset": "camisole.n.01", "name": "camisole"}, - {"id": 6229, "synset": "camlet.n.02", "name": "camlet"}, - {"id": 6230, "synset": "camouflage.n.03", "name": "camouflage"}, - {"id": 6231, "synset": "camouflage.n.02", "name": "camouflage"}, - {"id": 6232, "synset": "camp.n.01", "name": "camp"}, - {"id": 6233, "synset": "camp.n.03", "name": "camp"}, - {"id": 6234, "synset": "camp.n.07", "name": "camp"}, - {"id": 6235, "synset": "campaign_hat.n.01", "name": "campaign_hat"}, - {"id": 6236, "synset": "campanile.n.01", "name": "campanile"}, - {"id": 6237, "synset": "camp_chair.n.01", "name": "camp_chair"}, - {"id": 6238, "synset": "camper_trailer.n.01", "name": "camper_trailer"}, - {"id": 6239, "synset": "campstool.n.01", "name": "campstool"}, - {"id": 6240, "synset": "camshaft.n.01", "name": "camshaft"}, - {"id": 6241, "synset": "canal.n.03", "name": "canal"}, - {"id": 6242, "synset": "canal_boat.n.01", "name": "canal_boat"}, - {"id": 6243, "synset": "candelabrum.n.01", "name": "candelabrum"}, - {"id": 6244, "synset": "candid_camera.n.01", "name": "candid_camera"}, - {"id": 6245, "synset": "candlepin.n.01", "name": "candlepin"}, - {"id": 6246, "synset": "candlesnuffer.n.01", "name": "candlesnuffer"}, - {"id": 6247, "synset": "candlewick.n.02", "name": "candlewick"}, - {"id": 6248, "synset": "candy_thermometer.n.01", "name": "candy_thermometer"}, - {"id": 6249, "synset": "cane.n.03", "name": "cane"}, - {"id": 6250, "synset": "cangue.n.01", "name": "cangue"}, - {"id": 6251, "synset": "cannery.n.01", "name": "cannery"}, - {"id": 6252, "synset": "cannikin.n.02", "name": "cannikin"}, - {"id": 6253, "synset": "cannikin.n.01", "name": "cannikin"}, - {"id": 6254, "synset": "cannon.n.01", "name": "cannon"}, - {"id": 6255, "synset": "cannon.n.04", "name": "cannon"}, - {"id": 6256, "synset": "cannon.n.03", "name": "cannon"}, - {"id": 6257, "synset": "cannon.n.02", "name": "cannon"}, - {"id": 6258, "synset": "cannonball.n.01", "name": "cannonball"}, - {"id": 6259, "synset": "canopic_jar.n.01", "name": "canopic_jar"}, - {"id": 6260, "synset": "canopy.n.03", "name": "canopy"}, - {"id": 6261, "synset": "canopy.n.02", "name": "canopy"}, - {"id": 6262, "synset": "canopy.n.01", "name": "canopy"}, - {"id": 6263, "synset": "canteen.n.05", "name": "canteen"}, - {"id": 6264, "synset": "canteen.n.04", "name": "canteen"}, - {"id": 6265, "synset": "canteen.n.03", "name": "canteen"}, - {"id": 6266, "synset": "canteen.n.02", "name": "canteen"}, - {"id": 6267, "synset": "cant_hook.n.01", "name": "cant_hook"}, - {"id": 6268, "synset": "cantilever.n.01", "name": "cantilever"}, - {"id": 6269, "synset": "cantilever_bridge.n.01", "name": "cantilever_bridge"}, - {"id": 6270, "synset": "cantle.n.01", "name": "cantle"}, - {"id": 6271, "synset": "canton_crepe.n.01", "name": "Canton_crepe"}, - {"id": 6272, "synset": "canvas.n.01", "name": "canvas"}, - {"id": 6273, "synset": "canvas.n.06", "name": "canvas"}, - {"id": 6274, "synset": "canvas_tent.n.01", "name": "canvas_tent"}, - {"id": 6275, "synset": "cap.n.04", "name": "cap"}, - {"id": 6276, "synset": "capacitor.n.01", "name": "capacitor"}, - {"id": 6277, "synset": "caparison.n.01", "name": "caparison"}, - {"id": 6278, "synset": "capital_ship.n.01", "name": "capital_ship"}, - {"id": 6279, "synset": "capitol.n.01", "name": "capitol"}, - {"id": 6280, "synset": "cap_opener.n.01", "name": "cap_opener"}, - {"id": 6281, "synset": "capote.n.02", "name": "capote"}, - {"id": 6282, "synset": "capote.n.01", "name": "capote"}, - {"id": 6283, "synset": "cap_screw.n.01", "name": "cap_screw"}, - {"id": 6284, "synset": "capstan.n.01", "name": "capstan"}, - {"id": 6285, "synset": "capstone.n.02", "name": "capstone"}, - {"id": 6286, "synset": "capsule.n.01", "name": "capsule"}, - {"id": 6287, "synset": "captain's_chair.n.01", "name": "captain's_chair"}, - {"id": 6288, "synset": "carabiner.n.01", "name": "carabiner"}, - {"id": 6289, "synset": "carafe.n.01", "name": "carafe"}, - {"id": 6290, "synset": "caravansary.n.01", "name": "caravansary"}, - {"id": 6291, "synset": "carbine.n.01", "name": "carbine"}, - {"id": 6292, "synset": "car_bomb.n.01", "name": "car_bomb"}, - {"id": 6293, "synset": "carbon_arc_lamp.n.01", "name": "carbon_arc_lamp"}, - {"id": 6294, "synset": "carboy.n.01", "name": "carboy"}, - {"id": 6295, "synset": "carburetor.n.01", "name": "carburetor"}, - {"id": 6296, "synset": "car_carrier.n.01", "name": "car_carrier"}, - {"id": 6297, "synset": "cardcase.n.01", "name": "cardcase"}, - {"id": 6298, "synset": "cardiac_monitor.n.01", "name": "cardiac_monitor"}, - {"id": 6299, "synset": "card_index.n.01", "name": "card_index"}, - {"id": 6300, "synset": "cardiograph.n.01", "name": "cardiograph"}, - {"id": 6301, "synset": "cardioid_microphone.n.01", "name": "cardioid_microphone"}, - {"id": 6302, "synset": "car_door.n.01", "name": "car_door"}, - {"id": 6303, "synset": "cardroom.n.01", "name": "cardroom"}, - {"id": 6304, "synset": "card_table.n.02", "name": "card_table"}, - {"id": 6305, "synset": "card_table.n.01", "name": "card_table"}, - {"id": 6306, "synset": "car-ferry.n.01", "name": "car-ferry"}, - {"id": 6307, "synset": "cargo_area.n.01", "name": "cargo_area"}, - {"id": 6308, "synset": "cargo_container.n.01", "name": "cargo_container"}, - {"id": 6309, "synset": "cargo_door.n.01", "name": "cargo_door"}, - {"id": 6310, "synset": "cargo_hatch.n.01", "name": "cargo_hatch"}, - {"id": 6311, "synset": "cargo_helicopter.n.01", "name": "cargo_helicopter"}, - {"id": 6312, "synset": "cargo_liner.n.01", "name": "cargo_liner"}, - {"id": 6313, "synset": "carillon.n.01", "name": "carillon"}, - {"id": 6314, "synset": "car_mirror.n.01", "name": "car_mirror"}, - {"id": 6315, "synset": "caroche.n.01", "name": "caroche"}, - {"id": 6316, "synset": "carousel.n.02", "name": "carousel"}, - {"id": 6317, "synset": "carpenter's_hammer.n.01", "name": "carpenter's_hammer"}, - {"id": 6318, "synset": "carpenter's_kit.n.01", "name": "carpenter's_kit"}, - {"id": 6319, "synset": "carpenter's_level.n.01", "name": "carpenter's_level"}, - {"id": 6320, "synset": "carpenter's_mallet.n.01", "name": "carpenter's_mallet"}, - {"id": 6321, "synset": "carpenter's_rule.n.01", "name": "carpenter's_rule"}, - {"id": 6322, "synset": "carpenter's_square.n.01", "name": "carpenter's_square"}, - {"id": 6323, "synset": "carpetbag.n.01", "name": "carpetbag"}, - {"id": 6324, "synset": "carpet_beater.n.01", "name": "carpet_beater"}, - {"id": 6325, "synset": "carpet_loom.n.01", "name": "carpet_loom"}, - {"id": 6326, "synset": "carpet_pad.n.01", "name": "carpet_pad"}, - {"id": 6327, "synset": "carpet_sweeper.n.01", "name": "carpet_sweeper"}, - {"id": 6328, "synset": "carpet_tack.n.01", "name": "carpet_tack"}, - {"id": 6329, "synset": "carport.n.01", "name": "carport"}, - {"id": 6330, "synset": "carrack.n.01", "name": "carrack"}, - {"id": 6331, "synset": "carrel.n.02", "name": "carrel"}, - {"id": 6332, "synset": "carriage.n.04", "name": "carriage"}, - {"id": 6333, "synset": "carriage_bolt.n.01", "name": "carriage_bolt"}, - {"id": 6334, "synset": "carriageway.n.01", "name": "carriageway"}, - {"id": 6335, "synset": "carriage_wrench.n.01", "name": "carriage_wrench"}, - {"id": 6336, "synset": "carrick_bend.n.01", "name": "carrick_bend"}, - {"id": 6337, "synset": "carrier.n.10", "name": "carrier"}, - {"id": 6338, "synset": "carrycot.n.01", "name": "carrycot"}, - {"id": 6339, "synset": "car_seat.n.01", "name": "car_seat"}, - {"id": 6340, "synset": "car_tire.n.01", "name": "car_tire"}, - {"id": 6341, "synset": "cartouche.n.01", "name": "cartouche"}, - {"id": 6342, "synset": "car_train.n.01", "name": "car_train"}, - {"id": 6343, "synset": "cartridge.n.01", "name": "cartridge"}, - {"id": 6344, "synset": "cartridge.n.04", "name": "cartridge"}, - {"id": 6345, "synset": "cartridge_belt.n.01", "name": "cartridge_belt"}, - {"id": 6346, "synset": "cartridge_extractor.n.01", "name": "cartridge_extractor"}, - {"id": 6347, "synset": "cartridge_fuse.n.01", "name": "cartridge_fuse"}, - {"id": 6348, "synset": "cartridge_holder.n.01", "name": "cartridge_holder"}, - {"id": 6349, "synset": "cartwheel.n.01", "name": "cartwheel"}, - {"id": 6350, "synset": "carving_fork.n.01", "name": "carving_fork"}, - {"id": 6351, "synset": "carving_knife.n.01", "name": "carving_knife"}, - {"id": 6352, "synset": "car_wheel.n.01", "name": "car_wheel"}, - {"id": 6353, "synset": "caryatid.n.01", "name": "caryatid"}, - {"id": 6354, "synset": "cascade_liquefier.n.01", "name": "cascade_liquefier"}, - {"id": 6355, "synset": "cascade_transformer.n.01", "name": "cascade_transformer"}, - {"id": 6356, "synset": "case.n.05", "name": "case"}, - {"id": 6357, "synset": "case.n.20", "name": "case"}, - {"id": 6358, "synset": "case.n.18", "name": "case"}, - {"id": 6359, "synset": "casein_paint.n.01", "name": "casein_paint"}, - {"id": 6360, "synset": "case_knife.n.02", "name": "case_knife"}, - {"id": 6361, "synset": "case_knife.n.01", "name": "case_knife"}, - {"id": 6362, "synset": "casement.n.01", "name": "casement"}, - {"id": 6363, "synset": "casement_window.n.01", "name": "casement_window"}, - {"id": 6364, "synset": "casern.n.01", "name": "casern"}, - {"id": 6365, "synset": "case_shot.n.01", "name": "case_shot"}, - {"id": 6366, "synset": "cash_bar.n.01", "name": "cash_bar"}, - {"id": 6367, "synset": "cashbox.n.01", "name": "cashbox"}, - {"id": 6368, "synset": "cash_machine.n.01", "name": "cash_machine"}, - {"id": 6369, "synset": "cashmere.n.01", "name": "cashmere"}, - {"id": 6370, "synset": "casing.n.03", "name": "casing"}, - {"id": 6371, "synset": "casino.n.01", "name": "casino"}, - {"id": 6372, "synset": "casket.n.02", "name": "casket"}, - {"id": 6373, "synset": "casque.n.01", "name": "casque"}, - {"id": 6374, "synset": "casquet.n.01", "name": "casquet"}, - {"id": 6375, "synset": "cassegrainian_telescope.n.01", "name": "Cassegrainian_telescope"}, - {"id": 6376, "synset": "casserole.n.02", "name": "casserole"}, - {"id": 6377, "synset": "cassette_deck.n.01", "name": "cassette_deck"}, - {"id": 6378, "synset": "cassette_player.n.01", "name": "cassette_player"}, - {"id": 6379, "synset": "cassette_recorder.n.01", "name": "cassette_recorder"}, - {"id": 6380, "synset": "cassette_tape.n.01", "name": "cassette_tape"}, - {"id": 6381, "synset": "cassock.n.01", "name": "cassock"}, - {"id": 6382, "synset": "caster.n.03", "name": "caster"}, - {"id": 6383, "synset": "caster.n.02", "name": "caster"}, - {"id": 6384, "synset": "castle.n.02", "name": "castle"}, - {"id": 6385, "synset": "castle.n.03", "name": "castle"}, - {"id": 6386, "synset": "catacomb.n.01", "name": "catacomb"}, - {"id": 6387, "synset": "catafalque.n.01", "name": "catafalque"}, - {"id": 6388, "synset": "catalytic_converter.n.01", "name": "catalytic_converter"}, - {"id": 6389, "synset": "catalytic_cracker.n.01", "name": "catalytic_cracker"}, - {"id": 6390, "synset": "catamaran.n.01", "name": "catamaran"}, - {"id": 6391, "synset": "catapult.n.03", "name": "catapult"}, - {"id": 6392, "synset": "catapult.n.02", "name": "catapult"}, - {"id": 6393, "synset": "catboat.n.01", "name": "catboat"}, - {"id": 6394, "synset": "cat_box.n.01", "name": "cat_box"}, - {"id": 6395, "synset": "catch.n.07", "name": "catch"}, - {"id": 6396, "synset": "catchall.n.01", "name": "catchall"}, - {"id": 6397, "synset": "catcher's_mask.n.01", "name": "catcher's_mask"}, - {"id": 6398, "synset": "catchment.n.01", "name": "catchment"}, - {"id": 6399, "synset": "caterpillar.n.02", "name": "Caterpillar"}, - {"id": 6400, "synset": "cathedra.n.01", "name": "cathedra"}, - {"id": 6401, "synset": "cathedral.n.01", "name": "cathedral"}, - {"id": 6402, "synset": "cathedral.n.02", "name": "cathedral"}, - {"id": 6403, "synset": "catheter.n.01", "name": "catheter"}, - {"id": 6404, "synset": "cathode.n.01", "name": "cathode"}, - {"id": 6405, "synset": "cathode-ray_tube.n.01", "name": "cathode-ray_tube"}, - {"id": 6406, "synset": "cat-o'-nine-tails.n.01", "name": "cat-o'-nine-tails"}, - {"id": 6407, "synset": "cat's-paw.n.02", "name": "cat's-paw"}, - {"id": 6408, "synset": "catsup_bottle.n.01", "name": "catsup_bottle"}, - {"id": 6409, "synset": "cattle_car.n.01", "name": "cattle_car"}, - {"id": 6410, "synset": "cattle_guard.n.01", "name": "cattle_guard"}, - {"id": 6411, "synset": "cattleship.n.01", "name": "cattleship"}, - {"id": 6412, "synset": "cautery.n.01", "name": "cautery"}, - {"id": 6413, "synset": "cavalier_hat.n.01", "name": "cavalier_hat"}, - {"id": 6414, "synset": "cavalry_sword.n.01", "name": "cavalry_sword"}, - {"id": 6415, "synset": "cavetto.n.01", "name": "cavetto"}, - {"id": 6416, "synset": "cavity_wall.n.01", "name": "cavity_wall"}, - {"id": 6417, "synset": "c_battery.n.01", "name": "C_battery"}, - {"id": 6418, "synset": "c-clamp.n.01", "name": "C-clamp"}, - {"id": 6419, "synset": "cd_drive.n.01", "name": "CD_drive"}, - {"id": 6420, "synset": "cd-r.n.01", "name": "CD-R"}, - {"id": 6421, "synset": "cd-rom.n.01", "name": "CD-ROM"}, - {"id": 6422, "synset": "cd-rom_drive.n.01", "name": "CD-ROM_drive"}, - {"id": 6423, "synset": "cedar_chest.n.01", "name": "cedar_chest"}, - {"id": 6424, "synset": "ceiling.n.01", "name": "ceiling"}, - {"id": 6425, "synset": "celesta.n.01", "name": "celesta"}, - {"id": 6426, "synset": "cell.n.03", "name": "cell"}, - {"id": 6427, "synset": "cell.n.07", "name": "cell"}, - {"id": 6428, "synset": "cellar.n.03", "name": "cellar"}, - {"id": 6429, "synset": "cellblock.n.01", "name": "cellblock"}, - {"id": 6430, "synset": "cello.n.01", "name": "cello"}, - {"id": 6431, "synset": "cellophane.n.01", "name": "cellophane"}, - {"id": 6432, "synset": "cellulose_tape.n.01", "name": "cellulose_tape"}, - {"id": 6433, "synset": "cenotaph.n.01", "name": "cenotaph"}, - {"id": 6434, "synset": "censer.n.01", "name": "censer"}, - {"id": 6435, "synset": "center.n.03", "name": "center"}, - {"id": 6436, "synset": "center_punch.n.01", "name": "center_punch"}, - {"id": 6437, "synset": "centigrade_thermometer.n.01", "name": "Centigrade_thermometer"}, - {"id": 6438, "synset": "central_processing_unit.n.01", "name": "central_processing_unit"}, - {"id": 6439, "synset": "centrifugal_pump.n.01", "name": "centrifugal_pump"}, - {"id": 6440, "synset": "centrifuge.n.01", "name": "centrifuge"}, - {"id": 6441, "synset": "ceramic.n.01", "name": "ceramic"}, - {"id": 6442, "synset": "ceramic_ware.n.01", "name": "ceramic_ware"}, - {"id": 6443, "synset": "cereal_bowl.n.01", "name": "cereal_bowl"}, - {"id": 6444, "synset": "cereal_box.n.01", "name": "cereal_box"}, - {"id": 6445, "synset": "cerecloth.n.01", "name": "cerecloth"}, - {"id": 6446, "synset": "cesspool.n.01", "name": "cesspool"}, - {"id": 6447, "synset": "chachka.n.02", "name": "chachka"}, - {"id": 6448, "synset": "chador.n.01", "name": "chador"}, - {"id": 6449, "synset": "chafing_dish.n.01", "name": "chafing_dish"}, - {"id": 6450, "synset": "chain.n.03", "name": "chain"}, - {"id": 6451, "synset": "chain.n.05", "name": "chain"}, - {"id": 6452, "synset": "chainlink_fence.n.01", "name": "chainlink_fence"}, - {"id": 6453, "synset": "chain_printer.n.01", "name": "chain_printer"}, - {"id": 6454, "synset": "chain_saw.n.01", "name": "chain_saw"}, - {"id": 6455, "synset": "chain_store.n.01", "name": "chain_store"}, - {"id": 6456, "synset": "chain_tongs.n.01", "name": "chain_tongs"}, - {"id": 6457, "synset": "chain_wrench.n.01", "name": "chain_wrench"}, - {"id": 6458, "synset": "chair.n.05", "name": "chair"}, - {"id": 6459, "synset": "chair_of_state.n.01", "name": "chair_of_state"}, - {"id": 6460, "synset": "chairlift.n.01", "name": "chairlift"}, - {"id": 6461, "synset": "chaise.n.02", "name": "chaise"}, - {"id": 6462, "synset": "chalet.n.01", "name": "chalet"}, - {"id": 6463, "synset": "chalk.n.04", "name": "chalk"}, - {"id": 6464, "synset": "challis.n.01", "name": "challis"}, - {"id": 6465, "synset": "chamberpot.n.01", "name": "chamberpot"}, - {"id": 6466, "synset": "chambray.n.01", "name": "chambray"}, - {"id": 6467, "synset": "chamfer_bit.n.01", "name": "chamfer_bit"}, - {"id": 6468, "synset": "chamfer_plane.n.01", "name": "chamfer_plane"}, - {"id": 6469, "synset": "chamois_cloth.n.01", "name": "chamois_cloth"}, - {"id": 6470, "synset": "chancel.n.01", "name": "chancel"}, - {"id": 6471, "synset": "chancellery.n.01", "name": "chancellery"}, - {"id": 6472, "synset": "chancery.n.02", "name": "chancery"}, - {"id": 6473, "synset": "chandlery.n.01", "name": "chandlery"}, - {"id": 6474, "synset": "chanfron.n.01", "name": "chanfron"}, - {"id": 6475, "synset": "chanter.n.01", "name": "chanter"}, - {"id": 6476, "synset": "chantry.n.02", "name": "chantry"}, - {"id": 6477, "synset": "chapel.n.01", "name": "chapel"}, - {"id": 6478, "synset": "chapterhouse.n.02", "name": "chapterhouse"}, - {"id": 6479, "synset": "chapterhouse.n.01", "name": "chapterhouse"}, - {"id": 6480, "synset": "character_printer.n.01", "name": "character_printer"}, - {"id": 6481, "synset": "charcuterie.n.01", "name": "charcuterie"}, - { - "id": 6482, - "synset": "charge-exchange_accelerator.n.01", - "name": "charge-exchange_accelerator", - }, - {"id": 6483, "synset": "charger.n.02", "name": "charger"}, - {"id": 6484, "synset": "chariot.n.01", "name": "chariot"}, - {"id": 6485, "synset": "chariot.n.02", "name": "chariot"}, - {"id": 6486, "synset": "charnel_house.n.01", "name": "charnel_house"}, - {"id": 6487, "synset": "chassis.n.03", "name": "chassis"}, - {"id": 6488, "synset": "chassis.n.02", "name": "chassis"}, - {"id": 6489, "synset": "chasuble.n.01", "name": "chasuble"}, - {"id": 6490, "synset": "chateau.n.01", "name": "chateau"}, - {"id": 6491, "synset": "chatelaine.n.02", "name": "chatelaine"}, - {"id": 6492, "synset": "checker.n.03", "name": "checker"}, - {"id": 6493, "synset": "checkout.n.03", "name": "checkout"}, - {"id": 6494, "synset": "cheekpiece.n.01", "name": "cheekpiece"}, - {"id": 6495, "synset": "cheeseboard.n.01", "name": "cheeseboard"}, - {"id": 6496, "synset": "cheesecloth.n.01", "name": "cheesecloth"}, - {"id": 6497, "synset": "cheese_cutter.n.01", "name": "cheese_cutter"}, - {"id": 6498, "synset": "cheese_press.n.01", "name": "cheese_press"}, - {"id": 6499, "synset": "chemical_bomb.n.01", "name": "chemical_bomb"}, - {"id": 6500, "synset": "chemical_plant.n.01", "name": "chemical_plant"}, - {"id": 6501, "synset": "chemical_reactor.n.01", "name": "chemical_reactor"}, - {"id": 6502, "synset": "chemise.n.02", "name": "chemise"}, - {"id": 6503, "synset": "chemise.n.01", "name": "chemise"}, - {"id": 6504, "synset": "chenille.n.02", "name": "chenille"}, - {"id": 6505, "synset": "chessman.n.01", "name": "chessman"}, - {"id": 6506, "synset": "chest.n.02", "name": "chest"}, - {"id": 6507, "synset": "chesterfield.n.02", "name": "chesterfield"}, - {"id": 6508, "synset": "chest_of_drawers.n.01", "name": "chest_of_drawers"}, - {"id": 6509, "synset": "chest_protector.n.01", "name": "chest_protector"}, - {"id": 6510, "synset": "cheval-de-frise.n.01", "name": "cheval-de-frise"}, - {"id": 6511, "synset": "cheval_glass.n.01", "name": "cheval_glass"}, - {"id": 6512, "synset": "chicane.n.02", "name": "chicane"}, - {"id": 6513, "synset": "chicken_coop.n.01", "name": "chicken_coop"}, - {"id": 6514, "synset": "chicken_wire.n.01", "name": "chicken_wire"}, - {"id": 6515, "synset": "chicken_yard.n.01", "name": "chicken_yard"}, - {"id": 6516, "synset": "chiffon.n.01", "name": "chiffon"}, - {"id": 6517, "synset": "chiffonier.n.01", "name": "chiffonier"}, - {"id": 6518, "synset": "child's_room.n.01", "name": "child's_room"}, - {"id": 6519, "synset": "chimney_breast.n.01", "name": "chimney_breast"}, - {"id": 6520, "synset": "chimney_corner.n.01", "name": "chimney_corner"}, - {"id": 6521, "synset": "china.n.02", "name": "china"}, - {"id": 6522, "synset": "china_cabinet.n.01", "name": "china_cabinet"}, - {"id": 6523, "synset": "chinchilla.n.02", "name": "chinchilla"}, - {"id": 6524, "synset": "chinese_lantern.n.01", "name": "Chinese_lantern"}, - {"id": 6525, "synset": "chinese_puzzle.n.01", "name": "Chinese_puzzle"}, - {"id": 6526, "synset": "chinning_bar.n.01", "name": "chinning_bar"}, - {"id": 6527, "synset": "chino.n.02", "name": "chino"}, - {"id": 6528, "synset": "chino.n.01", "name": "chino"}, - {"id": 6529, "synset": "chin_rest.n.01", "name": "chin_rest"}, - {"id": 6530, "synset": "chin_strap.n.01", "name": "chin_strap"}, - {"id": 6531, "synset": "chintz.n.01", "name": "chintz"}, - {"id": 6532, "synset": "chip.n.07", "name": "chip"}, - {"id": 6533, "synset": "chisel.n.01", "name": "chisel"}, - {"id": 6534, "synset": "chlamys.n.02", "name": "chlamys"}, - {"id": 6535, "synset": "choir.n.03", "name": "choir"}, - {"id": 6536, "synset": "choir_loft.n.01", "name": "choir_loft"}, - {"id": 6537, "synset": "choke.n.02", "name": "choke"}, - {"id": 6538, "synset": "choke.n.01", "name": "choke"}, - {"id": 6539, "synset": "chokey.n.01", "name": "chokey"}, - {"id": 6540, "synset": "choo-choo.n.01", "name": "choo-choo"}, - {"id": 6541, "synset": "chopine.n.01", "name": "chopine"}, - {"id": 6542, "synset": "chordophone.n.01", "name": "chordophone"}, - {"id": 6543, "synset": "christmas_stocking.n.01", "name": "Christmas_stocking"}, - {"id": 6544, "synset": "chronograph.n.01", "name": "chronograph"}, - {"id": 6545, "synset": "chronometer.n.01", "name": "chronometer"}, - {"id": 6546, "synset": "chronoscope.n.01", "name": "chronoscope"}, - {"id": 6547, "synset": "chuck.n.03", "name": "chuck"}, - {"id": 6548, "synset": "chuck_wagon.n.01", "name": "chuck_wagon"}, - {"id": 6549, "synset": "chukka.n.02", "name": "chukka"}, - {"id": 6550, "synset": "church.n.02", "name": "church"}, - {"id": 6551, "synset": "church_bell.n.01", "name": "church_bell"}, - {"id": 6552, "synset": "church_hat.n.01", "name": "church_hat"}, - {"id": 6553, "synset": "church_key.n.01", "name": "church_key"}, - {"id": 6554, "synset": "church_tower.n.01", "name": "church_tower"}, - {"id": 6555, "synset": "churidars.n.01", "name": "churidars"}, - {"id": 6556, "synset": "churn.n.01", "name": "churn"}, - {"id": 6557, "synset": "ciderpress.n.01", "name": "ciderpress"}, - {"id": 6558, "synset": "cigar_band.n.01", "name": "cigar_band"}, - {"id": 6559, "synset": "cigar_cutter.n.01", "name": "cigar_cutter"}, - {"id": 6560, "synset": "cigarette_butt.n.01", "name": "cigarette_butt"}, - {"id": 6561, "synset": "cigarette_holder.n.01", "name": "cigarette_holder"}, - {"id": 6562, "synset": "cigar_lighter.n.01", "name": "cigar_lighter"}, - {"id": 6563, "synset": "cinch.n.02", "name": "cinch"}, - {"id": 6564, "synset": "cinema.n.02", "name": "cinema"}, - {"id": 6565, "synset": "cinquefoil.n.02", "name": "cinquefoil"}, - {"id": 6566, "synset": "circle.n.08", "name": "circle"}, - {"id": 6567, "synset": "circlet.n.02", "name": "circlet"}, - {"id": 6568, "synset": "circuit.n.01", "name": "circuit"}, - {"id": 6569, "synset": "circuit_board.n.01", "name": "circuit_board"}, - {"id": 6570, "synset": "circuit_breaker.n.01", "name": "circuit_breaker"}, - {"id": 6571, "synset": "circuitry.n.01", "name": "circuitry"}, - {"id": 6572, "synset": "circular_plane.n.01", "name": "circular_plane"}, - {"id": 6573, "synset": "circular_saw.n.01", "name": "circular_saw"}, - {"id": 6574, "synset": "circus_tent.n.01", "name": "circus_tent"}, - {"id": 6575, "synset": "cistern.n.03", "name": "cistern"}, - {"id": 6576, "synset": "cittern.n.01", "name": "cittern"}, - {"id": 6577, "synset": "city_hall.n.01", "name": "city_hall"}, - {"id": 6578, "synset": "cityscape.n.02", "name": "cityscape"}, - {"id": 6579, "synset": "city_university.n.01", "name": "city_university"}, - {"id": 6580, "synset": "civies.n.01", "name": "civies"}, - {"id": 6581, "synset": "civilian_clothing.n.01", "name": "civilian_clothing"}, - {"id": 6582, "synset": "clack_valve.n.01", "name": "clack_valve"}, - {"id": 6583, "synset": "clamp.n.01", "name": "clamp"}, - {"id": 6584, "synset": "clamshell.n.02", "name": "clamshell"}, - {"id": 6585, "synset": "clapper.n.03", "name": "clapper"}, - {"id": 6586, "synset": "clapperboard.n.01", "name": "clapperboard"}, - {"id": 6587, "synset": "clarence.n.01", "name": "clarence"}, - {"id": 6588, "synset": "clark_cell.n.01", "name": "Clark_cell"}, - {"id": 6589, "synset": "clasp_knife.n.01", "name": "clasp_knife"}, - {"id": 6590, "synset": "classroom.n.01", "name": "classroom"}, - {"id": 6591, "synset": "clavichord.n.01", "name": "clavichord"}, - {"id": 6592, "synset": "clavier.n.02", "name": "clavier"}, - {"id": 6593, "synset": "clay_pigeon.n.01", "name": "clay_pigeon"}, - {"id": 6594, "synset": "claymore_mine.n.01", "name": "claymore_mine"}, - {"id": 6595, "synset": "claymore.n.01", "name": "claymore"}, - {"id": 6596, "synset": "cleaners.n.01", "name": "cleaners"}, - {"id": 6597, "synset": "cleaning_implement.n.01", "name": "cleaning_implement"}, - {"id": 6598, "synset": "cleaning_pad.n.01", "name": "cleaning_pad"}, - {"id": 6599, "synset": "clean_room.n.01", "name": "clean_room"}, - {"id": 6600, "synset": "clearway.n.01", "name": "clearway"}, - {"id": 6601, "synset": "cleat.n.01", "name": "cleat"}, - {"id": 6602, "synset": "cleats.n.01", "name": "cleats"}, - {"id": 6603, "synset": "cleaver.n.01", "name": "cleaver"}, - {"id": 6604, "synset": "clerestory.n.01", "name": "clerestory"}, - {"id": 6605, "synset": "clevis.n.01", "name": "clevis"}, - {"id": 6606, "synset": "clews.n.01", "name": "clews"}, - {"id": 6607, "synset": "cliff_dwelling.n.01", "name": "cliff_dwelling"}, - {"id": 6608, "synset": "climbing_frame.n.01", "name": "climbing_frame"}, - {"id": 6609, "synset": "clinch.n.03", "name": "clinch"}, - {"id": 6610, "synset": "clinch.n.02", "name": "clinch"}, - {"id": 6611, "synset": "clincher.n.03", "name": "clincher"}, - {"id": 6612, "synset": "clinic.n.03", "name": "clinic"}, - {"id": 6613, "synset": "clinical_thermometer.n.01", "name": "clinical_thermometer"}, - {"id": 6614, "synset": "clinker.n.02", "name": "clinker"}, - {"id": 6615, "synset": "clinometer.n.01", "name": "clinometer"}, - {"id": 6616, "synset": "clip_lead.n.01", "name": "clip_lead"}, - {"id": 6617, "synset": "clip-on.n.01", "name": "clip-on"}, - {"id": 6618, "synset": "clipper.n.04", "name": "clipper"}, - {"id": 6619, "synset": "clipper.n.02", "name": "clipper"}, - {"id": 6620, "synset": "cloak.n.01", "name": "cloak"}, - {"id": 6621, "synset": "cloakroom.n.02", "name": "cloakroom"}, - {"id": 6622, "synset": "cloche.n.02", "name": "cloche"}, - {"id": 6623, "synset": "cloche.n.01", "name": "cloche"}, - {"id": 6624, "synset": "clock_pendulum.n.01", "name": "clock_pendulum"}, - {"id": 6625, "synset": "clock_radio.n.01", "name": "clock_radio"}, - {"id": 6626, "synset": "clockwork.n.01", "name": "clockwork"}, - {"id": 6627, "synset": "clog.n.01", "name": "clog"}, - {"id": 6628, "synset": "cloisonne.n.01", "name": "cloisonne"}, - {"id": 6629, "synset": "cloister.n.02", "name": "cloister"}, - {"id": 6630, "synset": "closed_circuit.n.01", "name": "closed_circuit"}, - {"id": 6631, "synset": "closed-circuit_television.n.01", "name": "closed-circuit_television"}, - {"id": 6632, "synset": "closed_loop.n.01", "name": "closed_loop"}, - {"id": 6633, "synset": "closet.n.04", "name": "closet"}, - {"id": 6634, "synset": "closeup_lens.n.01", "name": "closeup_lens"}, - {"id": 6635, "synset": "cloth_cap.n.01", "name": "cloth_cap"}, - {"id": 6636, "synset": "cloth_covering.n.01", "name": "cloth_covering"}, - {"id": 6637, "synset": "clothesbrush.n.01", "name": "clothesbrush"}, - {"id": 6638, "synset": "clothes_closet.n.01", "name": "clothes_closet"}, - {"id": 6639, "synset": "clothes_dryer.n.01", "name": "clothes_dryer"}, - {"id": 6640, "synset": "clotheshorse.n.01", "name": "clotheshorse"}, - {"id": 6641, "synset": "clothes_tree.n.01", "name": "clothes_tree"}, - {"id": 6642, "synset": "clothing.n.01", "name": "clothing"}, - {"id": 6643, "synset": "clothing_store.n.01", "name": "clothing_store"}, - {"id": 6644, "synset": "clout_nail.n.01", "name": "clout_nail"}, - {"id": 6645, "synset": "clove_hitch.n.01", "name": "clove_hitch"}, - {"id": 6646, "synset": "club_car.n.01", "name": "club_car"}, - {"id": 6647, "synset": "clubroom.n.01", "name": "clubroom"}, - {"id": 6648, "synset": "cluster_bomb.n.01", "name": "cluster_bomb"}, - {"id": 6649, "synset": "clutch.n.07", "name": "clutch"}, - {"id": 6650, "synset": "clutch.n.06", "name": "clutch"}, - {"id": 6651, "synset": "coach.n.04", "name": "coach"}, - {"id": 6652, "synset": "coach_house.n.01", "name": "coach_house"}, - {"id": 6653, "synset": "coal_car.n.01", "name": "coal_car"}, - {"id": 6654, "synset": "coal_chute.n.01", "name": "coal_chute"}, - {"id": 6655, "synset": "coal_house.n.01", "name": "coal_house"}, - {"id": 6656, "synset": "coal_shovel.n.01", "name": "coal_shovel"}, - {"id": 6657, "synset": "coaming.n.01", "name": "coaming"}, - {"id": 6658, "synset": "coaster_brake.n.01", "name": "coaster_brake"}, - {"id": 6659, "synset": "coat_button.n.01", "name": "coat_button"}, - {"id": 6660, "synset": "coat_closet.n.01", "name": "coat_closet"}, - {"id": 6661, "synset": "coatdress.n.01", "name": "coatdress"}, - {"id": 6662, "synset": "coatee.n.01", "name": "coatee"}, - {"id": 6663, "synset": "coating.n.01", "name": "coating"}, - {"id": 6664, "synset": "coating.n.03", "name": "coating"}, - {"id": 6665, "synset": "coat_of_paint.n.01", "name": "coat_of_paint"}, - {"id": 6666, "synset": "coattail.n.01", "name": "coattail"}, - {"id": 6667, "synset": "coaxial_cable.n.01", "name": "coaxial_cable"}, - {"id": 6668, "synset": "cobweb.n.03", "name": "cobweb"}, - {"id": 6669, "synset": "cobweb.n.01", "name": "cobweb"}, - { - "id": 6670, - "synset": "cockcroft_and_walton_accelerator.n.01", - "name": "Cockcroft_and_Walton_accelerator", - }, - {"id": 6671, "synset": "cocked_hat.n.01", "name": "cocked_hat"}, - {"id": 6672, "synset": "cockhorse.n.01", "name": "cockhorse"}, - {"id": 6673, "synset": "cockleshell.n.01", "name": "cockleshell"}, - {"id": 6674, "synset": "cockpit.n.01", "name": "cockpit"}, - {"id": 6675, "synset": "cockpit.n.03", "name": "cockpit"}, - {"id": 6676, "synset": "cockpit.n.02", "name": "cockpit"}, - {"id": 6677, "synset": "cockscomb.n.03", "name": "cockscomb"}, - {"id": 6678, "synset": "cocktail_dress.n.01", "name": "cocktail_dress"}, - {"id": 6679, "synset": "cocktail_lounge.n.01", "name": "cocktail_lounge"}, - {"id": 6680, "synset": "cocktail_shaker.n.01", "name": "cocktail_shaker"}, - {"id": 6681, "synset": "cocotte.n.02", "name": "cocotte"}, - {"id": 6682, "synset": "codpiece.n.01", "name": "codpiece"}, - {"id": 6683, "synset": "coelostat.n.01", "name": "coelostat"}, - {"id": 6684, "synset": "coffee_can.n.01", "name": "coffee_can"}, - {"id": 6685, "synset": "coffee_cup.n.01", "name": "coffee_cup"}, - {"id": 6686, "synset": "coffee_filter.n.01", "name": "coffee_filter"}, - {"id": 6687, "synset": "coffee_mill.n.01", "name": "coffee_mill"}, - {"id": 6688, "synset": "coffee_mug.n.01", "name": "coffee_mug"}, - {"id": 6689, "synset": "coffee_stall.n.01", "name": "coffee_stall"}, - {"id": 6690, "synset": "coffee_urn.n.01", "name": "coffee_urn"}, - {"id": 6691, "synset": "coffer.n.02", "name": "coffer"}, - {"id": 6692, "synset": "coffey_still.n.01", "name": "Coffey_still"}, - {"id": 6693, "synset": "coffin.n.01", "name": "coffin"}, - {"id": 6694, "synset": "cog.n.02", "name": "cog"}, - {"id": 6695, "synset": "coif.n.02", "name": "coif"}, - {"id": 6696, "synset": "coil.n.01", "name": "coil"}, - {"id": 6697, "synset": "coil.n.06", "name": "coil"}, - {"id": 6698, "synset": "coil.n.03", "name": "coil"}, - {"id": 6699, "synset": "coil_spring.n.01", "name": "coil_spring"}, - {"id": 6700, "synset": "coin_box.n.01", "name": "coin_box"}, - {"id": 6701, "synset": "cold_cathode.n.01", "name": "cold_cathode"}, - {"id": 6702, "synset": "cold_chisel.n.01", "name": "cold_chisel"}, - {"id": 6703, "synset": "cold_cream.n.01", "name": "cold_cream"}, - {"id": 6704, "synset": "cold_frame.n.01", "name": "cold_frame"}, - {"id": 6705, "synset": "collar.n.01", "name": "collar"}, - {"id": 6706, "synset": "collar.n.03", "name": "collar"}, - {"id": 6707, "synset": "college.n.03", "name": "college"}, - {"id": 6708, "synset": "collet.n.02", "name": "collet"}, - {"id": 6709, "synset": "collider.n.01", "name": "collider"}, - {"id": 6710, "synset": "colliery.n.01", "name": "colliery"}, - {"id": 6711, "synset": "collimator.n.02", "name": "collimator"}, - {"id": 6712, "synset": "collimator.n.01", "name": "collimator"}, - {"id": 6713, "synset": "cologne.n.02", "name": "cologne"}, - {"id": 6714, "synset": "colonnade.n.01", "name": "colonnade"}, - {"id": 6715, "synset": "colonoscope.n.01", "name": "colonoscope"}, - {"id": 6716, "synset": "colorimeter.n.01", "name": "colorimeter"}, - {"id": 6717, "synset": "colors.n.02", "name": "colors"}, - {"id": 6718, "synset": "color_television.n.01", "name": "color_television"}, - {"id": 6719, "synset": "color_tube.n.01", "name": "color_tube"}, - {"id": 6720, "synset": "color_wash.n.01", "name": "color_wash"}, - {"id": 6721, "synset": "colt.n.02", "name": "Colt"}, - {"id": 6722, "synset": "colter.n.01", "name": "colter"}, - {"id": 6723, "synset": "columbarium.n.03", "name": "columbarium"}, - {"id": 6724, "synset": "columbarium.n.02", "name": "columbarium"}, - {"id": 6725, "synset": "column.n.07", "name": "column"}, - {"id": 6726, "synset": "column.n.06", "name": "column"}, - {"id": 6727, "synset": "comb.n.01", "name": "comb"}, - {"id": 6728, "synset": "comb.n.03", "name": "comb"}, - {"id": 6729, "synset": "comber.n.03", "name": "comber"}, - {"id": 6730, "synset": "combination_plane.n.01", "name": "combination_plane"}, - {"id": 6731, "synset": "combine.n.01", "name": "combine"}, - {"id": 6732, "synset": "command_module.n.01", "name": "command_module"}, - {"id": 6733, "synset": "commissary.n.01", "name": "commissary"}, - {"id": 6734, "synset": "commissary.n.02", "name": "commissary"}, - {"id": 6735, "synset": "commodity.n.01", "name": "commodity"}, - {"id": 6736, "synset": "common_ax.n.01", "name": "common_ax"}, - {"id": 6737, "synset": "common_room.n.01", "name": "common_room"}, - {"id": 6738, "synset": "communications_satellite.n.01", "name": "communications_satellite"}, - {"id": 6739, "synset": "communication_system.n.01", "name": "communication_system"}, - {"id": 6740, "synset": "community_center.n.01", "name": "community_center"}, - {"id": 6741, "synset": "commutator.n.01", "name": "commutator"}, - {"id": 6742, "synset": "commuter.n.01", "name": "commuter"}, - {"id": 6743, "synset": "compact.n.01", "name": "compact"}, - {"id": 6744, "synset": "compact.n.03", "name": "compact"}, - {"id": 6745, "synset": "compact_disk.n.01", "name": "compact_disk"}, - {"id": 6746, "synset": "compact-disk_burner.n.01", "name": "compact-disk_burner"}, - {"id": 6747, "synset": "companionway.n.01", "name": "companionway"}, - {"id": 6748, "synset": "compartment.n.02", "name": "compartment"}, - {"id": 6749, "synset": "compartment.n.01", "name": "compartment"}, - {"id": 6750, "synset": "compass.n.04", "name": "compass"}, - {"id": 6751, "synset": "compass_card.n.01", "name": "compass_card"}, - {"id": 6752, "synset": "compass_saw.n.01", "name": "compass_saw"}, - {"id": 6753, "synset": "compound.n.03", "name": "compound"}, - {"id": 6754, "synset": "compound_lens.n.01", "name": "compound_lens"}, - {"id": 6755, "synset": "compound_lever.n.01", "name": "compound_lever"}, - {"id": 6756, "synset": "compound_microscope.n.01", "name": "compound_microscope"}, - {"id": 6757, "synset": "compress.n.01", "name": "compress"}, - {"id": 6758, "synset": "compression_bandage.n.01", "name": "compression_bandage"}, - {"id": 6759, "synset": "compressor.n.01", "name": "compressor"}, - {"id": 6760, "synset": "computer.n.01", "name": "computer"}, - {"id": 6761, "synset": "computer_circuit.n.01", "name": "computer_circuit"}, - { - "id": 6762, - "synset": "computerized_axial_tomography_scanner.n.01", - "name": "computerized_axial_tomography_scanner", - }, - {"id": 6763, "synset": "computer_monitor.n.01", "name": "computer_monitor"}, - {"id": 6764, "synset": "computer_network.n.01", "name": "computer_network"}, - {"id": 6765, "synset": "computer_screen.n.01", "name": "computer_screen"}, - {"id": 6766, "synset": "computer_store.n.01", "name": "computer_store"}, - {"id": 6767, "synset": "computer_system.n.01", "name": "computer_system"}, - {"id": 6768, "synset": "concentration_camp.n.01", "name": "concentration_camp"}, - {"id": 6769, "synset": "concert_grand.n.01", "name": "concert_grand"}, - {"id": 6770, "synset": "concert_hall.n.01", "name": "concert_hall"}, - {"id": 6771, "synset": "concertina.n.02", "name": "concertina"}, - {"id": 6772, "synset": "concertina.n.01", "name": "concertina"}, - {"id": 6773, "synset": "concrete_mixer.n.01", "name": "concrete_mixer"}, - {"id": 6774, "synset": "condensation_pump.n.01", "name": "condensation_pump"}, - {"id": 6775, "synset": "condenser.n.04", "name": "condenser"}, - {"id": 6776, "synset": "condenser.n.03", "name": "condenser"}, - {"id": 6777, "synset": "condenser.n.02", "name": "condenser"}, - {"id": 6778, "synset": "condenser_microphone.n.01", "name": "condenser_microphone"}, - {"id": 6779, "synset": "condominium.n.02", "name": "condominium"}, - {"id": 6780, "synset": "condominium.n.01", "name": "condominium"}, - {"id": 6781, "synset": "conductor.n.04", "name": "conductor"}, - {"id": 6782, "synset": "cone_clutch.n.01", "name": "cone_clutch"}, - {"id": 6783, "synset": "confectionery.n.02", "name": "confectionery"}, - {"id": 6784, "synset": "conference_center.n.01", "name": "conference_center"}, - {"id": 6785, "synset": "conference_room.n.01", "name": "conference_room"}, - {"id": 6786, "synset": "conference_table.n.01", "name": "conference_table"}, - {"id": 6787, "synset": "confessional.n.01", "name": "confessional"}, - {"id": 6788, "synset": "conformal_projection.n.01", "name": "conformal_projection"}, - {"id": 6789, "synset": "congress_boot.n.01", "name": "congress_boot"}, - {"id": 6790, "synset": "conic_projection.n.01", "name": "conic_projection"}, - {"id": 6791, "synset": "connecting_rod.n.01", "name": "connecting_rod"}, - {"id": 6792, "synset": "connecting_room.n.01", "name": "connecting_room"}, - {"id": 6793, "synset": "connection.n.03", "name": "connection"}, - {"id": 6794, "synset": "conning_tower.n.02", "name": "conning_tower"}, - {"id": 6795, "synset": "conning_tower.n.01", "name": "conning_tower"}, - {"id": 6796, "synset": "conservatory.n.03", "name": "conservatory"}, - {"id": 6797, "synset": "conservatory.n.02", "name": "conservatory"}, - {"id": 6798, "synset": "console.n.03", "name": "console"}, - {"id": 6799, "synset": "console.n.02", "name": "console"}, - {"id": 6800, "synset": "console_table.n.01", "name": "console_table"}, - {"id": 6801, "synset": "consulate.n.01", "name": "consulate"}, - {"id": 6802, "synset": "contact.n.07", "name": "contact"}, - {"id": 6803, "synset": "contact.n.09", "name": "contact"}, - {"id": 6804, "synset": "container.n.01", "name": "container"}, - {"id": 6805, "synset": "container_ship.n.01", "name": "container_ship"}, - {"id": 6806, "synset": "containment.n.02", "name": "containment"}, - {"id": 6807, "synset": "contrabassoon.n.01", "name": "contrabassoon"}, - {"id": 6808, "synset": "control_center.n.01", "name": "control_center"}, - {"id": 6809, "synset": "control_circuit.n.01", "name": "control_circuit"}, - {"id": 6810, "synset": "control_key.n.01", "name": "control_key"}, - {"id": 6811, "synset": "control_panel.n.01", "name": "control_panel"}, - {"id": 6812, "synset": "control_rod.n.01", "name": "control_rod"}, - {"id": 6813, "synset": "control_room.n.01", "name": "control_room"}, - {"id": 6814, "synset": "control_system.n.01", "name": "control_system"}, - {"id": 6815, "synset": "control_tower.n.01", "name": "control_tower"}, - {"id": 6816, "synset": "convector.n.01", "name": "convector"}, - {"id": 6817, "synset": "convenience_store.n.01", "name": "convenience_store"}, - {"id": 6818, "synset": "convent.n.01", "name": "convent"}, - {"id": 6819, "synset": "conventicle.n.02", "name": "conventicle"}, - {"id": 6820, "synset": "converging_lens.n.01", "name": "converging_lens"}, - {"id": 6821, "synset": "converter.n.01", "name": "converter"}, - {"id": 6822, "synset": "conveyance.n.03", "name": "conveyance"}, - {"id": 6823, "synset": "conveyer_belt.n.01", "name": "conveyer_belt"}, - {"id": 6824, "synset": "cookfire.n.01", "name": "cookfire"}, - {"id": 6825, "synset": "cookhouse.n.02", "name": "cookhouse"}, - {"id": 6826, "synset": "cookie_cutter.n.01", "name": "cookie_cutter"}, - {"id": 6827, "synset": "cookie_jar.n.01", "name": "cookie_jar"}, - {"id": 6828, "synset": "cookie_sheet.n.01", "name": "cookie_sheet"}, - {"id": 6829, "synset": "cookstove.n.01", "name": "cookstove"}, - {"id": 6830, "synset": "coolant_system.n.01", "name": "coolant_system"}, - {"id": 6831, "synset": "cooling_system.n.02", "name": "cooling_system"}, - {"id": 6832, "synset": "cooling_system.n.01", "name": "cooling_system"}, - {"id": 6833, "synset": "cooling_tower.n.01", "name": "cooling_tower"}, - {"id": 6834, "synset": "coonskin_cap.n.01", "name": "coonskin_cap"}, - {"id": 6835, "synset": "cope.n.02", "name": "cope"}, - {"id": 6836, "synset": "coping_saw.n.01", "name": "coping_saw"}, - {"id": 6837, "synset": "copperware.n.01", "name": "copperware"}, - {"id": 6838, "synset": "copyholder.n.01", "name": "copyholder"}, - {"id": 6839, "synset": "coquille.n.02", "name": "coquille"}, - {"id": 6840, "synset": "coracle.n.01", "name": "coracle"}, - {"id": 6841, "synset": "corbel.n.01", "name": "corbel"}, - {"id": 6842, "synset": "corbel_arch.n.01", "name": "corbel_arch"}, - {"id": 6843, "synset": "corbel_step.n.01", "name": "corbel_step"}, - {"id": 6844, "synset": "corbie_gable.n.01", "name": "corbie_gable"}, - {"id": 6845, "synset": "cord.n.04", "name": "cord"}, - {"id": 6846, "synset": "cord.n.03", "name": "cord"}, - {"id": 6847, "synset": "cordage.n.02", "name": "cordage"}, - {"id": 6848, "synset": "cords.n.01", "name": "cords"}, - {"id": 6849, "synset": "core.n.10", "name": "core"}, - {"id": 6850, "synset": "core_bit.n.01", "name": "core_bit"}, - {"id": 6851, "synset": "core_drill.n.01", "name": "core_drill"}, - {"id": 6852, "synset": "corer.n.01", "name": "corer"}, - {"id": 6853, "synset": "corker.n.02", "name": "corker"}, - {"id": 6854, "synset": "corncrib.n.01", "name": "corncrib"}, - {"id": 6855, "synset": "corner.n.11", "name": "corner"}, - {"id": 6856, "synset": "corner.n.03", "name": "corner"}, - {"id": 6857, "synset": "corner_post.n.01", "name": "corner_post"}, - {"id": 6858, "synset": "cornice.n.03", "name": "cornice"}, - {"id": 6859, "synset": "cornice.n.02", "name": "cornice"}, - {"id": 6860, "synset": "correctional_institution.n.01", "name": "correctional_institution"}, - {"id": 6861, "synset": "corrugated_fastener.n.01", "name": "corrugated_fastener"}, - {"id": 6862, "synset": "corselet.n.01", "name": "corselet"}, - {"id": 6863, "synset": "cosmetic.n.01", "name": "cosmetic"}, - {"id": 6864, "synset": "cosmotron.n.01", "name": "cosmotron"}, - {"id": 6865, "synset": "costume.n.01", "name": "costume"}, - {"id": 6866, "synset": "costume.n.02", "name": "costume"}, - {"id": 6867, "synset": "costume.n.03", "name": "costume"}, - {"id": 6868, "synset": "cosy.n.01", "name": "cosy"}, - {"id": 6869, "synset": "cot.n.03", "name": "cot"}, - {"id": 6870, "synset": "cottage_tent.n.01", "name": "cottage_tent"}, - {"id": 6871, "synset": "cotter.n.03", "name": "cotter"}, - {"id": 6872, "synset": "cotter_pin.n.01", "name": "cotter_pin"}, - {"id": 6873, "synset": "cotton.n.02", "name": "cotton"}, - {"id": 6874, "synset": "cotton_flannel.n.01", "name": "cotton_flannel"}, - {"id": 6875, "synset": "cotton_mill.n.01", "name": "cotton_mill"}, - {"id": 6876, "synset": "couch.n.03", "name": "couch"}, - {"id": 6877, "synset": "couch.n.02", "name": "couch"}, - {"id": 6878, "synset": "couchette.n.01", "name": "couchette"}, - {"id": 6879, "synset": "coude_telescope.n.01", "name": "coude_telescope"}, - {"id": 6880, "synset": "counter.n.01", "name": "counter"}, - {"id": 6881, "synset": "counter.n.03", "name": "counter"}, - {"id": 6882, "synset": "counter.n.02", "name": "counter"}, - {"id": 6883, "synset": "counterbore.n.01", "name": "counterbore"}, - {"id": 6884, "synset": "counter_tube.n.01", "name": "counter_tube"}, - {"id": 6885, "synset": "country_house.n.01", "name": "country_house"}, - {"id": 6886, "synset": "country_store.n.01", "name": "country_store"}, - {"id": 6887, "synset": "coupe.n.01", "name": "coupe"}, - {"id": 6888, "synset": "coupling.n.02", "name": "coupling"}, - {"id": 6889, "synset": "court.n.10", "name": "court"}, - {"id": 6890, "synset": "court.n.04", "name": "court"}, - {"id": 6891, "synset": "court.n.02", "name": "court"}, - {"id": 6892, "synset": "court.n.09", "name": "court"}, - {"id": 6893, "synset": "courtelle.n.01", "name": "Courtelle"}, - {"id": 6894, "synset": "courthouse.n.02", "name": "courthouse"}, - {"id": 6895, "synset": "courthouse.n.01", "name": "courthouse"}, - {"id": 6896, "synset": "covered_bridge.n.01", "name": "covered_bridge"}, - {"id": 6897, "synset": "covered_couch.n.01", "name": "covered_couch"}, - {"id": 6898, "synset": "covered_wagon.n.01", "name": "covered_wagon"}, - {"id": 6899, "synset": "covering.n.02", "name": "covering"}, - {"id": 6900, "synset": "coverlet.n.01", "name": "coverlet"}, - {"id": 6901, "synset": "cover_plate.n.01", "name": "cover_plate"}, - {"id": 6902, "synset": "cowbarn.n.01", "name": "cowbarn"}, - {"id": 6903, "synset": "cowboy_boot.n.01", "name": "cowboy_boot"}, - {"id": 6904, "synset": "cowhide.n.03", "name": "cowhide"}, - {"id": 6905, "synset": "cowl.n.02", "name": "cowl"}, - {"id": 6906, "synset": "cow_pen.n.01", "name": "cow_pen"}, - {"id": 6907, "synset": "cpu_board.n.01", "name": "CPU_board"}, - {"id": 6908, "synset": "crackle.n.02", "name": "crackle"}, - {"id": 6909, "synset": "cradle.n.01", "name": "cradle"}, - {"id": 6910, "synset": "craft.n.02", "name": "craft"}, - {"id": 6911, "synset": "cramp.n.03", "name": "cramp"}, - {"id": 6912, "synset": "crampon.n.02", "name": "crampon"}, - {"id": 6913, "synset": "crampon.n.01", "name": "crampon"}, - {"id": 6914, "synset": "crane.n.04", "name": "crane"}, - {"id": 6915, "synset": "craniometer.n.01", "name": "craniometer"}, - {"id": 6916, "synset": "crank.n.04", "name": "crank"}, - {"id": 6917, "synset": "crankcase.n.01", "name": "crankcase"}, - {"id": 6918, "synset": "crankshaft.n.01", "name": "crankshaft"}, - {"id": 6919, "synset": "crash_barrier.n.01", "name": "crash_barrier"}, - {"id": 6920, "synset": "crash_helmet.n.01", "name": "crash_helmet"}, - {"id": 6921, "synset": "cravat.n.01", "name": "cravat"}, - {"id": 6922, "synset": "crazy_quilt.n.01", "name": "crazy_quilt"}, - {"id": 6923, "synset": "cream.n.03", "name": "cream"}, - {"id": 6924, "synset": "creche.n.01", "name": "creche"}, - {"id": 6925, "synset": "creche.n.02", "name": "creche"}, - {"id": 6926, "synset": "credenza.n.01", "name": "credenza"}, - {"id": 6927, "synset": "creel.n.01", "name": "creel"}, - {"id": 6928, "synset": "crematory.n.02", "name": "crematory"}, - {"id": 6929, "synset": "crematory.n.01", "name": "crematory"}, - {"id": 6930, "synset": "crepe.n.03", "name": "crepe"}, - {"id": 6931, "synset": "crepe_de_chine.n.01", "name": "crepe_de_Chine"}, - {"id": 6932, "synset": "crescent_wrench.n.01", "name": "crescent_wrench"}, - {"id": 6933, "synset": "cretonne.n.01", "name": "cretonne"}, - {"id": 6934, "synset": "crib.n.03", "name": "crib"}, - {"id": 6935, "synset": "cricket_ball.n.01", "name": "cricket_ball"}, - {"id": 6936, "synset": "cricket_bat.n.01", "name": "cricket_bat"}, - {"id": 6937, "synset": "cricket_equipment.n.01", "name": "cricket_equipment"}, - {"id": 6938, "synset": "cringle.n.01", "name": "cringle"}, - {"id": 6939, "synset": "crinoline.n.03", "name": "crinoline"}, - {"id": 6940, "synset": "crinoline.n.02", "name": "crinoline"}, - {"id": 6941, "synset": "crochet_needle.n.01", "name": "crochet_needle"}, - {"id": 6942, "synset": "crock_pot.n.01", "name": "Crock_Pot"}, - {"id": 6943, "synset": "crook.n.03", "name": "crook"}, - {"id": 6944, "synset": "crookes_radiometer.n.01", "name": "Crookes_radiometer"}, - {"id": 6945, "synset": "crookes_tube.n.01", "name": "Crookes_tube"}, - {"id": 6946, "synset": "croquet_ball.n.01", "name": "croquet_ball"}, - {"id": 6947, "synset": "croquet_equipment.n.01", "name": "croquet_equipment"}, - {"id": 6948, "synset": "croquet_mallet.n.01", "name": "croquet_mallet"}, - {"id": 6949, "synset": "cross.n.01", "name": "cross"}, - {"id": 6950, "synset": "crossbar.n.03", "name": "crossbar"}, - {"id": 6951, "synset": "crossbar.n.02", "name": "crossbar"}, - {"id": 6952, "synset": "crossbench.n.01", "name": "crossbench"}, - {"id": 6953, "synset": "cross_bit.n.01", "name": "cross_bit"}, - {"id": 6954, "synset": "crossbow.n.01", "name": "crossbow"}, - {"id": 6955, "synset": "crosscut_saw.n.01", "name": "crosscut_saw"}, - {"id": 6956, "synset": "crossjack.n.01", "name": "crossjack"}, - {"id": 6957, "synset": "crosspiece.n.02", "name": "crosspiece"}, - {"id": 6958, "synset": "crotchet.n.04", "name": "crotchet"}, - {"id": 6959, "synset": "croupier's_rake.n.01", "name": "croupier's_rake"}, - {"id": 6960, "synset": "crown.n.11", "name": "crown"}, - {"id": 6961, "synset": "crown_jewels.n.01", "name": "crown_jewels"}, - {"id": 6962, "synset": "crown_lens.n.01", "name": "crown_lens"}, - {"id": 6963, "synset": "crow's_nest.n.01", "name": "crow's_nest"}, - {"id": 6964, "synset": "crucible.n.01", "name": "crucible"}, - {"id": 6965, "synset": "cruet.n.01", "name": "cruet"}, - {"id": 6966, "synset": "cruet-stand.n.01", "name": "cruet-stand"}, - {"id": 6967, "synset": "cruise_control.n.01", "name": "cruise_control"}, - {"id": 6968, "synset": "cruise_missile.n.01", "name": "cruise_missile"}, - {"id": 6969, "synset": "cruiser.n.02", "name": "cruiser"}, - {"id": 6970, "synset": "crupper.n.01", "name": "crupper"}, - {"id": 6971, "synset": "cruse.n.01", "name": "cruse"}, - {"id": 6972, "synset": "crusher.n.01", "name": "crusher"}, - {"id": 6973, "synset": "cryometer.n.01", "name": "cryometer"}, - {"id": 6974, "synset": "cryoscope.n.01", "name": "cryoscope"}, - {"id": 6975, "synset": "cryostat.n.01", "name": "cryostat"}, - {"id": 6976, "synset": "crypt.n.01", "name": "crypt"}, - {"id": 6977, "synset": "crystal.n.06", "name": "crystal"}, - {"id": 6978, "synset": "crystal_detector.n.01", "name": "crystal_detector"}, - {"id": 6979, "synset": "crystal_microphone.n.01", "name": "crystal_microphone"}, - {"id": 6980, "synset": "crystal_oscillator.n.01", "name": "crystal_oscillator"}, - {"id": 6981, "synset": "crystal_set.n.01", "name": "crystal_set"}, - {"id": 6982, "synset": "cubitiere.n.01", "name": "cubitiere"}, - {"id": 6983, "synset": "cucking_stool.n.01", "name": "cucking_stool"}, - {"id": 6984, "synset": "cuckoo_clock.n.01", "name": "cuckoo_clock"}, - {"id": 6985, "synset": "cuddy.n.01", "name": "cuddy"}, - {"id": 6986, "synset": "cudgel.n.01", "name": "cudgel"}, - {"id": 6987, "synset": "cue.n.04", "name": "cue"}, - {"id": 6988, "synset": "cue_ball.n.01", "name": "cue_ball"}, - {"id": 6989, "synset": "cuff.n.01", "name": "cuff"}, - {"id": 6990, "synset": "cuirass.n.01", "name": "cuirass"}, - {"id": 6991, "synset": "cuisse.n.01", "name": "cuisse"}, - {"id": 6992, "synset": "cul.n.01", "name": "cul"}, - {"id": 6993, "synset": "culdoscope.n.01", "name": "culdoscope"}, - {"id": 6994, "synset": "cullis.n.01", "name": "cullis"}, - {"id": 6995, "synset": "culotte.n.01", "name": "culotte"}, - {"id": 6996, "synset": "cultivator.n.02", "name": "cultivator"}, - {"id": 6997, "synset": "culverin.n.02", "name": "culverin"}, - {"id": 6998, "synset": "culverin.n.01", "name": "culverin"}, - {"id": 6999, "synset": "culvert.n.01", "name": "culvert"}, - {"id": 7000, "synset": "cup_hook.n.01", "name": "cup_hook"}, - {"id": 7001, "synset": "cupola.n.02", "name": "cupola"}, - {"id": 7002, "synset": "cupola.n.01", "name": "cupola"}, - {"id": 7003, "synset": "curb.n.02", "name": "curb"}, - {"id": 7004, "synset": "curb_roof.n.01", "name": "curb_roof"}, - {"id": 7005, "synset": "curbstone.n.01", "name": "curbstone"}, - {"id": 7006, "synset": "curette.n.01", "name": "curette"}, - {"id": 7007, "synset": "currycomb.n.01", "name": "currycomb"}, - {"id": 7008, "synset": "cursor.n.01", "name": "cursor"}, - {"id": 7009, "synset": "customhouse.n.01", "name": "customhouse"}, - {"id": 7010, "synset": "cutaway.n.01", "name": "cutaway"}, - {"id": 7011, "synset": "cutlas.n.01", "name": "cutlas"}, - {"id": 7012, "synset": "cutoff.n.03", "name": "cutoff"}, - {"id": 7013, "synset": "cutout.n.01", "name": "cutout"}, - {"id": 7014, "synset": "cutter.n.06", "name": "cutter"}, - {"id": 7015, "synset": "cutter.n.05", "name": "cutter"}, - {"id": 7016, "synset": "cutting_implement.n.01", "name": "cutting_implement"}, - {"id": 7017, "synset": "cutting_room.n.01", "name": "cutting_room"}, - {"id": 7018, "synset": "cutty_stool.n.01", "name": "cutty_stool"}, - {"id": 7019, "synset": "cutwork.n.01", "name": "cutwork"}, - {"id": 7020, "synset": "cybercafe.n.01", "name": "cybercafe"}, - {"id": 7021, "synset": "cyclopean_masonry.n.01", "name": "cyclopean_masonry"}, - {"id": 7022, "synset": "cyclostyle.n.01", "name": "cyclostyle"}, - {"id": 7023, "synset": "cyclotron.n.01", "name": "cyclotron"}, - {"id": 7024, "synset": "cylinder.n.03", "name": "cylinder"}, - {"id": 7025, "synset": "cylinder_lock.n.01", "name": "cylinder_lock"}, - {"id": 7026, "synset": "dacha.n.01", "name": "dacha"}, - {"id": 7027, "synset": "dacron.n.01", "name": "Dacron"}, - {"id": 7028, "synset": "dado.n.02", "name": "dado"}, - {"id": 7029, "synset": "dado_plane.n.01", "name": "dado_plane"}, - {"id": 7030, "synset": "dairy.n.01", "name": "dairy"}, - {"id": 7031, "synset": "dais.n.01", "name": "dais"}, - {"id": 7032, "synset": "daisy_print_wheel.n.01", "name": "daisy_print_wheel"}, - {"id": 7033, "synset": "daisywheel_printer.n.01", "name": "daisywheel_printer"}, - {"id": 7034, "synset": "dam.n.01", "name": "dam"}, - {"id": 7035, "synset": "damask.n.02", "name": "damask"}, - {"id": 7036, "synset": "dampener.n.01", "name": "dampener"}, - {"id": 7037, "synset": "damper.n.02", "name": "damper"}, - {"id": 7038, "synset": "damper_block.n.01", "name": "damper_block"}, - {"id": 7039, "synset": "dark_lantern.n.01", "name": "dark_lantern"}, - {"id": 7040, "synset": "darkroom.n.01", "name": "darkroom"}, - {"id": 7041, "synset": "darning_needle.n.01", "name": "darning_needle"}, - {"id": 7042, "synset": "dart.n.02", "name": "dart"}, - {"id": 7043, "synset": "dart.n.01", "name": "dart"}, - {"id": 7044, "synset": "dashboard.n.02", "name": "dashboard"}, - {"id": 7045, "synset": "dashiki.n.01", "name": "dashiki"}, - {"id": 7046, "synset": "dash-pot.n.01", "name": "dash-pot"}, - {"id": 7047, "synset": "data_converter.n.01", "name": "data_converter"}, - {"id": 7048, "synset": "data_input_device.n.01", "name": "data_input_device"}, - {"id": 7049, "synset": "data_multiplexer.n.01", "name": "data_multiplexer"}, - {"id": 7050, "synset": "data_system.n.01", "name": "data_system"}, - {"id": 7051, "synset": "davenport.n.03", "name": "davenport"}, - {"id": 7052, "synset": "davenport.n.02", "name": "davenport"}, - {"id": 7053, "synset": "davit.n.01", "name": "davit"}, - {"id": 7054, "synset": "daybed.n.01", "name": "daybed"}, - {"id": 7055, "synset": "daybook.n.02", "name": "daybook"}, - {"id": 7056, "synset": "day_nursery.n.01", "name": "day_nursery"}, - {"id": 7057, "synset": "day_school.n.03", "name": "day_school"}, - {"id": 7058, "synset": "dead_axle.n.01", "name": "dead_axle"}, - {"id": 7059, "synset": "deadeye.n.02", "name": "deadeye"}, - {"id": 7060, "synset": "deadhead.n.02", "name": "deadhead"}, - {"id": 7061, "synset": "deanery.n.01", "name": "deanery"}, - {"id": 7062, "synset": "deathbed.n.02", "name": "deathbed"}, - {"id": 7063, "synset": "death_camp.n.01", "name": "death_camp"}, - {"id": 7064, "synset": "death_house.n.01", "name": "death_house"}, - {"id": 7065, "synset": "death_knell.n.02", "name": "death_knell"}, - {"id": 7066, "synset": "death_seat.n.01", "name": "death_seat"}, - {"id": 7067, "synset": "deck.n.02", "name": "deck"}, - {"id": 7068, "synset": "deck.n.04", "name": "deck"}, - {"id": 7069, "synset": "deck-house.n.01", "name": "deck-house"}, - {"id": 7070, "synset": "deckle.n.02", "name": "deckle"}, - {"id": 7071, "synset": "deckle_edge.n.01", "name": "deckle_edge"}, - {"id": 7072, "synset": "declinometer.n.01", "name": "declinometer"}, - {"id": 7073, "synset": "decoder.n.02", "name": "decoder"}, - {"id": 7074, "synset": "decolletage.n.01", "name": "decolletage"}, - {"id": 7075, "synset": "decoupage.n.01", "name": "decoupage"}, - {"id": 7076, "synset": "dedicated_file_server.n.01", "name": "dedicated_file_server"}, - {"id": 7077, "synset": "deep-freeze.n.01", "name": "deep-freeze"}, - {"id": 7078, "synset": "deerstalker.n.01", "name": "deerstalker"}, - {"id": 7079, "synset": "defense_system.n.01", "name": "defense_system"}, - {"id": 7080, "synset": "defensive_structure.n.01", "name": "defensive_structure"}, - {"id": 7081, "synset": "defibrillator.n.01", "name": "defibrillator"}, - {"id": 7082, "synset": "defilade.n.01", "name": "defilade"}, - {"id": 7083, "synset": "deflector.n.01", "name": "deflector"}, - {"id": 7084, "synset": "delayed_action.n.01", "name": "delayed_action"}, - {"id": 7085, "synset": "delay_line.n.01", "name": "delay_line"}, - {"id": 7086, "synset": "delft.n.01", "name": "delft"}, - {"id": 7087, "synset": "delicatessen.n.02", "name": "delicatessen"}, - {"id": 7088, "synset": "delivery_truck.n.01", "name": "delivery_truck"}, - {"id": 7089, "synset": "delta_wing.n.01", "name": "delta_wing"}, - {"id": 7090, "synset": "demijohn.n.01", "name": "demijohn"}, - {"id": 7091, "synset": "demitasse.n.02", "name": "demitasse"}, - {"id": 7092, "synset": "den.n.04", "name": "den"}, - {"id": 7093, "synset": "denim.n.02", "name": "denim"}, - {"id": 7094, "synset": "densimeter.n.01", "name": "densimeter"}, - {"id": 7095, "synset": "densitometer.n.01", "name": "densitometer"}, - {"id": 7096, "synset": "dental_appliance.n.01", "name": "dental_appliance"}, - {"id": 7097, "synset": "dental_implant.n.01", "name": "dental_implant"}, - {"id": 7098, "synset": "dentist's_drill.n.01", "name": "dentist's_drill"}, - {"id": 7099, "synset": "denture.n.01", "name": "denture"}, - {"id": 7100, "synset": "deodorant.n.01", "name": "deodorant"}, - {"id": 7101, "synset": "department_store.n.01", "name": "department_store"}, - {"id": 7102, "synset": "departure_lounge.n.01", "name": "departure_lounge"}, - {"id": 7103, "synset": "depilatory.n.02", "name": "depilatory"}, - {"id": 7104, "synset": "depressor.n.03", "name": "depressor"}, - {"id": 7105, "synset": "depth_finder.n.01", "name": "depth_finder"}, - {"id": 7106, "synset": "depth_gauge.n.01", "name": "depth_gauge"}, - {"id": 7107, "synset": "derrick.n.02", "name": "derrick"}, - {"id": 7108, "synset": "derrick.n.01", "name": "derrick"}, - {"id": 7109, "synset": "derringer.n.01", "name": "derringer"}, - {"id": 7110, "synset": "desk_phone.n.01", "name": "desk_phone"}, - {"id": 7111, "synset": "desktop_computer.n.01", "name": "desktop_computer"}, - {"id": 7112, "synset": "dessert_spoon.n.01", "name": "dessert_spoon"}, - {"id": 7113, "synset": "destroyer.n.01", "name": "destroyer"}, - {"id": 7114, "synset": "destroyer_escort.n.01", "name": "destroyer_escort"}, - {"id": 7115, "synset": "detached_house.n.01", "name": "detached_house"}, - {"id": 7116, "synset": "detector.n.01", "name": "detector"}, - {"id": 7117, "synset": "detector.n.03", "name": "detector"}, - {"id": 7118, "synset": "detention_home.n.01", "name": "detention_home"}, - {"id": 7119, "synset": "detonating_fuse.n.01", "name": "detonating_fuse"}, - {"id": 7120, "synset": "detonator.n.01", "name": "detonator"}, - {"id": 7121, "synset": "developer.n.02", "name": "developer"}, - {"id": 7122, "synset": "device.n.01", "name": "device"}, - {"id": 7123, "synset": "dewar_flask.n.01", "name": "Dewar_flask"}, - {"id": 7124, "synset": "dhoti.n.01", "name": "dhoti"}, - {"id": 7125, "synset": "dhow.n.01", "name": "dhow"}, - {"id": 7126, "synset": "dial.n.04", "name": "dial"}, - {"id": 7127, "synset": "dial.n.03", "name": "dial"}, - {"id": 7128, "synset": "dial.n.02", "name": "dial"}, - {"id": 7129, "synset": "dialog_box.n.01", "name": "dialog_box"}, - {"id": 7130, "synset": "dial_telephone.n.01", "name": "dial_telephone"}, - {"id": 7131, "synset": "dialyzer.n.01", "name": "dialyzer"}, - {"id": 7132, "synset": "diamante.n.02", "name": "diamante"}, - {"id": 7133, "synset": "diaper.n.02", "name": "diaper"}, - {"id": 7134, "synset": "diaphone.n.01", "name": "diaphone"}, - {"id": 7135, "synset": "diaphragm.n.01", "name": "diaphragm"}, - {"id": 7136, "synset": "diaphragm.n.04", "name": "diaphragm"}, - {"id": 7137, "synset": "diathermy_machine.n.01", "name": "diathermy_machine"}, - {"id": 7138, "synset": "dibble.n.01", "name": "dibble"}, - {"id": 7139, "synset": "dice_cup.n.01", "name": "dice_cup"}, - {"id": 7140, "synset": "dicer.n.01", "name": "dicer"}, - {"id": 7141, "synset": "dickey.n.02", "name": "dickey"}, - {"id": 7142, "synset": "dickey.n.01", "name": "dickey"}, - {"id": 7143, "synset": "dictaphone.n.01", "name": "Dictaphone"}, - {"id": 7144, "synset": "die.n.03", "name": "die"}, - {"id": 7145, "synset": "diesel.n.02", "name": "diesel"}, - {"id": 7146, "synset": "diesel-electric_locomotive.n.01", "name": "diesel-electric_locomotive"}, - { - "id": 7147, - "synset": "diesel-hydraulic_locomotive.n.01", - "name": "diesel-hydraulic_locomotive", - }, - {"id": 7148, "synset": "diesel_locomotive.n.01", "name": "diesel_locomotive"}, - {"id": 7149, "synset": "diestock.n.01", "name": "diestock"}, - {"id": 7150, "synset": "differential_analyzer.n.01", "name": "differential_analyzer"}, - {"id": 7151, "synset": "differential_gear.n.01", "name": "differential_gear"}, - {"id": 7152, "synset": "diffuser.n.02", "name": "diffuser"}, - {"id": 7153, "synset": "diffuser.n.01", "name": "diffuser"}, - {"id": 7154, "synset": "digester.n.01", "name": "digester"}, - {"id": 7155, "synset": "diggings.n.02", "name": "diggings"}, - {"id": 7156, "synset": "digital-analog_converter.n.01", "name": "digital-analog_converter"}, - {"id": 7157, "synset": "digital_audiotape.n.01", "name": "digital_audiotape"}, - {"id": 7158, "synset": "digital_camera.n.01", "name": "digital_camera"}, - {"id": 7159, "synset": "digital_clock.n.01", "name": "digital_clock"}, - {"id": 7160, "synset": "digital_computer.n.01", "name": "digital_computer"}, - {"id": 7161, "synset": "digital_display.n.01", "name": "digital_display"}, - {"id": 7162, "synset": "digital_subscriber_line.n.01", "name": "digital_subscriber_line"}, - {"id": 7163, "synset": "digital_voltmeter.n.01", "name": "digital_voltmeter"}, - {"id": 7164, "synset": "digital_watch.n.01", "name": "digital_watch"}, - {"id": 7165, "synset": "digitizer.n.01", "name": "digitizer"}, - {"id": 7166, "synset": "dilator.n.03", "name": "dilator"}, - {"id": 7167, "synset": "dildo.n.01", "name": "dildo"}, - {"id": 7168, "synset": "dimity.n.01", "name": "dimity"}, - {"id": 7169, "synset": "dimmer.n.01", "name": "dimmer"}, - {"id": 7170, "synset": "diner.n.03", "name": "diner"}, - {"id": 7171, "synset": "dinette.n.01", "name": "dinette"}, - {"id": 7172, "synset": "dining_area.n.01", "name": "dining_area"}, - {"id": 7173, "synset": "dining_car.n.01", "name": "dining_car"}, - {"id": 7174, "synset": "dining-hall.n.01", "name": "dining-hall"}, - {"id": 7175, "synset": "dining_room.n.01", "name": "dining_room"}, - {"id": 7176, "synset": "dining-room_furniture.n.01", "name": "dining-room_furniture"}, - {"id": 7177, "synset": "dining-room_table.n.01", "name": "dining-room_table"}, - {"id": 7178, "synset": "dinner_bell.n.01", "name": "dinner_bell"}, - {"id": 7179, "synset": "dinner_dress.n.01", "name": "dinner_dress"}, - {"id": 7180, "synset": "dinner_napkin.n.01", "name": "dinner_napkin"}, - {"id": 7181, "synset": "dinner_pail.n.01", "name": "dinner_pail"}, - {"id": 7182, "synset": "dinner_table.n.01", "name": "dinner_table"}, - {"id": 7183, "synset": "dinner_theater.n.01", "name": "dinner_theater"}, - {"id": 7184, "synset": "diode.n.02", "name": "diode"}, - {"id": 7185, "synset": "diode.n.01", "name": "diode"}, - {"id": 7186, "synset": "dip.n.07", "name": "dip"}, - {"id": 7187, "synset": "diplomatic_building.n.01", "name": "diplomatic_building"}, - {"id": 7188, "synset": "dipole.n.02", "name": "dipole"}, - {"id": 7189, "synset": "dipper.n.01", "name": "dipper"}, - {"id": 7190, "synset": "dipstick.n.01", "name": "dipstick"}, - {"id": 7191, "synset": "dip_switch.n.01", "name": "DIP_switch"}, - {"id": 7192, "synset": "directional_antenna.n.01", "name": "directional_antenna"}, - {"id": 7193, "synset": "directional_microphone.n.01", "name": "directional_microphone"}, - {"id": 7194, "synset": "direction_finder.n.01", "name": "direction_finder"}, - {"id": 7195, "synset": "dirk.n.01", "name": "dirk"}, - {"id": 7196, "synset": "dirndl.n.02", "name": "dirndl"}, - {"id": 7197, "synset": "dirndl.n.01", "name": "dirndl"}, - {"id": 7198, "synset": "dirty_bomb.n.01", "name": "dirty_bomb"}, - {"id": 7199, "synset": "discharge_lamp.n.01", "name": "discharge_lamp"}, - {"id": 7200, "synset": "discharge_pipe.n.01", "name": "discharge_pipe"}, - {"id": 7201, "synset": "disco.n.02", "name": "disco"}, - {"id": 7202, "synset": "discount_house.n.01", "name": "discount_house"}, - {"id": 7203, "synset": "discus.n.02", "name": "discus"}, - {"id": 7204, "synset": "disguise.n.02", "name": "disguise"}, - {"id": 7205, "synset": "dishpan.n.01", "name": "dishpan"}, - {"id": 7206, "synset": "dish_rack.n.01", "name": "dish_rack"}, - {"id": 7207, "synset": "disk.n.02", "name": "disk"}, - {"id": 7208, "synset": "disk_brake.n.01", "name": "disk_brake"}, - {"id": 7209, "synset": "disk_clutch.n.01", "name": "disk_clutch"}, - {"id": 7210, "synset": "disk_controller.n.01", "name": "disk_controller"}, - {"id": 7211, "synset": "disk_drive.n.01", "name": "disk_drive"}, - {"id": 7212, "synset": "diskette.n.01", "name": "diskette"}, - {"id": 7213, "synset": "disk_harrow.n.01", "name": "disk_harrow"}, - {"id": 7214, "synset": "dispatch_case.n.01", "name": "dispatch_case"}, - {"id": 7215, "synset": "dispensary.n.01", "name": "dispensary"}, - {"id": 7216, "synset": "display.n.06", "name": "display"}, - {"id": 7217, "synset": "display_adapter.n.01", "name": "display_adapter"}, - {"id": 7218, "synset": "display_panel.n.01", "name": "display_panel"}, - {"id": 7219, "synset": "display_window.n.01", "name": "display_window"}, - {"id": 7220, "synset": "disposal.n.04", "name": "disposal"}, - {"id": 7221, "synset": "disrupting_explosive.n.01", "name": "disrupting_explosive"}, - {"id": 7222, "synset": "distaff.n.02", "name": "distaff"}, - {"id": 7223, "synset": "distillery.n.01", "name": "distillery"}, - {"id": 7224, "synset": "distributor.n.04", "name": "distributor"}, - {"id": 7225, "synset": "distributor_cam.n.01", "name": "distributor_cam"}, - {"id": 7226, "synset": "distributor_cap.n.01", "name": "distributor_cap"}, - {"id": 7227, "synset": "distributor_housing.n.01", "name": "distributor_housing"}, - {"id": 7228, "synset": "distributor_point.n.01", "name": "distributor_point"}, - {"id": 7229, "synset": "ditch.n.01", "name": "ditch"}, - {"id": 7230, "synset": "ditch_spade.n.01", "name": "ditch_spade"}, - {"id": 7231, "synset": "ditty_bag.n.01", "name": "ditty_bag"}, - {"id": 7232, "synset": "divan.n.01", "name": "divan"}, - {"id": 7233, "synset": "divan.n.04", "name": "divan"}, - {"id": 7234, "synset": "dive_bomber.n.01", "name": "dive_bomber"}, - {"id": 7235, "synset": "diverging_lens.n.01", "name": "diverging_lens"}, - {"id": 7236, "synset": "divided_highway.n.01", "name": "divided_highway"}, - {"id": 7237, "synset": "divider.n.04", "name": "divider"}, - {"id": 7238, "synset": "diving_bell.n.01", "name": "diving_bell"}, - {"id": 7239, "synset": "divining_rod.n.01", "name": "divining_rod"}, - {"id": 7240, "synset": "diving_suit.n.01", "name": "diving_suit"}, - {"id": 7241, "synset": "dixie.n.02", "name": "dixie"}, - {"id": 7242, "synset": "dock.n.05", "name": "dock"}, - {"id": 7243, "synset": "doeskin.n.02", "name": "doeskin"}, - {"id": 7244, "synset": "dogcart.n.01", "name": "dogcart"}, - {"id": 7245, "synset": "doggie_bag.n.01", "name": "doggie_bag"}, - {"id": 7246, "synset": "dogsled.n.01", "name": "dogsled"}, - {"id": 7247, "synset": "dog_wrench.n.01", "name": "dog_wrench"}, - {"id": 7248, "synset": "doily.n.01", "name": "doily"}, - {"id": 7249, "synset": "dolly.n.02", "name": "dolly"}, - {"id": 7250, "synset": "dolman.n.02", "name": "dolman"}, - {"id": 7251, "synset": "dolman.n.01", "name": "dolman"}, - {"id": 7252, "synset": "dolman_sleeve.n.01", "name": "dolman_sleeve"}, - {"id": 7253, "synset": "dolmen.n.01", "name": "dolmen"}, - {"id": 7254, "synset": "dome.n.04", "name": "dome"}, - {"id": 7255, "synset": "dome.n.03", "name": "dome"}, - {"id": 7256, "synset": "domino.n.03", "name": "domino"}, - {"id": 7257, "synset": "dongle.n.01", "name": "dongle"}, - {"id": 7258, "synset": "donkey_jacket.n.01", "name": "donkey_jacket"}, - {"id": 7259, "synset": "door.n.01", "name": "door"}, - {"id": 7260, "synset": "door.n.05", "name": "door"}, - {"id": 7261, "synset": "door.n.04", "name": "door"}, - {"id": 7262, "synset": "doorbell.n.01", "name": "doorbell"}, - {"id": 7263, "synset": "doorframe.n.01", "name": "doorframe"}, - {"id": 7264, "synset": "doorjamb.n.01", "name": "doorjamb"}, - {"id": 7265, "synset": "doorlock.n.01", "name": "doorlock"}, - {"id": 7266, "synset": "doornail.n.01", "name": "doornail"}, - {"id": 7267, "synset": "doorplate.n.01", "name": "doorplate"}, - {"id": 7268, "synset": "doorsill.n.01", "name": "doorsill"}, - {"id": 7269, "synset": "doorstop.n.01", "name": "doorstop"}, - {"id": 7270, "synset": "doppler_radar.n.01", "name": "Doppler_radar"}, - {"id": 7271, "synset": "dormer.n.01", "name": "dormer"}, - {"id": 7272, "synset": "dormer_window.n.01", "name": "dormer_window"}, - {"id": 7273, "synset": "dormitory.n.01", "name": "dormitory"}, - {"id": 7274, "synset": "dormitory.n.02", "name": "dormitory"}, - {"id": 7275, "synset": "dosemeter.n.01", "name": "dosemeter"}, - {"id": 7276, "synset": "dossal.n.01", "name": "dossal"}, - {"id": 7277, "synset": "dot_matrix_printer.n.01", "name": "dot_matrix_printer"}, - {"id": 7278, "synset": "double_bed.n.01", "name": "double_bed"}, - {"id": 7279, "synset": "double-bitted_ax.n.01", "name": "double-bitted_ax"}, - {"id": 7280, "synset": "double_boiler.n.01", "name": "double_boiler"}, - {"id": 7281, "synset": "double-breasted_jacket.n.01", "name": "double-breasted_jacket"}, - {"id": 7282, "synset": "double-breasted_suit.n.01", "name": "double-breasted_suit"}, - {"id": 7283, "synset": "double_door.n.01", "name": "double_door"}, - {"id": 7284, "synset": "double_glazing.n.01", "name": "double_glazing"}, - {"id": 7285, "synset": "double-hung_window.n.01", "name": "double-hung_window"}, - {"id": 7286, "synset": "double_knit.n.01", "name": "double_knit"}, - {"id": 7287, "synset": "doubler.n.01", "name": "doubler"}, - {"id": 7288, "synset": "double_reed.n.02", "name": "double_reed"}, - {"id": 7289, "synset": "double-reed_instrument.n.01", "name": "double-reed_instrument"}, - {"id": 7290, "synset": "doublet.n.01", "name": "doublet"}, - {"id": 7291, "synset": "doubletree.n.01", "name": "doubletree"}, - {"id": 7292, "synset": "douche.n.01", "name": "douche"}, - {"id": 7293, "synset": "dovecote.n.01", "name": "dovecote"}, - {"id": 7294, "synset": "dover's_powder.n.01", "name": "Dover's_powder"}, - {"id": 7295, "synset": "dovetail.n.01", "name": "dovetail"}, - {"id": 7296, "synset": "dovetail_plane.n.01", "name": "dovetail_plane"}, - {"id": 7297, "synset": "dowel.n.01", "name": "dowel"}, - {"id": 7298, "synset": "downstage.n.01", "name": "downstage"}, - {"id": 7299, "synset": "drafting_instrument.n.01", "name": "drafting_instrument"}, - {"id": 7300, "synset": "drafting_table.n.01", "name": "drafting_table"}, - {"id": 7301, "synset": "dragunov.n.01", "name": "Dragunov"}, - {"id": 7302, "synset": "drainage_ditch.n.01", "name": "drainage_ditch"}, - {"id": 7303, "synset": "drainage_system.n.01", "name": "drainage_system"}, - {"id": 7304, "synset": "drain_basket.n.01", "name": "drain_basket"}, - {"id": 7305, "synset": "drainplug.n.01", "name": "drainplug"}, - {"id": 7306, "synset": "drape.n.03", "name": "drape"}, - {"id": 7307, "synset": "drapery.n.02", "name": "drapery"}, - {"id": 7308, "synset": "drawbar.n.01", "name": "drawbar"}, - {"id": 7309, "synset": "drawbridge.n.01", "name": "drawbridge"}, - {"id": 7310, "synset": "drawing_chalk.n.01", "name": "drawing_chalk"}, - {"id": 7311, "synset": "drawing_room.n.01", "name": "drawing_room"}, - {"id": 7312, "synset": "drawing_room.n.02", "name": "drawing_room"}, - {"id": 7313, "synset": "drawknife.n.01", "name": "drawknife"}, - {"id": 7314, "synset": "drawstring_bag.n.01", "name": "drawstring_bag"}, - {"id": 7315, "synset": "dray.n.01", "name": "dray"}, - {"id": 7316, "synset": "dreadnought.n.01", "name": "dreadnought"}, - {"id": 7317, "synset": "dredge.n.01", "name": "dredge"}, - {"id": 7318, "synset": "dredger.n.01", "name": "dredger"}, - {"id": 7319, "synset": "dredging_bucket.n.01", "name": "dredging_bucket"}, - {"id": 7320, "synset": "dress_blues.n.01", "name": "dress_blues"}, - {"id": 7321, "synset": "dressing.n.04", "name": "dressing"}, - {"id": 7322, "synset": "dressing_case.n.01", "name": "dressing_case"}, - {"id": 7323, "synset": "dressing_gown.n.01", "name": "dressing_gown"}, - {"id": 7324, "synset": "dressing_room.n.01", "name": "dressing_room"}, - {"id": 7325, "synset": "dressing_sack.n.01", "name": "dressing_sack"}, - {"id": 7326, "synset": "dressing_table.n.01", "name": "dressing_table"}, - {"id": 7327, "synset": "dress_rack.n.01", "name": "dress_rack"}, - {"id": 7328, "synset": "dress_shirt.n.01", "name": "dress_shirt"}, - {"id": 7329, "synset": "dress_uniform.n.01", "name": "dress_uniform"}, - {"id": 7330, "synset": "drift_net.n.01", "name": "drift_net"}, - {"id": 7331, "synset": "electric_drill.n.01", "name": "electric_drill"}, - {"id": 7332, "synset": "drilling_platform.n.01", "name": "drilling_platform"}, - {"id": 7333, "synset": "drill_press.n.01", "name": "drill_press"}, - {"id": 7334, "synset": "drill_rig.n.01", "name": "drill_rig"}, - {"id": 7335, "synset": "drinking_fountain.n.01", "name": "drinking_fountain"}, - {"id": 7336, "synset": "drinking_vessel.n.01", "name": "drinking_vessel"}, - {"id": 7337, "synset": "drip_loop.n.01", "name": "drip_loop"}, - {"id": 7338, "synset": "drip_mat.n.01", "name": "drip_mat"}, - {"id": 7339, "synset": "drip_pan.n.02", "name": "drip_pan"}, - {"id": 7340, "synset": "dripping_pan.n.01", "name": "dripping_pan"}, - {"id": 7341, "synset": "drip_pot.n.01", "name": "drip_pot"}, - {"id": 7342, "synset": "drive.n.02", "name": "drive"}, - {"id": 7343, "synset": "drive.n.10", "name": "drive"}, - {"id": 7344, "synset": "drive_line.n.01", "name": "drive_line"}, - {"id": 7345, "synset": "driver.n.05", "name": "driver"}, - {"id": 7346, "synset": "driveshaft.n.01", "name": "driveshaft"}, - {"id": 7347, "synset": "driveway.n.01", "name": "driveway"}, - {"id": 7348, "synset": "driving_iron.n.01", "name": "driving_iron"}, - {"id": 7349, "synset": "driving_wheel.n.01", "name": "driving_wheel"}, - {"id": 7350, "synset": "drogue.n.04", "name": "drogue"}, - {"id": 7351, "synset": "drogue_parachute.n.01", "name": "drogue_parachute"}, - {"id": 7352, "synset": "drone.n.05", "name": "drone"}, - {"id": 7353, "synset": "drop_arch.n.01", "name": "drop_arch"}, - {"id": 7354, "synset": "drop_cloth.n.02", "name": "drop_cloth"}, - {"id": 7355, "synset": "drop_curtain.n.01", "name": "drop_curtain"}, - {"id": 7356, "synset": "drop_forge.n.01", "name": "drop_forge"}, - {"id": 7357, "synset": "drop-leaf_table.n.01", "name": "drop-leaf_table"}, - {"id": 7358, "synset": "droshky.n.01", "name": "droshky"}, - {"id": 7359, "synset": "drove.n.03", "name": "drove"}, - {"id": 7360, "synset": "drugget.n.01", "name": "drugget"}, - {"id": 7361, "synset": "drugstore.n.01", "name": "drugstore"}, - {"id": 7362, "synset": "drum.n.04", "name": "drum"}, - {"id": 7363, "synset": "drum_brake.n.01", "name": "drum_brake"}, - {"id": 7364, "synset": "drumhead.n.01", "name": "drumhead"}, - {"id": 7365, "synset": "drum_printer.n.01", "name": "drum_printer"}, - {"id": 7366, "synset": "drum_sander.n.01", "name": "drum_sander"}, - {"id": 7367, "synset": "dry_battery.n.01", "name": "dry_battery"}, - {"id": 7368, "synset": "dry-bulb_thermometer.n.01", "name": "dry-bulb_thermometer"}, - {"id": 7369, "synset": "dry_cell.n.01", "name": "dry_cell"}, - {"id": 7370, "synset": "dry_dock.n.01", "name": "dry_dock"}, - {"id": 7371, "synset": "dryer.n.01", "name": "dryer"}, - {"id": 7372, "synset": "dry_fly.n.01", "name": "dry_fly"}, - {"id": 7373, "synset": "dry_kiln.n.01", "name": "dry_kiln"}, - {"id": 7374, "synset": "dry_masonry.n.01", "name": "dry_masonry"}, - {"id": 7375, "synset": "dry_point.n.02", "name": "dry_point"}, - {"id": 7376, "synset": "dry_wall.n.02", "name": "dry_wall"}, - {"id": 7377, "synset": "dual_scan_display.n.01", "name": "dual_scan_display"}, - {"id": 7378, "synset": "duck.n.04", "name": "duck"}, - {"id": 7379, "synset": "duckboard.n.01", "name": "duckboard"}, - {"id": 7380, "synset": "duckpin.n.01", "name": "duckpin"}, - {"id": 7381, "synset": "dudeen.n.01", "name": "dudeen"}, - {"id": 7382, "synset": "duffel.n.02", "name": "duffel"}, - {"id": 7383, "synset": "duffel_coat.n.01", "name": "duffel_coat"}, - {"id": 7384, "synset": "dugout.n.01", "name": "dugout"}, - {"id": 7385, "synset": "dugout_canoe.n.01", "name": "dugout_canoe"}, - {"id": 7386, "synset": "dulciana.n.01", "name": "dulciana"}, - {"id": 7387, "synset": "dulcimer.n.02", "name": "dulcimer"}, - {"id": 7388, "synset": "dulcimer.n.01", "name": "dulcimer"}, - {"id": 7389, "synset": "dumb_bomb.n.01", "name": "dumb_bomb"}, - {"id": 7390, "synset": "dumbwaiter.n.01", "name": "dumbwaiter"}, - {"id": 7391, "synset": "dumdum.n.01", "name": "dumdum"}, - {"id": 7392, "synset": "dumpcart.n.01", "name": "dumpcart"}, - {"id": 7393, "synset": "dump_truck.n.01", "name": "dump_truck"}, - {"id": 7394, "synset": "dumpy_level.n.01", "name": "Dumpy_level"}, - {"id": 7395, "synset": "dunce_cap.n.01", "name": "dunce_cap"}, - {"id": 7396, "synset": "dune_buggy.n.01", "name": "dune_buggy"}, - {"id": 7397, "synset": "dungeon.n.02", "name": "dungeon"}, - {"id": 7398, "synset": "duplex_apartment.n.01", "name": "duplex_apartment"}, - {"id": 7399, "synset": "duplex_house.n.01", "name": "duplex_house"}, - {"id": 7400, "synset": "duplicator.n.01", "name": "duplicator"}, - {"id": 7401, "synset": "dust_bag.n.01", "name": "dust_bag"}, - {"id": 7402, "synset": "dustcloth.n.01", "name": "dustcloth"}, - {"id": 7403, "synset": "dust_cover.n.03", "name": "dust_cover"}, - {"id": 7404, "synset": "dust_cover.n.02", "name": "dust_cover"}, - {"id": 7405, "synset": "dustmop.n.01", "name": "dustmop"}, - {"id": 7406, "synset": "dutch_oven.n.01", "name": "Dutch_oven"}, - {"id": 7407, "synset": "dutch_oven.n.02", "name": "Dutch_oven"}, - {"id": 7408, "synset": "dwelling.n.01", "name": "dwelling"}, - {"id": 7409, "synset": "dye-works.n.01", "name": "dye-works"}, - {"id": 7410, "synset": "dynamo.n.01", "name": "dynamo"}, - {"id": 7411, "synset": "dynamometer.n.01", "name": "dynamometer"}, - {"id": 7412, "synset": "eames_chair.n.01", "name": "Eames_chair"}, - {"id": 7413, "synset": "earflap.n.01", "name": "earflap"}, - {"id": 7414, "synset": "early_warning_radar.n.01", "name": "early_warning_radar"}, - {"id": 7415, "synset": "early_warning_system.n.01", "name": "early_warning_system"}, - {"id": 7416, "synset": "earmuff.n.01", "name": "earmuff"}, - {"id": 7417, "synset": "earplug.n.02", "name": "earplug"}, - {"id": 7418, "synset": "earthenware.n.01", "name": "earthenware"}, - {"id": 7419, "synset": "earthwork.n.01", "name": "earthwork"}, - {"id": 7420, "synset": "easy_chair.n.01", "name": "easy_chair"}, - {"id": 7421, "synset": "eaves.n.01", "name": "eaves"}, - {"id": 7422, "synset": "ecclesiastical_attire.n.01", "name": "ecclesiastical_attire"}, - {"id": 7423, "synset": "echinus.n.01", "name": "echinus"}, - {"id": 7424, "synset": "echocardiograph.n.01", "name": "echocardiograph"}, - {"id": 7425, "synset": "edger.n.02", "name": "edger"}, - {"id": 7426, "synset": "edge_tool.n.01", "name": "edge_tool"}, - {"id": 7427, "synset": "efficiency_apartment.n.01", "name": "efficiency_apartment"}, - {"id": 7428, "synset": "egg-and-dart.n.01", "name": "egg-and-dart"}, - {"id": 7429, "synset": "egg_timer.n.01", "name": "egg_timer"}, - {"id": 7430, "synset": "eiderdown.n.01", "name": "eiderdown"}, - {"id": 7431, "synset": "eight_ball.n.01", "name": "eight_ball"}, - {"id": 7432, "synset": "ejection_seat.n.01", "name": "ejection_seat"}, - {"id": 7433, "synset": "elastic.n.02", "name": "elastic"}, - {"id": 7434, "synset": "elastic_bandage.n.01", "name": "elastic_bandage"}, - {"id": 7435, "synset": "elastoplast.n.01", "name": "Elastoplast"}, - {"id": 7436, "synset": "elbow.n.04", "name": "elbow"}, - {"id": 7437, "synset": "elbow_pad.n.01", "name": "elbow_pad"}, - {"id": 7438, "synset": "electric.n.01", "name": "electric"}, - {"id": 7439, "synset": "electrical_cable.n.01", "name": "electrical_cable"}, - {"id": 7440, "synset": "electrical_contact.n.01", "name": "electrical_contact"}, - {"id": 7441, "synset": "electrical_converter.n.01", "name": "electrical_converter"}, - {"id": 7442, "synset": "electrical_device.n.01", "name": "electrical_device"}, - {"id": 7443, "synset": "electrical_system.n.02", "name": "electrical_system"}, - {"id": 7444, "synset": "electric_bell.n.01", "name": "electric_bell"}, - {"id": 7445, "synset": "electric_blanket.n.01", "name": "electric_blanket"}, - {"id": 7446, "synset": "electric_clock.n.01", "name": "electric_clock"}, - {"id": 7447, "synset": "electric-discharge_lamp.n.01", "name": "electric-discharge_lamp"}, - {"id": 7448, "synset": "electric_fan.n.01", "name": "electric_fan"}, - {"id": 7449, "synset": "electric_frying_pan.n.01", "name": "electric_frying_pan"}, - {"id": 7450, "synset": "electric_furnace.n.01", "name": "electric_furnace"}, - {"id": 7451, "synset": "electric_guitar.n.01", "name": "electric_guitar"}, - {"id": 7452, "synset": "electric_hammer.n.01", "name": "electric_hammer"}, - {"id": 7453, "synset": "electric_heater.n.01", "name": "electric_heater"}, - {"id": 7454, "synset": "electric_lamp.n.01", "name": "electric_lamp"}, - {"id": 7455, "synset": "electric_locomotive.n.01", "name": "electric_locomotive"}, - {"id": 7456, "synset": "electric_meter.n.01", "name": "electric_meter"}, - {"id": 7457, "synset": "electric_mixer.n.01", "name": "electric_mixer"}, - {"id": 7458, "synset": "electric_motor.n.01", "name": "electric_motor"}, - {"id": 7459, "synset": "electric_organ.n.01", "name": "electric_organ"}, - {"id": 7460, "synset": "electric_range.n.01", "name": "electric_range"}, - {"id": 7461, "synset": "electric_toothbrush.n.01", "name": "electric_toothbrush"}, - {"id": 7462, "synset": "electric_typewriter.n.01", "name": "electric_typewriter"}, - { - "id": 7463, - "synset": "electro-acoustic_transducer.n.01", - "name": "electro-acoustic_transducer", - }, - {"id": 7464, "synset": "electrode.n.01", "name": "electrode"}, - {"id": 7465, "synset": "electrodynamometer.n.01", "name": "electrodynamometer"}, - {"id": 7466, "synset": "electroencephalograph.n.01", "name": "electroencephalograph"}, - {"id": 7467, "synset": "electrograph.n.01", "name": "electrograph"}, - {"id": 7468, "synset": "electrolytic.n.01", "name": "electrolytic"}, - {"id": 7469, "synset": "electrolytic_cell.n.01", "name": "electrolytic_cell"}, - {"id": 7470, "synset": "electromagnet.n.01", "name": "electromagnet"}, - {"id": 7471, "synset": "electrometer.n.01", "name": "electrometer"}, - {"id": 7472, "synset": "electromyograph.n.01", "name": "electromyograph"}, - {"id": 7473, "synset": "electron_accelerator.n.01", "name": "electron_accelerator"}, - {"id": 7474, "synset": "electron_gun.n.01", "name": "electron_gun"}, - {"id": 7475, "synset": "electronic_balance.n.01", "name": "electronic_balance"}, - {"id": 7476, "synset": "electronic_converter.n.01", "name": "electronic_converter"}, - {"id": 7477, "synset": "electronic_device.n.01", "name": "electronic_device"}, - {"id": 7478, "synset": "electronic_equipment.n.01", "name": "electronic_equipment"}, - {"id": 7479, "synset": "electronic_fetal_monitor.n.01", "name": "electronic_fetal_monitor"}, - {"id": 7480, "synset": "electronic_instrument.n.01", "name": "electronic_instrument"}, - {"id": 7481, "synset": "electronic_voltmeter.n.01", "name": "electronic_voltmeter"}, - {"id": 7482, "synset": "electron_microscope.n.01", "name": "electron_microscope"}, - {"id": 7483, "synset": "electron_multiplier.n.01", "name": "electron_multiplier"}, - {"id": 7484, "synset": "electrophorus.n.01", "name": "electrophorus"}, - {"id": 7485, "synset": "electroscope.n.01", "name": "electroscope"}, - {"id": 7486, "synset": "electrostatic_generator.n.01", "name": "electrostatic_generator"}, - {"id": 7487, "synset": "electrostatic_printer.n.01", "name": "electrostatic_printer"}, - {"id": 7488, "synset": "elevator.n.01", "name": "elevator"}, - {"id": 7489, "synset": "elevator.n.02", "name": "elevator"}, - {"id": 7490, "synset": "elevator_shaft.n.01", "name": "elevator_shaft"}, - {"id": 7491, "synset": "embankment.n.01", "name": "embankment"}, - {"id": 7492, "synset": "embassy.n.01", "name": "embassy"}, - {"id": 7493, "synset": "embellishment.n.02", "name": "embellishment"}, - {"id": 7494, "synset": "emergency_room.n.01", "name": "emergency_room"}, - {"id": 7495, "synset": "emesis_basin.n.01", "name": "emesis_basin"}, - {"id": 7496, "synset": "emitter.n.01", "name": "emitter"}, - {"id": 7497, "synset": "empty.n.01", "name": "empty"}, - {"id": 7498, "synset": "emulsion.n.02", "name": "emulsion"}, - {"id": 7499, "synset": "enamel.n.04", "name": "enamel"}, - {"id": 7500, "synset": "enamel.n.03", "name": "enamel"}, - {"id": 7501, "synset": "enamelware.n.01", "name": "enamelware"}, - {"id": 7502, "synset": "encaustic.n.01", "name": "encaustic"}, - {"id": 7503, "synset": "encephalogram.n.02", "name": "encephalogram"}, - {"id": 7504, "synset": "enclosure.n.01", "name": "enclosure"}, - {"id": 7505, "synset": "endoscope.n.01", "name": "endoscope"}, - {"id": 7506, "synset": "energizer.n.02", "name": "energizer"}, - {"id": 7507, "synset": "engine.n.01", "name": "engine"}, - {"id": 7508, "synset": "engine.n.04", "name": "engine"}, - {"id": 7509, "synset": "engineering.n.03", "name": "engineering"}, - {"id": 7510, "synset": "enginery.n.01", "name": "enginery"}, - {"id": 7511, "synset": "english_horn.n.01", "name": "English_horn"}, - {"id": 7512, "synset": "english_saddle.n.01", "name": "English_saddle"}, - {"id": 7513, "synset": "enlarger.n.01", "name": "enlarger"}, - {"id": 7514, "synset": "ensemble.n.05", "name": "ensemble"}, - {"id": 7515, "synset": "ensign.n.03", "name": "ensign"}, - {"id": 7516, "synset": "entablature.n.01", "name": "entablature"}, - {"id": 7517, "synset": "entertainment_center.n.01", "name": "entertainment_center"}, - {"id": 7518, "synset": "entrenching_tool.n.01", "name": "entrenching_tool"}, - {"id": 7519, "synset": "entrenchment.n.01", "name": "entrenchment"}, - {"id": 7520, "synset": "envelope.n.02", "name": "envelope"}, - {"id": 7521, "synset": "envelope.n.06", "name": "envelope"}, - {"id": 7522, "synset": "eolith.n.01", "name": "eolith"}, - {"id": 7523, "synset": "epauliere.n.01", "name": "epauliere"}, - {"id": 7524, "synset": "epee.n.01", "name": "epee"}, - {"id": 7525, "synset": "epergne.n.01", "name": "epergne"}, - {"id": 7526, "synset": "epicyclic_train.n.01", "name": "epicyclic_train"}, - {"id": 7527, "synset": "epidiascope.n.01", "name": "epidiascope"}, - {"id": 7528, "synset": "epilating_wax.n.01", "name": "epilating_wax"}, - {"id": 7529, "synset": "equalizer.n.01", "name": "equalizer"}, - {"id": 7530, "synset": "equatorial.n.01", "name": "equatorial"}, - {"id": 7531, "synset": "equipment.n.01", "name": "equipment"}, - { - "id": 7532, - "synset": "erasable_programmable_read-only_memory.n.01", - "name": "erasable_programmable_read-only_memory", - }, - {"id": 7533, "synset": "erecting_prism.n.01", "name": "erecting_prism"}, - {"id": 7534, "synset": "erection.n.02", "name": "erection"}, - {"id": 7535, "synset": "erlenmeyer_flask.n.01", "name": "Erlenmeyer_flask"}, - {"id": 7536, "synset": "escape_hatch.n.01", "name": "escape_hatch"}, - {"id": 7537, "synset": "escapement.n.01", "name": "escapement"}, - {"id": 7538, "synset": "escape_wheel.n.01", "name": "escape_wheel"}, - {"id": 7539, "synset": "escarpment.n.02", "name": "escarpment"}, - {"id": 7540, "synset": "escutcheon.n.03", "name": "escutcheon"}, - {"id": 7541, "synset": "esophagoscope.n.01", "name": "esophagoscope"}, - {"id": 7542, "synset": "espadrille.n.01", "name": "espadrille"}, - {"id": 7543, "synset": "espalier.n.01", "name": "espalier"}, - {"id": 7544, "synset": "espresso_maker.n.01", "name": "espresso_maker"}, - {"id": 7545, "synset": "espresso_shop.n.01", "name": "espresso_shop"}, - {"id": 7546, "synset": "establishment.n.04", "name": "establishment"}, - {"id": 7547, "synset": "estaminet.n.01", "name": "estaminet"}, - {"id": 7548, "synset": "estradiol_patch.n.01", "name": "estradiol_patch"}, - {"id": 7549, "synset": "etagere.n.01", "name": "etagere"}, - {"id": 7550, "synset": "etamine.n.01", "name": "etamine"}, - {"id": 7551, "synset": "etching.n.02", "name": "etching"}, - {"id": 7552, "synset": "ethernet.n.01", "name": "ethernet"}, - {"id": 7553, "synset": "ethernet_cable.n.01", "name": "ethernet_cable"}, - {"id": 7554, "synset": "eton_jacket.n.01", "name": "Eton_jacket"}, - {"id": 7555, "synset": "etui.n.01", "name": "etui"}, - {"id": 7556, "synset": "eudiometer.n.01", "name": "eudiometer"}, - {"id": 7557, "synset": "euphonium.n.01", "name": "euphonium"}, - {"id": 7558, "synset": "evaporative_cooler.n.01", "name": "evaporative_cooler"}, - {"id": 7559, "synset": "evening_bag.n.01", "name": "evening_bag"}, - {"id": 7560, "synset": "exercise_bike.n.01", "name": "exercise_bike"}, - {"id": 7561, "synset": "exercise_device.n.01", "name": "exercise_device"}, - {"id": 7562, "synset": "exhaust.n.02", "name": "exhaust"}, - {"id": 7563, "synset": "exhaust_fan.n.01", "name": "exhaust_fan"}, - {"id": 7564, "synset": "exhaust_valve.n.01", "name": "exhaust_valve"}, - {"id": 7565, "synset": "exhibition_hall.n.01", "name": "exhibition_hall"}, - {"id": 7566, "synset": "exocet.n.01", "name": "Exocet"}, - {"id": 7567, "synset": "expansion_bit.n.01", "name": "expansion_bit"}, - {"id": 7568, "synset": "expansion_bolt.n.01", "name": "expansion_bolt"}, - {"id": 7569, "synset": "explosive_detection_system.n.01", "name": "explosive_detection_system"}, - {"id": 7570, "synset": "explosive_device.n.01", "name": "explosive_device"}, - {"id": 7571, "synset": "explosive_trace_detection.n.01", "name": "explosive_trace_detection"}, - {"id": 7572, "synset": "express.n.02", "name": "express"}, - {"id": 7573, "synset": "extension.n.10", "name": "extension"}, - {"id": 7574, "synset": "extension_cord.n.01", "name": "extension_cord"}, - {"id": 7575, "synset": "external-combustion_engine.n.01", "name": "external-combustion_engine"}, - {"id": 7576, "synset": "external_drive.n.01", "name": "external_drive"}, - {"id": 7577, "synset": "extractor.n.01", "name": "extractor"}, - {"id": 7578, "synset": "eyebrow_pencil.n.01", "name": "eyebrow_pencil"}, - {"id": 7579, "synset": "eyecup.n.01", "name": "eyecup"}, - {"id": 7580, "synset": "eyeliner.n.01", "name": "eyeliner"}, - {"id": 7581, "synset": "eyepiece.n.01", "name": "eyepiece"}, - {"id": 7582, "synset": "eyeshadow.n.01", "name": "eyeshadow"}, - {"id": 7583, "synset": "fabric.n.01", "name": "fabric"}, - {"id": 7584, "synset": "facade.n.01", "name": "facade"}, - {"id": 7585, "synset": "face_guard.n.01", "name": "face_guard"}, - {"id": 7586, "synset": "face_mask.n.01", "name": "face_mask"}, - {"id": 7587, "synset": "faceplate.n.01", "name": "faceplate"}, - {"id": 7588, "synset": "face_powder.n.01", "name": "face_powder"}, - {"id": 7589, "synset": "face_veil.n.01", "name": "face_veil"}, - {"id": 7590, "synset": "facing.n.03", "name": "facing"}, - {"id": 7591, "synset": "facing.n.01", "name": "facing"}, - {"id": 7592, "synset": "facing.n.02", "name": "facing"}, - {"id": 7593, "synset": "facsimile.n.02", "name": "facsimile"}, - {"id": 7594, "synset": "factory.n.01", "name": "factory"}, - {"id": 7595, "synset": "factory_ship.n.01", "name": "factory_ship"}, - {"id": 7596, "synset": "fagot.n.02", "name": "fagot"}, - {"id": 7597, "synset": "fagot_stitch.n.01", "name": "fagot_stitch"}, - {"id": 7598, "synset": "fahrenheit_thermometer.n.01", "name": "Fahrenheit_thermometer"}, - {"id": 7599, "synset": "faience.n.01", "name": "faience"}, - {"id": 7600, "synset": "faille.n.01", "name": "faille"}, - {"id": 7601, "synset": "fairlead.n.01", "name": "fairlead"}, - {"id": 7602, "synset": "fairy_light.n.01", "name": "fairy_light"}, - {"id": 7603, "synset": "falchion.n.01", "name": "falchion"}, - {"id": 7604, "synset": "fallboard.n.01", "name": "fallboard"}, - {"id": 7605, "synset": "fallout_shelter.n.01", "name": "fallout_shelter"}, - {"id": 7606, "synset": "false_face.n.01", "name": "false_face"}, - {"id": 7607, "synset": "false_teeth.n.01", "name": "false_teeth"}, - {"id": 7608, "synset": "family_room.n.01", "name": "family_room"}, - {"id": 7609, "synset": "fan_belt.n.01", "name": "fan_belt"}, - {"id": 7610, "synset": "fan_blade.n.01", "name": "fan_blade"}, - {"id": 7611, "synset": "fancy_dress.n.01", "name": "fancy_dress"}, - {"id": 7612, "synset": "fanion.n.01", "name": "fanion"}, - {"id": 7613, "synset": "fanlight.n.03", "name": "fanlight"}, - {"id": 7614, "synset": "fanjet.n.02", "name": "fanjet"}, - {"id": 7615, "synset": "fanjet.n.01", "name": "fanjet"}, - {"id": 7616, "synset": "fanny_pack.n.01", "name": "fanny_pack"}, - {"id": 7617, "synset": "fan_tracery.n.01", "name": "fan_tracery"}, - {"id": 7618, "synset": "fan_vaulting.n.01", "name": "fan_vaulting"}, - {"id": 7619, "synset": "farm_building.n.01", "name": "farm_building"}, - {"id": 7620, "synset": "farmer's_market.n.01", "name": "farmer's_market"}, - {"id": 7621, "synset": "farmhouse.n.01", "name": "farmhouse"}, - {"id": 7622, "synset": "farm_machine.n.01", "name": "farm_machine"}, - {"id": 7623, "synset": "farmplace.n.01", "name": "farmplace"}, - {"id": 7624, "synset": "farmyard.n.01", "name": "farmyard"}, - {"id": 7625, "synset": "farthingale.n.01", "name": "farthingale"}, - {"id": 7626, "synset": "fastener.n.02", "name": "fastener"}, - {"id": 7627, "synset": "fast_reactor.n.01", "name": "fast_reactor"}, - {"id": 7628, "synset": "fat_farm.n.01", "name": "fat_farm"}, - {"id": 7629, "synset": "fatigues.n.01", "name": "fatigues"}, - {"id": 7630, "synset": "fauld.n.01", "name": "fauld"}, - {"id": 7631, "synset": "fauteuil.n.01", "name": "fauteuil"}, - {"id": 7632, "synset": "feather_boa.n.01", "name": "feather_boa"}, - {"id": 7633, "synset": "featheredge.n.01", "name": "featheredge"}, - {"id": 7634, "synset": "feedback_circuit.n.01", "name": "feedback_circuit"}, - {"id": 7635, "synset": "feedlot.n.01", "name": "feedlot"}, - {"id": 7636, "synset": "fell.n.02", "name": "fell"}, - {"id": 7637, "synset": "felloe.n.01", "name": "felloe"}, - {"id": 7638, "synset": "felt.n.01", "name": "felt"}, - {"id": 7639, "synset": "felt-tip_pen.n.01", "name": "felt-tip_pen"}, - {"id": 7640, "synset": "felucca.n.01", "name": "felucca"}, - {"id": 7641, "synset": "fence.n.01", "name": "fence"}, - {"id": 7642, "synset": "fencing_mask.n.01", "name": "fencing_mask"}, - {"id": 7643, "synset": "fencing_sword.n.01", "name": "fencing_sword"}, - {"id": 7644, "synset": "fender.n.01", "name": "fender"}, - {"id": 7645, "synset": "fender.n.02", "name": "fender"}, - {"id": 7646, "synset": "ferrule.n.01", "name": "ferrule"}, - {"id": 7647, "synset": "ferule.n.01", "name": "ferule"}, - {"id": 7648, "synset": "festoon.n.01", "name": "festoon"}, - {"id": 7649, "synset": "fetoscope.n.01", "name": "fetoscope"}, - {"id": 7650, "synset": "fetter.n.01", "name": "fetter"}, - {"id": 7651, "synset": "fez.n.02", "name": "fez"}, - {"id": 7652, "synset": "fiber.n.05", "name": "fiber"}, - {"id": 7653, "synset": "fiber_optic_cable.n.01", "name": "fiber_optic_cable"}, - {"id": 7654, "synset": "fiberscope.n.01", "name": "fiberscope"}, - {"id": 7655, "synset": "fichu.n.01", "name": "fichu"}, - {"id": 7656, "synset": "fiddlestick.n.01", "name": "fiddlestick"}, - {"id": 7657, "synset": "field_artillery.n.01", "name": "field_artillery"}, - {"id": 7658, "synset": "field_coil.n.01", "name": "field_coil"}, - {"id": 7659, "synset": "field-effect_transistor.n.01", "name": "field-effect_transistor"}, - {"id": 7660, "synset": "field-emission_microscope.n.01", "name": "field-emission_microscope"}, - {"id": 7661, "synset": "field_glass.n.01", "name": "field_glass"}, - {"id": 7662, "synset": "field_hockey_ball.n.01", "name": "field_hockey_ball"}, - {"id": 7663, "synset": "field_hospital.n.01", "name": "field_hospital"}, - {"id": 7664, "synset": "field_house.n.01", "name": "field_house"}, - {"id": 7665, "synset": "field_lens.n.01", "name": "field_lens"}, - {"id": 7666, "synset": "field_magnet.n.01", "name": "field_magnet"}, - { - "id": 7667, - "synset": "field-sequential_color_television.n.01", - "name": "field-sequential_color_television", - }, - {"id": 7668, "synset": "field_tent.n.01", "name": "field_tent"}, - {"id": 7669, "synset": "fieldwork.n.01", "name": "fieldwork"}, - {"id": 7670, "synset": "fife.n.01", "name": "fife"}, - {"id": 7671, "synset": "fifth_wheel.n.02", "name": "fifth_wheel"}, - {"id": 7672, "synset": "fighting_chair.n.01", "name": "fighting_chair"}, - {"id": 7673, "synset": "fig_leaf.n.02", "name": "fig_leaf"}, - {"id": 7674, "synset": "figure_eight.n.01", "name": "figure_eight"}, - {"id": 7675, "synset": "figure_loom.n.01", "name": "figure_loom"}, - {"id": 7676, "synset": "figure_skate.n.01", "name": "figure_skate"}, - {"id": 7677, "synset": "filament.n.04", "name": "filament"}, - {"id": 7678, "synset": "filature.n.01", "name": "filature"}, - {"id": 7679, "synset": "file_folder.n.01", "name": "file_folder"}, - {"id": 7680, "synset": "file_server.n.01", "name": "file_server"}, - {"id": 7681, "synset": "filigree.n.01", "name": "filigree"}, - {"id": 7682, "synset": "filling.n.05", "name": "filling"}, - {"id": 7683, "synset": "film.n.03", "name": "film"}, - {"id": 7684, "synset": "film.n.05", "name": "film"}, - {"id": 7685, "synset": "film_advance.n.01", "name": "film_advance"}, - {"id": 7686, "synset": "filter.n.01", "name": "filter"}, - {"id": 7687, "synset": "filter.n.02", "name": "filter"}, - {"id": 7688, "synset": "finder.n.03", "name": "finder"}, - {"id": 7689, "synset": "finery.n.01", "name": "finery"}, - {"id": 7690, "synset": "fine-tooth_comb.n.01", "name": "fine-tooth_comb"}, - {"id": 7691, "synset": "finger.n.03", "name": "finger"}, - {"id": 7692, "synset": "fingerboard.n.03", "name": "fingerboard"}, - {"id": 7693, "synset": "finger_bowl.n.01", "name": "finger_bowl"}, - {"id": 7694, "synset": "finger_paint.n.01", "name": "finger_paint"}, - {"id": 7695, "synset": "finger-painting.n.01", "name": "finger-painting"}, - {"id": 7696, "synset": "finger_plate.n.01", "name": "finger_plate"}, - {"id": 7697, "synset": "fingerstall.n.01", "name": "fingerstall"}, - {"id": 7698, "synset": "finish_coat.n.02", "name": "finish_coat"}, - {"id": 7699, "synset": "finish_coat.n.01", "name": "finish_coat"}, - {"id": 7700, "synset": "finisher.n.05", "name": "finisher"}, - {"id": 7701, "synset": "fin_keel.n.01", "name": "fin_keel"}, - {"id": 7702, "synset": "fipple.n.01", "name": "fipple"}, - {"id": 7703, "synset": "fipple_flute.n.01", "name": "fipple_flute"}, - {"id": 7704, "synset": "fire.n.04", "name": "fire"}, - {"id": 7705, "synset": "firearm.n.01", "name": "firearm"}, - {"id": 7706, "synset": "fire_bell.n.01", "name": "fire_bell"}, - {"id": 7707, "synset": "fireboat.n.01", "name": "fireboat"}, - {"id": 7708, "synset": "firebox.n.01", "name": "firebox"}, - {"id": 7709, "synset": "firebrick.n.01", "name": "firebrick"}, - {"id": 7710, "synset": "fire_control_radar.n.01", "name": "fire_control_radar"}, - {"id": 7711, "synset": "fire_control_system.n.01", "name": "fire_control_system"}, - {"id": 7712, "synset": "fire_iron.n.01", "name": "fire_iron"}, - {"id": 7713, "synset": "fireman's_ax.n.01", "name": "fireman's_ax"}, - {"id": 7714, "synset": "fire_screen.n.01", "name": "fire_screen"}, - {"id": 7715, "synset": "fire_tongs.n.01", "name": "fire_tongs"}, - {"id": 7716, "synset": "fire_tower.n.01", "name": "fire_tower"}, - {"id": 7717, "synset": "firewall.n.02", "name": "firewall"}, - {"id": 7718, "synset": "firing_chamber.n.01", "name": "firing_chamber"}, - {"id": 7719, "synset": "firing_pin.n.01", "name": "firing_pin"}, - {"id": 7720, "synset": "firkin.n.02", "name": "firkin"}, - {"id": 7721, "synset": "firmer_chisel.n.01", "name": "firmer_chisel"}, - {"id": 7722, "synset": "first-aid_station.n.01", "name": "first-aid_station"}, - {"id": 7723, "synset": "first_base.n.01", "name": "first_base"}, - {"id": 7724, "synset": "first_class.n.03", "name": "first_class"}, - {"id": 7725, "synset": "fisherman's_bend.n.01", "name": "fisherman's_bend"}, - {"id": 7726, "synset": "fisherman's_knot.n.01", "name": "fisherman's_knot"}, - {"id": 7727, "synset": "fisherman's_lure.n.01", "name": "fisherman's_lure"}, - {"id": 7728, "synset": "fishhook.n.01", "name": "fishhook"}, - {"id": 7729, "synset": "fishing_boat.n.01", "name": "fishing_boat"}, - {"id": 7730, "synset": "fishing_gear.n.01", "name": "fishing_gear"}, - {"id": 7731, "synset": "fish_joint.n.01", "name": "fish_joint"}, - {"id": 7732, "synset": "fish_knife.n.01", "name": "fish_knife"}, - {"id": 7733, "synset": "fishnet.n.01", "name": "fishnet"}, - {"id": 7734, "synset": "fish_slice.n.01", "name": "fish_slice"}, - {"id": 7735, "synset": "fitment.n.01", "name": "fitment"}, - {"id": 7736, "synset": "fixative.n.02", "name": "fixative"}, - {"id": 7737, "synset": "fixer-upper.n.01", "name": "fixer-upper"}, - {"id": 7738, "synset": "flageolet.n.02", "name": "flageolet"}, - {"id": 7739, "synset": "flagon.n.01", "name": "flagon"}, - {"id": 7740, "synset": "flagship.n.02", "name": "flagship"}, - {"id": 7741, "synset": "flail.n.01", "name": "flail"}, - {"id": 7742, "synset": "flambeau.n.01", "name": "flambeau"}, - {"id": 7743, "synset": "flamethrower.n.01", "name": "flamethrower"}, - {"id": 7744, "synset": "flange.n.01", "name": "flange"}, - {"id": 7745, "synset": "flannel.n.03", "name": "flannel"}, - {"id": 7746, "synset": "flannelette.n.01", "name": "flannelette"}, - {"id": 7747, "synset": "flap.n.05", "name": "flap"}, - {"id": 7748, "synset": "flash.n.09", "name": "flash"}, - {"id": 7749, "synset": "flash_camera.n.01", "name": "flash_camera"}, - {"id": 7750, "synset": "flasher.n.02", "name": "flasher"}, - {"id": 7751, "synset": "flashlight_battery.n.01", "name": "flashlight_battery"}, - {"id": 7752, "synset": "flash_memory.n.01", "name": "flash_memory"}, - {"id": 7753, "synset": "flask.n.01", "name": "flask"}, - {"id": 7754, "synset": "flat_arch.n.01", "name": "flat_arch"}, - {"id": 7755, "synset": "flatbed.n.02", "name": "flatbed"}, - {"id": 7756, "synset": "flatbed_press.n.01", "name": "flatbed_press"}, - {"id": 7757, "synset": "flat_bench.n.01", "name": "flat_bench"}, - {"id": 7758, "synset": "flatcar.n.01", "name": "flatcar"}, - {"id": 7759, "synset": "flat_file.n.01", "name": "flat_file"}, - {"id": 7760, "synset": "flatlet.n.01", "name": "flatlet"}, - {"id": 7761, "synset": "flat_panel_display.n.01", "name": "flat_panel_display"}, - {"id": 7762, "synset": "flats.n.01", "name": "flats"}, - {"id": 7763, "synset": "flat_tip_screwdriver.n.01", "name": "flat_tip_screwdriver"}, - { - "id": 7764, - "synset": "fleet_ballistic_missile_submarine.n.01", - "name": "fleet_ballistic_missile_submarine", - }, - {"id": 7765, "synset": "fleur-de-lis.n.02", "name": "fleur-de-lis"}, - {"id": 7766, "synset": "flight_simulator.n.01", "name": "flight_simulator"}, - {"id": 7767, "synset": "flintlock.n.02", "name": "flintlock"}, - {"id": 7768, "synset": "flintlock.n.01", "name": "flintlock"}, - {"id": 7769, "synset": "float.n.05", "name": "float"}, - {"id": 7770, "synset": "floating_dock.n.01", "name": "floating_dock"}, - {"id": 7771, "synset": "floatplane.n.01", "name": "floatplane"}, - {"id": 7772, "synset": "flood.n.03", "name": "flood"}, - {"id": 7773, "synset": "floor.n.01", "name": "floor"}, - {"id": 7774, "synset": "floor.n.02", "name": "floor"}, - {"id": 7775, "synset": "floor.n.09", "name": "floor"}, - {"id": 7776, "synset": "floorboard.n.02", "name": "floorboard"}, - {"id": 7777, "synset": "floor_cover.n.01", "name": "floor_cover"}, - {"id": 7778, "synset": "floor_joist.n.01", "name": "floor_joist"}, - {"id": 7779, "synset": "floor_lamp.n.01", "name": "floor_lamp"}, - {"id": 7780, "synset": "flophouse.n.01", "name": "flophouse"}, - {"id": 7781, "synset": "florist.n.02", "name": "florist"}, - {"id": 7782, "synset": "floss.n.01", "name": "floss"}, - {"id": 7783, "synset": "flotsam.n.01", "name": "flotsam"}, - {"id": 7784, "synset": "flour_bin.n.01", "name": "flour_bin"}, - {"id": 7785, "synset": "flour_mill.n.01", "name": "flour_mill"}, - {"id": 7786, "synset": "flowerbed.n.01", "name": "flowerbed"}, - {"id": 7787, "synset": "flugelhorn.n.01", "name": "flugelhorn"}, - {"id": 7788, "synset": "fluid_drive.n.01", "name": "fluid_drive"}, - {"id": 7789, "synset": "fluid_flywheel.n.01", "name": "fluid_flywheel"}, - {"id": 7790, "synset": "flume.n.02", "name": "flume"}, - {"id": 7791, "synset": "fluorescent_lamp.n.01", "name": "fluorescent_lamp"}, - {"id": 7792, "synset": "fluoroscope.n.01", "name": "fluoroscope"}, - {"id": 7793, "synset": "flush_toilet.n.01", "name": "flush_toilet"}, - {"id": 7794, "synset": "flute.n.01", "name": "flute"}, - {"id": 7795, "synset": "flux_applicator.n.01", "name": "flux_applicator"}, - {"id": 7796, "synset": "fluxmeter.n.01", "name": "fluxmeter"}, - {"id": 7797, "synset": "fly.n.05", "name": "fly"}, - {"id": 7798, "synset": "flying_boat.n.01", "name": "flying_boat"}, - {"id": 7799, "synset": "flying_buttress.n.01", "name": "flying_buttress"}, - {"id": 7800, "synset": "flying_carpet.n.01", "name": "flying_carpet"}, - {"id": 7801, "synset": "flying_jib.n.01", "name": "flying_jib"}, - {"id": 7802, "synset": "fly_rod.n.01", "name": "fly_rod"}, - {"id": 7803, "synset": "fly_tent.n.01", "name": "fly_tent"}, - {"id": 7804, "synset": "flytrap.n.01", "name": "flytrap"}, - {"id": 7805, "synset": "flywheel.n.01", "name": "flywheel"}, - {"id": 7806, "synset": "fob.n.03", "name": "fob"}, - {"id": 7807, "synset": "foghorn.n.02", "name": "foghorn"}, - {"id": 7808, "synset": "foglamp.n.01", "name": "foglamp"}, - {"id": 7809, "synset": "foil.n.05", "name": "foil"}, - {"id": 7810, "synset": "fold.n.06", "name": "fold"}, - {"id": 7811, "synset": "folder.n.02", "name": "folder"}, - {"id": 7812, "synset": "folding_door.n.01", "name": "folding_door"}, - {"id": 7813, "synset": "folding_saw.n.01", "name": "folding_saw"}, - {"id": 7814, "synset": "food_court.n.01", "name": "food_court"}, - {"id": 7815, "synset": "food_hamper.n.01", "name": "food_hamper"}, - {"id": 7816, "synset": "foot.n.11", "name": "foot"}, - {"id": 7817, "synset": "footage.n.01", "name": "footage"}, - {"id": 7818, "synset": "football_stadium.n.01", "name": "football_stadium"}, - {"id": 7819, "synset": "footbath.n.01", "name": "footbath"}, - {"id": 7820, "synset": "foot_brake.n.01", "name": "foot_brake"}, - {"id": 7821, "synset": "footbridge.n.01", "name": "footbridge"}, - {"id": 7822, "synset": "foothold.n.02", "name": "foothold"}, - {"id": 7823, "synset": "footlocker.n.01", "name": "footlocker"}, - {"id": 7824, "synset": "foot_rule.n.01", "name": "foot_rule"}, - {"id": 7825, "synset": "footwear.n.02", "name": "footwear"}, - {"id": 7826, "synset": "footwear.n.01", "name": "footwear"}, - {"id": 7827, "synset": "forceps.n.01", "name": "forceps"}, - {"id": 7828, "synset": "force_pump.n.01", "name": "force_pump"}, - {"id": 7829, "synset": "fore-and-after.n.01", "name": "fore-and-after"}, - {"id": 7830, "synset": "fore-and-aft_sail.n.01", "name": "fore-and-aft_sail"}, - {"id": 7831, "synset": "forecastle.n.01", "name": "forecastle"}, - {"id": 7832, "synset": "forecourt.n.01", "name": "forecourt"}, - {"id": 7833, "synset": "foredeck.n.01", "name": "foredeck"}, - {"id": 7834, "synset": "fore_edge.n.01", "name": "fore_edge"}, - {"id": 7835, "synset": "foreground.n.02", "name": "foreground"}, - {"id": 7836, "synset": "foremast.n.01", "name": "foremast"}, - {"id": 7837, "synset": "fore_plane.n.01", "name": "fore_plane"}, - {"id": 7838, "synset": "foresail.n.01", "name": "foresail"}, - {"id": 7839, "synset": "forestay.n.01", "name": "forestay"}, - {"id": 7840, "synset": "foretop.n.01", "name": "foretop"}, - {"id": 7841, "synset": "fore-topmast.n.01", "name": "fore-topmast"}, - {"id": 7842, "synset": "fore-topsail.n.01", "name": "fore-topsail"}, - {"id": 7843, "synset": "forge.n.01", "name": "forge"}, - {"id": 7844, "synset": "fork.n.04", "name": "fork"}, - {"id": 7845, "synset": "formalwear.n.01", "name": "formalwear"}, - {"id": 7846, "synset": "formica.n.01", "name": "Formica"}, - {"id": 7847, "synset": "fortification.n.01", "name": "fortification"}, - {"id": 7848, "synset": "fortress.n.01", "name": "fortress"}, - {"id": 7849, "synset": "forty-five.n.01", "name": "forty-five"}, - {"id": 7850, "synset": "foucault_pendulum.n.01", "name": "Foucault_pendulum"}, - {"id": 7851, "synset": "foulard.n.01", "name": "foulard"}, - {"id": 7852, "synset": "foul-weather_gear.n.01", "name": "foul-weather_gear"}, - {"id": 7853, "synset": "foundation_garment.n.01", "name": "foundation_garment"}, - {"id": 7854, "synset": "foundry.n.01", "name": "foundry"}, - {"id": 7855, "synset": "fountain.n.01", "name": "fountain"}, - {"id": 7856, "synset": "fountain_pen.n.01", "name": "fountain_pen"}, - {"id": 7857, "synset": "four-in-hand.n.01", "name": "four-in-hand"}, - {"id": 7858, "synset": "four-poster.n.01", "name": "four-poster"}, - {"id": 7859, "synset": "four-pounder.n.01", "name": "four-pounder"}, - {"id": 7860, "synset": "four-stroke_engine.n.01", "name": "four-stroke_engine"}, - {"id": 7861, "synset": "four-wheel_drive.n.02", "name": "four-wheel_drive"}, - {"id": 7862, "synset": "four-wheel_drive.n.01", "name": "four-wheel_drive"}, - {"id": 7863, "synset": "four-wheeler.n.01", "name": "four-wheeler"}, - {"id": 7864, "synset": "fowling_piece.n.01", "name": "fowling_piece"}, - {"id": 7865, "synset": "foxhole.n.01", "name": "foxhole"}, - {"id": 7866, "synset": "fragmentation_bomb.n.01", "name": "fragmentation_bomb"}, - {"id": 7867, "synset": "frail.n.02", "name": "frail"}, - {"id": 7868, "synset": "fraise.n.02", "name": "fraise"}, - {"id": 7869, "synset": "frame.n.10", "name": "frame"}, - {"id": 7870, "synset": "frame.n.01", "name": "frame"}, - {"id": 7871, "synset": "frame_buffer.n.01", "name": "frame_buffer"}, - {"id": 7872, "synset": "framework.n.03", "name": "framework"}, - {"id": 7873, "synset": "francis_turbine.n.01", "name": "Francis_turbine"}, - {"id": 7874, "synset": "franking_machine.n.01", "name": "franking_machine"}, - {"id": 7875, "synset": "free_house.n.01", "name": "free_house"}, - {"id": 7876, "synset": "free-reed.n.01", "name": "free-reed"}, - {"id": 7877, "synset": "free-reed_instrument.n.01", "name": "free-reed_instrument"}, - {"id": 7878, "synset": "freewheel.n.01", "name": "freewheel"}, - {"id": 7879, "synset": "freight_elevator.n.01", "name": "freight_elevator"}, - {"id": 7880, "synset": "freight_liner.n.01", "name": "freight_liner"}, - {"id": 7881, "synset": "freight_train.n.01", "name": "freight_train"}, - {"id": 7882, "synset": "french_door.n.01", "name": "French_door"}, - {"id": 7883, "synset": "french_horn.n.01", "name": "French_horn"}, - {"id": 7884, "synset": "french_polish.n.02", "name": "French_polish"}, - {"id": 7885, "synset": "french_roof.n.01", "name": "French_roof"}, - {"id": 7886, "synset": "french_window.n.01", "name": "French_window"}, - {"id": 7887, "synset": "fresnel_lens.n.01", "name": "Fresnel_lens"}, - {"id": 7888, "synset": "fret.n.04", "name": "fret"}, - {"id": 7889, "synset": "friary.n.01", "name": "friary"}, - {"id": 7890, "synset": "friction_clutch.n.01", "name": "friction_clutch"}, - {"id": 7891, "synset": "frieze.n.02", "name": "frieze"}, - {"id": 7892, "synset": "frieze.n.01", "name": "frieze"}, - {"id": 7893, "synset": "frigate.n.02", "name": "frigate"}, - {"id": 7894, "synset": "frigate.n.01", "name": "frigate"}, - {"id": 7895, "synset": "frill.n.03", "name": "frill"}, - {"id": 7896, "synset": "frock.n.01", "name": "frock"}, - {"id": 7897, "synset": "frock_coat.n.01", "name": "frock_coat"}, - {"id": 7898, "synset": "frontlet.n.01", "name": "frontlet"}, - {"id": 7899, "synset": "front_porch.n.01", "name": "front_porch"}, - {"id": 7900, "synset": "front_projector.n.01", "name": "front_projector"}, - {"id": 7901, "synset": "fruit_machine.n.01", "name": "fruit_machine"}, - {"id": 7902, "synset": "fuel_filter.n.01", "name": "fuel_filter"}, - {"id": 7903, "synset": "fuel_gauge.n.01", "name": "fuel_gauge"}, - {"id": 7904, "synset": "fuel_injection.n.01", "name": "fuel_injection"}, - {"id": 7905, "synset": "fuel_system.n.01", "name": "fuel_system"}, - {"id": 7906, "synset": "full-dress_uniform.n.01", "name": "full-dress_uniform"}, - {"id": 7907, "synset": "full_metal_jacket.n.01", "name": "full_metal_jacket"}, - {"id": 7908, "synset": "full_skirt.n.01", "name": "full_skirt"}, - {"id": 7909, "synset": "fumigator.n.02", "name": "fumigator"}, - {"id": 7910, "synset": "funeral_home.n.01", "name": "funeral_home"}, - {"id": 7911, "synset": "funny_wagon.n.01", "name": "funny_wagon"}, - {"id": 7912, "synset": "fur.n.03", "name": "fur"}, - {"id": 7913, "synset": "fur_coat.n.01", "name": "fur_coat"}, - {"id": 7914, "synset": "fur_hat.n.01", "name": "fur_hat"}, - {"id": 7915, "synset": "furnace.n.01", "name": "furnace"}, - {"id": 7916, "synset": "furnace_lining.n.01", "name": "furnace_lining"}, - {"id": 7917, "synset": "furnace_room.n.01", "name": "furnace_room"}, - {"id": 7918, "synset": "furnishing.n.02", "name": "furnishing"}, - {"id": 7919, "synset": "furnishing.n.01", "name": "furnishing"}, - {"id": 7920, "synset": "furniture.n.01", "name": "furniture"}, - {"id": 7921, "synset": "fur-piece.n.01", "name": "fur-piece"}, - {"id": 7922, "synset": "furrow.n.01", "name": "furrow"}, - {"id": 7923, "synset": "fuse.n.01", "name": "fuse"}, - {"id": 7924, "synset": "fusee_drive.n.01", "name": "fusee_drive"}, - {"id": 7925, "synset": "fuselage.n.01", "name": "fuselage"}, - {"id": 7926, "synset": "fusil.n.01", "name": "fusil"}, - {"id": 7927, "synset": "fustian.n.02", "name": "fustian"}, - {"id": 7928, "synset": "gabardine.n.01", "name": "gabardine"}, - {"id": 7929, "synset": "gable.n.01", "name": "gable"}, - {"id": 7930, "synset": "gable_roof.n.01", "name": "gable_roof"}, - {"id": 7931, "synset": "gadgetry.n.01", "name": "gadgetry"}, - {"id": 7932, "synset": "gaff.n.03", "name": "gaff"}, - {"id": 7933, "synset": "gaff.n.02", "name": "gaff"}, - {"id": 7934, "synset": "gaff.n.01", "name": "gaff"}, - {"id": 7935, "synset": "gaffsail.n.01", "name": "gaffsail"}, - {"id": 7936, "synset": "gaff_topsail.n.01", "name": "gaff_topsail"}, - {"id": 7937, "synset": "gaiter.n.03", "name": "gaiter"}, - {"id": 7938, "synset": "gaiter.n.02", "name": "gaiter"}, - {"id": 7939, "synset": "galilean_telescope.n.01", "name": "Galilean_telescope"}, - {"id": 7940, "synset": "galleon.n.01", "name": "galleon"}, - {"id": 7941, "synset": "gallery.n.04", "name": "gallery"}, - {"id": 7942, "synset": "gallery.n.03", "name": "gallery"}, - {"id": 7943, "synset": "galley.n.04", "name": "galley"}, - {"id": 7944, "synset": "galley.n.03", "name": "galley"}, - {"id": 7945, "synset": "galley.n.02", "name": "galley"}, - {"id": 7946, "synset": "gallows.n.01", "name": "gallows"}, - {"id": 7947, "synset": "gallows_tree.n.01", "name": "gallows_tree"}, - {"id": 7948, "synset": "galvanometer.n.01", "name": "galvanometer"}, - {"id": 7949, "synset": "gambling_house.n.01", "name": "gambling_house"}, - {"id": 7950, "synset": "gambrel.n.01", "name": "gambrel"}, - {"id": 7951, "synset": "game.n.09", "name": "game"}, - {"id": 7952, "synset": "gamebag.n.01", "name": "gamebag"}, - {"id": 7953, "synset": "game_equipment.n.01", "name": "game_equipment"}, - {"id": 7954, "synset": "gaming_table.n.01", "name": "gaming_table"}, - {"id": 7955, "synset": "gamp.n.01", "name": "gamp"}, - {"id": 7956, "synset": "gangplank.n.01", "name": "gangplank"}, - {"id": 7957, "synset": "gangsaw.n.01", "name": "gangsaw"}, - {"id": 7958, "synset": "gangway.n.01", "name": "gangway"}, - {"id": 7959, "synset": "gantlet.n.04", "name": "gantlet"}, - {"id": 7960, "synset": "gantry.n.01", "name": "gantry"}, - {"id": 7961, "synset": "garage.n.01", "name": "garage"}, - {"id": 7962, "synset": "garage.n.02", "name": "garage"}, - {"id": 7963, "synset": "garand_rifle.n.01", "name": "Garand_rifle"}, - {"id": 7964, "synset": "garboard.n.01", "name": "garboard"}, - {"id": 7965, "synset": "garden.n.01", "name": "garden"}, - {"id": 7966, "synset": "garden.n.03", "name": "garden"}, - {"id": 7967, "synset": "garden_rake.n.01", "name": "garden_rake"}, - {"id": 7968, "synset": "garden_spade.n.01", "name": "garden_spade"}, - {"id": 7969, "synset": "garden_tool.n.01", "name": "garden_tool"}, - {"id": 7970, "synset": "garden_trowel.n.01", "name": "garden_trowel"}, - {"id": 7971, "synset": "gargoyle.n.01", "name": "gargoyle"}, - {"id": 7972, "synset": "garibaldi.n.02", "name": "garibaldi"}, - {"id": 7973, "synset": "garlic_press.n.01", "name": "garlic_press"}, - {"id": 7974, "synset": "garment.n.01", "name": "garment"}, - {"id": 7975, "synset": "garment_bag.n.01", "name": "garment_bag"}, - {"id": 7976, "synset": "garrison_cap.n.01", "name": "garrison_cap"}, - {"id": 7977, "synset": "garrote.n.01", "name": "garrote"}, - {"id": 7978, "synset": "garter.n.01", "name": "garter"}, - {"id": 7979, "synset": "garter_belt.n.01", "name": "garter_belt"}, - {"id": 7980, "synset": "garter_stitch.n.01", "name": "garter_stitch"}, - {"id": 7981, "synset": "gas_guzzler.n.01", "name": "gas_guzzler"}, - {"id": 7982, "synset": "gas_shell.n.01", "name": "gas_shell"}, - {"id": 7983, "synset": "gas_bracket.n.01", "name": "gas_bracket"}, - {"id": 7984, "synset": "gas_burner.n.01", "name": "gas_burner"}, - {"id": 7985, "synset": "gas-cooled_reactor.n.01", "name": "gas-cooled_reactor"}, - {"id": 7986, "synset": "gas-discharge_tube.n.01", "name": "gas-discharge_tube"}, - {"id": 7987, "synset": "gas_engine.n.01", "name": "gas_engine"}, - {"id": 7988, "synset": "gas_fixture.n.01", "name": "gas_fixture"}, - {"id": 7989, "synset": "gas_furnace.n.01", "name": "gas_furnace"}, - {"id": 7990, "synset": "gas_gun.n.01", "name": "gas_gun"}, - {"id": 7991, "synset": "gas_heater.n.01", "name": "gas_heater"}, - {"id": 7992, "synset": "gas_holder.n.01", "name": "gas_holder"}, - {"id": 7993, "synset": "gasket.n.01", "name": "gasket"}, - {"id": 7994, "synset": "gas_lamp.n.01", "name": "gas_lamp"}, - {"id": 7995, "synset": "gas_maser.n.01", "name": "gas_maser"}, - {"id": 7996, "synset": "gas_meter.n.01", "name": "gas_meter"}, - {"id": 7997, "synset": "gasoline_engine.n.01", "name": "gasoline_engine"}, - {"id": 7998, "synset": "gasoline_gauge.n.01", "name": "gasoline_gauge"}, - {"id": 7999, "synset": "gas_oven.n.02", "name": "gas_oven"}, - {"id": 8000, "synset": "gas_oven.n.01", "name": "gas_oven"}, - {"id": 8001, "synset": "gas_pump.n.01", "name": "gas_pump"}, - {"id": 8002, "synset": "gas_range.n.01", "name": "gas_range"}, - {"id": 8003, "synset": "gas_ring.n.01", "name": "gas_ring"}, - {"id": 8004, "synset": "gas_tank.n.01", "name": "gas_tank"}, - {"id": 8005, "synset": "gas_thermometer.n.01", "name": "gas_thermometer"}, - {"id": 8006, "synset": "gastroscope.n.01", "name": "gastroscope"}, - {"id": 8007, "synset": "gas_turbine.n.01", "name": "gas_turbine"}, - {"id": 8008, "synset": "gas-turbine_ship.n.01", "name": "gas-turbine_ship"}, - {"id": 8009, "synset": "gat.n.01", "name": "gat"}, - {"id": 8010, "synset": "gate.n.01", "name": "gate"}, - {"id": 8011, "synset": "gatehouse.n.01", "name": "gatehouse"}, - {"id": 8012, "synset": "gateleg_table.n.01", "name": "gateleg_table"}, - {"id": 8013, "synset": "gatepost.n.01", "name": "gatepost"}, - {"id": 8014, "synset": "gathered_skirt.n.01", "name": "gathered_skirt"}, - {"id": 8015, "synset": "gatling_gun.n.01", "name": "Gatling_gun"}, - {"id": 8016, "synset": "gauge.n.01", "name": "gauge"}, - {"id": 8017, "synset": "gauntlet.n.03", "name": "gauntlet"}, - {"id": 8018, "synset": "gauntlet.n.02", "name": "gauntlet"}, - {"id": 8019, "synset": "gauze.n.02", "name": "gauze"}, - {"id": 8020, "synset": "gauze.n.01", "name": "gauze"}, - {"id": 8021, "synset": "gavel.n.01", "name": "gavel"}, - {"id": 8022, "synset": "gazebo.n.01", "name": "gazebo"}, - {"id": 8023, "synset": "gear.n.01", "name": "gear"}, - {"id": 8024, "synset": "gear.n.04", "name": "gear"}, - {"id": 8025, "synset": "gear.n.03", "name": "gear"}, - {"id": 8026, "synset": "gearbox.n.01", "name": "gearbox"}, - {"id": 8027, "synset": "gearing.n.01", "name": "gearing"}, - {"id": 8028, "synset": "gearset.n.01", "name": "gearset"}, - {"id": 8029, "synset": "gearshift.n.01", "name": "gearshift"}, - {"id": 8030, "synset": "geiger_counter.n.01", "name": "Geiger_counter"}, - {"id": 8031, "synset": "geiger_tube.n.01", "name": "Geiger_tube"}, - {"id": 8032, "synset": "gene_chip.n.01", "name": "gene_chip"}, - {"id": 8033, "synset": "general-purpose_bomb.n.01", "name": "general-purpose_bomb"}, - {"id": 8034, "synset": "generator.n.01", "name": "generator"}, - {"id": 8035, "synset": "generator.n.04", "name": "generator"}, - {"id": 8036, "synset": "geneva_gown.n.01", "name": "Geneva_gown"}, - {"id": 8037, "synset": "geodesic_dome.n.01", "name": "geodesic_dome"}, - {"id": 8038, "synset": "georgette.n.01", "name": "georgette"}, - {"id": 8039, "synset": "gharry.n.01", "name": "gharry"}, - {"id": 8040, "synset": "ghat.n.01", "name": "ghat"}, - {"id": 8041, "synset": "ghetto_blaster.n.01", "name": "ghetto_blaster"}, - {"id": 8042, "synset": "gift_shop.n.01", "name": "gift_shop"}, - {"id": 8043, "synset": "gift_wrapping.n.01", "name": "gift_wrapping"}, - {"id": 8044, "synset": "gig.n.05", "name": "gig"}, - {"id": 8045, "synset": "gig.n.04", "name": "gig"}, - {"id": 8046, "synset": "gig.n.01", "name": "gig"}, - {"id": 8047, "synset": "gig.n.03", "name": "gig"}, - {"id": 8048, "synset": "gildhall.n.01", "name": "gildhall"}, - {"id": 8049, "synset": "gill_net.n.01", "name": "gill_net"}, - {"id": 8050, "synset": "gilt.n.01", "name": "gilt"}, - {"id": 8051, "synset": "gimbal.n.01", "name": "gimbal"}, - {"id": 8052, "synset": "gingham.n.01", "name": "gingham"}, - {"id": 8053, "synset": "girandole.n.01", "name": "girandole"}, - {"id": 8054, "synset": "girder.n.01", "name": "girder"}, - {"id": 8055, "synset": "glass.n.07", "name": "glass"}, - {"id": 8056, "synset": "glass_cutter.n.03", "name": "glass_cutter"}, - {"id": 8057, "synset": "glasses_case.n.01", "name": "glasses_case"}, - {"id": 8058, "synset": "glebe_house.n.01", "name": "glebe_house"}, - {"id": 8059, "synset": "glengarry.n.01", "name": "Glengarry"}, - {"id": 8060, "synset": "glider.n.01", "name": "glider"}, - {"id": 8061, "synset": "global_positioning_system.n.01", "name": "Global_Positioning_System"}, - {"id": 8062, "synset": "glockenspiel.n.01", "name": "glockenspiel"}, - {"id": 8063, "synset": "glory_hole.n.01", "name": "glory_hole"}, - {"id": 8064, "synset": "glove_compartment.n.01", "name": "glove_compartment"}, - {"id": 8065, "synset": "glow_lamp.n.01", "name": "glow_lamp"}, - {"id": 8066, "synset": "glow_tube.n.01", "name": "glow_tube"}, - {"id": 8067, "synset": "glyptic_art.n.01", "name": "glyptic_art"}, - {"id": 8068, "synset": "glyptics.n.01", "name": "glyptics"}, - {"id": 8069, "synset": "gnomon.n.01", "name": "gnomon"}, - {"id": 8070, "synset": "goal.n.03", "name": "goal"}, - {"id": 8071, "synset": "goalmouth.n.01", "name": "goalmouth"}, - {"id": 8072, "synset": "goalpost.n.01", "name": "goalpost"}, - {"id": 8073, "synset": "goblet.n.01", "name": "goblet"}, - {"id": 8074, "synset": "godown.n.01", "name": "godown"}, - {"id": 8075, "synset": "go-kart.n.01", "name": "go-kart"}, - {"id": 8076, "synset": "gold_plate.n.02", "name": "gold_plate"}, - {"id": 8077, "synset": "golf_bag.n.01", "name": "golf_bag"}, - {"id": 8078, "synset": "golf_ball.n.01", "name": "golf_ball"}, - {"id": 8079, "synset": "golf-club_head.n.01", "name": "golf-club_head"}, - {"id": 8080, "synset": "golf_equipment.n.01", "name": "golf_equipment"}, - {"id": 8081, "synset": "golf_glove.n.01", "name": "golf_glove"}, - {"id": 8082, "synset": "golliwog.n.01", "name": "golliwog"}, - {"id": 8083, "synset": "gong.n.01", "name": "gong"}, - {"id": 8084, "synset": "goniometer.n.01", "name": "goniometer"}, - {"id": 8085, "synset": "gordian_knot.n.02", "name": "Gordian_knot"}, - {"id": 8086, "synset": "gorget.n.01", "name": "gorget"}, - {"id": 8087, "synset": "gossamer.n.01", "name": "gossamer"}, - {"id": 8088, "synset": "gothic_arch.n.01", "name": "Gothic_arch"}, - {"id": 8089, "synset": "gouache.n.01", "name": "gouache"}, - {"id": 8090, "synset": "gouge.n.02", "name": "gouge"}, - {"id": 8091, "synset": "gourd.n.01", "name": "gourd"}, - {"id": 8092, "synset": "government_building.n.01", "name": "government_building"}, - {"id": 8093, "synset": "government_office.n.01", "name": "government_office"}, - {"id": 8094, "synset": "gown.n.01", "name": "gown"}, - {"id": 8095, "synset": "gown.n.05", "name": "gown"}, - {"id": 8096, "synset": "gown.n.04", "name": "gown"}, - {"id": 8097, "synset": "grab.n.01", "name": "grab"}, - {"id": 8098, "synset": "grab_bag.n.02", "name": "grab_bag"}, - {"id": 8099, "synset": "grab_bar.n.01", "name": "grab_bar"}, - {"id": 8100, "synset": "grace_cup.n.01", "name": "grace_cup"}, - {"id": 8101, "synset": "grade_separation.n.01", "name": "grade_separation"}, - {"id": 8102, "synset": "graduated_cylinder.n.01", "name": "graduated_cylinder"}, - {"id": 8103, "synset": "graffito.n.01", "name": "graffito"}, - {"id": 8104, "synset": "gramophone.n.01", "name": "gramophone"}, - {"id": 8105, "synset": "granary.n.01", "name": "granary"}, - {"id": 8106, "synset": "grandfather_clock.n.01", "name": "grandfather_clock"}, - {"id": 8107, "synset": "grand_piano.n.01", "name": "grand_piano"}, - {"id": 8108, "synset": "graniteware.n.01", "name": "graniteware"}, - {"id": 8109, "synset": "granny_knot.n.01", "name": "granny_knot"}, - {"id": 8110, "synset": "grape_arbor.n.01", "name": "grape_arbor"}, - {"id": 8111, "synset": "grapnel.n.02", "name": "grapnel"}, - {"id": 8112, "synset": "grapnel.n.01", "name": "grapnel"}, - {"id": 8113, "synset": "grass_skirt.n.01", "name": "grass_skirt"}, - {"id": 8114, "synset": "grate.n.01", "name": "grate"}, - {"id": 8115, "synset": "grate.n.03", "name": "grate"}, - {"id": 8116, "synset": "graver.n.01", "name": "graver"}, - {"id": 8117, "synset": "gravimeter.n.02", "name": "gravimeter"}, - {"id": 8118, "synset": "gravure.n.03", "name": "gravure"}, - {"id": 8119, "synset": "grey.n.06", "name": "grey"}, - {"id": 8120, "synset": "grease-gun.n.01", "name": "grease-gun"}, - {"id": 8121, "synset": "greasepaint.n.01", "name": "greasepaint"}, - {"id": 8122, "synset": "greasy_spoon.n.01", "name": "greasy_spoon"}, - {"id": 8123, "synset": "greatcoat.n.01", "name": "greatcoat"}, - {"id": 8124, "synset": "great_hall.n.01", "name": "great_hall"}, - {"id": 8125, "synset": "greave.n.01", "name": "greave"}, - {"id": 8126, "synset": "greengrocery.n.02", "name": "greengrocery"}, - {"id": 8127, "synset": "greenhouse.n.01", "name": "greenhouse"}, - {"id": 8128, "synset": "grenade.n.01", "name": "grenade"}, - {"id": 8129, "synset": "grid.n.05", "name": "grid"}, - {"id": 8130, "synset": "grille.n.02", "name": "grille"}, - {"id": 8131, "synset": "grillroom.n.01", "name": "grillroom"}, - {"id": 8132, "synset": "grinder.n.04", "name": "grinder"}, - {"id": 8133, "synset": "grinding_wheel.n.01", "name": "grinding_wheel"}, - {"id": 8134, "synset": "grindstone.n.01", "name": "grindstone"}, - {"id": 8135, "synset": "gripsack.n.01", "name": "gripsack"}, - {"id": 8136, "synset": "gristmill.n.01", "name": "gristmill"}, - {"id": 8137, "synset": "grocery_store.n.01", "name": "grocery_store"}, - {"id": 8138, "synset": "grogram.n.01", "name": "grogram"}, - {"id": 8139, "synset": "groined_vault.n.01", "name": "groined_vault"}, - {"id": 8140, "synset": "groover.n.01", "name": "groover"}, - {"id": 8141, "synset": "grosgrain.n.01", "name": "grosgrain"}, - {"id": 8142, "synset": "gros_point.n.01", "name": "gros_point"}, - {"id": 8143, "synset": "ground.n.09", "name": "ground"}, - {"id": 8144, "synset": "ground_bait.n.01", "name": "ground_bait"}, - {"id": 8145, "synset": "ground_control.n.01", "name": "ground_control"}, - {"id": 8146, "synset": "ground_floor.n.01", "name": "ground_floor"}, - {"id": 8147, "synset": "groundsheet.n.01", "name": "groundsheet"}, - {"id": 8148, "synset": "g-string.n.01", "name": "G-string"}, - {"id": 8149, "synset": "guard.n.03", "name": "guard"}, - {"id": 8150, "synset": "guard_boat.n.01", "name": "guard_boat"}, - {"id": 8151, "synset": "guardroom.n.02", "name": "guardroom"}, - {"id": 8152, "synset": "guardroom.n.01", "name": "guardroom"}, - {"id": 8153, "synset": "guard_ship.n.01", "name": "guard_ship"}, - {"id": 8154, "synset": "guard's_van.n.01", "name": "guard's_van"}, - {"id": 8155, "synset": "gueridon.n.01", "name": "gueridon"}, - {"id": 8156, "synset": "guarnerius.n.03", "name": "Guarnerius"}, - {"id": 8157, "synset": "guesthouse.n.01", "name": "guesthouse"}, - {"id": 8158, "synset": "guestroom.n.01", "name": "guestroom"}, - {"id": 8159, "synset": "guidance_system.n.01", "name": "guidance_system"}, - {"id": 8160, "synset": "guided_missile.n.01", "name": "guided_missile"}, - {"id": 8161, "synset": "guided_missile_cruiser.n.01", "name": "guided_missile_cruiser"}, - {"id": 8162, "synset": "guided_missile_frigate.n.01", "name": "guided_missile_frigate"}, - {"id": 8163, "synset": "guildhall.n.01", "name": "guildhall"}, - {"id": 8164, "synset": "guilloche.n.01", "name": "guilloche"}, - {"id": 8165, "synset": "guillotine.n.02", "name": "guillotine"}, - {"id": 8166, "synset": "guimpe.n.02", "name": "guimpe"}, - {"id": 8167, "synset": "guimpe.n.01", "name": "guimpe"}, - {"id": 8168, "synset": "guitar_pick.n.01", "name": "guitar_pick"}, - {"id": 8169, "synset": "gulag.n.01", "name": "gulag"}, - {"id": 8170, "synset": "gunboat.n.01", "name": "gunboat"}, - {"id": 8171, "synset": "gun_carriage.n.01", "name": "gun_carriage"}, - {"id": 8172, "synset": "gun_case.n.01", "name": "gun_case"}, - {"id": 8173, "synset": "gun_emplacement.n.01", "name": "gun_emplacement"}, - {"id": 8174, "synset": "gun_enclosure.n.01", "name": "gun_enclosure"}, - {"id": 8175, "synset": "gunlock.n.01", "name": "gunlock"}, - {"id": 8176, "synset": "gunnery.n.01", "name": "gunnery"}, - {"id": 8177, "synset": "gunnysack.n.01", "name": "gunnysack"}, - {"id": 8178, "synset": "gun_pendulum.n.01", "name": "gun_pendulum"}, - {"id": 8179, "synset": "gun_room.n.01", "name": "gun_room"}, - {"id": 8180, "synset": "gunsight.n.01", "name": "gunsight"}, - {"id": 8181, "synset": "gun_trigger.n.01", "name": "gun_trigger"}, - {"id": 8182, "synset": "gurney.n.01", "name": "gurney"}, - {"id": 8183, "synset": "gusher.n.01", "name": "gusher"}, - {"id": 8184, "synset": "gusset.n.03", "name": "gusset"}, - {"id": 8185, "synset": "gusset.n.02", "name": "gusset"}, - {"id": 8186, "synset": "guy.n.03", "name": "guy"}, - {"id": 8187, "synset": "gymnastic_apparatus.n.01", "name": "gymnastic_apparatus"}, - {"id": 8188, "synset": "gym_shoe.n.01", "name": "gym_shoe"}, - {"id": 8189, "synset": "gym_suit.n.01", "name": "gym_suit"}, - {"id": 8190, "synset": "gymslip.n.01", "name": "gymslip"}, - {"id": 8191, "synset": "gypsy_cab.n.01", "name": "gypsy_cab"}, - {"id": 8192, "synset": "gyrocompass.n.01", "name": "gyrocompass"}, - {"id": 8193, "synset": "gyroscope.n.01", "name": "gyroscope"}, - {"id": 8194, "synset": "gyrostabilizer.n.01", "name": "gyrostabilizer"}, - {"id": 8195, "synset": "habergeon.n.01", "name": "habergeon"}, - {"id": 8196, "synset": "habit.n.03", "name": "habit"}, - {"id": 8197, "synset": "habit.n.05", "name": "habit"}, - {"id": 8198, "synset": "hacienda.n.02", "name": "hacienda"}, - {"id": 8199, "synset": "hacksaw.n.01", "name": "hacksaw"}, - {"id": 8200, "synset": "haft.n.01", "name": "haft"}, - {"id": 8201, "synset": "haircloth.n.01", "name": "haircloth"}, - {"id": 8202, "synset": "hairdressing.n.01", "name": "hairdressing"}, - {"id": 8203, "synset": "hairpiece.n.01", "name": "hairpiece"}, - {"id": 8204, "synset": "hair_shirt.n.01", "name": "hair_shirt"}, - {"id": 8205, "synset": "hair_slide.n.01", "name": "hair_slide"}, - {"id": 8206, "synset": "hair_spray.n.01", "name": "hair_spray"}, - {"id": 8207, "synset": "hairspring.n.01", "name": "hairspring"}, - {"id": 8208, "synset": "hair_trigger.n.01", "name": "hair_trigger"}, - {"id": 8209, "synset": "halberd.n.01", "name": "halberd"}, - {"id": 8210, "synset": "half_binding.n.01", "name": "half_binding"}, - {"id": 8211, "synset": "half_hatchet.n.01", "name": "half_hatchet"}, - {"id": 8212, "synset": "half_hitch.n.01", "name": "half_hitch"}, - {"id": 8213, "synset": "half_track.n.01", "name": "half_track"}, - {"id": 8214, "synset": "hall.n.13", "name": "hall"}, - {"id": 8215, "synset": "hall.n.03", "name": "hall"}, - {"id": 8216, "synset": "hall.n.12", "name": "hall"}, - {"id": 8217, "synset": "hall_of_fame.n.01", "name": "Hall_of_Fame"}, - {"id": 8218, "synset": "hall_of_residence.n.01", "name": "hall_of_residence"}, - {"id": 8219, "synset": "hallstand.n.01", "name": "hallstand"}, - {"id": 8220, "synset": "halter.n.01", "name": "halter"}, - {"id": 8221, "synset": "hame.n.01", "name": "hame"}, - {"id": 8222, "synset": "hammer.n.07", "name": "hammer"}, - {"id": 8223, "synset": "hammer.n.05", "name": "hammer"}, - {"id": 8224, "synset": "hammerhead.n.02", "name": "hammerhead"}, - {"id": 8225, "synset": "hand.n.08", "name": "hand"}, - {"id": 8226, "synset": "handball.n.01", "name": "handball"}, - {"id": 8227, "synset": "handbarrow.n.01", "name": "handbarrow"}, - {"id": 8228, "synset": "handbell.n.01", "name": "handbell"}, - {"id": 8229, "synset": "handbow.n.01", "name": "handbow"}, - {"id": 8230, "synset": "hand_brake.n.01", "name": "hand_brake"}, - {"id": 8231, "synset": "hand_calculator.n.01", "name": "hand_calculator"}, - {"id": 8232, "synset": "handcar.n.01", "name": "handcar"}, - {"id": 8233, "synset": "hand_cream.n.01", "name": "hand_cream"}, - {"id": 8234, "synset": "hand_drill.n.01", "name": "hand_drill"}, - {"id": 8235, "synset": "hand_glass.n.02", "name": "hand_glass"}, - {"id": 8236, "synset": "hand_grenade.n.01", "name": "hand_grenade"}, - {"id": 8237, "synset": "hand-held_computer.n.01", "name": "hand-held_computer"}, - {"id": 8238, "synset": "handhold.n.01", "name": "handhold"}, - {"id": 8239, "synset": "handlebar.n.01", "name": "handlebar"}, - {"id": 8240, "synset": "handloom.n.01", "name": "handloom"}, - {"id": 8241, "synset": "hand_lotion.n.01", "name": "hand_lotion"}, - {"id": 8242, "synset": "hand_luggage.n.01", "name": "hand_luggage"}, - {"id": 8243, "synset": "hand-me-down.n.01", "name": "hand-me-down"}, - {"id": 8244, "synset": "hand_mower.n.01", "name": "hand_mower"}, - {"id": 8245, "synset": "hand_pump.n.01", "name": "hand_pump"}, - {"id": 8246, "synset": "handrest.n.01", "name": "handrest"}, - {"id": 8247, "synset": "handset.n.01", "name": "handset"}, - {"id": 8248, "synset": "hand_shovel.n.01", "name": "hand_shovel"}, - {"id": 8249, "synset": "handspike.n.01", "name": "handspike"}, - {"id": 8250, "synset": "handstamp.n.01", "name": "handstamp"}, - {"id": 8251, "synset": "hand_throttle.n.01", "name": "hand_throttle"}, - {"id": 8252, "synset": "hand_tool.n.01", "name": "hand_tool"}, - {"id": 8253, "synset": "hand_truck.n.01", "name": "hand_truck"}, - {"id": 8254, "synset": "handwear.n.01", "name": "handwear"}, - {"id": 8255, "synset": "handwheel.n.02", "name": "handwheel"}, - {"id": 8256, "synset": "handwheel.n.01", "name": "handwheel"}, - {"id": 8257, "synset": "hangar_queen.n.01", "name": "hangar_queen"}, - {"id": 8258, "synset": "hanger.n.02", "name": "hanger"}, - {"id": 8259, "synset": "hang_glider.n.02", "name": "hang_glider"}, - {"id": 8260, "synset": "hangman's_rope.n.01", "name": "hangman's_rope"}, - {"id": 8261, "synset": "hank.n.01", "name": "hank"}, - {"id": 8262, "synset": "hansom.n.01", "name": "hansom"}, - {"id": 8263, "synset": "harbor.n.02", "name": "harbor"}, - {"id": 8264, "synset": "hard_disc.n.01", "name": "hard_disc"}, - {"id": 8265, "synset": "hard_hat.n.02", "name": "hard_hat"}, - {"id": 8266, "synset": "hardtop.n.01", "name": "hardtop"}, - {"id": 8267, "synset": "hardware.n.02", "name": "hardware"}, - {"id": 8268, "synset": "hardware_store.n.01", "name": "hardware_store"}, - {"id": 8269, "synset": "harmonica.n.01", "name": "harmonica"}, - {"id": 8270, "synset": "harness.n.02", "name": "harness"}, - {"id": 8271, "synset": "harness.n.01", "name": "harness"}, - {"id": 8272, "synset": "harp.n.01", "name": "harp"}, - {"id": 8273, "synset": "harp.n.02", "name": "harp"}, - {"id": 8274, "synset": "harpoon.n.01", "name": "harpoon"}, - {"id": 8275, "synset": "harpoon_gun.n.01", "name": "harpoon_gun"}, - {"id": 8276, "synset": "harpoon_log.n.01", "name": "harpoon_log"}, - {"id": 8277, "synset": "harpsichord.n.01", "name": "harpsichord"}, - {"id": 8278, "synset": "harris_tweed.n.01", "name": "Harris_Tweed"}, - {"id": 8279, "synset": "harrow.n.01", "name": "harrow"}, - {"id": 8280, "synset": "harvester.n.02", "name": "harvester"}, - {"id": 8281, "synset": "hash_house.n.01", "name": "hash_house"}, - {"id": 8282, "synset": "hasp.n.01", "name": "hasp"}, - {"id": 8283, "synset": "hatch.n.03", "name": "hatch"}, - {"id": 8284, "synset": "hatchback.n.02", "name": "hatchback"}, - {"id": 8285, "synset": "hatchback.n.01", "name": "hatchback"}, - {"id": 8286, "synset": "hatchel.n.01", "name": "hatchel"}, - {"id": 8287, "synset": "hatchet.n.02", "name": "hatchet"}, - {"id": 8288, "synset": "hatpin.n.01", "name": "hatpin"}, - {"id": 8289, "synset": "hauberk.n.01", "name": "hauberk"}, - {"id": 8290, "synset": "hawaiian_guitar.n.01", "name": "Hawaiian_guitar"}, - {"id": 8291, "synset": "hawse.n.01", "name": "hawse"}, - {"id": 8292, "synset": "hawser.n.01", "name": "hawser"}, - {"id": 8293, "synset": "hawser_bend.n.01", "name": "hawser_bend"}, - {"id": 8294, "synset": "hay_bale.n.01", "name": "hay_bale"}, - {"id": 8295, "synset": "hayfork.n.01", "name": "hayfork"}, - {"id": 8296, "synset": "hayloft.n.01", "name": "hayloft"}, - {"id": 8297, "synset": "haymaker.n.01", "name": "haymaker"}, - {"id": 8298, "synset": "hayrack.n.02", "name": "hayrack"}, - {"id": 8299, "synset": "hayrack.n.01", "name": "hayrack"}, - {"id": 8300, "synset": "hazard.n.03", "name": "hazard"}, - {"id": 8301, "synset": "head.n.31", "name": "head"}, - {"id": 8302, "synset": "head.n.30", "name": "head"}, - {"id": 8303, "synset": "head.n.29", "name": "head"}, - {"id": 8304, "synset": "headdress.n.01", "name": "headdress"}, - {"id": 8305, "synset": "header.n.05", "name": "header"}, - {"id": 8306, "synset": "header.n.04", "name": "header"}, - {"id": 8307, "synset": "header.n.03", "name": "header"}, - {"id": 8308, "synset": "header.n.02", "name": "header"}, - {"id": 8309, "synset": "headfast.n.01", "name": "headfast"}, - {"id": 8310, "synset": "head_gasket.n.01", "name": "head_gasket"}, - {"id": 8311, "synset": "head_gate.n.02", "name": "head_gate"}, - {"id": 8312, "synset": "headgear.n.03", "name": "headgear"}, - {"id": 8313, "synset": "headpiece.n.02", "name": "headpiece"}, - {"id": 8314, "synset": "headpin.n.01", "name": "headpin"}, - {"id": 8315, "synset": "headquarters.n.01", "name": "headquarters"}, - {"id": 8316, "synset": "headrace.n.01", "name": "headrace"}, - {"id": 8317, "synset": "headrest.n.02", "name": "headrest"}, - {"id": 8318, "synset": "headsail.n.01", "name": "headsail"}, - {"id": 8319, "synset": "head_shop.n.01", "name": "head_shop"}, - {"id": 8320, "synset": "headstock.n.01", "name": "headstock"}, - {"id": 8321, "synset": "health_spa.n.01", "name": "health_spa"}, - {"id": 8322, "synset": "hearing_aid.n.02", "name": "hearing_aid"}, - {"id": 8323, "synset": "hearing_aid.n.01", "name": "hearing_aid"}, - {"id": 8324, "synset": "hearse.n.01", "name": "hearse"}, - {"id": 8325, "synset": "hearth.n.02", "name": "hearth"}, - {"id": 8326, "synset": "hearthrug.n.01", "name": "hearthrug"}, - {"id": 8327, "synset": "heart-lung_machine.n.01", "name": "heart-lung_machine"}, - {"id": 8328, "synset": "heat_engine.n.01", "name": "heat_engine"}, - {"id": 8329, "synset": "heat_exchanger.n.01", "name": "heat_exchanger"}, - {"id": 8330, "synset": "heating_pad.n.01", "name": "heating_pad"}, - {"id": 8331, "synset": "heat_lamp.n.01", "name": "heat_lamp"}, - {"id": 8332, "synset": "heat_pump.n.01", "name": "heat_pump"}, - {"id": 8333, "synset": "heat-seeking_missile.n.01", "name": "heat-seeking_missile"}, - {"id": 8334, "synset": "heat_shield.n.01", "name": "heat_shield"}, - {"id": 8335, "synset": "heat_sink.n.01", "name": "heat_sink"}, - {"id": 8336, "synset": "heaume.n.01", "name": "heaume"}, - {"id": 8337, "synset": "heaver.n.01", "name": "heaver"}, - {"id": 8338, "synset": "heavier-than-air_craft.n.01", "name": "heavier-than-air_craft"}, - {"id": 8339, "synset": "heckelphone.n.01", "name": "heckelphone"}, - {"id": 8340, "synset": "hectograph.n.01", "name": "hectograph"}, - {"id": 8341, "synset": "hedge.n.01", "name": "hedge"}, - {"id": 8342, "synset": "hedge_trimmer.n.01", "name": "hedge_trimmer"}, - {"id": 8343, "synset": "helicon.n.01", "name": "helicon"}, - {"id": 8344, "synset": "heliograph.n.01", "name": "heliograph"}, - {"id": 8345, "synset": "heliometer.n.01", "name": "heliometer"}, - {"id": 8346, "synset": "helm.n.01", "name": "helm"}, - {"id": 8347, "synset": "helmet.n.01", "name": "helmet"}, - {"id": 8348, "synset": "hematocrit.n.02", "name": "hematocrit"}, - {"id": 8349, "synset": "hemming-stitch.n.01", "name": "hemming-stitch"}, - {"id": 8350, "synset": "hemostat.n.01", "name": "hemostat"}, - {"id": 8351, "synset": "hemstitch.n.01", "name": "hemstitch"}, - {"id": 8352, "synset": "henroost.n.01", "name": "henroost"}, - {"id": 8353, "synset": "heraldry.n.02", "name": "heraldry"}, - {"id": 8354, "synset": "hermitage.n.01", "name": "hermitage"}, - {"id": 8355, "synset": "herringbone.n.01", "name": "herringbone"}, - {"id": 8356, "synset": "herringbone.n.02", "name": "herringbone"}, - {"id": 8357, "synset": "herschelian_telescope.n.01", "name": "Herschelian_telescope"}, - {"id": 8358, "synset": "hessian_boot.n.01", "name": "Hessian_boot"}, - {"id": 8359, "synset": "heterodyne_receiver.n.01", "name": "heterodyne_receiver"}, - {"id": 8360, "synset": "hibachi.n.01", "name": "hibachi"}, - {"id": 8361, "synset": "hideaway.n.02", "name": "hideaway"}, - {"id": 8362, "synset": "hi-fi.n.01", "name": "hi-fi"}, - {"id": 8363, "synset": "high_altar.n.01", "name": "high_altar"}, - {"id": 8364, "synset": "high-angle_gun.n.01", "name": "high-angle_gun"}, - {"id": 8365, "synset": "highball_glass.n.01", "name": "highball_glass"}, - {"id": 8366, "synset": "highboard.n.01", "name": "highboard"}, - {"id": 8367, "synset": "highboy.n.01", "name": "highboy"}, - {"id": 8368, "synset": "high_gear.n.01", "name": "high_gear"}, - {"id": 8369, "synset": "high-hat_cymbal.n.01", "name": "high-hat_cymbal"}, - {"id": 8370, "synset": "highlighter.n.02", "name": "highlighter"}, - {"id": 8371, "synset": "highlighter.n.01", "name": "highlighter"}, - {"id": 8372, "synset": "high-pass_filter.n.01", "name": "high-pass_filter"}, - {"id": 8373, "synset": "high-rise.n.01", "name": "high-rise"}, - {"id": 8374, "synset": "high_table.n.01", "name": "high_table"}, - {"id": 8375, "synset": "high-warp_loom.n.01", "name": "high-warp_loom"}, - {"id": 8376, "synset": "hijab.n.01", "name": "hijab"}, - {"id": 8377, "synset": "hinging_post.n.01", "name": "hinging_post"}, - {"id": 8378, "synset": "hip_boot.n.01", "name": "hip_boot"}, - {"id": 8379, "synset": "hipflask.n.01", "name": "hipflask"}, - {"id": 8380, "synset": "hip_pad.n.01", "name": "hip_pad"}, - {"id": 8381, "synset": "hip_pocket.n.01", "name": "hip_pocket"}, - {"id": 8382, "synset": "hippodrome.n.01", "name": "hippodrome"}, - {"id": 8383, "synset": "hip_roof.n.01", "name": "hip_roof"}, - {"id": 8384, "synset": "hitch.n.05", "name": "hitch"}, - {"id": 8385, "synset": "hitch.n.04", "name": "hitch"}, - {"id": 8386, "synset": "hitching_post.n.01", "name": "hitching_post"}, - {"id": 8387, "synset": "hitchrack.n.01", "name": "hitchrack"}, - {"id": 8388, "synset": "hob.n.03", "name": "hob"}, - {"id": 8389, "synset": "hobble_skirt.n.01", "name": "hobble_skirt"}, - {"id": 8390, "synset": "hockey_skate.n.01", "name": "hockey_skate"}, - {"id": 8391, "synset": "hod.n.01", "name": "hod"}, - {"id": 8392, "synset": "hodoscope.n.01", "name": "hodoscope"}, - {"id": 8393, "synset": "hoe.n.01", "name": "hoe"}, - {"id": 8394, "synset": "hoe_handle.n.01", "name": "hoe_handle"}, - {"id": 8395, "synset": "hogshead.n.02", "name": "hogshead"}, - {"id": 8396, "synset": "hoist.n.01", "name": "hoist"}, - {"id": 8397, "synset": "hold.n.07", "name": "hold"}, - {"id": 8398, "synset": "holder.n.01", "name": "holder"}, - {"id": 8399, "synset": "holding_cell.n.01", "name": "holding_cell"}, - {"id": 8400, "synset": "holding_device.n.01", "name": "holding_device"}, - {"id": 8401, "synset": "holding_pen.n.01", "name": "holding_pen"}, - {"id": 8402, "synset": "hollowware.n.01", "name": "hollowware"}, - {"id": 8403, "synset": "holster.n.01", "name": "holster"}, - {"id": 8404, "synset": "holster.n.02", "name": "holster"}, - {"id": 8405, "synset": "holy_of_holies.n.02", "name": "holy_of_holies"}, - {"id": 8406, "synset": "home.n.09", "name": "home"}, - {"id": 8407, "synset": "home_appliance.n.01", "name": "home_appliance"}, - {"id": 8408, "synset": "home_computer.n.01", "name": "home_computer"}, - {"id": 8409, "synset": "home_room.n.01", "name": "home_room"}, - {"id": 8410, "synset": "homespun.n.01", "name": "homespun"}, - {"id": 8411, "synset": "homestead.n.03", "name": "homestead"}, - {"id": 8412, "synset": "home_theater.n.01", "name": "home_theater"}, - {"id": 8413, "synset": "homing_torpedo.n.01", "name": "homing_torpedo"}, - {"id": 8414, "synset": "hone.n.01", "name": "hone"}, - {"id": 8415, "synset": "honeycomb.n.02", "name": "honeycomb"}, - {"id": 8416, "synset": "hood.n.09", "name": "hood"}, - {"id": 8417, "synset": "hood.n.08", "name": "hood"}, - {"id": 8418, "synset": "hood.n.07", "name": "hood"}, - {"id": 8419, "synset": "hood.n.05", "name": "hood"}, - {"id": 8420, "synset": "hood_latch.n.01", "name": "hood_latch"}, - {"id": 8421, "synset": "hook.n.04", "name": "hook"}, - {"id": 8422, "synset": "hook.n.01", "name": "hook"}, - {"id": 8423, "synset": "hook_and_eye.n.01", "name": "hook_and_eye"}, - {"id": 8424, "synset": "hookup.n.02", "name": "hookup"}, - {"id": 8425, "synset": "hookup.n.01", "name": "hookup"}, - {"id": 8426, "synset": "hook_wrench.n.01", "name": "hook_wrench"}, - {"id": 8427, "synset": "hoopskirt.n.01", "name": "hoopskirt"}, - {"id": 8428, "synset": "hoosegow.n.01", "name": "hoosegow"}, - {"id": 8429, "synset": "hoover.n.04", "name": "Hoover"}, - {"id": 8430, "synset": "hope_chest.n.01", "name": "hope_chest"}, - {"id": 8431, "synset": "hopper.n.01", "name": "hopper"}, - {"id": 8432, "synset": "hopsacking.n.01", "name": "hopsacking"}, - {"id": 8433, "synset": "horizontal_bar.n.01", "name": "horizontal_bar"}, - {"id": 8434, "synset": "horizontal_stabilizer.n.01", "name": "horizontal_stabilizer"}, - {"id": 8435, "synset": "horizontal_tail.n.01", "name": "horizontal_tail"}, - {"id": 8436, "synset": "horn.n.09", "name": "horn"}, - {"id": 8437, "synset": "horn.n.01", "name": "horn"}, - {"id": 8438, "synset": "horn.n.08", "name": "horn"}, - {"id": 8439, "synset": "horn_button.n.01", "name": "horn_button"}, - {"id": 8440, "synset": "hornpipe.n.03", "name": "hornpipe"}, - {"id": 8441, "synset": "horse.n.02", "name": "horse"}, - {"id": 8442, "synset": "horsebox.n.01", "name": "horsebox"}, - {"id": 8443, "synset": "horsecar.n.01", "name": "horsecar"}, - {"id": 8444, "synset": "horse_cart.n.01", "name": "horse_cart"}, - {"id": 8445, "synset": "horsecloth.n.01", "name": "horsecloth"}, - {"id": 8446, "synset": "horse-drawn_vehicle.n.01", "name": "horse-drawn_vehicle"}, - {"id": 8447, "synset": "horsehair.n.02", "name": "horsehair"}, - {"id": 8448, "synset": "horsehair_wig.n.01", "name": "horsehair_wig"}, - {"id": 8449, "synset": "horseless_carriage.n.01", "name": "horseless_carriage"}, - {"id": 8450, "synset": "horse_pistol.n.01", "name": "horse_pistol"}, - {"id": 8451, "synset": "horseshoe.n.02", "name": "horseshoe"}, - {"id": 8452, "synset": "horseshoe.n.01", "name": "horseshoe"}, - {"id": 8453, "synset": "horse-trail.n.01", "name": "horse-trail"}, - {"id": 8454, "synset": "horsewhip.n.01", "name": "horsewhip"}, - {"id": 8455, "synset": "hose.n.02", "name": "hose"}, - {"id": 8456, "synset": "hosiery.n.01", "name": "hosiery"}, - {"id": 8457, "synset": "hospice.n.01", "name": "hospice"}, - {"id": 8458, "synset": "hospital.n.01", "name": "hospital"}, - {"id": 8459, "synset": "hospital_bed.n.01", "name": "hospital_bed"}, - {"id": 8460, "synset": "hospital_room.n.01", "name": "hospital_room"}, - {"id": 8461, "synset": "hospital_ship.n.01", "name": "hospital_ship"}, - {"id": 8462, "synset": "hospital_train.n.01", "name": "hospital_train"}, - {"id": 8463, "synset": "hostel.n.02", "name": "hostel"}, - {"id": 8464, "synset": "hostel.n.01", "name": "hostel"}, - {"id": 8465, "synset": "hotel.n.01", "name": "hotel"}, - {"id": 8466, "synset": "hotel-casino.n.02", "name": "hotel-casino"}, - {"id": 8467, "synset": "hotel-casino.n.01", "name": "hotel-casino"}, - {"id": 8468, "synset": "hotel_room.n.01", "name": "hotel_room"}, - {"id": 8469, "synset": "hot_line.n.01", "name": "hot_line"}, - {"id": 8470, "synset": "hot_pants.n.02", "name": "hot_pants"}, - {"id": 8471, "synset": "hot_rod.n.01", "name": "hot_rod"}, - {"id": 8472, "synset": "hot_spot.n.03", "name": "hot_spot"}, - {"id": 8473, "synset": "hot_tub.n.01", "name": "hot_tub"}, - {"id": 8474, "synset": "hot-water_bottle.n.01", "name": "hot-water_bottle"}, - {"id": 8475, "synset": "houndstooth_check.n.01", "name": "houndstooth_check"}, - {"id": 8476, "synset": "hour_hand.n.01", "name": "hour_hand"}, - {"id": 8477, "synset": "house.n.01", "name": "house"}, - {"id": 8478, "synset": "house.n.12", "name": "house"}, - {"id": 8479, "synset": "houselights.n.01", "name": "houselights"}, - {"id": 8480, "synset": "house_of_cards.n.02", "name": "house_of_cards"}, - {"id": 8481, "synset": "house_of_correction.n.01", "name": "house_of_correction"}, - {"id": 8482, "synset": "house_paint.n.01", "name": "house_paint"}, - {"id": 8483, "synset": "housetop.n.01", "name": "housetop"}, - {"id": 8484, "synset": "housing.n.01", "name": "housing"}, - {"id": 8485, "synset": "hovel.n.01", "name": "hovel"}, - {"id": 8486, "synset": "hovercraft.n.01", "name": "hovercraft"}, - {"id": 8487, "synset": "howdah.n.01", "name": "howdah"}, - {"id": 8488, "synset": "huarache.n.01", "name": "huarache"}, - {"id": 8489, "synset": "hub-and-spoke.n.01", "name": "hub-and-spoke"}, - {"id": 8490, "synset": "hubcap.n.01", "name": "hubcap"}, - {"id": 8491, "synset": "huck.n.01", "name": "huck"}, - {"id": 8492, "synset": "hug-me-tight.n.01", "name": "hug-me-tight"}, - {"id": 8493, "synset": "hula-hoop.n.01", "name": "hula-hoop"}, - {"id": 8494, "synset": "hulk.n.02", "name": "hulk"}, - {"id": 8495, "synset": "hull.n.06", "name": "hull"}, - {"id": 8496, "synset": "humeral_veil.n.01", "name": "humeral_veil"}, - {"id": 8497, "synset": "humvee.n.01", "name": "Humvee"}, - {"id": 8498, "synset": "hunter.n.04", "name": "hunter"}, - {"id": 8499, "synset": "hunting_knife.n.01", "name": "hunting_knife"}, - {"id": 8500, "synset": "hurdle.n.01", "name": "hurdle"}, - {"id": 8501, "synset": "hurricane_deck.n.01", "name": "hurricane_deck"}, - {"id": 8502, "synset": "hurricane_lamp.n.01", "name": "hurricane_lamp"}, - {"id": 8503, "synset": "hut.n.01", "name": "hut"}, - {"id": 8504, "synset": "hutch.n.01", "name": "hutch"}, - {"id": 8505, "synset": "hutment.n.01", "name": "hutment"}, - {"id": 8506, "synset": "hydraulic_brake.n.01", "name": "hydraulic_brake"}, - {"id": 8507, "synset": "hydraulic_press.n.01", "name": "hydraulic_press"}, - {"id": 8508, "synset": "hydraulic_pump.n.01", "name": "hydraulic_pump"}, - {"id": 8509, "synset": "hydraulic_system.n.01", "name": "hydraulic_system"}, - {"id": 8510, "synset": "hydraulic_transmission.n.01", "name": "hydraulic_transmission"}, - {"id": 8511, "synset": "hydroelectric_turbine.n.01", "name": "hydroelectric_turbine"}, - {"id": 8512, "synset": "hydrofoil.n.02", "name": "hydrofoil"}, - {"id": 8513, "synset": "hydrofoil.n.01", "name": "hydrofoil"}, - {"id": 8514, "synset": "hydrogen_bomb.n.01", "name": "hydrogen_bomb"}, - {"id": 8515, "synset": "hydrometer.n.01", "name": "hydrometer"}, - {"id": 8516, "synset": "hygrodeik.n.01", "name": "hygrodeik"}, - {"id": 8517, "synset": "hygrometer.n.01", "name": "hygrometer"}, - {"id": 8518, "synset": "hygroscope.n.01", "name": "hygroscope"}, - {"id": 8519, "synset": "hyperbaric_chamber.n.01", "name": "hyperbaric_chamber"}, - {"id": 8520, "synset": "hypercoaster.n.01", "name": "hypercoaster"}, - {"id": 8521, "synset": "hypermarket.n.01", "name": "hypermarket"}, - {"id": 8522, "synset": "hypodermic_needle.n.01", "name": "hypodermic_needle"}, - {"id": 8523, "synset": "hypodermic_syringe.n.01", "name": "hypodermic_syringe"}, - {"id": 8524, "synset": "hypsometer.n.01", "name": "hypsometer"}, - {"id": 8525, "synset": "hysterosalpingogram.n.01", "name": "hysterosalpingogram"}, - {"id": 8526, "synset": "i-beam.n.01", "name": "I-beam"}, - {"id": 8527, "synset": "ice_ax.n.01", "name": "ice_ax"}, - {"id": 8528, "synset": "iceboat.n.02", "name": "iceboat"}, - {"id": 8529, "synset": "icebreaker.n.01", "name": "icebreaker"}, - {"id": 8530, "synset": "iced-tea_spoon.n.01", "name": "iced-tea_spoon"}, - {"id": 8531, "synset": "ice_hockey_rink.n.01", "name": "ice_hockey_rink"}, - {"id": 8532, "synset": "ice_machine.n.01", "name": "ice_machine"}, - {"id": 8533, "synset": "icepick.n.01", "name": "icepick"}, - {"id": 8534, "synset": "ice_rink.n.01", "name": "ice_rink"}, - {"id": 8535, "synset": "ice_tongs.n.01", "name": "ice_tongs"}, - {"id": 8536, "synset": "icetray.n.01", "name": "icetray"}, - {"id": 8537, "synset": "iconoscope.n.01", "name": "iconoscope"}, - {"id": 8538, "synset": "identikit.n.01", "name": "Identikit"}, - {"id": 8539, "synset": "idle_pulley.n.01", "name": "idle_pulley"}, - {"id": 8540, "synset": "igloo.n.01", "name": "igloo"}, - {"id": 8541, "synset": "ignition_coil.n.01", "name": "ignition_coil"}, - {"id": 8542, "synset": "ignition_key.n.01", "name": "ignition_key"}, - {"id": 8543, "synset": "ignition_switch.n.01", "name": "ignition_switch"}, - {"id": 8544, "synset": "imaret.n.01", "name": "imaret"}, - {"id": 8545, "synset": "immovable_bandage.n.01", "name": "immovable_bandage"}, - {"id": 8546, "synset": "impact_printer.n.01", "name": "impact_printer"}, - {"id": 8547, "synset": "impeller.n.01", "name": "impeller"}, - {"id": 8548, "synset": "implant.n.01", "name": "implant"}, - {"id": 8549, "synset": "implement.n.01", "name": "implement"}, - {"id": 8550, "synset": "impression.n.07", "name": "impression"}, - {"id": 8551, "synset": "imprint.n.05", "name": "imprint"}, - { - "id": 8552, - "synset": "improvised_explosive_device.n.01", - "name": "improvised_explosive_device", - }, - {"id": 8553, "synset": "impulse_turbine.n.01", "name": "impulse_turbine"}, - {"id": 8554, "synset": "in-basket.n.01", "name": "in-basket"}, - {"id": 8555, "synset": "incendiary_bomb.n.01", "name": "incendiary_bomb"}, - {"id": 8556, "synset": "incinerator.n.01", "name": "incinerator"}, - {"id": 8557, "synset": "inclined_plane.n.01", "name": "inclined_plane"}, - {"id": 8558, "synset": "inclinometer.n.02", "name": "inclinometer"}, - {"id": 8559, "synset": "inclinometer.n.01", "name": "inclinometer"}, - {"id": 8560, "synset": "incrustation.n.03", "name": "incrustation"}, - {"id": 8561, "synset": "incubator.n.01", "name": "incubator"}, - {"id": 8562, "synset": "index_register.n.01", "name": "index_register"}, - {"id": 8563, "synset": "indiaman.n.01", "name": "Indiaman"}, - {"id": 8564, "synset": "indian_club.n.01", "name": "Indian_club"}, - {"id": 8565, "synset": "indicator.n.03", "name": "indicator"}, - {"id": 8566, "synset": "induction_coil.n.01", "name": "induction_coil"}, - {"id": 8567, "synset": "inductor.n.01", "name": "inductor"}, - {"id": 8568, "synset": "industrial_watercourse.n.01", "name": "industrial_watercourse"}, - {"id": 8569, "synset": "inertial_guidance_system.n.01", "name": "inertial_guidance_system"}, - {"id": 8570, "synset": "inflater.n.01", "name": "inflater"}, - {"id": 8571, "synset": "injector.n.01", "name": "injector"}, - {"id": 8572, "synset": "ink_bottle.n.01", "name": "ink_bottle"}, - {"id": 8573, "synset": "ink_eraser.n.01", "name": "ink_eraser"}, - {"id": 8574, "synset": "ink-jet_printer.n.01", "name": "ink-jet_printer"}, - {"id": 8575, "synset": "inkle.n.01", "name": "inkle"}, - {"id": 8576, "synset": "inkstand.n.02", "name": "inkstand"}, - {"id": 8577, "synset": "inkwell.n.01", "name": "inkwell"}, - {"id": 8578, "synset": "inlay.n.01", "name": "inlay"}, - {"id": 8579, "synset": "inside_caliper.n.01", "name": "inside_caliper"}, - {"id": 8580, "synset": "insole.n.01", "name": "insole"}, - {"id": 8581, "synset": "instep.n.02", "name": "instep"}, - {"id": 8582, "synset": "instillator.n.01", "name": "instillator"}, - {"id": 8583, "synset": "institution.n.02", "name": "institution"}, - {"id": 8584, "synset": "instrument.n.01", "name": "instrument"}, - {"id": 8585, "synset": "instrument_of_punishment.n.01", "name": "instrument_of_punishment"}, - {"id": 8586, "synset": "instrument_of_torture.n.01", "name": "instrument_of_torture"}, - {"id": 8587, "synset": "intaglio.n.02", "name": "intaglio"}, - {"id": 8588, "synset": "intake_valve.n.01", "name": "intake_valve"}, - {"id": 8589, "synset": "integrated_circuit.n.01", "name": "integrated_circuit"}, - {"id": 8590, "synset": "integrator.n.01", "name": "integrator"}, - {"id": 8591, "synset": "intelnet.n.01", "name": "Intelnet"}, - {"id": 8592, "synset": "interceptor.n.01", "name": "interceptor"}, - {"id": 8593, "synset": "interchange.n.01", "name": "interchange"}, - {"id": 8594, "synset": "intercommunication_system.n.01", "name": "intercommunication_system"}, - { - "id": 8595, - "synset": "intercontinental_ballistic_missile.n.01", - "name": "intercontinental_ballistic_missile", - }, - {"id": 8596, "synset": "interface.n.04", "name": "interface"}, - {"id": 8597, "synset": "interferometer.n.01", "name": "interferometer"}, - {"id": 8598, "synset": "interior_door.n.01", "name": "interior_door"}, - {"id": 8599, "synset": "internal-combustion_engine.n.01", "name": "internal-combustion_engine"}, - {"id": 8600, "synset": "internal_drive.n.01", "name": "internal_drive"}, - {"id": 8601, "synset": "internet.n.01", "name": "internet"}, - {"id": 8602, "synset": "interphone.n.01", "name": "interphone"}, - {"id": 8603, "synset": "interrupter.n.01", "name": "interrupter"}, - {"id": 8604, "synset": "intersection.n.02", "name": "intersection"}, - {"id": 8605, "synset": "interstice.n.02", "name": "interstice"}, - {"id": 8606, "synset": "intraocular_lens.n.01", "name": "intraocular_lens"}, - {"id": 8607, "synset": "intravenous_pyelogram.n.01", "name": "intravenous_pyelogram"}, - {"id": 8608, "synset": "inverter.n.01", "name": "inverter"}, - {"id": 8609, "synset": "ion_engine.n.01", "name": "ion_engine"}, - {"id": 8610, "synset": "ionization_chamber.n.01", "name": "ionization_chamber"}, - {"id": 8611, "synset": "video_ipod.n.01", "name": "video_iPod"}, - {"id": 8612, "synset": "iron.n.02", "name": "iron"}, - {"id": 8613, "synset": "iron.n.03", "name": "iron"}, - {"id": 8614, "synset": "irons.n.01", "name": "irons"}, - {"id": 8615, "synset": "ironclad.n.01", "name": "ironclad"}, - {"id": 8616, "synset": "iron_foundry.n.01", "name": "iron_foundry"}, - {"id": 8617, "synset": "iron_horse.n.01", "name": "iron_horse"}, - {"id": 8618, "synset": "ironing.n.01", "name": "ironing"}, - {"id": 8619, "synset": "iron_lung.n.01", "name": "iron_lung"}, - {"id": 8620, "synset": "ironmongery.n.01", "name": "ironmongery"}, - {"id": 8621, "synset": "ironworks.n.01", "name": "ironworks"}, - {"id": 8622, "synset": "irrigation_ditch.n.01", "name": "irrigation_ditch"}, - {"id": 8623, "synset": "izar.n.01", "name": "izar"}, - {"id": 8624, "synset": "jabot.n.01", "name": "jabot"}, - {"id": 8625, "synset": "jack.n.10", "name": "jack"}, - {"id": 8626, "synset": "jack.n.07", "name": "jack"}, - {"id": 8627, "synset": "jack.n.06", "name": "jack"}, - {"id": 8628, "synset": "jack.n.05", "name": "jack"}, - {"id": 8629, "synset": "jacket.n.02", "name": "jacket"}, - {"id": 8630, "synset": "jacket.n.05", "name": "jacket"}, - {"id": 8631, "synset": "jack-in-the-box.n.01", "name": "jack-in-the-box"}, - {"id": 8632, "synset": "jack-o'-lantern.n.02", "name": "jack-o'-lantern"}, - {"id": 8633, "synset": "jack_plane.n.01", "name": "jack_plane"}, - {"id": 8634, "synset": "jacob's_ladder.n.02", "name": "Jacob's_ladder"}, - {"id": 8635, "synset": "jaconet.n.01", "name": "jaconet"}, - {"id": 8636, "synset": "jacquard_loom.n.01", "name": "Jacquard_loom"}, - {"id": 8637, "synset": "jacquard.n.02", "name": "jacquard"}, - {"id": 8638, "synset": "jag.n.03", "name": "jag"}, - {"id": 8639, "synset": "jail.n.01", "name": "jail"}, - {"id": 8640, "synset": "jalousie.n.02", "name": "jalousie"}, - {"id": 8641, "synset": "jamb.n.01", "name": "jamb"}, - {"id": 8642, "synset": "jammer.n.01", "name": "jammer"}, - {"id": 8643, "synset": "jampot.n.01", "name": "jampot"}, - {"id": 8644, "synset": "japan.n.04", "name": "japan"}, - {"id": 8645, "synset": "jarvik_heart.n.01", "name": "Jarvik_heart"}, - {"id": 8646, "synset": "jaunting_car.n.01", "name": "jaunting_car"}, - {"id": 8647, "synset": "javelin.n.02", "name": "javelin"}, - {"id": 8648, "synset": "jaw.n.03", "name": "jaw"}, - {"id": 8649, "synset": "jaws_of_life.n.01", "name": "Jaws_of_Life"}, - {"id": 8650, "synset": "jellaba.n.01", "name": "jellaba"}, - {"id": 8651, "synset": "jerkin.n.01", "name": "jerkin"}, - {"id": 8652, "synset": "jeroboam.n.02", "name": "jeroboam"}, - {"id": 8653, "synset": "jersey.n.04", "name": "jersey"}, - {"id": 8654, "synset": "jet_bridge.n.01", "name": "jet_bridge"}, - {"id": 8655, "synset": "jet_engine.n.01", "name": "jet_engine"}, - {"id": 8656, "synset": "jetliner.n.01", "name": "jetliner"}, - {"id": 8657, "synset": "jeweler's_glass.n.01", "name": "jeweler's_glass"}, - {"id": 8658, "synset": "jewelled_headdress.n.01", "name": "jewelled_headdress"}, - {"id": 8659, "synset": "jew's_harp.n.01", "name": "jew's_harp"}, - {"id": 8660, "synset": "jib.n.01", "name": "jib"}, - {"id": 8661, "synset": "jibboom.n.01", "name": "jibboom"}, - {"id": 8662, "synset": "jig.n.03", "name": "jig"}, - {"id": 8663, "synset": "jig.n.02", "name": "jig"}, - {"id": 8664, "synset": "jiggermast.n.01", "name": "jiggermast"}, - {"id": 8665, "synset": "jigsaw.n.02", "name": "jigsaw"}, - {"id": 8666, "synset": "jigsaw_puzzle.n.01", "name": "jigsaw_puzzle"}, - {"id": 8667, "synset": "jinrikisha.n.01", "name": "jinrikisha"}, - {"id": 8668, "synset": "jobcentre.n.01", "name": "jobcentre"}, - {"id": 8669, "synset": "jodhpurs.n.01", "name": "jodhpurs"}, - {"id": 8670, "synset": "jodhpur.n.01", "name": "jodhpur"}, - {"id": 8671, "synset": "joinery.n.01", "name": "joinery"}, - {"id": 8672, "synset": "joint.n.05", "name": "joint"}, - { - "id": 8673, - "synset": "joint_direct_attack_munition.n.01", - "name": "Joint_Direct_Attack_Munition", - }, - {"id": 8674, "synset": "jointer.n.01", "name": "jointer"}, - {"id": 8675, "synset": "joist.n.01", "name": "joist"}, - {"id": 8676, "synset": "jolly_boat.n.01", "name": "jolly_boat"}, - {"id": 8677, "synset": "jorum.n.01", "name": "jorum"}, - {"id": 8678, "synset": "joss_house.n.01", "name": "joss_house"}, - {"id": 8679, "synset": "journal_bearing.n.01", "name": "journal_bearing"}, - {"id": 8680, "synset": "journal_box.n.01", "name": "journal_box"}, - {"id": 8681, "synset": "jungle_gym.n.01", "name": "jungle_gym"}, - {"id": 8682, "synset": "junk.n.02", "name": "junk"}, - {"id": 8683, "synset": "jug.n.01", "name": "jug"}, - {"id": 8684, "synset": "jukebox.n.01", "name": "jukebox"}, - {"id": 8685, "synset": "jumbojet.n.01", "name": "jumbojet"}, - {"id": 8686, "synset": "jumper.n.07", "name": "jumper"}, - {"id": 8687, "synset": "jumper.n.06", "name": "jumper"}, - {"id": 8688, "synset": "jumper.n.05", "name": "jumper"}, - {"id": 8689, "synset": "jumper.n.04", "name": "jumper"}, - {"id": 8690, "synset": "jumper_cable.n.01", "name": "jumper_cable"}, - {"id": 8691, "synset": "jump_seat.n.01", "name": "jump_seat"}, - {"id": 8692, "synset": "jump_suit.n.02", "name": "jump_suit"}, - {"id": 8693, "synset": "junction.n.01", "name": "junction"}, - {"id": 8694, "synset": "junction.n.04", "name": "junction"}, - {"id": 8695, "synset": "junction_barrier.n.01", "name": "junction_barrier"}, - {"id": 8696, "synset": "junk_shop.n.01", "name": "junk_shop"}, - {"id": 8697, "synset": "jury_box.n.01", "name": "jury_box"}, - {"id": 8698, "synset": "jury_mast.n.01", "name": "jury_mast"}, - {"id": 8699, "synset": "kachina.n.03", "name": "kachina"}, - {"id": 8700, "synset": "kaffiyeh.n.01", "name": "kaffiyeh"}, - {"id": 8701, "synset": "kalansuwa.n.01", "name": "kalansuwa"}, - {"id": 8702, "synset": "kalashnikov.n.01", "name": "Kalashnikov"}, - {"id": 8703, "synset": "kameez.n.01", "name": "kameez"}, - {"id": 8704, "synset": "kanzu.n.01", "name": "kanzu"}, - {"id": 8705, "synset": "katharometer.n.01", "name": "katharometer"}, - {"id": 8706, "synset": "kazoo.n.01", "name": "kazoo"}, - {"id": 8707, "synset": "keel.n.03", "name": "keel"}, - {"id": 8708, "synset": "keelboat.n.01", "name": "keelboat"}, - {"id": 8709, "synset": "keelson.n.01", "name": "keelson"}, - {"id": 8710, "synset": "keep.n.02", "name": "keep"}, - {"id": 8711, "synset": "kepi.n.01", "name": "kepi"}, - {"id": 8712, "synset": "keratoscope.n.01", "name": "keratoscope"}, - {"id": 8713, "synset": "kerchief.n.01", "name": "kerchief"}, - {"id": 8714, "synset": "ketch.n.01", "name": "ketch"}, - {"id": 8715, "synset": "kettle.n.04", "name": "kettle"}, - {"id": 8716, "synset": "key.n.15", "name": "key"}, - {"id": 8717, "synset": "keyboard.n.01", "name": "keyboard"}, - {"id": 8718, "synset": "keyboard_buffer.n.01", "name": "keyboard_buffer"}, - {"id": 8719, "synset": "keyboard_instrument.n.01", "name": "keyboard_instrument"}, - {"id": 8720, "synset": "keyhole.n.01", "name": "keyhole"}, - {"id": 8721, "synset": "keyhole_saw.n.01", "name": "keyhole_saw"}, - {"id": 8722, "synset": "khadi.n.01", "name": "khadi"}, - {"id": 8723, "synset": "khaki.n.01", "name": "khaki"}, - {"id": 8724, "synset": "khakis.n.01", "name": "khakis"}, - {"id": 8725, "synset": "khimar.n.01", "name": "khimar"}, - {"id": 8726, "synset": "khukuri.n.01", "name": "khukuri"}, - {"id": 8727, "synset": "kick_pleat.n.01", "name": "kick_pleat"}, - {"id": 8728, "synset": "kicksorter.n.01", "name": "kicksorter"}, - {"id": 8729, "synset": "kickstand.n.01", "name": "kickstand"}, - {"id": 8730, "synset": "kick_starter.n.01", "name": "kick_starter"}, - {"id": 8731, "synset": "kid_glove.n.01", "name": "kid_glove"}, - {"id": 8732, "synset": "kiln.n.01", "name": "kiln"}, - {"id": 8733, "synset": "kinescope.n.01", "name": "kinescope"}, - {"id": 8734, "synset": "kinetoscope.n.01", "name": "Kinetoscope"}, - {"id": 8735, "synset": "king.n.10", "name": "king"}, - {"id": 8736, "synset": "king.n.08", "name": "king"}, - {"id": 8737, "synset": "kingbolt.n.01", "name": "kingbolt"}, - {"id": 8738, "synset": "king_post.n.01", "name": "king_post"}, - {"id": 8739, "synset": "kipp's_apparatus.n.01", "name": "Kipp's_apparatus"}, - {"id": 8740, "synset": "kirk.n.01", "name": "kirk"}, - {"id": 8741, "synset": "kirpan.n.01", "name": "kirpan"}, - {"id": 8742, "synset": "kirtle.n.02", "name": "kirtle"}, - {"id": 8743, "synset": "kirtle.n.01", "name": "kirtle"}, - {"id": 8744, "synset": "kit.n.02", "name": "kit"}, - {"id": 8745, "synset": "kit.n.01", "name": "kit"}, - {"id": 8746, "synset": "kitbag.n.01", "name": "kitbag"}, - {"id": 8747, "synset": "kitchen.n.01", "name": "kitchen"}, - {"id": 8748, "synset": "kitchen_appliance.n.01", "name": "kitchen_appliance"}, - {"id": 8749, "synset": "kitchenette.n.01", "name": "kitchenette"}, - {"id": 8750, "synset": "kitchen_utensil.n.01", "name": "kitchen_utensil"}, - {"id": 8751, "synset": "kitchenware.n.01", "name": "kitchenware"}, - {"id": 8752, "synset": "kite_balloon.n.01", "name": "kite_balloon"}, - {"id": 8753, "synset": "klaxon.n.01", "name": "klaxon"}, - {"id": 8754, "synset": "klieg_light.n.01", "name": "klieg_light"}, - {"id": 8755, "synset": "klystron.n.01", "name": "klystron"}, - {"id": 8756, "synset": "knee_brace.n.01", "name": "knee_brace"}, - {"id": 8757, "synset": "knee-high.n.01", "name": "knee-high"}, - {"id": 8758, "synset": "knee_piece.n.01", "name": "knee_piece"}, - {"id": 8759, "synset": "knife.n.02", "name": "knife"}, - {"id": 8760, "synset": "knife_blade.n.01", "name": "knife_blade"}, - {"id": 8761, "synset": "knight.n.02", "name": "knight"}, - {"id": 8762, "synset": "knit.n.01", "name": "knit"}, - {"id": 8763, "synset": "knitting_machine.n.01", "name": "knitting_machine"}, - {"id": 8764, "synset": "knitwear.n.01", "name": "knitwear"}, - {"id": 8765, "synset": "knob.n.01", "name": "knob"}, - {"id": 8766, "synset": "knob.n.04", "name": "knob"}, - {"id": 8767, "synset": "knobble.n.01", "name": "knobble"}, - {"id": 8768, "synset": "knobkerrie.n.01", "name": "knobkerrie"}, - {"id": 8769, "synset": "knot.n.02", "name": "knot"}, - {"id": 8770, "synset": "knuckle_joint.n.02", "name": "knuckle_joint"}, - {"id": 8771, "synset": "kohl.n.01", "name": "kohl"}, - {"id": 8772, "synset": "koto.n.01", "name": "koto"}, - {"id": 8773, "synset": "kraal.n.02", "name": "kraal"}, - {"id": 8774, "synset": "kremlin.n.02", "name": "kremlin"}, - {"id": 8775, "synset": "kris.n.01", "name": "kris"}, - {"id": 8776, "synset": "krummhorn.n.01", "name": "krummhorn"}, - {"id": 8777, "synset": "kundt's_tube.n.01", "name": "Kundt's_tube"}, - {"id": 8778, "synset": "kurdistan.n.02", "name": "Kurdistan"}, - {"id": 8779, "synset": "kurta.n.01", "name": "kurta"}, - {"id": 8780, "synset": "kylix.n.01", "name": "kylix"}, - {"id": 8781, "synset": "kymograph.n.01", "name": "kymograph"}, - {"id": 8782, "synset": "lab_bench.n.01", "name": "lab_bench"}, - {"id": 8783, "synset": "lace.n.02", "name": "lace"}, - {"id": 8784, "synset": "lacquer.n.02", "name": "lacquer"}, - {"id": 8785, "synset": "lacquerware.n.01", "name": "lacquerware"}, - {"id": 8786, "synset": "lacrosse_ball.n.01", "name": "lacrosse_ball"}, - {"id": 8787, "synset": "ladder-back.n.02", "name": "ladder-back"}, - {"id": 8788, "synset": "ladder-back.n.01", "name": "ladder-back"}, - {"id": 8789, "synset": "ladder_truck.n.01", "name": "ladder_truck"}, - {"id": 8790, "synset": "ladies'_room.n.01", "name": "ladies'_room"}, - {"id": 8791, "synset": "lady_chapel.n.01", "name": "lady_chapel"}, - {"id": 8792, "synset": "lagerphone.n.01", "name": "lagerphone"}, - {"id": 8793, "synset": "lag_screw.n.01", "name": "lag_screw"}, - {"id": 8794, "synset": "lake_dwelling.n.01", "name": "lake_dwelling"}, - {"id": 8795, "synset": "lally.n.01", "name": "lally"}, - {"id": 8796, "synset": "lamasery.n.01", "name": "lamasery"}, - {"id": 8797, "synset": "lambrequin.n.02", "name": "lambrequin"}, - {"id": 8798, "synset": "lame.n.02", "name": "lame"}, - {"id": 8799, "synset": "laminar_flow_clean_room.n.01", "name": "laminar_flow_clean_room"}, - {"id": 8800, "synset": "laminate.n.01", "name": "laminate"}, - {"id": 8801, "synset": "lamination.n.01", "name": "lamination"}, - {"id": 8802, "synset": "lamp.n.01", "name": "lamp"}, - {"id": 8803, "synset": "lamp_house.n.01", "name": "lamp_house"}, - {"id": 8804, "synset": "lanai.n.02", "name": "lanai"}, - {"id": 8805, "synset": "lancet_arch.n.01", "name": "lancet_arch"}, - {"id": 8806, "synset": "lancet_window.n.01", "name": "lancet_window"}, - {"id": 8807, "synset": "landau.n.02", "name": "landau"}, - {"id": 8808, "synset": "lander.n.02", "name": "lander"}, - {"id": 8809, "synset": "landing_craft.n.01", "name": "landing_craft"}, - {"id": 8810, "synset": "landing_flap.n.01", "name": "landing_flap"}, - {"id": 8811, "synset": "landing_gear.n.01", "name": "landing_gear"}, - {"id": 8812, "synset": "landing_net.n.01", "name": "landing_net"}, - {"id": 8813, "synset": "landing_skid.n.01", "name": "landing_skid"}, - {"id": 8814, "synset": "land_line.n.01", "name": "land_line"}, - {"id": 8815, "synset": "land_mine.n.01", "name": "land_mine"}, - {"id": 8816, "synset": "land_office.n.01", "name": "land_office"}, - {"id": 8817, "synset": "lanolin.n.02", "name": "lanolin"}, - {"id": 8818, "synset": "lanyard.n.01", "name": "lanyard"}, - {"id": 8819, "synset": "lap.n.03", "name": "lap"}, - {"id": 8820, "synset": "laparoscope.n.01", "name": "laparoscope"}, - {"id": 8821, "synset": "lapboard.n.01", "name": "lapboard"}, - {"id": 8822, "synset": "lapel.n.01", "name": "lapel"}, - {"id": 8823, "synset": "lap_joint.n.01", "name": "lap_joint"}, - {"id": 8824, "synset": "laryngoscope.n.01", "name": "laryngoscope"}, - {"id": 8825, "synset": "laser.n.01", "name": "laser"}, - {"id": 8826, "synset": "laser-guided_bomb.n.01", "name": "laser-guided_bomb"}, - {"id": 8827, "synset": "laser_printer.n.01", "name": "laser_printer"}, - {"id": 8828, "synset": "lash.n.02", "name": "lash"}, - {"id": 8829, "synset": "lashing.n.02", "name": "lashing"}, - {"id": 8830, "synset": "lasso.n.02", "name": "lasso"}, - {"id": 8831, "synset": "latch.n.01", "name": "latch"}, - {"id": 8832, "synset": "latchet.n.01", "name": "latchet"}, - {"id": 8833, "synset": "latchkey.n.01", "name": "latchkey"}, - {"id": 8834, "synset": "lateen.n.01", "name": "lateen"}, - {"id": 8835, "synset": "latex_paint.n.01", "name": "latex_paint"}, - {"id": 8836, "synset": "lath.n.01", "name": "lath"}, - {"id": 8837, "synset": "lathe.n.01", "name": "lathe"}, - {"id": 8838, "synset": "latrine.n.01", "name": "latrine"}, - {"id": 8839, "synset": "lattice.n.03", "name": "lattice"}, - {"id": 8840, "synset": "launch.n.01", "name": "launch"}, - {"id": 8841, "synset": "launcher.n.01", "name": "launcher"}, - {"id": 8842, "synset": "laundry.n.01", "name": "laundry"}, - {"id": 8843, "synset": "laundry_cart.n.01", "name": "laundry_cart"}, - {"id": 8844, "synset": "laundry_truck.n.01", "name": "laundry_truck"}, - {"id": 8845, "synset": "lavalava.n.01", "name": "lavalava"}, - {"id": 8846, "synset": "lavaliere.n.01", "name": "lavaliere"}, - {"id": 8847, "synset": "laver.n.02", "name": "laver"}, - {"id": 8848, "synset": "lawn_chair.n.01", "name": "lawn_chair"}, - {"id": 8849, "synset": "lawn_furniture.n.01", "name": "lawn_furniture"}, - {"id": 8850, "synset": "layette.n.01", "name": "layette"}, - {"id": 8851, "synset": "lead-acid_battery.n.01", "name": "lead-acid_battery"}, - {"id": 8852, "synset": "lead-in.n.02", "name": "lead-in"}, - {"id": 8853, "synset": "leading_rein.n.01", "name": "leading_rein"}, - {"id": 8854, "synset": "lead_pencil.n.01", "name": "lead_pencil"}, - {"id": 8855, "synset": "leaf_spring.n.01", "name": "leaf_spring"}, - {"id": 8856, "synset": "lean-to.n.01", "name": "lean-to"}, - {"id": 8857, "synset": "lean-to_tent.n.01", "name": "lean-to_tent"}, - {"id": 8858, "synset": "leash.n.01", "name": "leash"}, - {"id": 8859, "synset": "leatherette.n.01", "name": "leatherette"}, - {"id": 8860, "synset": "leather_strip.n.01", "name": "leather_strip"}, - {"id": 8861, "synset": "leclanche_cell.n.01", "name": "Leclanche_cell"}, - {"id": 8862, "synset": "lectern.n.01", "name": "lectern"}, - {"id": 8863, "synset": "lecture_room.n.01", "name": "lecture_room"}, - {"id": 8864, "synset": "lederhosen.n.01", "name": "lederhosen"}, - {"id": 8865, "synset": "ledger_board.n.01", "name": "ledger_board"}, - {"id": 8866, "synset": "leg.n.07", "name": "leg"}, - {"id": 8867, "synset": "leg.n.03", "name": "leg"}, - {"id": 8868, "synset": "leiden_jar.n.01", "name": "Leiden_jar"}, - {"id": 8869, "synset": "leisure_wear.n.01", "name": "leisure_wear"}, - {"id": 8870, "synset": "lens.n.01", "name": "lens"}, - {"id": 8871, "synset": "lens.n.05", "name": "lens"}, - {"id": 8872, "synset": "lens_cap.n.01", "name": "lens_cap"}, - {"id": 8873, "synset": "lens_implant.n.01", "name": "lens_implant"}, - {"id": 8874, "synset": "leotard.n.01", "name": "leotard"}, - {"id": 8875, "synset": "letter_case.n.01", "name": "letter_case"}, - {"id": 8876, "synset": "letter_opener.n.01", "name": "letter_opener"}, - {"id": 8877, "synset": "levee.n.03", "name": "levee"}, - {"id": 8878, "synset": "level.n.05", "name": "level"}, - {"id": 8879, "synset": "lever.n.01", "name": "lever"}, - {"id": 8880, "synset": "lever.n.03", "name": "lever"}, - {"id": 8881, "synset": "lever.n.02", "name": "lever"}, - {"id": 8882, "synset": "lever_lock.n.01", "name": "lever_lock"}, - {"id": 8883, "synset": "levi's.n.01", "name": "Levi's"}, - {"id": 8884, "synset": "liberty_ship.n.01", "name": "Liberty_ship"}, - {"id": 8885, "synset": "library.n.01", "name": "library"}, - {"id": 8886, "synset": "library.n.05", "name": "library"}, - {"id": 8887, "synset": "lid.n.02", "name": "lid"}, - {"id": 8888, "synset": "liebig_condenser.n.01", "name": "Liebig_condenser"}, - {"id": 8889, "synset": "lie_detector.n.01", "name": "lie_detector"}, - {"id": 8890, "synset": "lifeboat.n.01", "name": "lifeboat"}, - {"id": 8891, "synset": "life_office.n.01", "name": "life_office"}, - {"id": 8892, "synset": "life_preserver.n.01", "name": "life_preserver"}, - {"id": 8893, "synset": "life-support_system.n.02", "name": "life-support_system"}, - {"id": 8894, "synset": "life-support_system.n.01", "name": "life-support_system"}, - {"id": 8895, "synset": "lifting_device.n.01", "name": "lifting_device"}, - {"id": 8896, "synset": "lift_pump.n.01", "name": "lift_pump"}, - {"id": 8897, "synset": "ligament.n.02", "name": "ligament"}, - {"id": 8898, "synset": "ligature.n.03", "name": "ligature"}, - {"id": 8899, "synset": "light.n.02", "name": "light"}, - {"id": 8900, "synset": "light_arm.n.01", "name": "light_arm"}, - {"id": 8901, "synset": "light_circuit.n.01", "name": "light_circuit"}, - {"id": 8902, "synset": "light-emitting_diode.n.01", "name": "light-emitting_diode"}, - {"id": 8903, "synset": "lighter.n.02", "name": "lighter"}, - {"id": 8904, "synset": "lighter-than-air_craft.n.01", "name": "lighter-than-air_craft"}, - {"id": 8905, "synset": "light_filter.n.01", "name": "light_filter"}, - {"id": 8906, "synset": "lighting.n.02", "name": "lighting"}, - {"id": 8907, "synset": "light_machine_gun.n.01", "name": "light_machine_gun"}, - {"id": 8908, "synset": "light_meter.n.01", "name": "light_meter"}, - {"id": 8909, "synset": "light_microscope.n.01", "name": "light_microscope"}, - {"id": 8910, "synset": "light_pen.n.01", "name": "light_pen"}, - {"id": 8911, "synset": "lightship.n.01", "name": "lightship"}, - {"id": 8912, "synset": "lilo.n.01", "name": "Lilo"}, - {"id": 8913, "synset": "limber.n.01", "name": "limber"}, - {"id": 8914, "synset": "limekiln.n.01", "name": "limekiln"}, - {"id": 8915, "synset": "limiter.n.01", "name": "limiter"}, - {"id": 8916, "synset": "linear_accelerator.n.01", "name": "linear_accelerator"}, - {"id": 8917, "synset": "linen.n.01", "name": "linen"}, - {"id": 8918, "synset": "line_printer.n.01", "name": "line_printer"}, - {"id": 8919, "synset": "liner.n.04", "name": "liner"}, - {"id": 8920, "synset": "liner.n.03", "name": "liner"}, - {"id": 8921, "synset": "lingerie.n.01", "name": "lingerie"}, - {"id": 8922, "synset": "lining.n.01", "name": "lining"}, - {"id": 8923, "synset": "link.n.09", "name": "link"}, - {"id": 8924, "synset": "linkage.n.03", "name": "linkage"}, - {"id": 8925, "synset": "link_trainer.n.01", "name": "Link_trainer"}, - {"id": 8926, "synset": "linocut.n.02", "name": "linocut"}, - {"id": 8927, "synset": "linoleum_knife.n.01", "name": "linoleum_knife"}, - {"id": 8928, "synset": "linotype.n.01", "name": "Linotype"}, - {"id": 8929, "synset": "linsey-woolsey.n.01", "name": "linsey-woolsey"}, - {"id": 8930, "synset": "linstock.n.01", "name": "linstock"}, - {"id": 8931, "synset": "lion-jaw_forceps.n.01", "name": "lion-jaw_forceps"}, - {"id": 8932, "synset": "lip-gloss.n.01", "name": "lip-gloss"}, - {"id": 8933, "synset": "lipstick.n.01", "name": "lipstick"}, - {"id": 8934, "synset": "liqueur_glass.n.01", "name": "liqueur_glass"}, - {"id": 8935, "synset": "liquid_crystal_display.n.01", "name": "liquid_crystal_display"}, - {"id": 8936, "synset": "liquid_metal_reactor.n.01", "name": "liquid_metal_reactor"}, - {"id": 8937, "synset": "lisle.n.01", "name": "lisle"}, - {"id": 8938, "synset": "lister.n.03", "name": "lister"}, - {"id": 8939, "synset": "litterbin.n.01", "name": "litterbin"}, - {"id": 8940, "synset": "little_theater.n.01", "name": "little_theater"}, - {"id": 8941, "synset": "live_axle.n.01", "name": "live_axle"}, - {"id": 8942, "synset": "living_quarters.n.01", "name": "living_quarters"}, - {"id": 8943, "synset": "living_room.n.01", "name": "living_room"}, - {"id": 8944, "synset": "load.n.09", "name": "load"}, - {"id": 8945, "synset": "loafer.n.02", "name": "Loafer"}, - {"id": 8946, "synset": "loaner.n.02", "name": "loaner"}, - {"id": 8947, "synset": "lobe.n.04", "name": "lobe"}, - {"id": 8948, "synset": "lobster_pot.n.01", "name": "lobster_pot"}, - {"id": 8949, "synset": "local.n.01", "name": "local"}, - {"id": 8950, "synset": "local_area_network.n.01", "name": "local_area_network"}, - {"id": 8951, "synset": "local_oscillator.n.01", "name": "local_oscillator"}, - {"id": 8952, "synset": "lochaber_ax.n.01", "name": "Lochaber_ax"}, - {"id": 8953, "synset": "lock.n.01", "name": "lock"}, - {"id": 8954, "synset": "lock.n.05", "name": "lock"}, - {"id": 8955, "synset": "lock.n.04", "name": "lock"}, - {"id": 8956, "synset": "lock.n.03", "name": "lock"}, - {"id": 8957, "synset": "lockage.n.02", "name": "lockage"}, - {"id": 8958, "synset": "locker.n.02", "name": "locker"}, - {"id": 8959, "synset": "locker_room.n.01", "name": "locker_room"}, - {"id": 8960, "synset": "locket.n.01", "name": "locket"}, - {"id": 8961, "synset": "lock-gate.n.01", "name": "lock-gate"}, - {"id": 8962, "synset": "locking_pliers.n.01", "name": "locking_pliers"}, - {"id": 8963, "synset": "lockring.n.01", "name": "lockring"}, - {"id": 8964, "synset": "lockstitch.n.01", "name": "lockstitch"}, - {"id": 8965, "synset": "lockup.n.01", "name": "lockup"}, - {"id": 8966, "synset": "locomotive.n.01", "name": "locomotive"}, - {"id": 8967, "synset": "lodge.n.05", "name": "lodge"}, - {"id": 8968, "synset": "lodge.n.04", "name": "lodge"}, - {"id": 8969, "synset": "lodge.n.03", "name": "lodge"}, - {"id": 8970, "synset": "lodging_house.n.01", "name": "lodging_house"}, - {"id": 8971, "synset": "loft.n.02", "name": "loft"}, - {"id": 8972, "synset": "loft.n.04", "name": "loft"}, - {"id": 8973, "synset": "loft.n.01", "name": "loft"}, - {"id": 8974, "synset": "log_cabin.n.01", "name": "log_cabin"}, - {"id": 8975, "synset": "loggia.n.01", "name": "loggia"}, - {"id": 8976, "synset": "longbow.n.01", "name": "longbow"}, - {"id": 8977, "synset": "long_iron.n.01", "name": "long_iron"}, - {"id": 8978, "synset": "long_johns.n.01", "name": "long_johns"}, - {"id": 8979, "synset": "long_sleeve.n.01", "name": "long_sleeve"}, - {"id": 8980, "synset": "long_tom.n.01", "name": "long_tom"}, - {"id": 8981, "synset": "long_trousers.n.01", "name": "long_trousers"}, - {"id": 8982, "synset": "long_underwear.n.01", "name": "long_underwear"}, - {"id": 8983, "synset": "looking_glass.n.01", "name": "looking_glass"}, - {"id": 8984, "synset": "lookout.n.03", "name": "lookout"}, - {"id": 8985, "synset": "loom.n.01", "name": "loom"}, - {"id": 8986, "synset": "loop_knot.n.01", "name": "loop_knot"}, - {"id": 8987, "synset": "lorgnette.n.01", "name": "lorgnette"}, - {"id": 8988, "synset": "lorraine_cross.n.01", "name": "Lorraine_cross"}, - {"id": 8989, "synset": "lorry.n.02", "name": "lorry"}, - {"id": 8990, "synset": "lota.n.01", "name": "lota"}, - {"id": 8991, "synset": "lotion.n.01", "name": "lotion"}, - {"id": 8992, "synset": "lounge.n.02", "name": "lounge"}, - {"id": 8993, "synset": "lounger.n.03", "name": "lounger"}, - {"id": 8994, "synset": "lounging_jacket.n.01", "name": "lounging_jacket"}, - {"id": 8995, "synset": "lounging_pajama.n.01", "name": "lounging_pajama"}, - {"id": 8996, "synset": "loungewear.n.01", "name": "loungewear"}, - {"id": 8997, "synset": "loupe.n.01", "name": "loupe"}, - {"id": 8998, "synset": "louvered_window.n.01", "name": "louvered_window"}, - {"id": 8999, "synset": "love_knot.n.01", "name": "love_knot"}, - {"id": 9000, "synset": "loving_cup.n.01", "name": "loving_cup"}, - {"id": 9001, "synset": "lowboy.n.01", "name": "lowboy"}, - {"id": 9002, "synset": "low-pass_filter.n.01", "name": "low-pass_filter"}, - {"id": 9003, "synset": "low-warp-loom.n.01", "name": "low-warp-loom"}, - {"id": 9004, "synset": "lp.n.01", "name": "LP"}, - {"id": 9005, "synset": "l-plate.n.01", "name": "L-plate"}, - {"id": 9006, "synset": "lubber's_hole.n.01", "name": "lubber's_hole"}, - {"id": 9007, "synset": "lubricating_system.n.01", "name": "lubricating_system"}, - {"id": 9008, "synset": "luff.n.01", "name": "luff"}, - {"id": 9009, "synset": "lug.n.03", "name": "lug"}, - {"id": 9010, "synset": "luge.n.01", "name": "luge"}, - {"id": 9011, "synset": "luger.n.01", "name": "Luger"}, - {"id": 9012, "synset": "luggage_carrier.n.01", "name": "luggage_carrier"}, - {"id": 9013, "synset": "luggage_compartment.n.01", "name": "luggage_compartment"}, - {"id": 9014, "synset": "luggage_rack.n.01", "name": "luggage_rack"}, - {"id": 9015, "synset": "lugger.n.01", "name": "lugger"}, - {"id": 9016, "synset": "lugsail.n.01", "name": "lugsail"}, - {"id": 9017, "synset": "lug_wrench.n.01", "name": "lug_wrench"}, - {"id": 9018, "synset": "lumberjack.n.02", "name": "lumberjack"}, - {"id": 9019, "synset": "lumbermill.n.01", "name": "lumbermill"}, - {"id": 9020, "synset": "lunar_excursion_module.n.01", "name": "lunar_excursion_module"}, - {"id": 9021, "synset": "lunchroom.n.01", "name": "lunchroom"}, - {"id": 9022, "synset": "lunette.n.01", "name": "lunette"}, - {"id": 9023, "synset": "lungi.n.01", "name": "lungi"}, - {"id": 9024, "synset": "lunula.n.02", "name": "lunula"}, - {"id": 9025, "synset": "lusterware.n.01", "name": "lusterware"}, - {"id": 9026, "synset": "lute.n.02", "name": "lute"}, - {"id": 9027, "synset": "luxury_liner.n.01", "name": "luxury_liner"}, - {"id": 9028, "synset": "lyceum.n.02", "name": "lyceum"}, - {"id": 9029, "synset": "lychgate.n.01", "name": "lychgate"}, - {"id": 9030, "synset": "lyre.n.01", "name": "lyre"}, - {"id": 9031, "synset": "machete.n.01", "name": "machete"}, - {"id": 9032, "synset": "machicolation.n.01", "name": "machicolation"}, - {"id": 9033, "synset": "machine.n.01", "name": "machine"}, - {"id": 9034, "synset": "machine.n.04", "name": "machine"}, - {"id": 9035, "synset": "machine_bolt.n.01", "name": "machine_bolt"}, - {"id": 9036, "synset": "machinery.n.01", "name": "machinery"}, - {"id": 9037, "synset": "machine_screw.n.01", "name": "machine_screw"}, - {"id": 9038, "synset": "machine_tool.n.01", "name": "machine_tool"}, - {"id": 9039, "synset": "machinist's_vise.n.01", "name": "machinist's_vise"}, - {"id": 9040, "synset": "machmeter.n.01", "name": "machmeter"}, - {"id": 9041, "synset": "mackinaw.n.04", "name": "mackinaw"}, - {"id": 9042, "synset": "mackinaw.n.03", "name": "mackinaw"}, - {"id": 9043, "synset": "mackinaw.n.01", "name": "mackinaw"}, - {"id": 9044, "synset": "mackintosh.n.01", "name": "mackintosh"}, - {"id": 9045, "synset": "macrame.n.01", "name": "macrame"}, - {"id": 9046, "synset": "madras.n.03", "name": "madras"}, - {"id": 9047, "synset": "mae_west.n.02", "name": "Mae_West"}, - {"id": 9048, "synset": "magazine_rack.n.01", "name": "magazine_rack"}, - {"id": 9049, "synset": "magic_lantern.n.01", "name": "magic_lantern"}, - {"id": 9050, "synset": "magnetic_bottle.n.01", "name": "magnetic_bottle"}, - {"id": 9051, "synset": "magnetic_compass.n.01", "name": "magnetic_compass"}, - {"id": 9052, "synset": "magnetic_core_memory.n.01", "name": "magnetic_core_memory"}, - {"id": 9053, "synset": "magnetic_disk.n.01", "name": "magnetic_disk"}, - {"id": 9054, "synset": "magnetic_head.n.01", "name": "magnetic_head"}, - {"id": 9055, "synset": "magnetic_mine.n.01", "name": "magnetic_mine"}, - {"id": 9056, "synset": "magnetic_needle.n.01", "name": "magnetic_needle"}, - {"id": 9057, "synset": "magnetic_recorder.n.01", "name": "magnetic_recorder"}, - {"id": 9058, "synset": "magnetic_stripe.n.01", "name": "magnetic_stripe"}, - {"id": 9059, "synset": "magnetic_tape.n.01", "name": "magnetic_tape"}, - {"id": 9060, "synset": "magneto.n.01", "name": "magneto"}, - {"id": 9061, "synset": "magnetometer.n.01", "name": "magnetometer"}, - {"id": 9062, "synset": "magnetron.n.01", "name": "magnetron"}, - {"id": 9063, "synset": "magnifier.n.01", "name": "magnifier"}, - {"id": 9064, "synset": "magnum.n.01", "name": "magnum"}, - {"id": 9065, "synset": "magnus_hitch.n.01", "name": "magnus_hitch"}, - {"id": 9066, "synset": "mail.n.03", "name": "mail"}, - {"id": 9067, "synset": "mailbag.n.02", "name": "mailbag"}, - {"id": 9068, "synset": "mailbag.n.01", "name": "mailbag"}, - {"id": 9069, "synset": "mailboat.n.01", "name": "mailboat"}, - {"id": 9070, "synset": "mail_car.n.01", "name": "mail_car"}, - {"id": 9071, "synset": "maildrop.n.01", "name": "maildrop"}, - {"id": 9072, "synset": "mailer.n.04", "name": "mailer"}, - {"id": 9073, "synset": "maillot.n.02", "name": "maillot"}, - {"id": 9074, "synset": "maillot.n.01", "name": "maillot"}, - {"id": 9075, "synset": "mailsorter.n.01", "name": "mailsorter"}, - {"id": 9076, "synset": "mail_train.n.01", "name": "mail_train"}, - {"id": 9077, "synset": "mainframe.n.01", "name": "mainframe"}, - {"id": 9078, "synset": "mainmast.n.01", "name": "mainmast"}, - {"id": 9079, "synset": "main_rotor.n.01", "name": "main_rotor"}, - {"id": 9080, "synset": "mainsail.n.01", "name": "mainsail"}, - {"id": 9081, "synset": "mainspring.n.01", "name": "mainspring"}, - {"id": 9082, "synset": "main-topmast.n.01", "name": "main-topmast"}, - {"id": 9083, "synset": "main-topsail.n.01", "name": "main-topsail"}, - {"id": 9084, "synset": "main_yard.n.01", "name": "main_yard"}, - {"id": 9085, "synset": "maisonette.n.02", "name": "maisonette"}, - {"id": 9086, "synset": "majolica.n.01", "name": "majolica"}, - {"id": 9087, "synset": "makeup.n.01", "name": "makeup"}, - {"id": 9088, "synset": "maksutov_telescope.n.01", "name": "Maksutov_telescope"}, - {"id": 9089, "synset": "malacca.n.02", "name": "malacca"}, - {"id": 9090, "synset": "mallet.n.03", "name": "mallet"}, - {"id": 9091, "synset": "mallet.n.02", "name": "mallet"}, - {"id": 9092, "synset": "mammogram.n.01", "name": "mammogram"}, - {"id": 9093, "synset": "mandola.n.01", "name": "mandola"}, - {"id": 9094, "synset": "mandolin.n.01", "name": "mandolin"}, - {"id": 9095, "synset": "mangle.n.01", "name": "mangle"}, - {"id": 9096, "synset": "manhole_cover.n.01", "name": "manhole_cover"}, - {"id": 9097, "synset": "man-of-war.n.01", "name": "man-of-war"}, - {"id": 9098, "synset": "manometer.n.01", "name": "manometer"}, - {"id": 9099, "synset": "manor.n.01", "name": "manor"}, - {"id": 9100, "synset": "manor_hall.n.01", "name": "manor_hall"}, - {"id": 9101, "synset": "manpad.n.01", "name": "MANPAD"}, - {"id": 9102, "synset": "mansard.n.01", "name": "mansard"}, - {"id": 9103, "synset": "manse.n.02", "name": "manse"}, - {"id": 9104, "synset": "mansion.n.02", "name": "mansion"}, - {"id": 9105, "synset": "mantel.n.01", "name": "mantel"}, - {"id": 9106, "synset": "mantelet.n.02", "name": "mantelet"}, - {"id": 9107, "synset": "mantilla.n.01", "name": "mantilla"}, - {"id": 9108, "synset": "mao_jacket.n.01", "name": "Mao_jacket"}, - {"id": 9109, "synset": "maquiladora.n.01", "name": "maquiladora"}, - {"id": 9110, "synset": "maraca.n.01", "name": "maraca"}, - {"id": 9111, "synset": "marble.n.02", "name": "marble"}, - {"id": 9112, "synset": "marching_order.n.01", "name": "marching_order"}, - {"id": 9113, "synset": "marimba.n.01", "name": "marimba"}, - {"id": 9114, "synset": "marina.n.01", "name": "marina"}, - {"id": 9115, "synset": "marketplace.n.02", "name": "marketplace"}, - {"id": 9116, "synset": "marlinespike.n.01", "name": "marlinespike"}, - {"id": 9117, "synset": "marocain.n.01", "name": "marocain"}, - {"id": 9118, "synset": "marquee.n.02", "name": "marquee"}, - {"id": 9119, "synset": "marquetry.n.01", "name": "marquetry"}, - {"id": 9120, "synset": "marriage_bed.n.01", "name": "marriage_bed"}, - {"id": 9121, "synset": "martello_tower.n.01", "name": "martello_tower"}, - {"id": 9122, "synset": "martingale.n.01", "name": "martingale"}, - {"id": 9123, "synset": "mascara.n.01", "name": "mascara"}, - {"id": 9124, "synset": "maser.n.01", "name": "maser"}, - {"id": 9125, "synset": "mashie.n.01", "name": "mashie"}, - {"id": 9126, "synset": "mashie_niblick.n.01", "name": "mashie_niblick"}, - {"id": 9127, "synset": "masjid.n.01", "name": "masjid"}, - {"id": 9128, "synset": "mask.n.01", "name": "mask"}, - {"id": 9129, "synset": "masonite.n.01", "name": "Masonite"}, - {"id": 9130, "synset": "mason_jar.n.01", "name": "Mason_jar"}, - {"id": 9131, "synset": "masonry.n.01", "name": "masonry"}, - {"id": 9132, "synset": "mason's_level.n.01", "name": "mason's_level"}, - {"id": 9133, "synset": "massage_parlor.n.02", "name": "massage_parlor"}, - {"id": 9134, "synset": "massage_parlor.n.01", "name": "massage_parlor"}, - {"id": 9135, "synset": "mass_spectrograph.n.01", "name": "mass_spectrograph"}, - {"id": 9136, "synset": "mass_spectrometer.n.01", "name": "mass_spectrometer"}, - {"id": 9137, "synset": "mast.n.04", "name": "mast"}, - {"id": 9138, "synset": "mastaba.n.01", "name": "mastaba"}, - {"id": 9139, "synset": "master_bedroom.n.01", "name": "master_bedroom"}, - {"id": 9140, "synset": "masterpiece.n.01", "name": "masterpiece"}, - {"id": 9141, "synset": "mat.n.01", "name": "mat"}, - {"id": 9142, "synset": "match.n.01", "name": "match"}, - {"id": 9143, "synset": "match.n.03", "name": "match"}, - {"id": 9144, "synset": "matchboard.n.01", "name": "matchboard"}, - {"id": 9145, "synset": "matchbook.n.01", "name": "matchbook"}, - {"id": 9146, "synset": "matchlock.n.01", "name": "matchlock"}, - {"id": 9147, "synset": "match_plane.n.01", "name": "match_plane"}, - {"id": 9148, "synset": "matchstick.n.01", "name": "matchstick"}, - {"id": 9149, "synset": "material.n.04", "name": "material"}, - {"id": 9150, "synset": "materiel.n.01", "name": "materiel"}, - {"id": 9151, "synset": "maternity_hospital.n.01", "name": "maternity_hospital"}, - {"id": 9152, "synset": "maternity_ward.n.01", "name": "maternity_ward"}, - {"id": 9153, "synset": "matrix.n.06", "name": "matrix"}, - {"id": 9154, "synset": "matthew_walker.n.01", "name": "Matthew_Walker"}, - {"id": 9155, "synset": "matting.n.01", "name": "matting"}, - {"id": 9156, "synset": "mattock.n.01", "name": "mattock"}, - {"id": 9157, "synset": "mattress_cover.n.01", "name": "mattress_cover"}, - {"id": 9158, "synset": "maul.n.01", "name": "maul"}, - {"id": 9159, "synset": "maulstick.n.01", "name": "maulstick"}, - {"id": 9160, "synset": "mauser.n.02", "name": "Mauser"}, - {"id": 9161, "synset": "mausoleum.n.01", "name": "mausoleum"}, - {"id": 9162, "synset": "maxi.n.01", "name": "maxi"}, - {"id": 9163, "synset": "maxim_gun.n.01", "name": "Maxim_gun"}, - { - "id": 9164, - "synset": "maximum_and_minimum_thermometer.n.01", - "name": "maximum_and_minimum_thermometer", - }, - {"id": 9165, "synset": "maypole.n.01", "name": "maypole"}, - {"id": 9166, "synset": "maze.n.01", "name": "maze"}, - {"id": 9167, "synset": "mazer.n.01", "name": "mazer"}, - {"id": 9168, "synset": "means.n.02", "name": "means"}, - {"id": 9169, "synset": "measure.n.09", "name": "measure"}, - {"id": 9170, "synset": "measuring_instrument.n.01", "name": "measuring_instrument"}, - {"id": 9171, "synset": "meat_counter.n.01", "name": "meat_counter"}, - {"id": 9172, "synset": "meat_grinder.n.01", "name": "meat_grinder"}, - {"id": 9173, "synset": "meat_hook.n.01", "name": "meat_hook"}, - {"id": 9174, "synset": "meat_house.n.02", "name": "meat_house"}, - {"id": 9175, "synset": "meat_safe.n.01", "name": "meat_safe"}, - {"id": 9176, "synset": "meat_thermometer.n.01", "name": "meat_thermometer"}, - {"id": 9177, "synset": "mechanical_device.n.01", "name": "mechanical_device"}, - {"id": 9178, "synset": "mechanical_piano.n.01", "name": "mechanical_piano"}, - {"id": 9179, "synset": "mechanical_system.n.01", "name": "mechanical_system"}, - {"id": 9180, "synset": "mechanism.n.05", "name": "mechanism"}, - {"id": 9181, "synset": "medical_building.n.01", "name": "medical_building"}, - {"id": 9182, "synset": "medical_instrument.n.01", "name": "medical_instrument"}, - {"id": 9183, "synset": "medicine_ball.n.01", "name": "medicine_ball"}, - {"id": 9184, "synset": "medicine_chest.n.01", "name": "medicine_chest"}, - {"id": 9185, "synset": "medline.n.01", "name": "MEDLINE"}, - {"id": 9186, "synset": "megalith.n.01", "name": "megalith"}, - {"id": 9187, "synset": "megaphone.n.01", "name": "megaphone"}, - {"id": 9188, "synset": "memorial.n.03", "name": "memorial"}, - {"id": 9189, "synset": "memory.n.04", "name": "memory"}, - {"id": 9190, "synset": "memory_chip.n.01", "name": "memory_chip"}, - {"id": 9191, "synset": "memory_device.n.01", "name": "memory_device"}, - {"id": 9192, "synset": "menagerie.n.02", "name": "menagerie"}, - {"id": 9193, "synset": "mending.n.01", "name": "mending"}, - {"id": 9194, "synset": "menhir.n.01", "name": "menhir"}, - {"id": 9195, "synset": "menorah.n.02", "name": "menorah"}, - {"id": 9196, "synset": "menorah.n.01", "name": "Menorah"}, - {"id": 9197, "synset": "man's_clothing.n.01", "name": "man's_clothing"}, - {"id": 9198, "synset": "men's_room.n.01", "name": "men's_room"}, - {"id": 9199, "synset": "mercantile_establishment.n.01", "name": "mercantile_establishment"}, - {"id": 9200, "synset": "mercury_barometer.n.01", "name": "mercury_barometer"}, - {"id": 9201, "synset": "mercury_cell.n.01", "name": "mercury_cell"}, - {"id": 9202, "synset": "mercury_thermometer.n.01", "name": "mercury_thermometer"}, - {"id": 9203, "synset": "mercury-vapor_lamp.n.01", "name": "mercury-vapor_lamp"}, - {"id": 9204, "synset": "mercy_seat.n.02", "name": "mercy_seat"}, - {"id": 9205, "synset": "merlon.n.01", "name": "merlon"}, - {"id": 9206, "synset": "mess.n.05", "name": "mess"}, - {"id": 9207, "synset": "mess_jacket.n.01", "name": "mess_jacket"}, - {"id": 9208, "synset": "mess_kit.n.01", "name": "mess_kit"}, - {"id": 9209, "synset": "messuage.n.01", "name": "messuage"}, - {"id": 9210, "synset": "metal_detector.n.01", "name": "metal_detector"}, - {"id": 9211, "synset": "metallic.n.01", "name": "metallic"}, - {"id": 9212, "synset": "metal_screw.n.01", "name": "metal_screw"}, - {"id": 9213, "synset": "metal_wood.n.01", "name": "metal_wood"}, - {"id": 9214, "synset": "meteorological_balloon.n.01", "name": "meteorological_balloon"}, - {"id": 9215, "synset": "meter.n.02", "name": "meter"}, - {"id": 9216, "synset": "meterstick.n.01", "name": "meterstick"}, - {"id": 9217, "synset": "metronome.n.01", "name": "metronome"}, - {"id": 9218, "synset": "mezzanine.n.02", "name": "mezzanine"}, - {"id": 9219, "synset": "mezzanine.n.01", "name": "mezzanine"}, - {"id": 9220, "synset": "microbalance.n.01", "name": "microbalance"}, - {"id": 9221, "synset": "microbrewery.n.01", "name": "microbrewery"}, - {"id": 9222, "synset": "microfiche.n.01", "name": "microfiche"}, - {"id": 9223, "synset": "microfilm.n.01", "name": "microfilm"}, - {"id": 9224, "synset": "micrometer.n.02", "name": "micrometer"}, - {"id": 9225, "synset": "microprocessor.n.01", "name": "microprocessor"}, - {"id": 9226, "synset": "microtome.n.01", "name": "microtome"}, - { - "id": 9227, - "synset": "microwave_diathermy_machine.n.01", - "name": "microwave_diathermy_machine", - }, - { - "id": 9228, - "synset": "microwave_linear_accelerator.n.01", - "name": "microwave_linear_accelerator", - }, - {"id": 9229, "synset": "middy.n.01", "name": "middy"}, - {"id": 9230, "synset": "midiron.n.01", "name": "midiron"}, - {"id": 9231, "synset": "mihrab.n.02", "name": "mihrab"}, - {"id": 9232, "synset": "mihrab.n.01", "name": "mihrab"}, - {"id": 9233, "synset": "military_hospital.n.01", "name": "military_hospital"}, - {"id": 9234, "synset": "military_quarters.n.01", "name": "military_quarters"}, - {"id": 9235, "synset": "military_uniform.n.01", "name": "military_uniform"}, - {"id": 9236, "synset": "military_vehicle.n.01", "name": "military_vehicle"}, - {"id": 9237, "synset": "milk_bar.n.01", "name": "milk_bar"}, - {"id": 9238, "synset": "milk_float.n.01", "name": "milk_float"}, - {"id": 9239, "synset": "milking_machine.n.01", "name": "milking_machine"}, - {"id": 9240, "synset": "milking_stool.n.01", "name": "milking_stool"}, - {"id": 9241, "synset": "milk_wagon.n.01", "name": "milk_wagon"}, - {"id": 9242, "synset": "mill.n.04", "name": "mill"}, - {"id": 9243, "synset": "milldam.n.01", "name": "milldam"}, - {"id": 9244, "synset": "miller.n.05", "name": "miller"}, - {"id": 9245, "synset": "milliammeter.n.01", "name": "milliammeter"}, - {"id": 9246, "synset": "millinery.n.02", "name": "millinery"}, - {"id": 9247, "synset": "millinery.n.01", "name": "millinery"}, - {"id": 9248, "synset": "milling.n.01", "name": "milling"}, - {"id": 9249, "synset": "millivoltmeter.n.01", "name": "millivoltmeter"}, - {"id": 9250, "synset": "millstone.n.03", "name": "millstone"}, - {"id": 9251, "synset": "millstone.n.02", "name": "millstone"}, - {"id": 9252, "synset": "millwheel.n.01", "name": "millwheel"}, - {"id": 9253, "synset": "mimeograph.n.01", "name": "mimeograph"}, - {"id": 9254, "synset": "minaret.n.01", "name": "minaret"}, - {"id": 9255, "synset": "mincer.n.01", "name": "mincer"}, - {"id": 9256, "synset": "mine.n.02", "name": "mine"}, - {"id": 9257, "synset": "mine_detector.n.01", "name": "mine_detector"}, - {"id": 9258, "synset": "minelayer.n.01", "name": "minelayer"}, - {"id": 9259, "synset": "mineshaft.n.01", "name": "mineshaft"}, - {"id": 9260, "synset": "minibar.n.01", "name": "minibar"}, - {"id": 9261, "synset": "minibike.n.01", "name": "minibike"}, - {"id": 9262, "synset": "minibus.n.01", "name": "minibus"}, - {"id": 9263, "synset": "minicar.n.01", "name": "minicar"}, - {"id": 9264, "synset": "minicomputer.n.01", "name": "minicomputer"}, - {"id": 9265, "synset": "ministry.n.02", "name": "ministry"}, - {"id": 9266, "synset": "miniskirt.n.01", "name": "miniskirt"}, - {"id": 9267, "synset": "minisub.n.01", "name": "minisub"}, - {"id": 9268, "synset": "miniver.n.01", "name": "miniver"}, - {"id": 9269, "synset": "mink.n.02", "name": "mink"}, - {"id": 9270, "synset": "minster.n.01", "name": "minster"}, - {"id": 9271, "synset": "mint.n.06", "name": "mint"}, - {"id": 9272, "synset": "minute_hand.n.01", "name": "minute_hand"}, - {"id": 9273, "synset": "minuteman.n.02", "name": "Minuteman"}, - {"id": 9274, "synset": "missile.n.01", "name": "missile"}, - {"id": 9275, "synset": "missile_defense_system.n.01", "name": "missile_defense_system"}, - {"id": 9276, "synset": "miter_box.n.01", "name": "miter_box"}, - {"id": 9277, "synset": "miter_joint.n.01", "name": "miter_joint"}, - {"id": 9278, "synset": "mixer.n.03", "name": "mixer"}, - {"id": 9279, "synset": "mixing_bowl.n.01", "name": "mixing_bowl"}, - {"id": 9280, "synset": "mixing_faucet.n.01", "name": "mixing_faucet"}, - {"id": 9281, "synset": "mizzen.n.02", "name": "mizzen"}, - {"id": 9282, "synset": "mizzenmast.n.01", "name": "mizzenmast"}, - {"id": 9283, "synset": "mobcap.n.01", "name": "mobcap"}, - {"id": 9284, "synset": "mobile_home.n.01", "name": "mobile_home"}, - {"id": 9285, "synset": "moccasin.n.01", "name": "moccasin"}, - {"id": 9286, "synset": "mock-up.n.01", "name": "mock-up"}, - {"id": 9287, "synset": "mod_con.n.01", "name": "mod_con"}, - {"id": 9288, "synset": "model_t.n.01", "name": "Model_T"}, - {"id": 9289, "synset": "modem.n.01", "name": "modem"}, - {"id": 9290, "synset": "modillion.n.01", "name": "modillion"}, - {"id": 9291, "synset": "module.n.03", "name": "module"}, - {"id": 9292, "synset": "module.n.02", "name": "module"}, - {"id": 9293, "synset": "mohair.n.01", "name": "mohair"}, - {"id": 9294, "synset": "moire.n.01", "name": "moire"}, - {"id": 9295, "synset": "mold.n.02", "name": "mold"}, - {"id": 9296, "synset": "moldboard.n.01", "name": "moldboard"}, - {"id": 9297, "synset": "moldboard_plow.n.01", "name": "moldboard_plow"}, - {"id": 9298, "synset": "moleskin.n.01", "name": "moleskin"}, - {"id": 9299, "synset": "molotov_cocktail.n.01", "name": "Molotov_cocktail"}, - {"id": 9300, "synset": "monastery.n.01", "name": "monastery"}, - {"id": 9301, "synset": "monastic_habit.n.01", "name": "monastic_habit"}, - {"id": 9302, "synset": "moneybag.n.01", "name": "moneybag"}, - {"id": 9303, "synset": "money_belt.n.01", "name": "money_belt"}, - {"id": 9304, "synset": "monitor.n.06", "name": "monitor"}, - {"id": 9305, "synset": "monitor.n.05", "name": "monitor"}, - {"id": 9306, "synset": "monkey-wrench.n.01", "name": "monkey-wrench"}, - {"id": 9307, "synset": "monk's_cloth.n.01", "name": "monk's_cloth"}, - {"id": 9308, "synset": "monochrome.n.01", "name": "monochrome"}, - {"id": 9309, "synset": "monocle.n.01", "name": "monocle"}, - {"id": 9310, "synset": "monofocal_lens_implant.n.01", "name": "monofocal_lens_implant"}, - {"id": 9311, "synset": "monoplane.n.01", "name": "monoplane"}, - {"id": 9312, "synset": "monotype.n.02", "name": "monotype"}, - {"id": 9313, "synset": "monstrance.n.02", "name": "monstrance"}, - {"id": 9314, "synset": "mooring_tower.n.01", "name": "mooring_tower"}, - {"id": 9315, "synset": "moorish_arch.n.01", "name": "Moorish_arch"}, - {"id": 9316, "synset": "moped.n.01", "name": "moped"}, - {"id": 9317, "synset": "mop_handle.n.01", "name": "mop_handle"}, - {"id": 9318, "synset": "moquette.n.01", "name": "moquette"}, - {"id": 9319, "synset": "morgue.n.01", "name": "morgue"}, - {"id": 9320, "synset": "morion.n.01", "name": "morion"}, - {"id": 9321, "synset": "morning_dress.n.02", "name": "morning_dress"}, - {"id": 9322, "synset": "morning_dress.n.01", "name": "morning_dress"}, - {"id": 9323, "synset": "morning_room.n.01", "name": "morning_room"}, - {"id": 9324, "synset": "morris_chair.n.01", "name": "Morris_chair"}, - {"id": 9325, "synset": "mortar.n.01", "name": "mortar"}, - {"id": 9326, "synset": "mortar.n.03", "name": "mortar"}, - {"id": 9327, "synset": "mortarboard.n.02", "name": "mortarboard"}, - {"id": 9328, "synset": "mortise_joint.n.02", "name": "mortise_joint"}, - {"id": 9329, "synset": "mosaic.n.05", "name": "mosaic"}, - {"id": 9330, "synset": "mosque.n.01", "name": "mosque"}, - {"id": 9331, "synset": "mosquito_net.n.01", "name": "mosquito_net"}, - {"id": 9332, "synset": "motel.n.01", "name": "motel"}, - {"id": 9333, "synset": "motel_room.n.01", "name": "motel_room"}, - {"id": 9334, "synset": "mother_hubbard.n.01", "name": "Mother_Hubbard"}, - {"id": 9335, "synset": "motion-picture_camera.n.01", "name": "motion-picture_camera"}, - {"id": 9336, "synset": "motion-picture_film.n.01", "name": "motion-picture_film"}, - {"id": 9337, "synset": "motley.n.03", "name": "motley"}, - {"id": 9338, "synset": "motley.n.02", "name": "motley"}, - {"id": 9339, "synset": "motorboat.n.01", "name": "motorboat"}, - {"id": 9340, "synset": "motor_hotel.n.01", "name": "motor_hotel"}, - {"id": 9341, "synset": "motorized_wheelchair.n.01", "name": "motorized_wheelchair"}, - {"id": 9342, "synset": "mound.n.04", "name": "mound"}, - {"id": 9343, "synset": "mount.n.04", "name": "mount"}, - {"id": 9344, "synset": "mountain_bike.n.01", "name": "mountain_bike"}, - {"id": 9345, "synset": "mountain_tent.n.01", "name": "mountain_tent"}, - {"id": 9346, "synset": "mouse_button.n.01", "name": "mouse_button"}, - {"id": 9347, "synset": "mousetrap.n.01", "name": "mousetrap"}, - {"id": 9348, "synset": "mousse.n.03", "name": "mousse"}, - {"id": 9349, "synset": "mouthpiece.n.06", "name": "mouthpiece"}, - {"id": 9350, "synset": "mouthpiece.n.02", "name": "mouthpiece"}, - {"id": 9351, "synset": "mouthpiece.n.04", "name": "mouthpiece"}, - {"id": 9352, "synset": "movement.n.10", "name": "movement"}, - {"id": 9353, "synset": "movie_projector.n.01", "name": "movie_projector"}, - {"id": 9354, "synset": "moving-coil_galvanometer.n.01", "name": "moving-coil_galvanometer"}, - {"id": 9355, "synset": "moving_van.n.01", "name": "moving_van"}, - {"id": 9356, "synset": "mud_brick.n.01", "name": "mud_brick"}, - {"id": 9357, "synset": "mudguard.n.01", "name": "mudguard"}, - {"id": 9358, "synset": "mudhif.n.01", "name": "mudhif"}, - {"id": 9359, "synset": "muff.n.01", "name": "muff"}, - {"id": 9360, "synset": "muffle.n.01", "name": "muffle"}, - {"id": 9361, "synset": "muffler.n.02", "name": "muffler"}, - {"id": 9362, "synset": "mufti.n.02", "name": "mufti"}, - {"id": 9363, "synset": "mulch.n.01", "name": "mulch"}, - {"id": 9364, "synset": "mule.n.02", "name": "mule"}, - {"id": 9365, "synset": "multichannel_recorder.n.01", "name": "multichannel_recorder"}, - {"id": 9366, "synset": "multiengine_airplane.n.01", "name": "multiengine_airplane"}, - {"id": 9367, "synset": "multiplex.n.02", "name": "multiplex"}, - {"id": 9368, "synset": "multiplexer.n.01", "name": "multiplexer"}, - {"id": 9369, "synset": "multiprocessor.n.01", "name": "multiprocessor"}, - {"id": 9370, "synset": "multistage_rocket.n.01", "name": "multistage_rocket"}, - {"id": 9371, "synset": "munition.n.02", "name": "munition"}, - {"id": 9372, "synset": "murphy_bed.n.01", "name": "Murphy_bed"}, - {"id": 9373, "synset": "musette.n.01", "name": "musette"}, - {"id": 9374, "synset": "musette_pipe.n.01", "name": "musette_pipe"}, - {"id": 9375, "synset": "museum.n.01", "name": "museum"}, - {"id": 9376, "synset": "mushroom_anchor.n.01", "name": "mushroom_anchor"}, - {"id": 9377, "synset": "music_box.n.01", "name": "music_box"}, - {"id": 9378, "synset": "music_hall.n.01", "name": "music_hall"}, - {"id": 9379, "synset": "music_school.n.02", "name": "music_school"}, - {"id": 9380, "synset": "music_stand.n.01", "name": "music_stand"}, - {"id": 9381, "synset": "musket.n.01", "name": "musket"}, - {"id": 9382, "synset": "musket_ball.n.01", "name": "musket_ball"}, - {"id": 9383, "synset": "muslin.n.01", "name": "muslin"}, - {"id": 9384, "synset": "mustache_cup.n.01", "name": "mustache_cup"}, - {"id": 9385, "synset": "mustard_plaster.n.01", "name": "mustard_plaster"}, - {"id": 9386, "synset": "mute.n.02", "name": "mute"}, - {"id": 9387, "synset": "muzzle_loader.n.01", "name": "muzzle_loader"}, - {"id": 9388, "synset": "muzzle.n.03", "name": "muzzle"}, - {"id": 9389, "synset": "myelogram.n.01", "name": "myelogram"}, - {"id": 9390, "synset": "nacelle.n.01", "name": "nacelle"}, - {"id": 9391, "synset": "nail.n.02", "name": "nail"}, - {"id": 9392, "synset": "nailbrush.n.01", "name": "nailbrush"}, - {"id": 9393, "synset": "nailhead.n.02", "name": "nailhead"}, - {"id": 9394, "synset": "nailhead.n.01", "name": "nailhead"}, - {"id": 9395, "synset": "nail_polish.n.01", "name": "nail_polish"}, - {"id": 9396, "synset": "nainsook.n.01", "name": "nainsook"}, - {"id": 9397, "synset": "napier's_bones.n.01", "name": "Napier's_bones"}, - {"id": 9398, "synset": "nard.n.01", "name": "nard"}, - {"id": 9399, "synset": "narrowbody_aircraft.n.01", "name": "narrowbody_aircraft"}, - {"id": 9400, "synset": "narrow_wale.n.01", "name": "narrow_wale"}, - {"id": 9401, "synset": "narthex.n.02", "name": "narthex"}, - {"id": 9402, "synset": "narthex.n.01", "name": "narthex"}, - {"id": 9403, "synset": "nasotracheal_tube.n.01", "name": "nasotracheal_tube"}, - {"id": 9404, "synset": "national_monument.n.01", "name": "national_monument"}, - {"id": 9405, "synset": "nautilus.n.01", "name": "nautilus"}, - {"id": 9406, "synset": "navigational_system.n.01", "name": "navigational_system"}, - {"id": 9407, "synset": "naval_equipment.n.01", "name": "naval_equipment"}, - {"id": 9408, "synset": "naval_gun.n.01", "name": "naval_gun"}, - {"id": 9409, "synset": "naval_missile.n.01", "name": "naval_missile"}, - {"id": 9410, "synset": "naval_radar.n.01", "name": "naval_radar"}, - {"id": 9411, "synset": "naval_tactical_data_system.n.01", "name": "naval_tactical_data_system"}, - {"id": 9412, "synset": "naval_weaponry.n.01", "name": "naval_weaponry"}, - {"id": 9413, "synset": "nave.n.01", "name": "nave"}, - {"id": 9414, "synset": "navigational_instrument.n.01", "name": "navigational_instrument"}, - {"id": 9415, "synset": "nebuchadnezzar.n.02", "name": "nebuchadnezzar"}, - {"id": 9416, "synset": "neckband.n.01", "name": "neckband"}, - {"id": 9417, "synset": "neck_brace.n.01", "name": "neck_brace"}, - {"id": 9418, "synset": "neckcloth.n.01", "name": "neckcloth"}, - {"id": 9419, "synset": "necklet.n.01", "name": "necklet"}, - {"id": 9420, "synset": "neckline.n.01", "name": "neckline"}, - {"id": 9421, "synset": "neckpiece.n.01", "name": "neckpiece"}, - {"id": 9422, "synset": "neckwear.n.01", "name": "neckwear"}, - {"id": 9423, "synset": "needle.n.02", "name": "needle"}, - {"id": 9424, "synset": "needlenose_pliers.n.01", "name": "needlenose_pliers"}, - {"id": 9425, "synset": "needlework.n.01", "name": "needlework"}, - {"id": 9426, "synset": "negative.n.02", "name": "negative"}, - {"id": 9427, "synset": "negative_magnetic_pole.n.01", "name": "negative_magnetic_pole"}, - {"id": 9428, "synset": "negative_pole.n.01", "name": "negative_pole"}, - {"id": 9429, "synset": "negligee.n.01", "name": "negligee"}, - {"id": 9430, "synset": "neolith.n.01", "name": "neolith"}, - {"id": 9431, "synset": "neon_lamp.n.01", "name": "neon_lamp"}, - {"id": 9432, "synset": "nephoscope.n.01", "name": "nephoscope"}, - {"id": 9433, "synset": "nest.n.05", "name": "nest"}, - {"id": 9434, "synset": "nest_egg.n.02", "name": "nest_egg"}, - {"id": 9435, "synset": "net.n.06", "name": "net"}, - {"id": 9436, "synset": "net.n.02", "name": "net"}, - {"id": 9437, "synset": "net.n.05", "name": "net"}, - {"id": 9438, "synset": "net.n.04", "name": "net"}, - {"id": 9439, "synset": "network.n.05", "name": "network"}, - {"id": 9440, "synset": "network.n.04", "name": "network"}, - {"id": 9441, "synset": "neutron_bomb.n.01", "name": "neutron_bomb"}, - {"id": 9442, "synset": "newel.n.02", "name": "newel"}, - {"id": 9443, "synset": "newel_post.n.01", "name": "newel_post"}, - {"id": 9444, "synset": "newspaper.n.03", "name": "newspaper"}, - {"id": 9445, "synset": "newsroom.n.03", "name": "newsroom"}, - {"id": 9446, "synset": "newsroom.n.02", "name": "newsroom"}, - {"id": 9447, "synset": "newtonian_telescope.n.01", "name": "Newtonian_telescope"}, - {"id": 9448, "synset": "nib.n.01", "name": "nib"}, - {"id": 9449, "synset": "niblick.n.01", "name": "niblick"}, - {"id": 9450, "synset": "nicad.n.01", "name": "nicad"}, - {"id": 9451, "synset": "nickel-iron_battery.n.01", "name": "nickel-iron_battery"}, - {"id": 9452, "synset": "nicol_prism.n.01", "name": "Nicol_prism"}, - {"id": 9453, "synset": "night_bell.n.01", "name": "night_bell"}, - {"id": 9454, "synset": "nightcap.n.02", "name": "nightcap"}, - {"id": 9455, "synset": "nightgown.n.01", "name": "nightgown"}, - {"id": 9456, "synset": "night_latch.n.01", "name": "night_latch"}, - {"id": 9457, "synset": "night-light.n.01", "name": "night-light"}, - {"id": 9458, "synset": "nightshirt.n.01", "name": "nightshirt"}, - {"id": 9459, "synset": "ninepin.n.01", "name": "ninepin"}, - {"id": 9460, "synset": "ninepin_ball.n.01", "name": "ninepin_ball"}, - {"id": 9461, "synset": "ninon.n.01", "name": "ninon"}, - {"id": 9462, "synset": "nipple.n.02", "name": "nipple"}, - {"id": 9463, "synset": "nipple_shield.n.01", "name": "nipple_shield"}, - {"id": 9464, "synset": "niqab.n.01", "name": "niqab"}, - {"id": 9465, "synset": "nissen_hut.n.01", "name": "Nissen_hut"}, - {"id": 9466, "synset": "nogging.n.01", "name": "nogging"}, - {"id": 9467, "synset": "noisemaker.n.01", "name": "noisemaker"}, - {"id": 9468, "synset": "nonsmoker.n.02", "name": "nonsmoker"}, - {"id": 9469, "synset": "non-volatile_storage.n.01", "name": "non-volatile_storage"}, - {"id": 9470, "synset": "norfolk_jacket.n.01", "name": "Norfolk_jacket"}, - {"id": 9471, "synset": "noria.n.01", "name": "noria"}, - {"id": 9472, "synset": "nose_flute.n.01", "name": "nose_flute"}, - {"id": 9473, "synset": "nosewheel.n.01", "name": "nosewheel"}, - {"id": 9474, "synset": "notebook.n.02", "name": "notebook"}, - {"id": 9475, "synset": "nuclear-powered_ship.n.01", "name": "nuclear-powered_ship"}, - {"id": 9476, "synset": "nuclear_reactor.n.01", "name": "nuclear_reactor"}, - {"id": 9477, "synset": "nuclear_rocket.n.01", "name": "nuclear_rocket"}, - {"id": 9478, "synset": "nuclear_weapon.n.01", "name": "nuclear_weapon"}, - {"id": 9479, "synset": "nude.n.01", "name": "nude"}, - {"id": 9480, "synset": "numdah.n.01", "name": "numdah"}, - {"id": 9481, "synset": "nun's_habit.n.01", "name": "nun's_habit"}, - {"id": 9482, "synset": "nursery.n.01", "name": "nursery"}, - {"id": 9483, "synset": "nut_and_bolt.n.01", "name": "nut_and_bolt"}, - {"id": 9484, "synset": "nylon.n.02", "name": "nylon"}, - {"id": 9485, "synset": "nylons.n.01", "name": "nylons"}, - {"id": 9486, "synset": "oast.n.01", "name": "oast"}, - {"id": 9487, "synset": "oast_house.n.01", "name": "oast_house"}, - {"id": 9488, "synset": "obelisk.n.01", "name": "obelisk"}, - {"id": 9489, "synset": "object_ball.n.01", "name": "object_ball"}, - {"id": 9490, "synset": "objective.n.02", "name": "objective"}, - {"id": 9491, "synset": "oblique_bandage.n.01", "name": "oblique_bandage"}, - {"id": 9492, "synset": "oboe.n.01", "name": "oboe"}, - {"id": 9493, "synset": "oboe_da_caccia.n.01", "name": "oboe_da_caccia"}, - {"id": 9494, "synset": "oboe_d'amore.n.01", "name": "oboe_d'amore"}, - {"id": 9495, "synset": "observation_dome.n.01", "name": "observation_dome"}, - {"id": 9496, "synset": "observatory.n.01", "name": "observatory"}, - {"id": 9497, "synset": "obstacle.n.02", "name": "obstacle"}, - {"id": 9498, "synset": "obturator.n.01", "name": "obturator"}, - {"id": 9499, "synset": "ocarina.n.01", "name": "ocarina"}, - {"id": 9500, "synset": "octant.n.01", "name": "octant"}, - {"id": 9501, "synset": "odd-leg_caliper.n.01", "name": "odd-leg_caliper"}, - {"id": 9502, "synset": "odometer.n.01", "name": "odometer"}, - {"id": 9503, "synset": "oeil_de_boeuf.n.01", "name": "oeil_de_boeuf"}, - {"id": 9504, "synset": "office.n.01", "name": "office"}, - {"id": 9505, "synset": "office_building.n.01", "name": "office_building"}, - {"id": 9506, "synset": "office_furniture.n.01", "name": "office_furniture"}, - {"id": 9507, "synset": "officer's_mess.n.01", "name": "officer's_mess"}, - {"id": 9508, "synset": "off-line_equipment.n.01", "name": "off-line_equipment"}, - {"id": 9509, "synset": "ogee.n.01", "name": "ogee"}, - {"id": 9510, "synset": "ogee_arch.n.01", "name": "ogee_arch"}, - {"id": 9511, "synset": "ohmmeter.n.01", "name": "ohmmeter"}, - {"id": 9512, "synset": "oil.n.02", "name": "oil"}, - {"id": 9513, "synset": "oilcan.n.01", "name": "oilcan"}, - {"id": 9514, "synset": "oilcloth.n.01", "name": "oilcloth"}, - {"id": 9515, "synset": "oil_filter.n.01", "name": "oil_filter"}, - {"id": 9516, "synset": "oil_heater.n.01", "name": "oil_heater"}, - {"id": 9517, "synset": "oil_paint.n.01", "name": "oil_paint"}, - {"id": 9518, "synset": "oil_pump.n.01", "name": "oil_pump"}, - {"id": 9519, "synset": "oil_refinery.n.01", "name": "oil_refinery"}, - {"id": 9520, "synset": "oilskin.n.01", "name": "oilskin"}, - {"id": 9521, "synset": "oil_slick.n.01", "name": "oil_slick"}, - {"id": 9522, "synset": "oilstone.n.01", "name": "oilstone"}, - {"id": 9523, "synset": "oil_tanker.n.01", "name": "oil_tanker"}, - {"id": 9524, "synset": "old_school_tie.n.01", "name": "old_school_tie"}, - {"id": 9525, "synset": "olive_drab.n.03", "name": "olive_drab"}, - {"id": 9526, "synset": "olive_drab.n.02", "name": "olive_drab"}, - {"id": 9527, "synset": "olympian_zeus.n.01", "name": "Olympian_Zeus"}, - {"id": 9528, "synset": "omelet_pan.n.01", "name": "omelet_pan"}, - {"id": 9529, "synset": "omnidirectional_antenna.n.01", "name": "omnidirectional_antenna"}, - {"id": 9530, "synset": "omnirange.n.01", "name": "omnirange"}, - {"id": 9531, "synset": "onion_dome.n.01", "name": "onion_dome"}, - {"id": 9532, "synset": "open-air_market.n.01", "name": "open-air_market"}, - {"id": 9533, "synset": "open_circuit.n.01", "name": "open_circuit"}, - {"id": 9534, "synset": "open-end_wrench.n.01", "name": "open-end_wrench"}, - {"id": 9535, "synset": "opener.n.03", "name": "opener"}, - {"id": 9536, "synset": "open-hearth_furnace.n.01", "name": "open-hearth_furnace"}, - {"id": 9537, "synset": "openside_plane.n.01", "name": "openside_plane"}, - {"id": 9538, "synset": "open_sight.n.01", "name": "open_sight"}, - {"id": 9539, "synset": "openwork.n.01", "name": "openwork"}, - {"id": 9540, "synset": "opera.n.03", "name": "opera"}, - {"id": 9541, "synset": "opera_cloak.n.01", "name": "opera_cloak"}, - {"id": 9542, "synset": "operating_microscope.n.01", "name": "operating_microscope"}, - {"id": 9543, "synset": "operating_room.n.01", "name": "operating_room"}, - {"id": 9544, "synset": "operating_table.n.01", "name": "operating_table"}, - {"id": 9545, "synset": "ophthalmoscope.n.01", "name": "ophthalmoscope"}, - {"id": 9546, "synset": "optical_device.n.01", "name": "optical_device"}, - {"id": 9547, "synset": "optical_disk.n.01", "name": "optical_disk"}, - {"id": 9548, "synset": "optical_instrument.n.01", "name": "optical_instrument"}, - {"id": 9549, "synset": "optical_pyrometer.n.01", "name": "optical_pyrometer"}, - {"id": 9550, "synset": "optical_telescope.n.01", "name": "optical_telescope"}, - {"id": 9551, "synset": "orchestra_pit.n.01", "name": "orchestra_pit"}, - {"id": 9552, "synset": "ordinary.n.04", "name": "ordinary"}, - {"id": 9553, "synset": "organ.n.05", "name": "organ"}, - {"id": 9554, "synset": "organdy.n.01", "name": "organdy"}, - { - "id": 9555, - "synset": "organic_light-emitting_diode.n.01", - "name": "organic_light-emitting_diode", - }, - {"id": 9556, "synset": "organ_loft.n.01", "name": "organ_loft"}, - {"id": 9557, "synset": "organ_pipe.n.01", "name": "organ_pipe"}, - {"id": 9558, "synset": "organza.n.01", "name": "organza"}, - {"id": 9559, "synset": "oriel.n.01", "name": "oriel"}, - {"id": 9560, "synset": "oriflamme.n.02", "name": "oriflamme"}, - {"id": 9561, "synset": "o_ring.n.01", "name": "O_ring"}, - {"id": 9562, "synset": "orlon.n.01", "name": "Orlon"}, - {"id": 9563, "synset": "orlop_deck.n.01", "name": "orlop_deck"}, - {"id": 9564, "synset": "orphanage.n.02", "name": "orphanage"}, - {"id": 9565, "synset": "orphrey.n.01", "name": "orphrey"}, - {"id": 9566, "synset": "orrery.n.01", "name": "orrery"}, - {"id": 9567, "synset": "orthicon.n.01", "name": "orthicon"}, - {"id": 9568, "synset": "orthochromatic_film.n.01", "name": "orthochromatic_film"}, - {"id": 9569, "synset": "orthopter.n.01", "name": "orthopter"}, - {"id": 9570, "synset": "orthoscope.n.01", "name": "orthoscope"}, - {"id": 9571, "synset": "oscillograph.n.01", "name": "oscillograph"}, - {"id": 9572, "synset": "oscilloscope.n.01", "name": "oscilloscope"}, - {"id": 9573, "synset": "ossuary.n.01", "name": "ossuary"}, - {"id": 9574, "synset": "otoscope.n.01", "name": "otoscope"}, - {"id": 9575, "synset": "oubliette.n.01", "name": "oubliette"}, - {"id": 9576, "synset": "out-basket.n.01", "name": "out-basket"}, - {"id": 9577, "synset": "outboard_motor.n.01", "name": "outboard_motor"}, - {"id": 9578, "synset": "outboard_motorboat.n.01", "name": "outboard_motorboat"}, - {"id": 9579, "synset": "outbuilding.n.01", "name": "outbuilding"}, - {"id": 9580, "synset": "outerwear.n.01", "name": "outerwear"}, - {"id": 9581, "synset": "outfall.n.01", "name": "outfall"}, - {"id": 9582, "synset": "outfit.n.02", "name": "outfit"}, - {"id": 9583, "synset": "outfitter.n.02", "name": "outfitter"}, - {"id": 9584, "synset": "outhouse.n.01", "name": "outhouse"}, - {"id": 9585, "synset": "output_device.n.01", "name": "output_device"}, - {"id": 9586, "synset": "outrigger.n.01", "name": "outrigger"}, - {"id": 9587, "synset": "outrigger_canoe.n.01", "name": "outrigger_canoe"}, - {"id": 9588, "synset": "outside_caliper.n.01", "name": "outside_caliper"}, - {"id": 9589, "synset": "outside_mirror.n.01", "name": "outside_mirror"}, - {"id": 9590, "synset": "outwork.n.01", "name": "outwork"}, - {"id": 9591, "synset": "oven_thermometer.n.01", "name": "oven_thermometer"}, - {"id": 9592, "synset": "overall.n.02", "name": "overall"}, - {"id": 9593, "synset": "overcoat.n.02", "name": "overcoat"}, - {"id": 9594, "synset": "overdrive.n.02", "name": "overdrive"}, - {"id": 9595, "synset": "overgarment.n.01", "name": "overgarment"}, - {"id": 9596, "synset": "overhand_knot.n.01", "name": "overhand_knot"}, - {"id": 9597, "synset": "overhang.n.01", "name": "overhang"}, - {"id": 9598, "synset": "overhead_projector.n.01", "name": "overhead_projector"}, - {"id": 9599, "synset": "overmantel.n.01", "name": "overmantel"}, - {"id": 9600, "synset": "overnighter.n.02", "name": "overnighter"}, - {"id": 9601, "synset": "overpass.n.01", "name": "overpass"}, - {"id": 9602, "synset": "override.n.01", "name": "override"}, - {"id": 9603, "synset": "overshoe.n.01", "name": "overshoe"}, - {"id": 9604, "synset": "overskirt.n.01", "name": "overskirt"}, - {"id": 9605, "synset": "oxbow.n.03", "name": "oxbow"}, - {"id": 9606, "synset": "oxbridge.n.01", "name": "Oxbridge"}, - {"id": 9607, "synset": "oxcart.n.01", "name": "oxcart"}, - {"id": 9608, "synset": "oxeye.n.03", "name": "oxeye"}, - {"id": 9609, "synset": "oxford.n.04", "name": "oxford"}, - {"id": 9610, "synset": "oximeter.n.01", "name": "oximeter"}, - {"id": 9611, "synset": "oxyacetylene_torch.n.01", "name": "oxyacetylene_torch"}, - {"id": 9612, "synset": "oxygen_mask.n.01", "name": "oxygen_mask"}, - {"id": 9613, "synset": "oyster_bar.n.01", "name": "oyster_bar"}, - {"id": 9614, "synset": "oyster_bed.n.01", "name": "oyster_bed"}, - {"id": 9615, "synset": "pace_car.n.01", "name": "pace_car"}, - {"id": 9616, "synset": "pacemaker.n.03", "name": "pacemaker"}, - {"id": 9617, "synset": "pack.n.03", "name": "pack"}, - {"id": 9618, "synset": "pack.n.09", "name": "pack"}, - {"id": 9619, "synset": "pack.n.07", "name": "pack"}, - {"id": 9620, "synset": "package.n.02", "name": "package"}, - {"id": 9621, "synset": "package_store.n.01", "name": "package_store"}, - {"id": 9622, "synset": "packaging.n.03", "name": "packaging"}, - {"id": 9623, "synset": "packing_box.n.02", "name": "packing_box"}, - {"id": 9624, "synset": "packinghouse.n.02", "name": "packinghouse"}, - {"id": 9625, "synset": "packinghouse.n.01", "name": "packinghouse"}, - {"id": 9626, "synset": "packing_needle.n.01", "name": "packing_needle"}, - {"id": 9627, "synset": "packsaddle.n.01", "name": "packsaddle"}, - {"id": 9628, "synset": "paddle.n.02", "name": "paddle"}, - {"id": 9629, "synset": "paddle.n.01", "name": "paddle"}, - {"id": 9630, "synset": "paddle_box.n.01", "name": "paddle_box"}, - {"id": 9631, "synset": "paddle_steamer.n.01", "name": "paddle_steamer"}, - {"id": 9632, "synset": "paddlewheel.n.01", "name": "paddlewheel"}, - {"id": 9633, "synset": "paddock.n.01", "name": "paddock"}, - {"id": 9634, "synset": "page_printer.n.01", "name": "page_printer"}, - {"id": 9635, "synset": "paint.n.01", "name": "paint"}, - {"id": 9636, "synset": "paintball.n.01", "name": "paintball"}, - {"id": 9637, "synset": "paintball_gun.n.01", "name": "paintball_gun"}, - {"id": 9638, "synset": "paintbox.n.01", "name": "paintbox"}, - {"id": 9639, "synset": "paisley.n.01", "name": "paisley"}, - {"id": 9640, "synset": "pajama.n.01", "name": "pajama"}, - {"id": 9641, "synset": "palace.n.04", "name": "palace"}, - {"id": 9642, "synset": "palace.n.01", "name": "palace"}, - {"id": 9643, "synset": "palace.n.03", "name": "palace"}, - {"id": 9644, "synset": "palanquin.n.01", "name": "palanquin"}, - {"id": 9645, "synset": "paleolith.n.01", "name": "paleolith"}, - {"id": 9646, "synset": "palestra.n.01", "name": "palestra"}, - {"id": 9647, "synset": "palette_knife.n.01", "name": "palette_knife"}, - {"id": 9648, "synset": "palisade.n.01", "name": "palisade"}, - {"id": 9649, "synset": "pallet.n.03", "name": "pallet"}, - {"id": 9650, "synset": "pallette.n.01", "name": "pallette"}, - {"id": 9651, "synset": "pallium.n.04", "name": "pallium"}, - {"id": 9652, "synset": "pallium.n.03", "name": "pallium"}, - {"id": 9653, "synset": "pancake_turner.n.01", "name": "pancake_turner"}, - {"id": 9654, "synset": "panchromatic_film.n.01", "name": "panchromatic_film"}, - {"id": 9655, "synset": "panda_car.n.01", "name": "panda_car"}, - {"id": 9656, "synset": "paneling.n.01", "name": "paneling"}, - {"id": 9657, "synset": "panhandle.n.02", "name": "panhandle"}, - {"id": 9658, "synset": "panic_button.n.01", "name": "panic_button"}, - {"id": 9659, "synset": "pannier.n.02", "name": "pannier"}, - {"id": 9660, "synset": "pannier.n.01", "name": "pannier"}, - {"id": 9661, "synset": "pannikin.n.01", "name": "pannikin"}, - {"id": 9662, "synset": "panopticon.n.02", "name": "panopticon"}, - {"id": 9663, "synset": "panopticon.n.01", "name": "panopticon"}, - {"id": 9664, "synset": "panpipe.n.01", "name": "panpipe"}, - {"id": 9665, "synset": "pantaloon.n.03", "name": "pantaloon"}, - {"id": 9666, "synset": "pantechnicon.n.01", "name": "pantechnicon"}, - {"id": 9667, "synset": "pantheon.n.03", "name": "pantheon"}, - {"id": 9668, "synset": "pantheon.n.02", "name": "pantheon"}, - {"id": 9669, "synset": "pantie.n.01", "name": "pantie"}, - {"id": 9670, "synset": "panting.n.02", "name": "panting"}, - {"id": 9671, "synset": "pant_leg.n.01", "name": "pant_leg"}, - {"id": 9672, "synset": "pantograph.n.01", "name": "pantograph"}, - {"id": 9673, "synset": "pantry.n.01", "name": "pantry"}, - {"id": 9674, "synset": "pants_suit.n.01", "name": "pants_suit"}, - {"id": 9675, "synset": "panty_girdle.n.01", "name": "panty_girdle"}, - {"id": 9676, "synset": "panzer.n.01", "name": "panzer"}, - {"id": 9677, "synset": "paper_chain.n.01", "name": "paper_chain"}, - {"id": 9678, "synset": "paper_clip.n.01", "name": "paper_clip"}, - {"id": 9679, "synset": "paper_cutter.n.01", "name": "paper_cutter"}, - {"id": 9680, "synset": "paper_fastener.n.01", "name": "paper_fastener"}, - {"id": 9681, "synset": "paper_feed.n.01", "name": "paper_feed"}, - {"id": 9682, "synset": "paper_mill.n.01", "name": "paper_mill"}, - {"id": 9683, "synset": "parabolic_mirror.n.01", "name": "parabolic_mirror"}, - {"id": 9684, "synset": "parabolic_reflector.n.01", "name": "parabolic_reflector"}, - {"id": 9685, "synset": "parallel_bars.n.01", "name": "parallel_bars"}, - {"id": 9686, "synset": "parallel_circuit.n.01", "name": "parallel_circuit"}, - {"id": 9687, "synset": "parallel_interface.n.01", "name": "parallel_interface"}, - {"id": 9688, "synset": "parang.n.01", "name": "parang"}, - {"id": 9689, "synset": "parapet.n.02", "name": "parapet"}, - {"id": 9690, "synset": "parapet.n.01", "name": "parapet"}, - {"id": 9691, "synset": "parer.n.02", "name": "parer"}, - {"id": 9692, "synset": "parfait_glass.n.01", "name": "parfait_glass"}, - {"id": 9693, "synset": "pargeting.n.02", "name": "pargeting"}, - {"id": 9694, "synset": "pari-mutuel_machine.n.01", "name": "pari-mutuel_machine"}, - {"id": 9695, "synset": "park_bench.n.01", "name": "park_bench"}, - {"id": 9696, "synset": "parlor.n.01", "name": "parlor"}, - {"id": 9697, "synset": "parquet.n.01", "name": "parquet"}, - {"id": 9698, "synset": "parquetry.n.01", "name": "parquetry"}, - {"id": 9699, "synset": "parsonage.n.01", "name": "parsonage"}, - {"id": 9700, "synset": "parsons_table.n.01", "name": "Parsons_table"}, - {"id": 9701, "synset": "partial_denture.n.01", "name": "partial_denture"}, - {"id": 9702, "synset": "particle_detector.n.01", "name": "particle_detector"}, - {"id": 9703, "synset": "partition.n.01", "name": "partition"}, - {"id": 9704, "synset": "parts_bin.n.01", "name": "parts_bin"}, - {"id": 9705, "synset": "party_line.n.02", "name": "party_line"}, - {"id": 9706, "synset": "party_wall.n.01", "name": "party_wall"}, - {"id": 9707, "synset": "parvis.n.01", "name": "parvis"}, - {"id": 9708, "synset": "passenger_train.n.01", "name": "passenger_train"}, - {"id": 9709, "synset": "passenger_van.n.01", "name": "passenger_van"}, - {"id": 9710, "synset": "passe-partout.n.02", "name": "passe-partout"}, - {"id": 9711, "synset": "passive_matrix_display.n.01", "name": "passive_matrix_display"}, - {"id": 9712, "synset": "passkey.n.01", "name": "passkey"}, - {"id": 9713, "synset": "pass-through.n.01", "name": "pass-through"}, - {"id": 9714, "synset": "pastry_cart.n.01", "name": "pastry_cart"}, - {"id": 9715, "synset": "patch.n.03", "name": "patch"}, - {"id": 9716, "synset": "patchcord.n.01", "name": "patchcord"}, - {"id": 9717, "synset": "patchouli.n.02", "name": "patchouli"}, - {"id": 9718, "synset": "patch_pocket.n.01", "name": "patch_pocket"}, - {"id": 9719, "synset": "patchwork.n.02", "name": "patchwork"}, - {"id": 9720, "synset": "patent_log.n.01", "name": "patent_log"}, - {"id": 9721, "synset": "paternoster.n.02", "name": "paternoster"}, - {"id": 9722, "synset": "patina.n.01", "name": "patina"}, - {"id": 9723, "synset": "patio.n.01", "name": "patio"}, - {"id": 9724, "synset": "patisserie.n.01", "name": "patisserie"}, - {"id": 9725, "synset": "patka.n.01", "name": "patka"}, - {"id": 9726, "synset": "patrol_boat.n.01", "name": "patrol_boat"}, - {"id": 9727, "synset": "patty-pan.n.01", "name": "patty-pan"}, - {"id": 9728, "synset": "pave.n.01", "name": "pave"}, - {"id": 9729, "synset": "pavilion.n.01", "name": "pavilion"}, - {"id": 9730, "synset": "pavior.n.01", "name": "pavior"}, - {"id": 9731, "synset": "pavis.n.01", "name": "pavis"}, - {"id": 9732, "synset": "pawn.n.03", "name": "pawn"}, - {"id": 9733, "synset": "pawnbroker's_shop.n.01", "name": "pawnbroker's_shop"}, - {"id": 9734, "synset": "pay-phone.n.01", "name": "pay-phone"}, - {"id": 9735, "synset": "pc_board.n.01", "name": "PC_board"}, - {"id": 9736, "synset": "peach_orchard.n.01", "name": "peach_orchard"}, - {"id": 9737, "synset": "pea_jacket.n.01", "name": "pea_jacket"}, - {"id": 9738, "synset": "peavey.n.01", "name": "peavey"}, - {"id": 9739, "synset": "pectoral.n.02", "name": "pectoral"}, - {"id": 9740, "synset": "pedal.n.02", "name": "pedal"}, - {"id": 9741, "synset": "pedal_pusher.n.01", "name": "pedal_pusher"}, - {"id": 9742, "synset": "pedestal.n.03", "name": "pedestal"}, - {"id": 9743, "synset": "pedestal_table.n.01", "name": "pedestal_table"}, - {"id": 9744, "synset": "pedestrian_crossing.n.01", "name": "pedestrian_crossing"}, - {"id": 9745, "synset": "pedicab.n.01", "name": "pedicab"}, - {"id": 9746, "synset": "pediment.n.01", "name": "pediment"}, - {"id": 9747, "synset": "pedometer.n.01", "name": "pedometer"}, - {"id": 9748, "synset": "peep_sight.n.01", "name": "peep_sight"}, - {"id": 9749, "synset": "peg.n.01", "name": "peg"}, - {"id": 9750, "synset": "peg.n.06", "name": "peg"}, - {"id": 9751, "synset": "peg.n.05", "name": "peg"}, - {"id": 9752, "synset": "pelham.n.01", "name": "Pelham"}, - {"id": 9753, "synset": "pelican_crossing.n.01", "name": "pelican_crossing"}, - {"id": 9754, "synset": "pelisse.n.01", "name": "pelisse"}, - {"id": 9755, "synset": "pelvimeter.n.01", "name": "pelvimeter"}, - {"id": 9756, "synset": "penal_colony.n.01", "name": "penal_colony"}, - {"id": 9757, "synset": "penal_institution.n.01", "name": "penal_institution"}, - {"id": 9758, "synset": "penalty_box.n.01", "name": "penalty_box"}, - {"id": 9759, "synset": "pen-and-ink.n.01", "name": "pen-and-ink"}, - {"id": 9760, "synset": "pencil.n.04", "name": "pencil"}, - {"id": 9761, "synset": "pendant_earring.n.01", "name": "pendant_earring"}, - {"id": 9762, "synset": "pendulum_clock.n.01", "name": "pendulum_clock"}, - {"id": 9763, "synset": "pendulum_watch.n.01", "name": "pendulum_watch"}, - {"id": 9764, "synset": "penetration_bomb.n.01", "name": "penetration_bomb"}, - {"id": 9765, "synset": "penile_implant.n.01", "name": "penile_implant"}, - {"id": 9766, "synset": "penitentiary.n.01", "name": "penitentiary"}, - {"id": 9767, "synset": "penknife.n.01", "name": "penknife"}, - {"id": 9768, "synset": "penlight.n.01", "name": "penlight"}, - {"id": 9769, "synset": "pennant.n.03", "name": "pennant"}, - {"id": 9770, "synset": "pennywhistle.n.01", "name": "pennywhistle"}, - {"id": 9771, "synset": "penthouse.n.01", "name": "penthouse"}, - {"id": 9772, "synset": "pentode.n.01", "name": "pentode"}, - {"id": 9773, "synset": "peplos.n.01", "name": "peplos"}, - {"id": 9774, "synset": "peplum.n.01", "name": "peplum"}, - {"id": 9775, "synset": "pepper_shaker.n.01", "name": "pepper_shaker"}, - {"id": 9776, "synset": "pepper_spray.n.01", "name": "pepper_spray"}, - {"id": 9777, "synset": "percale.n.01", "name": "percale"}, - {"id": 9778, "synset": "percolator.n.01", "name": "percolator"}, - {"id": 9779, "synset": "percussion_cap.n.01", "name": "percussion_cap"}, - {"id": 9780, "synset": "percussion_instrument.n.01", "name": "percussion_instrument"}, - {"id": 9781, "synset": "perforation.n.01", "name": "perforation"}, - {"id": 9782, "synset": "perfumery.n.03", "name": "perfumery"}, - {"id": 9783, "synset": "perfumery.n.02", "name": "perfumery"}, - {"id": 9784, "synset": "perfumery.n.01", "name": "perfumery"}, - {"id": 9785, "synset": "peripheral.n.01", "name": "peripheral"}, - {"id": 9786, "synset": "periscope.n.01", "name": "periscope"}, - {"id": 9787, "synset": "peristyle.n.01", "name": "peristyle"}, - {"id": 9788, "synset": "periwig.n.01", "name": "periwig"}, - {"id": 9789, "synset": "permanent_press.n.01", "name": "permanent_press"}, - {"id": 9790, "synset": "perpetual_motion_machine.n.01", "name": "perpetual_motion_machine"}, - {"id": 9791, "synset": "personal_computer.n.01", "name": "personal_computer"}, - {"id": 9792, "synset": "personal_digital_assistant.n.01", "name": "personal_digital_assistant"}, - {"id": 9793, "synset": "personnel_carrier.n.01", "name": "personnel_carrier"}, - {"id": 9794, "synset": "pestle.n.03", "name": "pestle"}, - {"id": 9795, "synset": "pestle.n.02", "name": "pestle"}, - {"id": 9796, "synset": "petcock.n.01", "name": "petcock"}, - {"id": 9797, "synset": "petri_dish.n.01", "name": "Petri_dish"}, - {"id": 9798, "synset": "petrolatum_gauze.n.01", "name": "petrolatum_gauze"}, - {"id": 9799, "synset": "pet_shop.n.01", "name": "pet_shop"}, - {"id": 9800, "synset": "petticoat.n.01", "name": "petticoat"}, - {"id": 9801, "synset": "phial.n.01", "name": "phial"}, - {"id": 9802, "synset": "phillips_screw.n.01", "name": "Phillips_screw"}, - {"id": 9803, "synset": "phillips_screwdriver.n.01", "name": "Phillips_screwdriver"}, - {"id": 9804, "synset": "phonograph_needle.n.01", "name": "phonograph_needle"}, - {"id": 9805, "synset": "photocathode.n.01", "name": "photocathode"}, - {"id": 9806, "synset": "photocoagulator.n.01", "name": "photocoagulator"}, - {"id": 9807, "synset": "photocopier.n.01", "name": "photocopier"}, - {"id": 9808, "synset": "photographic_equipment.n.01", "name": "photographic_equipment"}, - {"id": 9809, "synset": "photographic_paper.n.01", "name": "photographic_paper"}, - {"id": 9810, "synset": "photometer.n.01", "name": "photometer"}, - {"id": 9811, "synset": "photomicrograph.n.01", "name": "photomicrograph"}, - {"id": 9812, "synset": "photostat.n.02", "name": "Photostat"}, - {"id": 9813, "synset": "photostat.n.01", "name": "photostat"}, - {"id": 9814, "synset": "physical_pendulum.n.01", "name": "physical_pendulum"}, - {"id": 9815, "synset": "piano_action.n.01", "name": "piano_action"}, - {"id": 9816, "synset": "piano_keyboard.n.01", "name": "piano_keyboard"}, - {"id": 9817, "synset": "piano_wire.n.01", "name": "piano_wire"}, - {"id": 9818, "synset": "piccolo.n.01", "name": "piccolo"}, - {"id": 9819, "synset": "pick.n.07", "name": "pick"}, - {"id": 9820, "synset": "pick.n.06", "name": "pick"}, - {"id": 9821, "synset": "pick.n.05", "name": "pick"}, - {"id": 9822, "synset": "pickelhaube.n.01", "name": "pickelhaube"}, - {"id": 9823, "synset": "picket_boat.n.01", "name": "picket_boat"}, - {"id": 9824, "synset": "picket_fence.n.01", "name": "picket_fence"}, - {"id": 9825, "synset": "picket_ship.n.01", "name": "picket_ship"}, - {"id": 9826, "synset": "pickle_barrel.n.01", "name": "pickle_barrel"}, - {"id": 9827, "synset": "picture_frame.n.01", "name": "picture_frame"}, - {"id": 9828, "synset": "picture_hat.n.01", "name": "picture_hat"}, - {"id": 9829, "synset": "picture_rail.n.01", "name": "picture_rail"}, - {"id": 9830, "synset": "picture_window.n.01", "name": "picture_window"}, - {"id": 9831, "synset": "piece_of_cloth.n.01", "name": "piece_of_cloth"}, - {"id": 9832, "synset": "pied-a-terre.n.01", "name": "pied-a-terre"}, - {"id": 9833, "synset": "pier.n.03", "name": "pier"}, - {"id": 9834, "synset": "pier.n.02", "name": "pier"}, - {"id": 9835, "synset": "pier_arch.n.01", "name": "pier_arch"}, - {"id": 9836, "synset": "pier_glass.n.01", "name": "pier_glass"}, - {"id": 9837, "synset": "pier_table.n.01", "name": "pier_table"}, - {"id": 9838, "synset": "pieta.n.01", "name": "pieta"}, - {"id": 9839, "synset": "piezometer.n.01", "name": "piezometer"}, - {"id": 9840, "synset": "pig_bed.n.01", "name": "pig_bed"}, - {"id": 9841, "synset": "piggery.n.01", "name": "piggery"}, - {"id": 9842, "synset": "pilaster.n.01", "name": "pilaster"}, - {"id": 9843, "synset": "pile.n.06", "name": "pile"}, - {"id": 9844, "synset": "pile_driver.n.01", "name": "pile_driver"}, - {"id": 9845, "synset": "pill_bottle.n.01", "name": "pill_bottle"}, - {"id": 9846, "synset": "pillbox.n.01", "name": "pillbox"}, - {"id": 9847, "synset": "pillion.n.01", "name": "pillion"}, - {"id": 9848, "synset": "pillory.n.01", "name": "pillory"}, - {"id": 9849, "synset": "pillow_block.n.01", "name": "pillow_block"}, - {"id": 9850, "synset": "pillow_lace.n.01", "name": "pillow_lace"}, - {"id": 9851, "synset": "pillow_sham.n.01", "name": "pillow_sham"}, - {"id": 9852, "synset": "pilot_bit.n.01", "name": "pilot_bit"}, - {"id": 9853, "synset": "pilot_boat.n.01", "name": "pilot_boat"}, - {"id": 9854, "synset": "pilot_burner.n.01", "name": "pilot_burner"}, - {"id": 9855, "synset": "pilot_cloth.n.01", "name": "pilot_cloth"}, - {"id": 9856, "synset": "pilot_engine.n.01", "name": "pilot_engine"}, - {"id": 9857, "synset": "pilothouse.n.01", "name": "pilothouse"}, - {"id": 9858, "synset": "pilot_light.n.02", "name": "pilot_light"}, - {"id": 9859, "synset": "pin.n.08", "name": "pin"}, - {"id": 9860, "synset": "pin.n.07", "name": "pin"}, - {"id": 9861, "synset": "pinata.n.01", "name": "pinata"}, - {"id": 9862, "synset": "pinball_machine.n.01", "name": "pinball_machine"}, - {"id": 9863, "synset": "pince-nez.n.01", "name": "pince-nez"}, - {"id": 9864, "synset": "pincer.n.01", "name": "pincer"}, - {"id": 9865, "synset": "pinch_bar.n.01", "name": "pinch_bar"}, - {"id": 9866, "synset": "pincurl_clip.n.01", "name": "pincurl_clip"}, - {"id": 9867, "synset": "pinfold.n.01", "name": "pinfold"}, - {"id": 9868, "synset": "pinhead.n.02", "name": "pinhead"}, - {"id": 9869, "synset": "pinion.n.01", "name": "pinion"}, - {"id": 9870, "synset": "pinnacle.n.01", "name": "pinnacle"}, - {"id": 9871, "synset": "pinprick.n.02", "name": "pinprick"}, - {"id": 9872, "synset": "pinstripe.n.03", "name": "pinstripe"}, - {"id": 9873, "synset": "pinstripe.n.02", "name": "pinstripe"}, - {"id": 9874, "synset": "pinstripe.n.01", "name": "pinstripe"}, - {"id": 9875, "synset": "pintle.n.01", "name": "pintle"}, - {"id": 9876, "synset": "pinwheel.n.02", "name": "pinwheel"}, - {"id": 9877, "synset": "tabor_pipe.n.01", "name": "tabor_pipe"}, - {"id": 9878, "synset": "pipe.n.04", "name": "pipe"}, - {"id": 9879, "synset": "pipe_bomb.n.01", "name": "pipe_bomb"}, - {"id": 9880, "synset": "pipe_cleaner.n.01", "name": "pipe_cleaner"}, - {"id": 9881, "synset": "pipe_cutter.n.01", "name": "pipe_cutter"}, - {"id": 9882, "synset": "pipefitting.n.01", "name": "pipefitting"}, - {"id": 9883, "synset": "pipet.n.01", "name": "pipet"}, - {"id": 9884, "synset": "pipe_vise.n.01", "name": "pipe_vise"}, - {"id": 9885, "synset": "pipe_wrench.n.01", "name": "pipe_wrench"}, - {"id": 9886, "synset": "pique.n.01", "name": "pique"}, - {"id": 9887, "synset": "pirate.n.03", "name": "pirate"}, - {"id": 9888, "synset": "piste.n.02", "name": "piste"}, - {"id": 9889, "synset": "pistol_grip.n.01", "name": "pistol_grip"}, - {"id": 9890, "synset": "piston.n.02", "name": "piston"}, - {"id": 9891, "synset": "piston_ring.n.01", "name": "piston_ring"}, - {"id": 9892, "synset": "piston_rod.n.01", "name": "piston_rod"}, - {"id": 9893, "synset": "pit.n.07", "name": "pit"}, - {"id": 9894, "synset": "pitching_wedge.n.01", "name": "pitching_wedge"}, - {"id": 9895, "synset": "pitch_pipe.n.01", "name": "pitch_pipe"}, - {"id": 9896, "synset": "pith_hat.n.01", "name": "pith_hat"}, - {"id": 9897, "synset": "piton.n.01", "name": "piton"}, - {"id": 9898, "synset": "pitot-static_tube.n.01", "name": "Pitot-static_tube"}, - {"id": 9899, "synset": "pitot_tube.n.01", "name": "Pitot_tube"}, - {"id": 9900, "synset": "pitsaw.n.01", "name": "pitsaw"}, - {"id": 9901, "synset": "pivot.n.02", "name": "pivot"}, - {"id": 9902, "synset": "pivoting_window.n.01", "name": "pivoting_window"}, - {"id": 9903, "synset": "pizzeria.n.01", "name": "pizzeria"}, - {"id": 9904, "synset": "place_of_business.n.01", "name": "place_of_business"}, - {"id": 9905, "synset": "place_of_worship.n.01", "name": "place_of_worship"}, - {"id": 9906, "synset": "placket.n.01", "name": "placket"}, - {"id": 9907, "synset": "planchet.n.01", "name": "planchet"}, - {"id": 9908, "synset": "plane.n.05", "name": "plane"}, - {"id": 9909, "synset": "plane.n.04", "name": "plane"}, - {"id": 9910, "synset": "plane_seat.n.01", "name": "plane_seat"}, - {"id": 9911, "synset": "planetarium.n.03", "name": "planetarium"}, - {"id": 9912, "synset": "planetarium.n.02", "name": "planetarium"}, - {"id": 9913, "synset": "planetarium.n.01", "name": "planetarium"}, - {"id": 9914, "synset": "planetary_gear.n.01", "name": "planetary_gear"}, - {"id": 9915, "synset": "plank-bed.n.01", "name": "plank-bed"}, - {"id": 9916, "synset": "planking.n.02", "name": "planking"}, - {"id": 9917, "synset": "planner.n.02", "name": "planner"}, - {"id": 9918, "synset": "plant.n.01", "name": "plant"}, - {"id": 9919, "synset": "planter.n.03", "name": "planter"}, - {"id": 9920, "synset": "plaster.n.05", "name": "plaster"}, - {"id": 9921, "synset": "plasterboard.n.01", "name": "plasterboard"}, - {"id": 9922, "synset": "plastering_trowel.n.01", "name": "plastering_trowel"}, - {"id": 9923, "synset": "plastic_bag.n.01", "name": "plastic_bag"}, - {"id": 9924, "synset": "plastic_bomb.n.01", "name": "plastic_bomb"}, - {"id": 9925, "synset": "plastic_laminate.n.01", "name": "plastic_laminate"}, - {"id": 9926, "synset": "plastic_wrap.n.01", "name": "plastic_wrap"}, - {"id": 9927, "synset": "plastron.n.03", "name": "plastron"}, - {"id": 9928, "synset": "plastron.n.02", "name": "plastron"}, - {"id": 9929, "synset": "plastron.n.01", "name": "plastron"}, - {"id": 9930, "synset": "plate.n.14", "name": "plate"}, - {"id": 9931, "synset": "plate.n.13", "name": "plate"}, - {"id": 9932, "synset": "plate.n.12", "name": "plate"}, - {"id": 9933, "synset": "platen.n.03", "name": "platen"}, - {"id": 9934, "synset": "platen.n.01", "name": "platen"}, - {"id": 9935, "synset": "plate_rack.n.01", "name": "plate_rack"}, - {"id": 9936, "synset": "plate_rail.n.01", "name": "plate_rail"}, - {"id": 9937, "synset": "platform.n.01", "name": "platform"}, - {"id": 9938, "synset": "platform.n.04", "name": "platform"}, - {"id": 9939, "synset": "platform.n.03", "name": "platform"}, - {"id": 9940, "synset": "platform_bed.n.01", "name": "platform_bed"}, - {"id": 9941, "synset": "platform_rocker.n.01", "name": "platform_rocker"}, - {"id": 9942, "synset": "plating.n.01", "name": "plating"}, - {"id": 9943, "synset": "playback.n.02", "name": "playback"}, - {"id": 9944, "synset": "playbox.n.01", "name": "playbox"}, - {"id": 9945, "synset": "playground.n.02", "name": "playground"}, - {"id": 9946, "synset": "playsuit.n.01", "name": "playsuit"}, - {"id": 9947, "synset": "plaza.n.02", "name": "plaza"}, - {"id": 9948, "synset": "pleat.n.01", "name": "pleat"}, - {"id": 9949, "synset": "plenum.n.02", "name": "plenum"}, - {"id": 9950, "synset": "plethysmograph.n.01", "name": "plethysmograph"}, - {"id": 9951, "synset": "pleximeter.n.01", "name": "pleximeter"}, - {"id": 9952, "synset": "plexor.n.01", "name": "plexor"}, - {"id": 9953, "synset": "plimsoll.n.02", "name": "plimsoll"}, - {"id": 9954, "synset": "plotter.n.04", "name": "plotter"}, - {"id": 9955, "synset": "plug.n.01", "name": "plug"}, - {"id": 9956, "synset": "plug.n.05", "name": "plug"}, - {"id": 9957, "synset": "plug_fuse.n.01", "name": "plug_fuse"}, - {"id": 9958, "synset": "plughole.n.01", "name": "plughole"}, - {"id": 9959, "synset": "plumb_bob.n.01", "name": "plumb_bob"}, - {"id": 9960, "synset": "plumb_level.n.01", "name": "plumb_level"}, - {"id": 9961, "synset": "plunger.n.03", "name": "plunger"}, - {"id": 9962, "synset": "plus_fours.n.01", "name": "plus_fours"}, - {"id": 9963, "synset": "plush.n.01", "name": "plush"}, - {"id": 9964, "synset": "plywood.n.01", "name": "plywood"}, - {"id": 9965, "synset": "pneumatic_drill.n.01", "name": "pneumatic_drill"}, - {"id": 9966, "synset": "p-n_junction.n.01", "name": "p-n_junction"}, - {"id": 9967, "synset": "p-n-p_transistor.n.01", "name": "p-n-p_transistor"}, - {"id": 9968, "synset": "poacher.n.02", "name": "poacher"}, - {"id": 9969, "synset": "pocket.n.01", "name": "pocket"}, - {"id": 9970, "synset": "pocket_battleship.n.01", "name": "pocket_battleship"}, - {"id": 9971, "synset": "pocketcomb.n.01", "name": "pocketcomb"}, - {"id": 9972, "synset": "pocket_flap.n.01", "name": "pocket_flap"}, - {"id": 9973, "synset": "pocket-handkerchief.n.01", "name": "pocket-handkerchief"}, - {"id": 9974, "synset": "pod.n.04", "name": "pod"}, - {"id": 9975, "synset": "pogo_stick.n.01", "name": "pogo_stick"}, - {"id": 9976, "synset": "point-and-shoot_camera.n.01", "name": "point-and-shoot_camera"}, - {"id": 9977, "synset": "pointed_arch.n.01", "name": "pointed_arch"}, - {"id": 9978, "synset": "pointing_trowel.n.01", "name": "pointing_trowel"}, - {"id": 9979, "synset": "point_lace.n.01", "name": "point_lace"}, - {"id": 9980, "synset": "polarimeter.n.01", "name": "polarimeter"}, - {"id": 9981, "synset": "polaroid.n.01", "name": "Polaroid"}, - {"id": 9982, "synset": "polaroid_camera.n.01", "name": "Polaroid_camera"}, - {"id": 9983, "synset": "pole.n.09", "name": "pole"}, - {"id": 9984, "synset": "poleax.n.02", "name": "poleax"}, - {"id": 9985, "synset": "poleax.n.01", "name": "poleax"}, - {"id": 9986, "synset": "police_boat.n.01", "name": "police_boat"}, - {"id": 9987, "synset": "police_van.n.01", "name": "police_van"}, - {"id": 9988, "synset": "polling_booth.n.01", "name": "polling_booth"}, - {"id": 9989, "synset": "polo_ball.n.01", "name": "polo_ball"}, - {"id": 9990, "synset": "polo_mallet.n.01", "name": "polo_mallet"}, - {"id": 9991, "synset": "polonaise.n.01", "name": "polonaise"}, - {"id": 9992, "synset": "polyester.n.03", "name": "polyester"}, - {"id": 9993, "synset": "polygraph.n.01", "name": "polygraph"}, - {"id": 9994, "synset": "pomade.n.01", "name": "pomade"}, - {"id": 9995, "synset": "pommel_horse.n.01", "name": "pommel_horse"}, - {"id": 9996, "synset": "pongee.n.01", "name": "pongee"}, - {"id": 9997, "synset": "poniard.n.01", "name": "poniard"}, - {"id": 9998, "synset": "pontifical.n.01", "name": "pontifical"}, - {"id": 9999, "synset": "pontoon.n.01", "name": "pontoon"}, - {"id": 10000, "synset": "pontoon_bridge.n.01", "name": "pontoon_bridge"}, - {"id": 10001, "synset": "pony_cart.n.01", "name": "pony_cart"}, - {"id": 10002, "synset": "pool_ball.n.01", "name": "pool_ball"}, - {"id": 10003, "synset": "poolroom.n.01", "name": "poolroom"}, - {"id": 10004, "synset": "poop_deck.n.01", "name": "poop_deck"}, - {"id": 10005, "synset": "poor_box.n.01", "name": "poor_box"}, - {"id": 10006, "synset": "poorhouse.n.01", "name": "poorhouse"}, - {"id": 10007, "synset": "pop_bottle.n.01", "name": "pop_bottle"}, - {"id": 10008, "synset": "popgun.n.01", "name": "popgun"}, - {"id": 10009, "synset": "poplin.n.01", "name": "poplin"}, - {"id": 10010, "synset": "popper.n.03", "name": "popper"}, - {"id": 10011, "synset": "poppet.n.01", "name": "poppet"}, - {"id": 10012, "synset": "pop_tent.n.01", "name": "pop_tent"}, - {"id": 10013, "synset": "porcelain.n.01", "name": "porcelain"}, - {"id": 10014, "synset": "porch.n.01", "name": "porch"}, - {"id": 10015, "synset": "porkpie.n.01", "name": "porkpie"}, - {"id": 10016, "synset": "porringer.n.01", "name": "porringer"}, - {"id": 10017, "synset": "portable.n.01", "name": "portable"}, - {"id": 10018, "synset": "portable_computer.n.01", "name": "portable_computer"}, - {"id": 10019, "synset": "portable_circular_saw.n.01", "name": "portable_circular_saw"}, - {"id": 10020, "synset": "portcullis.n.01", "name": "portcullis"}, - {"id": 10021, "synset": "porte-cochere.n.02", "name": "porte-cochere"}, - {"id": 10022, "synset": "porte-cochere.n.01", "name": "porte-cochere"}, - {"id": 10023, "synset": "portfolio.n.01", "name": "portfolio"}, - {"id": 10024, "synset": "porthole.n.01", "name": "porthole"}, - {"id": 10025, "synset": "portico.n.01", "name": "portico"}, - {"id": 10026, "synset": "portiere.n.01", "name": "portiere"}, - {"id": 10027, "synset": "portmanteau.n.02", "name": "portmanteau"}, - {"id": 10028, "synset": "portrait_camera.n.01", "name": "portrait_camera"}, - {"id": 10029, "synset": "portrait_lens.n.01", "name": "portrait_lens"}, - {"id": 10030, "synset": "positive_pole.n.02", "name": "positive_pole"}, - {"id": 10031, "synset": "positive_pole.n.01", "name": "positive_pole"}, - { - "id": 10032, - "synset": "positron_emission_tomography_scanner.n.01", - "name": "positron_emission_tomography_scanner", - }, - {"id": 10033, "synset": "post.n.04", "name": "post"}, - {"id": 10034, "synset": "postage_meter.n.01", "name": "postage_meter"}, - {"id": 10035, "synset": "post_and_lintel.n.01", "name": "post_and_lintel"}, - {"id": 10036, "synset": "post_chaise.n.01", "name": "post_chaise"}, - {"id": 10037, "synset": "postern.n.01", "name": "postern"}, - {"id": 10038, "synset": "post_exchange.n.01", "name": "post_exchange"}, - {"id": 10039, "synset": "posthole_digger.n.01", "name": "posthole_digger"}, - {"id": 10040, "synset": "post_horn.n.01", "name": "post_horn"}, - {"id": 10041, "synset": "posthouse.n.01", "name": "posthouse"}, - {"id": 10042, "synset": "potbelly.n.02", "name": "potbelly"}, - {"id": 10043, "synset": "potemkin_village.n.01", "name": "Potemkin_village"}, - {"id": 10044, "synset": "potential_divider.n.01", "name": "potential_divider"}, - {"id": 10045, "synset": "potentiometer.n.02", "name": "potentiometer"}, - {"id": 10046, "synset": "potentiometer.n.01", "name": "potentiometer"}, - {"id": 10047, "synset": "potpourri.n.03", "name": "potpourri"}, - {"id": 10048, "synset": "potsherd.n.01", "name": "potsherd"}, - {"id": 10049, "synset": "potter's_wheel.n.01", "name": "potter's_wheel"}, - {"id": 10050, "synset": "pottle.n.01", "name": "pottle"}, - {"id": 10051, "synset": "potty_seat.n.01", "name": "potty_seat"}, - {"id": 10052, "synset": "poultice.n.01", "name": "poultice"}, - {"id": 10053, "synset": "pound.n.13", "name": "pound"}, - {"id": 10054, "synset": "pound_net.n.01", "name": "pound_net"}, - {"id": 10055, "synset": "powder.n.03", "name": "powder"}, - {"id": 10056, "synset": "powder_and_shot.n.01", "name": "powder_and_shot"}, - {"id": 10057, "synset": "powdered_mustard.n.01", "name": "powdered_mustard"}, - {"id": 10058, "synset": "powder_horn.n.01", "name": "powder_horn"}, - {"id": 10059, "synset": "powder_keg.n.02", "name": "powder_keg"}, - {"id": 10060, "synset": "power_brake.n.01", "name": "power_brake"}, - {"id": 10061, "synset": "power_cord.n.01", "name": "power_cord"}, - {"id": 10062, "synset": "power_drill.n.01", "name": "power_drill"}, - {"id": 10063, "synset": "power_line.n.01", "name": "power_line"}, - {"id": 10064, "synset": "power_loom.n.01", "name": "power_loom"}, - {"id": 10065, "synset": "power_mower.n.01", "name": "power_mower"}, - {"id": 10066, "synset": "power_pack.n.01", "name": "power_pack"}, - {"id": 10067, "synset": "power_saw.n.01", "name": "power_saw"}, - {"id": 10068, "synset": "power_steering.n.01", "name": "power_steering"}, - {"id": 10069, "synset": "power_takeoff.n.01", "name": "power_takeoff"}, - {"id": 10070, "synset": "power_tool.n.01", "name": "power_tool"}, - {"id": 10071, "synset": "praetorium.n.01", "name": "praetorium"}, - {"id": 10072, "synset": "prayer_rug.n.01", "name": "prayer_rug"}, - {"id": 10073, "synset": "prayer_shawl.n.01", "name": "prayer_shawl"}, - {"id": 10074, "synset": "precipitator.n.01", "name": "precipitator"}, - {"id": 10075, "synset": "prefab.n.01", "name": "prefab"}, - {"id": 10076, "synset": "presbytery.n.01", "name": "presbytery"}, - {"id": 10077, "synset": "presence_chamber.n.01", "name": "presence_chamber"}, - {"id": 10078, "synset": "press.n.07", "name": "press"}, - {"id": 10079, "synset": "press.n.03", "name": "press"}, - {"id": 10080, "synset": "press.n.06", "name": "press"}, - {"id": 10081, "synset": "press_box.n.01", "name": "press_box"}, - {"id": 10082, "synset": "press_gallery.n.01", "name": "press_gallery"}, - {"id": 10083, "synset": "press_of_sail.n.01", "name": "press_of_sail"}, - {"id": 10084, "synset": "pressure_cabin.n.01", "name": "pressure_cabin"}, - {"id": 10085, "synset": "pressure_cooker.n.01", "name": "pressure_cooker"}, - {"id": 10086, "synset": "pressure_dome.n.01", "name": "pressure_dome"}, - {"id": 10087, "synset": "pressure_gauge.n.01", "name": "pressure_gauge"}, - {"id": 10088, "synset": "pressurized_water_reactor.n.01", "name": "pressurized_water_reactor"}, - {"id": 10089, "synset": "pressure_suit.n.01", "name": "pressure_suit"}, - {"id": 10090, "synset": "pricket.n.01", "name": "pricket"}, - {"id": 10091, "synset": "prie-dieu.n.01", "name": "prie-dieu"}, - {"id": 10092, "synset": "primary_coil.n.01", "name": "primary_coil"}, - {"id": 10093, "synset": "primus_stove.n.01", "name": "Primus_stove"}, - {"id": 10094, "synset": "prince_albert.n.02", "name": "Prince_Albert"}, - {"id": 10095, "synset": "print.n.06", "name": "print"}, - {"id": 10096, "synset": "print_buffer.n.01", "name": "print_buffer"}, - {"id": 10097, "synset": "printed_circuit.n.01", "name": "printed_circuit"}, - {"id": 10098, "synset": "printer.n.02", "name": "printer"}, - {"id": 10099, "synset": "printer_cable.n.01", "name": "printer_cable"}, - {"id": 10100, "synset": "priory.n.01", "name": "priory"}, - {"id": 10101, "synset": "prison.n.01", "name": "prison"}, - {"id": 10102, "synset": "prison_camp.n.01", "name": "prison_camp"}, - {"id": 10103, "synset": "privateer.n.02", "name": "privateer"}, - {"id": 10104, "synset": "private_line.n.01", "name": "private_line"}, - {"id": 10105, "synset": "privet_hedge.n.01", "name": "privet_hedge"}, - {"id": 10106, "synset": "probe.n.02", "name": "probe"}, - {"id": 10107, "synset": "proctoscope.n.01", "name": "proctoscope"}, - {"id": 10108, "synset": "prod.n.02", "name": "prod"}, - {"id": 10109, "synset": "production_line.n.01", "name": "production_line"}, - {"id": 10110, "synset": "projector.n.01", "name": "projector"}, - {"id": 10111, "synset": "prolonge.n.01", "name": "prolonge"}, - {"id": 10112, "synset": "prolonge_knot.n.01", "name": "prolonge_knot"}, - {"id": 10113, "synset": "prompter.n.02", "name": "prompter"}, - {"id": 10114, "synset": "prong.n.01", "name": "prong"}, - {"id": 10115, "synset": "propeller_plane.n.01", "name": "propeller_plane"}, - {"id": 10116, "synset": "propjet.n.01", "name": "propjet"}, - {"id": 10117, "synset": "proportional_counter_tube.n.01", "name": "proportional_counter_tube"}, - {"id": 10118, "synset": "propulsion_system.n.01", "name": "propulsion_system"}, - {"id": 10119, "synset": "proscenium.n.02", "name": "proscenium"}, - {"id": 10120, "synset": "proscenium_arch.n.01", "name": "proscenium_arch"}, - {"id": 10121, "synset": "prosthesis.n.01", "name": "prosthesis"}, - {"id": 10122, "synset": "protective_covering.n.01", "name": "protective_covering"}, - {"id": 10123, "synset": "protective_garment.n.01", "name": "protective_garment"}, - {"id": 10124, "synset": "proton_accelerator.n.01", "name": "proton_accelerator"}, - {"id": 10125, "synset": "protractor.n.01", "name": "protractor"}, - {"id": 10126, "synset": "pruner.n.02", "name": "pruner"}, - {"id": 10127, "synset": "pruning_knife.n.01", "name": "pruning_knife"}, - {"id": 10128, "synset": "pruning_saw.n.01", "name": "pruning_saw"}, - {"id": 10129, "synset": "pruning_shears.n.01", "name": "pruning_shears"}, - {"id": 10130, "synset": "psaltery.n.01", "name": "psaltery"}, - {"id": 10131, "synset": "psychrometer.n.01", "name": "psychrometer"}, - {"id": 10132, "synset": "pt_boat.n.01", "name": "PT_boat"}, - {"id": 10133, "synset": "public_address_system.n.01", "name": "public_address_system"}, - {"id": 10134, "synset": "public_house.n.01", "name": "public_house"}, - {"id": 10135, "synset": "public_toilet.n.01", "name": "public_toilet"}, - {"id": 10136, "synset": "public_transport.n.01", "name": "public_transport"}, - {"id": 10137, "synset": "public_works.n.01", "name": "public_works"}, - {"id": 10138, "synset": "puck.n.02", "name": "puck"}, - {"id": 10139, "synset": "pull.n.04", "name": "pull"}, - {"id": 10140, "synset": "pullback.n.01", "name": "pullback"}, - {"id": 10141, "synset": "pull_chain.n.01", "name": "pull_chain"}, - {"id": 10142, "synset": "pulley.n.01", "name": "pulley"}, - {"id": 10143, "synset": "pull-off.n.01", "name": "pull-off"}, - {"id": 10144, "synset": "pullman.n.01", "name": "Pullman"}, - {"id": 10145, "synset": "pullover.n.01", "name": "pullover"}, - {"id": 10146, "synset": "pull-through.n.01", "name": "pull-through"}, - {"id": 10147, "synset": "pulse_counter.n.01", "name": "pulse_counter"}, - {"id": 10148, "synset": "pulse_generator.n.01", "name": "pulse_generator"}, - {"id": 10149, "synset": "pulse_timing_circuit.n.01", "name": "pulse_timing_circuit"}, - {"id": 10150, "synset": "pump.n.01", "name": "pump"}, - {"id": 10151, "synset": "pump.n.03", "name": "pump"}, - {"id": 10152, "synset": "pump_action.n.01", "name": "pump_action"}, - {"id": 10153, "synset": "pump_house.n.01", "name": "pump_house"}, - {"id": 10154, "synset": "pump_room.n.01", "name": "pump_room"}, - {"id": 10155, "synset": "pump-type_pliers.n.01", "name": "pump-type_pliers"}, - {"id": 10156, "synset": "pump_well.n.01", "name": "pump_well"}, - {"id": 10157, "synset": "punchboard.n.01", "name": "punchboard"}, - {"id": 10158, "synset": "punch_bowl.n.01", "name": "punch_bowl"}, - {"id": 10159, "synset": "punching_bag.n.02", "name": "punching_bag"}, - {"id": 10160, "synset": "punch_pliers.n.01", "name": "punch_pliers"}, - {"id": 10161, "synset": "punch_press.n.01", "name": "punch_press"}, - {"id": 10162, "synset": "punnet.n.01", "name": "punnet"}, - {"id": 10163, "synset": "punt.n.02", "name": "punt"}, - {"id": 10164, "synset": "pup_tent.n.01", "name": "pup_tent"}, - {"id": 10165, "synset": "purdah.n.03", "name": "purdah"}, - {"id": 10166, "synset": "purifier.n.01", "name": "purifier"}, - {"id": 10167, "synset": "purl.n.02", "name": "purl"}, - {"id": 10168, "synset": "purse.n.03", "name": "purse"}, - {"id": 10169, "synset": "push-bike.n.01", "name": "push-bike"}, - {"id": 10170, "synset": "push_broom.n.01", "name": "push_broom"}, - {"id": 10171, "synset": "push_button.n.01", "name": "push_button"}, - {"id": 10172, "synset": "push-button_radio.n.01", "name": "push-button_radio"}, - {"id": 10173, "synset": "pusher.n.04", "name": "pusher"}, - {"id": 10174, "synset": "put-put.n.01", "name": "put-put"}, - {"id": 10175, "synset": "puttee.n.01", "name": "puttee"}, - {"id": 10176, "synset": "putter.n.02", "name": "putter"}, - {"id": 10177, "synset": "putty_knife.n.01", "name": "putty_knife"}, - {"id": 10178, "synset": "puzzle.n.02", "name": "puzzle"}, - {"id": 10179, "synset": "pylon.n.02", "name": "pylon"}, - {"id": 10180, "synset": "pylon.n.01", "name": "pylon"}, - {"id": 10181, "synset": "pyramidal_tent.n.01", "name": "pyramidal_tent"}, - {"id": 10182, "synset": "pyrograph.n.01", "name": "pyrograph"}, - {"id": 10183, "synset": "pyrometer.n.01", "name": "pyrometer"}, - {"id": 10184, "synset": "pyrometric_cone.n.01", "name": "pyrometric_cone"}, - {"id": 10185, "synset": "pyrostat.n.01", "name": "pyrostat"}, - {"id": 10186, "synset": "pyx.n.02", "name": "pyx"}, - {"id": 10187, "synset": "pyx.n.01", "name": "pyx"}, - {"id": 10188, "synset": "pyxis.n.03", "name": "pyxis"}, - {"id": 10189, "synset": "quad.n.04", "name": "quad"}, - {"id": 10190, "synset": "quadrant.n.04", "name": "quadrant"}, - {"id": 10191, "synset": "quadraphony.n.01", "name": "quadraphony"}, - {"id": 10192, "synset": "quartering.n.02", "name": "quartering"}, - {"id": 10193, "synset": "quarterstaff.n.01", "name": "quarterstaff"}, - {"id": 10194, "synset": "quartz_battery.n.01", "name": "quartz_battery"}, - {"id": 10195, "synset": "quartz_lamp.n.01", "name": "quartz_lamp"}, - {"id": 10196, "synset": "queen.n.08", "name": "queen"}, - {"id": 10197, "synset": "queen.n.07", "name": "queen"}, - {"id": 10198, "synset": "queen_post.n.01", "name": "queen_post"}, - {"id": 10199, "synset": "quern.n.01", "name": "quern"}, - {"id": 10200, "synset": "quill.n.01", "name": "quill"}, - {"id": 10201, "synset": "quilted_bedspread.n.01", "name": "quilted_bedspread"}, - {"id": 10202, "synset": "quilting.n.02", "name": "quilting"}, - {"id": 10203, "synset": "quipu.n.01", "name": "quipu"}, - {"id": 10204, "synset": "quirk_molding.n.01", "name": "quirk_molding"}, - {"id": 10205, "synset": "quirt.n.01", "name": "quirt"}, - {"id": 10206, "synset": "quiver.n.03", "name": "quiver"}, - {"id": 10207, "synset": "quoin.n.02", "name": "quoin"}, - {"id": 10208, "synset": "quoit.n.01", "name": "quoit"}, - {"id": 10209, "synset": "qwerty_keyboard.n.01", "name": "QWERTY_keyboard"}, - {"id": 10210, "synset": "rabbet.n.01", "name": "rabbet"}, - {"id": 10211, "synset": "rabbet_joint.n.01", "name": "rabbet_joint"}, - {"id": 10212, "synset": "rabbit_ears.n.01", "name": "rabbit_ears"}, - {"id": 10213, "synset": "rabbit_hutch.n.01", "name": "rabbit_hutch"}, - {"id": 10214, "synset": "raceabout.n.01", "name": "raceabout"}, - {"id": 10215, "synset": "raceway.n.01", "name": "raceway"}, - {"id": 10216, "synset": "racing_boat.n.01", "name": "racing_boat"}, - {"id": 10217, "synset": "racing_gig.n.01", "name": "racing_gig"}, - {"id": 10218, "synset": "racing_skiff.n.01", "name": "racing_skiff"}, - {"id": 10219, "synset": "rack.n.05", "name": "rack"}, - {"id": 10220, "synset": "rack.n.01", "name": "rack"}, - {"id": 10221, "synset": "rack.n.04", "name": "rack"}, - {"id": 10222, "synset": "rack_and_pinion.n.01", "name": "rack_and_pinion"}, - {"id": 10223, "synset": "racquetball.n.01", "name": "racquetball"}, - {"id": 10224, "synset": "radial.n.01", "name": "radial"}, - {"id": 10225, "synset": "radial_engine.n.01", "name": "radial_engine"}, - {"id": 10226, "synset": "radiation_pyrometer.n.01", "name": "radiation_pyrometer"}, - {"id": 10227, "synset": "radiator.n.02", "name": "radiator"}, - {"id": 10228, "synset": "radiator_cap.n.01", "name": "radiator_cap"}, - {"id": 10229, "synset": "radiator_hose.n.01", "name": "radiator_hose"}, - {"id": 10230, "synset": "radio.n.03", "name": "radio"}, - {"id": 10231, "synset": "radio_antenna.n.01", "name": "radio_antenna"}, - {"id": 10232, "synset": "radio_chassis.n.01", "name": "radio_chassis"}, - {"id": 10233, "synset": "radio_compass.n.01", "name": "radio_compass"}, - {"id": 10234, "synset": "radiogram.n.02", "name": "radiogram"}, - {"id": 10235, "synset": "radio_interferometer.n.01", "name": "radio_interferometer"}, - {"id": 10236, "synset": "radio_link.n.01", "name": "radio_link"}, - {"id": 10237, "synset": "radiometer.n.01", "name": "radiometer"}, - {"id": 10238, "synset": "radiomicrometer.n.01", "name": "radiomicrometer"}, - {"id": 10239, "synset": "radio-phonograph.n.01", "name": "radio-phonograph"}, - {"id": 10240, "synset": "radiotelegraph.n.02", "name": "radiotelegraph"}, - {"id": 10241, "synset": "radiotelephone.n.02", "name": "radiotelephone"}, - {"id": 10242, "synset": "radio_telescope.n.01", "name": "radio_telescope"}, - {"id": 10243, "synset": "radiotherapy_equipment.n.01", "name": "radiotherapy_equipment"}, - {"id": 10244, "synset": "radio_transmitter.n.01", "name": "radio_transmitter"}, - {"id": 10245, "synset": "radome.n.01", "name": "radome"}, - {"id": 10246, "synset": "rafter.n.01", "name": "rafter"}, - {"id": 10247, "synset": "raft_foundation.n.01", "name": "raft_foundation"}, - {"id": 10248, "synset": "rag.n.01", "name": "rag"}, - {"id": 10249, "synset": "ragbag.n.02", "name": "ragbag"}, - {"id": 10250, "synset": "raglan.n.01", "name": "raglan"}, - {"id": 10251, "synset": "raglan_sleeve.n.01", "name": "raglan_sleeve"}, - {"id": 10252, "synset": "rail.n.04", "name": "rail"}, - {"id": 10253, "synset": "rail_fence.n.01", "name": "rail_fence"}, - {"id": 10254, "synset": "railhead.n.01", "name": "railhead"}, - {"id": 10255, "synset": "railing.n.01", "name": "railing"}, - {"id": 10256, "synset": "railing.n.02", "name": "railing"}, - {"id": 10257, "synset": "railroad_bed.n.01", "name": "railroad_bed"}, - {"id": 10258, "synset": "railroad_tunnel.n.01", "name": "railroad_tunnel"}, - {"id": 10259, "synset": "rain_barrel.n.01", "name": "rain_barrel"}, - {"id": 10260, "synset": "rain_gauge.n.01", "name": "rain_gauge"}, - {"id": 10261, "synset": "rain_stick.n.01", "name": "rain_stick"}, - {"id": 10262, "synset": "rake.n.03", "name": "rake"}, - {"id": 10263, "synset": "rake_handle.n.01", "name": "rake_handle"}, - {"id": 10264, "synset": "ram_disk.n.01", "name": "RAM_disk"}, - {"id": 10265, "synset": "ramekin.n.02", "name": "ramekin"}, - {"id": 10266, "synset": "ramjet.n.01", "name": "ramjet"}, - {"id": 10267, "synset": "rammer.n.01", "name": "rammer"}, - {"id": 10268, "synset": "ramp.n.01", "name": "ramp"}, - {"id": 10269, "synset": "rampant_arch.n.01", "name": "rampant_arch"}, - {"id": 10270, "synset": "rampart.n.01", "name": "rampart"}, - {"id": 10271, "synset": "ramrod.n.01", "name": "ramrod"}, - {"id": 10272, "synset": "ramrod.n.03", "name": "ramrod"}, - {"id": 10273, "synset": "ranch.n.01", "name": "ranch"}, - {"id": 10274, "synset": "ranch_house.n.01", "name": "ranch_house"}, - {"id": 10275, "synset": "random-access_memory.n.01", "name": "random-access_memory"}, - {"id": 10276, "synset": "rangefinder.n.01", "name": "rangefinder"}, - {"id": 10277, "synset": "range_hood.n.01", "name": "range_hood"}, - {"id": 10278, "synset": "range_pole.n.01", "name": "range_pole"}, - {"id": 10279, "synset": "rapier.n.01", "name": "rapier"}, - {"id": 10280, "synset": "rariora.n.01", "name": "rariora"}, - {"id": 10281, "synset": "rasp.n.02", "name": "rasp"}, - {"id": 10282, "synset": "ratchet.n.01", "name": "ratchet"}, - {"id": 10283, "synset": "ratchet_wheel.n.01", "name": "ratchet_wheel"}, - {"id": 10284, "synset": "rathskeller.n.01", "name": "rathskeller"}, - {"id": 10285, "synset": "ratline.n.01", "name": "ratline"}, - {"id": 10286, "synset": "rat-tail_file.n.01", "name": "rat-tail_file"}, - {"id": 10287, "synset": "rattan.n.03", "name": "rattan"}, - {"id": 10288, "synset": "rattrap.n.03", "name": "rattrap"}, - {"id": 10289, "synset": "rayon.n.01", "name": "rayon"}, - {"id": 10290, "synset": "razor.n.01", "name": "razor"}, - { - "id": 10291, - "synset": "reaction-propulsion_engine.n.01", - "name": "reaction-propulsion_engine", - }, - {"id": 10292, "synset": "reaction_turbine.n.01", "name": "reaction_turbine"}, - {"id": 10293, "synset": "reactor.n.01", "name": "reactor"}, - {"id": 10294, "synset": "reading_lamp.n.01", "name": "reading_lamp"}, - {"id": 10295, "synset": "reading_room.n.01", "name": "reading_room"}, - {"id": 10296, "synset": "read-only_memory.n.01", "name": "read-only_memory"}, - {"id": 10297, "synset": "read-only_memory_chip.n.01", "name": "read-only_memory_chip"}, - {"id": 10298, "synset": "readout.n.03", "name": "readout"}, - {"id": 10299, "synset": "read/write_head.n.01", "name": "read/write_head"}, - {"id": 10300, "synset": "ready-to-wear.n.01", "name": "ready-to-wear"}, - {"id": 10301, "synset": "real_storage.n.01", "name": "real_storage"}, - {"id": 10302, "synset": "reamer.n.02", "name": "reamer"}, - {"id": 10303, "synset": "reaumur_thermometer.n.01", "name": "Reaumur_thermometer"}, - {"id": 10304, "synset": "rebozo.n.01", "name": "rebozo"}, - {"id": 10305, "synset": "receiver.n.01", "name": "receiver"}, - {"id": 10306, "synset": "receptacle.n.01", "name": "receptacle"}, - {"id": 10307, "synset": "reception_desk.n.01", "name": "reception_desk"}, - {"id": 10308, "synset": "reception_room.n.01", "name": "reception_room"}, - {"id": 10309, "synset": "recess.n.04", "name": "recess"}, - {"id": 10310, "synset": "reciprocating_engine.n.01", "name": "reciprocating_engine"}, - {"id": 10311, "synset": "reconnaissance_plane.n.01", "name": "reconnaissance_plane"}, - {"id": 10312, "synset": "reconnaissance_vehicle.n.01", "name": "reconnaissance_vehicle"}, - {"id": 10313, "synset": "record_changer.n.01", "name": "record_changer"}, - {"id": 10314, "synset": "recorder.n.01", "name": "recorder"}, - {"id": 10315, "synset": "recording.n.03", "name": "recording"}, - {"id": 10316, "synset": "recording_system.n.01", "name": "recording_system"}, - {"id": 10317, "synset": "record_sleeve.n.01", "name": "record_sleeve"}, - {"id": 10318, "synset": "recovery_room.n.01", "name": "recovery_room"}, - {"id": 10319, "synset": "recreational_vehicle.n.01", "name": "recreational_vehicle"}, - {"id": 10320, "synset": "recreation_room.n.01", "name": "recreation_room"}, - {"id": 10321, "synset": "recycling_bin.n.01", "name": "recycling_bin"}, - {"id": 10322, "synset": "recycling_plant.n.01", "name": "recycling_plant"}, - {"id": 10323, "synset": "redbrick_university.n.01", "name": "redbrick_university"}, - {"id": 10324, "synset": "red_carpet.n.01", "name": "red_carpet"}, - {"id": 10325, "synset": "redoubt.n.02", "name": "redoubt"}, - {"id": 10326, "synset": "redoubt.n.01", "name": "redoubt"}, - {"id": 10327, "synset": "reduction_gear.n.01", "name": "reduction_gear"}, - {"id": 10328, "synset": "reed_pipe.n.01", "name": "reed_pipe"}, - {"id": 10329, "synset": "reed_stop.n.01", "name": "reed_stop"}, - {"id": 10330, "synset": "reef_knot.n.01", "name": "reef_knot"}, - {"id": 10331, "synset": "reel.n.03", "name": "reel"}, - {"id": 10332, "synset": "reel.n.01", "name": "reel"}, - {"id": 10333, "synset": "refectory.n.01", "name": "refectory"}, - {"id": 10334, "synset": "refectory_table.n.01", "name": "refectory_table"}, - {"id": 10335, "synset": "refinery.n.01", "name": "refinery"}, - {"id": 10336, "synset": "reflecting_telescope.n.01", "name": "reflecting_telescope"}, - {"id": 10337, "synset": "reflectometer.n.01", "name": "reflectometer"}, - {"id": 10338, "synset": "reflex_camera.n.01", "name": "reflex_camera"}, - {"id": 10339, "synset": "reflux_condenser.n.01", "name": "reflux_condenser"}, - {"id": 10340, "synset": "reformatory.n.01", "name": "reformatory"}, - {"id": 10341, "synset": "reformer.n.02", "name": "reformer"}, - {"id": 10342, "synset": "refracting_telescope.n.01", "name": "refracting_telescope"}, - {"id": 10343, "synset": "refractometer.n.01", "name": "refractometer"}, - {"id": 10344, "synset": "refrigeration_system.n.01", "name": "refrigeration_system"}, - {"id": 10345, "synset": "refrigerator.n.01", "name": "refrigerator"}, - {"id": 10346, "synset": "refrigerator_car.n.01", "name": "refrigerator_car"}, - {"id": 10347, "synset": "refuge.n.03", "name": "refuge"}, - {"id": 10348, "synset": "regalia.n.01", "name": "regalia"}, - {"id": 10349, "synset": "regimentals.n.01", "name": "regimentals"}, - {"id": 10350, "synset": "regulator.n.01", "name": "regulator"}, - {"id": 10351, "synset": "rein.n.01", "name": "rein"}, - {"id": 10352, "synset": "relay.n.05", "name": "relay"}, - {"id": 10353, "synset": "release.n.08", "name": "release"}, - {"id": 10354, "synset": "religious_residence.n.01", "name": "religious_residence"}, - {"id": 10355, "synset": "reliquary.n.01", "name": "reliquary"}, - {"id": 10356, "synset": "remote_terminal.n.01", "name": "remote_terminal"}, - {"id": 10357, "synset": "removable_disk.n.01", "name": "removable_disk"}, - {"id": 10358, "synset": "rendering.n.05", "name": "rendering"}, - {"id": 10359, "synset": "rep.n.02", "name": "rep"}, - {"id": 10360, "synset": "repair_shop.n.01", "name": "repair_shop"}, - {"id": 10361, "synset": "repeater.n.04", "name": "repeater"}, - {"id": 10362, "synset": "repeating_firearm.n.01", "name": "repeating_firearm"}, - {"id": 10363, "synset": "repository.n.03", "name": "repository"}, - {"id": 10364, "synset": "reproducer.n.01", "name": "reproducer"}, - {"id": 10365, "synset": "rerebrace.n.01", "name": "rerebrace"}, - {"id": 10366, "synset": "rescue_equipment.n.01", "name": "rescue_equipment"}, - {"id": 10367, "synset": "research_center.n.01", "name": "research_center"}, - {"id": 10368, "synset": "reseau.n.02", "name": "reseau"}, - {"id": 10369, "synset": "reservoir.n.03", "name": "reservoir"}, - {"id": 10370, "synset": "reset.n.01", "name": "reset"}, - {"id": 10371, "synset": "reset_button.n.01", "name": "reset_button"}, - {"id": 10372, "synset": "residence.n.02", "name": "residence"}, - {"id": 10373, "synset": "resistance_pyrometer.n.01", "name": "resistance_pyrometer"}, - {"id": 10374, "synset": "resistor.n.01", "name": "resistor"}, - {"id": 10375, "synset": "resonator.n.03", "name": "resonator"}, - {"id": 10376, "synset": "resonator.n.01", "name": "resonator"}, - {"id": 10377, "synset": "resort_hotel.n.02", "name": "resort_hotel"}, - {"id": 10378, "synset": "respirator.n.01", "name": "respirator"}, - {"id": 10379, "synset": "restaurant.n.01", "name": "restaurant"}, - {"id": 10380, "synset": "rest_house.n.01", "name": "rest_house"}, - {"id": 10381, "synset": "restraint.n.06", "name": "restraint"}, - {"id": 10382, "synset": "resuscitator.n.01", "name": "resuscitator"}, - {"id": 10383, "synset": "retainer.n.03", "name": "retainer"}, - {"id": 10384, "synset": "retaining_wall.n.01", "name": "retaining_wall"}, - {"id": 10385, "synset": "reticle.n.01", "name": "reticle"}, - {"id": 10386, "synset": "reticulation.n.02", "name": "reticulation"}, - {"id": 10387, "synset": "reticule.n.01", "name": "reticule"}, - {"id": 10388, "synset": "retort.n.02", "name": "retort"}, - {"id": 10389, "synset": "retractor.n.01", "name": "retractor"}, - {"id": 10390, "synset": "return_key.n.01", "name": "return_key"}, - {"id": 10391, "synset": "reverberatory_furnace.n.01", "name": "reverberatory_furnace"}, - {"id": 10392, "synset": "revers.n.01", "name": "revers"}, - {"id": 10393, "synset": "reverse.n.02", "name": "reverse"}, - {"id": 10394, "synset": "reversible.n.01", "name": "reversible"}, - {"id": 10395, "synset": "revetment.n.02", "name": "revetment"}, - {"id": 10396, "synset": "revetment.n.01", "name": "revetment"}, - {"id": 10397, "synset": "revolver.n.01", "name": "revolver"}, - {"id": 10398, "synset": "revolving_door.n.02", "name": "revolving_door"}, - {"id": 10399, "synset": "rheometer.n.01", "name": "rheometer"}, - {"id": 10400, "synset": "rheostat.n.01", "name": "rheostat"}, - {"id": 10401, "synset": "rhinoscope.n.01", "name": "rhinoscope"}, - {"id": 10402, "synset": "rib.n.01", "name": "rib"}, - {"id": 10403, "synset": "riband.n.01", "name": "riband"}, - {"id": 10404, "synset": "ribbed_vault.n.01", "name": "ribbed_vault"}, - {"id": 10405, "synset": "ribbing.n.01", "name": "ribbing"}, - {"id": 10406, "synset": "ribbon_development.n.01", "name": "ribbon_development"}, - {"id": 10407, "synset": "rib_joint_pliers.n.01", "name": "rib_joint_pliers"}, - {"id": 10408, "synset": "ricer.n.01", "name": "ricer"}, - {"id": 10409, "synset": "riddle.n.02", "name": "riddle"}, - {"id": 10410, "synset": "ride.n.02", "name": "ride"}, - {"id": 10411, "synset": "ridge.n.06", "name": "ridge"}, - {"id": 10412, "synset": "ridge_rope.n.01", "name": "ridge_rope"}, - {"id": 10413, "synset": "riding_boot.n.01", "name": "riding_boot"}, - {"id": 10414, "synset": "riding_crop.n.01", "name": "riding_crop"}, - {"id": 10415, "synset": "riding_mower.n.01", "name": "riding_mower"}, - {"id": 10416, "synset": "rifle_ball.n.01", "name": "rifle_ball"}, - {"id": 10417, "synset": "rifle_grenade.n.01", "name": "rifle_grenade"}, - {"id": 10418, "synset": "rig.n.01", "name": "rig"}, - {"id": 10419, "synset": "rigger.n.02", "name": "rigger"}, - {"id": 10420, "synset": "rigger.n.04", "name": "rigger"}, - {"id": 10421, "synset": "rigging.n.01", "name": "rigging"}, - {"id": 10422, "synset": "rigout.n.01", "name": "rigout"}, - {"id": 10423, "synset": "ringlet.n.03", "name": "ringlet"}, - {"id": 10424, "synset": "rings.n.01", "name": "rings"}, - {"id": 10425, "synset": "rink.n.01", "name": "rink"}, - {"id": 10426, "synset": "riot_gun.n.01", "name": "riot_gun"}, - {"id": 10427, "synset": "ripcord.n.02", "name": "ripcord"}, - {"id": 10428, "synset": "ripcord.n.01", "name": "ripcord"}, - {"id": 10429, "synset": "ripping_bar.n.01", "name": "ripping_bar"}, - {"id": 10430, "synset": "ripping_chisel.n.01", "name": "ripping_chisel"}, - {"id": 10431, "synset": "ripsaw.n.01", "name": "ripsaw"}, - {"id": 10432, "synset": "riser.n.03", "name": "riser"}, - {"id": 10433, "synset": "riser.n.02", "name": "riser"}, - {"id": 10434, "synset": "ritz.n.03", "name": "Ritz"}, - {"id": 10435, "synset": "rivet.n.02", "name": "rivet"}, - {"id": 10436, "synset": "riveting_machine.n.01", "name": "riveting_machine"}, - {"id": 10437, "synset": "roach_clip.n.01", "name": "roach_clip"}, - {"id": 10438, "synset": "road.n.01", "name": "road"}, - {"id": 10439, "synset": "roadbed.n.01", "name": "roadbed"}, - {"id": 10440, "synset": "roadblock.n.02", "name": "roadblock"}, - {"id": 10441, "synset": "roadhouse.n.01", "name": "roadhouse"}, - {"id": 10442, "synset": "roadster.n.01", "name": "roadster"}, - {"id": 10443, "synset": "roadway.n.01", "name": "roadway"}, - {"id": 10444, "synset": "roaster.n.04", "name": "roaster"}, - {"id": 10445, "synset": "robotics_equipment.n.01", "name": "robotics_equipment"}, - {"id": 10446, "synset": "rochon_prism.n.01", "name": "Rochon_prism"}, - {"id": 10447, "synset": "rock_bit.n.01", "name": "rock_bit"}, - {"id": 10448, "synset": "rocker.n.07", "name": "rocker"}, - {"id": 10449, "synset": "rocker.n.05", "name": "rocker"}, - {"id": 10450, "synset": "rocker_arm.n.01", "name": "rocker_arm"}, - {"id": 10451, "synset": "rocket.n.02", "name": "rocket"}, - {"id": 10452, "synset": "rocket.n.01", "name": "rocket"}, - {"id": 10453, "synset": "rod.n.01", "name": "rod"}, - {"id": 10454, "synset": "rodeo.n.02", "name": "rodeo"}, - {"id": 10455, "synset": "roll.n.04", "name": "roll"}, - {"id": 10456, "synset": "roller.n.04", "name": "roller"}, - {"id": 10457, "synset": "roller.n.03", "name": "roller"}, - {"id": 10458, "synset": "roller_bandage.n.01", "name": "roller_bandage"}, - {"id": 10459, "synset": "in-line_skate.n.01", "name": "in-line_skate"}, - {"id": 10460, "synset": "roller_blind.n.01", "name": "roller_blind"}, - {"id": 10461, "synset": "roller_coaster.n.02", "name": "roller_coaster"}, - {"id": 10462, "synset": "roller_towel.n.01", "name": "roller_towel"}, - {"id": 10463, "synset": "roll_film.n.01", "name": "roll_film"}, - {"id": 10464, "synset": "rolling_hitch.n.01", "name": "rolling_hitch"}, - {"id": 10465, "synset": "rolling_mill.n.01", "name": "rolling_mill"}, - {"id": 10466, "synset": "rolling_stock.n.01", "name": "rolling_stock"}, - {"id": 10467, "synset": "roll-on.n.02", "name": "roll-on"}, - {"id": 10468, "synset": "roll-on.n.01", "name": "roll-on"}, - {"id": 10469, "synset": "roll-on_roll-off.n.01", "name": "roll-on_roll-off"}, - {"id": 10470, "synset": "rolodex.n.01", "name": "Rolodex"}, - {"id": 10471, "synset": "roman_arch.n.01", "name": "Roman_arch"}, - {"id": 10472, "synset": "roman_building.n.01", "name": "Roman_building"}, - {"id": 10473, "synset": "romper.n.02", "name": "romper"}, - {"id": 10474, "synset": "rood_screen.n.01", "name": "rood_screen"}, - {"id": 10475, "synset": "roof.n.01", "name": "roof"}, - {"id": 10476, "synset": "roof.n.02", "name": "roof"}, - {"id": 10477, "synset": "roofing.n.01", "name": "roofing"}, - {"id": 10478, "synset": "room.n.01", "name": "room"}, - {"id": 10479, "synset": "roomette.n.01", "name": "roomette"}, - {"id": 10480, "synset": "room_light.n.01", "name": "room_light"}, - {"id": 10481, "synset": "roost.n.01", "name": "roost"}, - {"id": 10482, "synset": "rope.n.01", "name": "rope"}, - {"id": 10483, "synset": "rope_bridge.n.01", "name": "rope_bridge"}, - {"id": 10484, "synset": "rope_tow.n.01", "name": "rope_tow"}, - {"id": 10485, "synset": "rose_water.n.01", "name": "rose_water"}, - {"id": 10486, "synset": "rose_window.n.01", "name": "rose_window"}, - {"id": 10487, "synset": "rosin_bag.n.01", "name": "rosin_bag"}, - {"id": 10488, "synset": "rotary_actuator.n.01", "name": "rotary_actuator"}, - {"id": 10489, "synset": "rotary_engine.n.01", "name": "rotary_engine"}, - {"id": 10490, "synset": "rotary_press.n.01", "name": "rotary_press"}, - {"id": 10491, "synset": "rotating_mechanism.n.01", "name": "rotating_mechanism"}, - {"id": 10492, "synset": "rotating_shaft.n.01", "name": "rotating_shaft"}, - {"id": 10493, "synset": "rotisserie.n.02", "name": "rotisserie"}, - {"id": 10494, "synset": "rotisserie.n.01", "name": "rotisserie"}, - {"id": 10495, "synset": "rotor.n.03", "name": "rotor"}, - {"id": 10496, "synset": "rotor.n.01", "name": "rotor"}, - {"id": 10497, "synset": "rotor.n.02", "name": "rotor"}, - {"id": 10498, "synset": "rotor_blade.n.01", "name": "rotor_blade"}, - {"id": 10499, "synset": "rotor_head.n.01", "name": "rotor_head"}, - {"id": 10500, "synset": "rotunda.n.02", "name": "rotunda"}, - {"id": 10501, "synset": "rotunda.n.01", "name": "rotunda"}, - {"id": 10502, "synset": "rouge.n.01", "name": "rouge"}, - {"id": 10503, "synset": "roughcast.n.02", "name": "roughcast"}, - {"id": 10504, "synset": "rouleau.n.02", "name": "rouleau"}, - {"id": 10505, "synset": "roulette.n.02", "name": "roulette"}, - {"id": 10506, "synset": "roulette_ball.n.01", "name": "roulette_ball"}, - {"id": 10507, "synset": "roulette_wheel.n.01", "name": "roulette_wheel"}, - {"id": 10508, "synset": "round.n.01", "name": "round"}, - {"id": 10509, "synset": "round_arch.n.01", "name": "round_arch"}, - {"id": 10510, "synset": "round-bottom_flask.n.01", "name": "round-bottom_flask"}, - {"id": 10511, "synset": "roundel.n.02", "name": "roundel"}, - {"id": 10512, "synset": "round_file.n.01", "name": "round_file"}, - {"id": 10513, "synset": "roundhouse.n.01", "name": "roundhouse"}, - {"id": 10514, "synset": "router.n.03", "name": "router"}, - {"id": 10515, "synset": "router_plane.n.01", "name": "router_plane"}, - {"id": 10516, "synset": "rowel.n.01", "name": "rowel"}, - {"id": 10517, "synset": "row_house.n.01", "name": "row_house"}, - {"id": 10518, "synset": "rowing_boat.n.01", "name": "rowing_boat"}, - {"id": 10519, "synset": "rowlock_arch.n.01", "name": "rowlock_arch"}, - {"id": 10520, "synset": "royal.n.01", "name": "royal"}, - {"id": 10521, "synset": "royal_mast.n.01", "name": "royal_mast"}, - {"id": 10522, "synset": "rubber_boot.n.01", "name": "rubber_boot"}, - {"id": 10523, "synset": "rubber_bullet.n.01", "name": "rubber_bullet"}, - {"id": 10524, "synset": "rubber_eraser.n.01", "name": "rubber_eraser"}, - {"id": 10525, "synset": "rudder.n.02", "name": "rudder"}, - {"id": 10526, "synset": "rudder.n.01", "name": "rudder"}, - {"id": 10527, "synset": "rudder_blade.n.01", "name": "rudder_blade"}, - {"id": 10528, "synset": "rug.n.01", "name": "rug"}, - {"id": 10529, "synset": "rugby_ball.n.01", "name": "rugby_ball"}, - {"id": 10530, "synset": "ruin.n.02", "name": "ruin"}, - {"id": 10531, "synset": "rule.n.12", "name": "rule"}, - {"id": 10532, "synset": "rumble.n.02", "name": "rumble"}, - {"id": 10533, "synset": "rumble_seat.n.01", "name": "rumble_seat"}, - {"id": 10534, "synset": "rummer.n.01", "name": "rummer"}, - {"id": 10535, "synset": "rumpus_room.n.01", "name": "rumpus_room"}, - {"id": 10536, "synset": "runcible_spoon.n.01", "name": "runcible_spoon"}, - {"id": 10537, "synset": "rundle.n.01", "name": "rundle"}, - {"id": 10538, "synset": "running_shoe.n.01", "name": "running_shoe"}, - {"id": 10539, "synset": "running_suit.n.01", "name": "running_suit"}, - {"id": 10540, "synset": "runway.n.04", "name": "runway"}, - {"id": 10541, "synset": "rushlight.n.01", "name": "rushlight"}, - {"id": 10542, "synset": "russet.n.01", "name": "russet"}, - {"id": 10543, "synset": "rya.n.01", "name": "rya"}, - {"id": 10544, "synset": "saber.n.01", "name": "saber"}, - {"id": 10545, "synset": "saber_saw.n.01", "name": "saber_saw"}, - {"id": 10546, "synset": "sable.n.04", "name": "sable"}, - {"id": 10547, "synset": "sable.n.01", "name": "sable"}, - {"id": 10548, "synset": "sable_coat.n.01", "name": "sable_coat"}, - {"id": 10549, "synset": "sabot.n.01", "name": "sabot"}, - {"id": 10550, "synset": "sachet.n.01", "name": "sachet"}, - {"id": 10551, "synset": "sack.n.05", "name": "sack"}, - {"id": 10552, "synset": "sackbut.n.01", "name": "sackbut"}, - {"id": 10553, "synset": "sackcloth.n.02", "name": "sackcloth"}, - {"id": 10554, "synset": "sackcloth.n.01", "name": "sackcloth"}, - {"id": 10555, "synset": "sack_coat.n.01", "name": "sack_coat"}, - {"id": 10556, "synset": "sacking.n.01", "name": "sacking"}, - {"id": 10557, "synset": "saddle_oxford.n.01", "name": "saddle_oxford"}, - {"id": 10558, "synset": "saddlery.n.02", "name": "saddlery"}, - {"id": 10559, "synset": "saddle_seat.n.01", "name": "saddle_seat"}, - {"id": 10560, "synset": "saddle_stitch.n.01", "name": "saddle_stitch"}, - {"id": 10561, "synset": "safe.n.01", "name": "safe"}, - {"id": 10562, "synset": "safe.n.02", "name": "safe"}, - {"id": 10563, "synset": "safe-deposit.n.01", "name": "safe-deposit"}, - {"id": 10564, "synset": "safe_house.n.01", "name": "safe_house"}, - {"id": 10565, "synset": "safety_arch.n.01", "name": "safety_arch"}, - {"id": 10566, "synset": "safety_belt.n.01", "name": "safety_belt"}, - {"id": 10567, "synset": "safety_bicycle.n.01", "name": "safety_bicycle"}, - {"id": 10568, "synset": "safety_bolt.n.01", "name": "safety_bolt"}, - {"id": 10569, "synset": "safety_curtain.n.01", "name": "safety_curtain"}, - {"id": 10570, "synset": "safety_fuse.n.01", "name": "safety_fuse"}, - {"id": 10571, "synset": "safety_lamp.n.01", "name": "safety_lamp"}, - {"id": 10572, "synset": "safety_match.n.01", "name": "safety_match"}, - {"id": 10573, "synset": "safety_net.n.02", "name": "safety_net"}, - {"id": 10574, "synset": "safety_rail.n.01", "name": "safety_rail"}, - {"id": 10575, "synset": "safety_razor.n.01", "name": "safety_razor"}, - {"id": 10576, "synset": "safety_valve.n.01", "name": "safety_valve"}, - {"id": 10577, "synset": "sail.n.03", "name": "sail"}, - {"id": 10578, "synset": "sailboat.n.01", "name": "sailboat"}, - {"id": 10579, "synset": "sailcloth.n.01", "name": "sailcloth"}, - {"id": 10580, "synset": "sailing_vessel.n.01", "name": "sailing_vessel"}, - {"id": 10581, "synset": "sailing_warship.n.01", "name": "sailing_warship"}, - {"id": 10582, "synset": "sailor_cap.n.01", "name": "sailor_cap"}, - {"id": 10583, "synset": "sailor_suit.n.01", "name": "sailor_suit"}, - {"id": 10584, "synset": "salad_bar.n.01", "name": "salad_bar"}, - {"id": 10585, "synset": "salad_bowl.n.02", "name": "salad_bowl"}, - {"id": 10586, "synset": "salinometer.n.01", "name": "salinometer"}, - {"id": 10587, "synset": "sallet.n.01", "name": "sallet"}, - {"id": 10588, "synset": "salon.n.03", "name": "salon"}, - {"id": 10589, "synset": "salon.n.01", "name": "salon"}, - {"id": 10590, "synset": "salon.n.02", "name": "salon"}, - {"id": 10591, "synset": "saltbox.n.01", "name": "saltbox"}, - {"id": 10592, "synset": "saltcellar.n.01", "name": "saltcellar"}, - {"id": 10593, "synset": "saltworks.n.01", "name": "saltworks"}, - {"id": 10594, "synset": "salver.n.01", "name": "salver"}, - {"id": 10595, "synset": "salwar.n.01", "name": "salwar"}, - {"id": 10596, "synset": "sam_browne_belt.n.01", "name": "Sam_Browne_belt"}, - {"id": 10597, "synset": "samisen.n.01", "name": "samisen"}, - {"id": 10598, "synset": "samite.n.01", "name": "samite"}, - {"id": 10599, "synset": "samovar.n.01", "name": "samovar"}, - {"id": 10600, "synset": "sampan.n.01", "name": "sampan"}, - {"id": 10601, "synset": "sandbag.n.01", "name": "sandbag"}, - {"id": 10602, "synset": "sandblaster.n.01", "name": "sandblaster"}, - {"id": 10603, "synset": "sandbox.n.01", "name": "sandbox"}, - {"id": 10604, "synset": "sandglass.n.01", "name": "sandglass"}, - {"id": 10605, "synset": "sand_wedge.n.01", "name": "sand_wedge"}, - {"id": 10606, "synset": "sandwich_board.n.01", "name": "sandwich_board"}, - {"id": 10607, "synset": "sanitary_napkin.n.01", "name": "sanitary_napkin"}, - {"id": 10608, "synset": "cling_film.n.01", "name": "cling_film"}, - {"id": 10609, "synset": "sarcenet.n.01", "name": "sarcenet"}, - {"id": 10610, "synset": "sarcophagus.n.01", "name": "sarcophagus"}, - {"id": 10611, "synset": "sari.n.01", "name": "sari"}, - {"id": 10612, "synset": "sarong.n.01", "name": "sarong"}, - {"id": 10613, "synset": "sash.n.01", "name": "sash"}, - {"id": 10614, "synset": "sash_fastener.n.01", "name": "sash_fastener"}, - {"id": 10615, "synset": "sash_window.n.01", "name": "sash_window"}, - {"id": 10616, "synset": "sateen.n.01", "name": "sateen"}, - {"id": 10617, "synset": "satellite.n.01", "name": "satellite"}, - {"id": 10618, "synset": "satellite_receiver.n.01", "name": "satellite_receiver"}, - {"id": 10619, "synset": "satellite_television.n.01", "name": "satellite_television"}, - {"id": 10620, "synset": "satellite_transmitter.n.01", "name": "satellite_transmitter"}, - {"id": 10621, "synset": "satin.n.01", "name": "satin"}, - {"id": 10622, "synset": "saturday_night_special.n.01", "name": "Saturday_night_special"}, - {"id": 10623, "synset": "saucepot.n.01", "name": "saucepot"}, - {"id": 10624, "synset": "sauna.n.01", "name": "sauna"}, - {"id": 10625, "synset": "savings_bank.n.02", "name": "savings_bank"}, - {"id": 10626, "synset": "saw.n.02", "name": "saw"}, - {"id": 10627, "synset": "sawed-off_shotgun.n.01", "name": "sawed-off_shotgun"}, - {"id": 10628, "synset": "sawmill.n.01", "name": "sawmill"}, - {"id": 10629, "synset": "saw_set.n.01", "name": "saw_set"}, - {"id": 10630, "synset": "saxhorn.n.01", "name": "saxhorn"}, - {"id": 10631, "synset": "scabbard.n.01", "name": "scabbard"}, - {"id": 10632, "synset": "scaffolding.n.01", "name": "scaffolding"}, - {"id": 10633, "synset": "scale.n.08", "name": "scale"}, - {"id": 10634, "synset": "scaler.n.01", "name": "scaler"}, - {"id": 10635, "synset": "scaling_ladder.n.01", "name": "scaling_ladder"}, - {"id": 10636, "synset": "scalpel.n.01", "name": "scalpel"}, - {"id": 10637, "synset": "scanner.n.04", "name": "scanner"}, - {"id": 10638, "synset": "scanner.n.03", "name": "scanner"}, - {"id": 10639, "synset": "scanner.n.02", "name": "scanner"}, - {"id": 10640, "synset": "scantling.n.01", "name": "scantling"}, - {"id": 10641, "synset": "scarf_joint.n.01", "name": "scarf_joint"}, - {"id": 10642, "synset": "scatter_rug.n.01", "name": "scatter_rug"}, - {"id": 10643, "synset": "scauper.n.01", "name": "scauper"}, - {"id": 10644, "synset": "schmidt_telescope.n.01", "name": "Schmidt_telescope"}, - {"id": 10645, "synset": "school.n.02", "name": "school"}, - {"id": 10646, "synset": "schoolbag.n.01", "name": "schoolbag"}, - {"id": 10647, "synset": "school_bell.n.01", "name": "school_bell"}, - {"id": 10648, "synset": "school_ship.n.01", "name": "school_ship"}, - {"id": 10649, "synset": "school_system.n.01", "name": "school_system"}, - {"id": 10650, "synset": "schooner.n.02", "name": "schooner"}, - {"id": 10651, "synset": "schooner.n.01", "name": "schooner"}, - {"id": 10652, "synset": "scientific_instrument.n.01", "name": "scientific_instrument"}, - {"id": 10653, "synset": "scimitar.n.01", "name": "scimitar"}, - {"id": 10654, "synset": "scintillation_counter.n.01", "name": "scintillation_counter"}, - {"id": 10655, "synset": "sclerometer.n.01", "name": "sclerometer"}, - {"id": 10656, "synset": "scoinson_arch.n.01", "name": "scoinson_arch"}, - {"id": 10657, "synset": "sconce.n.04", "name": "sconce"}, - {"id": 10658, "synset": "sconce.n.03", "name": "sconce"}, - {"id": 10659, "synset": "scoop.n.06", "name": "scoop"}, - {"id": 10660, "synset": "scooter.n.02", "name": "scooter"}, - {"id": 10661, "synset": "scouring_pad.n.01", "name": "scouring_pad"}, - {"id": 10662, "synset": "scow.n.02", "name": "scow"}, - {"id": 10663, "synset": "scow.n.01", "name": "scow"}, - {"id": 10664, "synset": "scratcher.n.03", "name": "scratcher"}, - {"id": 10665, "synset": "screen.n.05", "name": "screen"}, - {"id": 10666, "synset": "screen.n.04", "name": "screen"}, - {"id": 10667, "synset": "screen.n.09", "name": "screen"}, - {"id": 10668, "synset": "screen.n.03", "name": "screen"}, - {"id": 10669, "synset": "screen_door.n.01", "name": "screen_door"}, - {"id": 10670, "synset": "screening.n.02", "name": "screening"}, - {"id": 10671, "synset": "screw.n.04", "name": "screw"}, - {"id": 10672, "synset": "screw.n.03", "name": "screw"}, - {"id": 10673, "synset": "screw.n.02", "name": "screw"}, - {"id": 10674, "synset": "screw_eye.n.01", "name": "screw_eye"}, - {"id": 10675, "synset": "screw_key.n.01", "name": "screw_key"}, - {"id": 10676, "synset": "screw_thread.n.01", "name": "screw_thread"}, - {"id": 10677, "synset": "screwtop.n.01", "name": "screwtop"}, - {"id": 10678, "synset": "screw_wrench.n.01", "name": "screw_wrench"}, - {"id": 10679, "synset": "scriber.n.01", "name": "scriber"}, - {"id": 10680, "synset": "scrim.n.01", "name": "scrim"}, - {"id": 10681, "synset": "scrimshaw.n.01", "name": "scrimshaw"}, - {"id": 10682, "synset": "scriptorium.n.01", "name": "scriptorium"}, - {"id": 10683, "synset": "scrubber.n.03", "name": "scrubber"}, - {"id": 10684, "synset": "scrub_plane.n.01", "name": "scrub_plane"}, - {"id": 10685, "synset": "scuffer.n.01", "name": "scuffer"}, - {"id": 10686, "synset": "scuffle.n.02", "name": "scuffle"}, - {"id": 10687, "synset": "scull.n.02", "name": "scull"}, - {"id": 10688, "synset": "scull.n.01", "name": "scull"}, - {"id": 10689, "synset": "scullery.n.01", "name": "scullery"}, - {"id": 10690, "synset": "scuttle.n.01", "name": "scuttle"}, - {"id": 10691, "synset": "scyphus.n.01", "name": "scyphus"}, - {"id": 10692, "synset": "scythe.n.01", "name": "scythe"}, - {"id": 10693, "synset": "seabag.n.01", "name": "seabag"}, - {"id": 10694, "synset": "sea_boat.n.01", "name": "sea_boat"}, - {"id": 10695, "synset": "sea_chest.n.01", "name": "sea_chest"}, - {"id": 10696, "synset": "sealing_wax.n.01", "name": "sealing_wax"}, - {"id": 10697, "synset": "sealskin.n.02", "name": "sealskin"}, - {"id": 10698, "synset": "seam.n.01", "name": "seam"}, - {"id": 10699, "synset": "searchlight.n.01", "name": "searchlight"}, - {"id": 10700, "synset": "searing_iron.n.01", "name": "searing_iron"}, - {"id": 10701, "synset": "seat.n.04", "name": "seat"}, - {"id": 10702, "synset": "seat.n.03", "name": "seat"}, - {"id": 10703, "synset": "seat.n.09", "name": "seat"}, - {"id": 10704, "synset": "seat_belt.n.01", "name": "seat_belt"}, - {"id": 10705, "synset": "secateurs.n.01", "name": "secateurs"}, - {"id": 10706, "synset": "secondary_coil.n.01", "name": "secondary_coil"}, - {"id": 10707, "synset": "second_balcony.n.01", "name": "second_balcony"}, - {"id": 10708, "synset": "second_base.n.01", "name": "second_base"}, - {"id": 10709, "synset": "second_hand.n.02", "name": "second_hand"}, - {"id": 10710, "synset": "secretary.n.04", "name": "secretary"}, - {"id": 10711, "synset": "sectional.n.01", "name": "sectional"}, - {"id": 10712, "synset": "security_blanket.n.02", "name": "security_blanket"}, - {"id": 10713, "synset": "security_system.n.02", "name": "security_system"}, - {"id": 10714, "synset": "security_system.n.01", "name": "security_system"}, - {"id": 10715, "synset": "sedan.n.01", "name": "sedan"}, - {"id": 10716, "synset": "sedan.n.02", "name": "sedan"}, - {"id": 10717, "synset": "seeder.n.02", "name": "seeder"}, - {"id": 10718, "synset": "seeker.n.02", "name": "seeker"}, - {"id": 10719, "synset": "seersucker.n.01", "name": "seersucker"}, - {"id": 10720, "synset": "segmental_arch.n.01", "name": "segmental_arch"}, - {"id": 10721, "synset": "segway.n.01", "name": "Segway"}, - {"id": 10722, "synset": "seidel.n.01", "name": "seidel"}, - {"id": 10723, "synset": "seine.n.02", "name": "seine"}, - {"id": 10724, "synset": "seismograph.n.01", "name": "seismograph"}, - {"id": 10725, "synset": "selector.n.02", "name": "selector"}, - {"id": 10726, "synset": "selenium_cell.n.01", "name": "selenium_cell"}, - {"id": 10727, "synset": "self-propelled_vehicle.n.01", "name": "self-propelled_vehicle"}, - { - "id": 10728, - "synset": "self-registering_thermometer.n.01", - "name": "self-registering_thermometer", - }, - {"id": 10729, "synset": "self-starter.n.02", "name": "self-starter"}, - {"id": 10730, "synset": "selsyn.n.01", "name": "selsyn"}, - {"id": 10731, "synset": "selvage.n.02", "name": "selvage"}, - {"id": 10732, "synset": "semaphore.n.01", "name": "semaphore"}, - {"id": 10733, "synset": "semiautomatic_firearm.n.01", "name": "semiautomatic_firearm"}, - {"id": 10734, "synset": "semiautomatic_pistol.n.01", "name": "semiautomatic_pistol"}, - {"id": 10735, "synset": "semiconductor_device.n.01", "name": "semiconductor_device"}, - {"id": 10736, "synset": "semi-detached_house.n.01", "name": "semi-detached_house"}, - {"id": 10737, "synset": "semigloss.n.01", "name": "semigloss"}, - {"id": 10738, "synset": "semitrailer.n.01", "name": "semitrailer"}, - {"id": 10739, "synset": "sennit.n.01", "name": "sennit"}, - {"id": 10740, "synset": "sensitometer.n.01", "name": "sensitometer"}, - {"id": 10741, "synset": "sentry_box.n.01", "name": "sentry_box"}, - {"id": 10742, "synset": "separate.n.02", "name": "separate"}, - {"id": 10743, "synset": "septic_tank.n.01", "name": "septic_tank"}, - {"id": 10744, "synset": "sequence.n.03", "name": "sequence"}, - {"id": 10745, "synset": "sequencer.n.01", "name": "sequencer"}, - {"id": 10746, "synset": "serape.n.01", "name": "serape"}, - {"id": 10747, "synset": "serge.n.01", "name": "serge"}, - {"id": 10748, "synset": "serger.n.01", "name": "serger"}, - {"id": 10749, "synset": "serial_port.n.01", "name": "serial_port"}, - {"id": 10750, "synset": "serpent.n.03", "name": "serpent"}, - {"id": 10751, "synset": "serration.n.03", "name": "serration"}, - {"id": 10752, "synset": "server.n.04", "name": "server"}, - {"id": 10753, "synset": "server.n.03", "name": "server"}, - {"id": 10754, "synset": "service_club.n.02", "name": "service_club"}, - {"id": 10755, "synset": "serving_cart.n.01", "name": "serving_cart"}, - {"id": 10756, "synset": "serving_dish.n.01", "name": "serving_dish"}, - {"id": 10757, "synset": "servo.n.01", "name": "servo"}, - {"id": 10758, "synset": "set.n.13", "name": "set"}, - {"id": 10759, "synset": "set_gun.n.01", "name": "set_gun"}, - {"id": 10760, "synset": "setscrew.n.02", "name": "setscrew"}, - {"id": 10761, "synset": "setscrew.n.01", "name": "setscrew"}, - {"id": 10762, "synset": "set_square.n.01", "name": "set_square"}, - {"id": 10763, "synset": "settee.n.02", "name": "settee"}, - {"id": 10764, "synset": "settle.n.01", "name": "settle"}, - {"id": 10765, "synset": "settlement_house.n.01", "name": "settlement_house"}, - {"id": 10766, "synset": "seventy-eight.n.02", "name": "seventy-eight"}, - { - "id": 10767, - "synset": "seven_wonders_of_the_ancient_world.n.01", - "name": "Seven_Wonders_of_the_Ancient_World", - }, - {"id": 10768, "synset": "sewage_disposal_plant.n.01", "name": "sewage_disposal_plant"}, - {"id": 10769, "synset": "sewer.n.01", "name": "sewer"}, - {"id": 10770, "synset": "sewing_basket.n.01", "name": "sewing_basket"}, - {"id": 10771, "synset": "sewing_kit.n.01", "name": "sewing_kit"}, - {"id": 10772, "synset": "sewing_needle.n.01", "name": "sewing_needle"}, - {"id": 10773, "synset": "sewing_room.n.01", "name": "sewing_room"}, - {"id": 10774, "synset": "sextant.n.02", "name": "sextant"}, - {"id": 10775, "synset": "sgraffito.n.01", "name": "sgraffito"}, - {"id": 10776, "synset": "shackle.n.01", "name": "shackle"}, - {"id": 10777, "synset": "shackle.n.02", "name": "shackle"}, - {"id": 10778, "synset": "shade.n.03", "name": "shade"}, - {"id": 10779, "synset": "shadow_box.n.01", "name": "shadow_box"}, - {"id": 10780, "synset": "shaft.n.03", "name": "shaft"}, - {"id": 10781, "synset": "shag_rug.n.01", "name": "shag_rug"}, - {"id": 10782, "synset": "shank.n.04", "name": "shank"}, - {"id": 10783, "synset": "shank.n.03", "name": "shank"}, - {"id": 10784, "synset": "shantung.n.01", "name": "shantung"}, - {"id": 10785, "synset": "shaper.n.02", "name": "shaper"}, - {"id": 10786, "synset": "shaping_tool.n.01", "name": "shaping_tool"}, - {"id": 10787, "synset": "sharkskin.n.01", "name": "sharkskin"}, - {"id": 10788, "synset": "shaving_brush.n.01", "name": "shaving_brush"}, - {"id": 10789, "synset": "shaving_foam.n.01", "name": "shaving_foam"}, - {"id": 10790, "synset": "shawm.n.01", "name": "shawm"}, - {"id": 10791, "synset": "sheath.n.01", "name": "sheath"}, - {"id": 10792, "synset": "sheathing.n.01", "name": "sheathing"}, - {"id": 10793, "synset": "shed.n.01", "name": "shed"}, - {"id": 10794, "synset": "sheep_bell.n.01", "name": "sheep_bell"}, - {"id": 10795, "synset": "sheepshank.n.01", "name": "sheepshank"}, - {"id": 10796, "synset": "sheepskin_coat.n.01", "name": "sheepskin_coat"}, - {"id": 10797, "synset": "sheepwalk.n.01", "name": "sheepwalk"}, - {"id": 10798, "synset": "sheet.n.03", "name": "sheet"}, - {"id": 10799, "synset": "sheet_bend.n.01", "name": "sheet_bend"}, - {"id": 10800, "synset": "sheeting.n.01", "name": "sheeting"}, - {"id": 10801, "synset": "sheet_pile.n.01", "name": "sheet_pile"}, - {"id": 10802, "synset": "sheetrock.n.01", "name": "Sheetrock"}, - {"id": 10803, "synset": "shelf.n.01", "name": "shelf"}, - {"id": 10804, "synset": "shelf_bracket.n.01", "name": "shelf_bracket"}, - {"id": 10805, "synset": "shell.n.01", "name": "shell"}, - {"id": 10806, "synset": "shell.n.08", "name": "shell"}, - {"id": 10807, "synset": "shell.n.07", "name": "shell"}, - {"id": 10808, "synset": "shellac.n.02", "name": "shellac"}, - {"id": 10809, "synset": "shelter.n.01", "name": "shelter"}, - {"id": 10810, "synset": "shelter.n.02", "name": "shelter"}, - {"id": 10811, "synset": "shelter.n.05", "name": "shelter"}, - {"id": 10812, "synset": "sheltered_workshop.n.01", "name": "sheltered_workshop"}, - {"id": 10813, "synset": "sheraton.n.01", "name": "Sheraton"}, - {"id": 10814, "synset": "shield.n.01", "name": "shield"}, - {"id": 10815, "synset": "shielding.n.03", "name": "shielding"}, - {"id": 10816, "synset": "shift_key.n.01", "name": "shift_key"}, - {"id": 10817, "synset": "shillelagh.n.01", "name": "shillelagh"}, - {"id": 10818, "synset": "shim.n.01", "name": "shim"}, - {"id": 10819, "synset": "shingle.n.03", "name": "shingle"}, - {"id": 10820, "synset": "shin_guard.n.01", "name": "shin_guard"}, - {"id": 10821, "synset": "ship.n.01", "name": "ship"}, - {"id": 10822, "synset": "shipboard_system.n.01", "name": "shipboard_system"}, - {"id": 10823, "synset": "shipping.n.02", "name": "shipping"}, - {"id": 10824, "synset": "shipping_room.n.01", "name": "shipping_room"}, - { - "id": 10825, - "synset": "ship-towed_long-range_acoustic_detection_system.n.01", - "name": "ship-towed_long-range_acoustic_detection_system", - }, - {"id": 10826, "synset": "shipwreck.n.01", "name": "shipwreck"}, - {"id": 10827, "synset": "shirt_button.n.01", "name": "shirt_button"}, - {"id": 10828, "synset": "shirtdress.n.01", "name": "shirtdress"}, - {"id": 10829, "synset": "shirtfront.n.01", "name": "shirtfront"}, - {"id": 10830, "synset": "shirting.n.01", "name": "shirting"}, - {"id": 10831, "synset": "shirtsleeve.n.01", "name": "shirtsleeve"}, - {"id": 10832, "synset": "shirttail.n.02", "name": "shirttail"}, - {"id": 10833, "synset": "shirtwaist.n.01", "name": "shirtwaist"}, - {"id": 10834, "synset": "shiv.n.01", "name": "shiv"}, - {"id": 10835, "synset": "shock_absorber.n.01", "name": "shock_absorber"}, - {"id": 10836, "synset": "shoe.n.02", "name": "shoe"}, - {"id": 10837, "synset": "shoebox.n.02", "name": "shoebox"}, - {"id": 10838, "synset": "shoehorn.n.01", "name": "shoehorn"}, - {"id": 10839, "synset": "shoe_shop.n.01", "name": "shoe_shop"}, - {"id": 10840, "synset": "shoetree.n.01", "name": "shoetree"}, - {"id": 10841, "synset": "shofar.n.01", "name": "shofar"}, - {"id": 10842, "synset": "shoji.n.01", "name": "shoji"}, - {"id": 10843, "synset": "shooting_brake.n.01", "name": "shooting_brake"}, - {"id": 10844, "synset": "shooting_lodge.n.01", "name": "shooting_lodge"}, - {"id": 10845, "synset": "shooting_stick.n.01", "name": "shooting_stick"}, - {"id": 10846, "synset": "shop.n.01", "name": "shop"}, - {"id": 10847, "synset": "shop_bell.n.01", "name": "shop_bell"}, - {"id": 10848, "synset": "shopping_basket.n.01", "name": "shopping_basket"}, - {"id": 10849, "synset": "short_circuit.n.01", "name": "short_circuit"}, - {"id": 10850, "synset": "short_iron.n.01", "name": "short_iron"}, - {"id": 10851, "synset": "short_sleeve.n.01", "name": "short_sleeve"}, - { - "id": 10852, - "synset": "shortwave_diathermy_machine.n.01", - "name": "shortwave_diathermy_machine", - }, - {"id": 10853, "synset": "shot.n.12", "name": "shot"}, - {"id": 10854, "synset": "shotgun.n.01", "name": "shotgun"}, - {"id": 10855, "synset": "shotgun_shell.n.01", "name": "shotgun_shell"}, - {"id": 10856, "synset": "shot_tower.n.01", "name": "shot_tower"}, - {"id": 10857, "synset": "shoulder.n.04", "name": "shoulder"}, - {"id": 10858, "synset": "shouldered_arch.n.01", "name": "shouldered_arch"}, - {"id": 10859, "synset": "shoulder_holster.n.01", "name": "shoulder_holster"}, - {"id": 10860, "synset": "shoulder_pad.n.01", "name": "shoulder_pad"}, - {"id": 10861, "synset": "shoulder_patch.n.01", "name": "shoulder_patch"}, - {"id": 10862, "synset": "shovel.n.03", "name": "shovel"}, - {"id": 10863, "synset": "shovel_hat.n.01", "name": "shovel_hat"}, - {"id": 10864, "synset": "showboat.n.01", "name": "showboat"}, - {"id": 10865, "synset": "shower_room.n.01", "name": "shower_room"}, - {"id": 10866, "synset": "shower_stall.n.01", "name": "shower_stall"}, - {"id": 10867, "synset": "showroom.n.01", "name": "showroom"}, - {"id": 10868, "synset": "shrapnel.n.01", "name": "shrapnel"}, - {"id": 10869, "synset": "shrimper.n.01", "name": "shrimper"}, - {"id": 10870, "synset": "shrine.n.01", "name": "shrine"}, - {"id": 10871, "synset": "shrink-wrap.n.01", "name": "shrink-wrap"}, - {"id": 10872, "synset": "shunt.n.03", "name": "shunt"}, - {"id": 10873, "synset": "shunt.n.02", "name": "shunt"}, - {"id": 10874, "synset": "shunter.n.01", "name": "shunter"}, - {"id": 10875, "synset": "shutter.n.02", "name": "shutter"}, - {"id": 10876, "synset": "shutter.n.01", "name": "shutter"}, - {"id": 10877, "synset": "shuttle.n.03", "name": "shuttle"}, - {"id": 10878, "synset": "shuttle.n.02", "name": "shuttle"}, - {"id": 10879, "synset": "shuttle_bus.n.01", "name": "shuttle_bus"}, - {"id": 10880, "synset": "shuttlecock.n.01", "name": "shuttlecock"}, - {"id": 10881, "synset": "shuttle_helicopter.n.01", "name": "shuttle_helicopter"}, - {"id": 10882, "synset": "sibley_tent.n.01", "name": "Sibley_tent"}, - {"id": 10883, "synset": "sickbay.n.01", "name": "sickbay"}, - {"id": 10884, "synset": "sickbed.n.01", "name": "sickbed"}, - {"id": 10885, "synset": "sickle.n.01", "name": "sickle"}, - {"id": 10886, "synset": "sickroom.n.01", "name": "sickroom"}, - {"id": 10887, "synset": "sideboard.n.02", "name": "sideboard"}, - {"id": 10888, "synset": "sidecar.n.02", "name": "sidecar"}, - {"id": 10889, "synset": "side_chapel.n.01", "name": "side_chapel"}, - {"id": 10890, "synset": "sidelight.n.01", "name": "sidelight"}, - {"id": 10891, "synset": "sidesaddle.n.01", "name": "sidesaddle"}, - {"id": 10892, "synset": "sidewalk.n.01", "name": "sidewalk"}, - {"id": 10893, "synset": "sidewall.n.02", "name": "sidewall"}, - {"id": 10894, "synset": "side-wheeler.n.01", "name": "side-wheeler"}, - {"id": 10895, "synset": "sidewinder.n.02", "name": "sidewinder"}, - {"id": 10896, "synset": "sieve.n.01", "name": "sieve"}, - {"id": 10897, "synset": "sifter.n.01", "name": "sifter"}, - {"id": 10898, "synset": "sights.n.01", "name": "sights"}, - {"id": 10899, "synset": "sigmoidoscope.n.01", "name": "sigmoidoscope"}, - {"id": 10900, "synset": "signal_box.n.01", "name": "signal_box"}, - {"id": 10901, "synset": "signaling_device.n.01", "name": "signaling_device"}, - {"id": 10902, "synset": "silencer.n.02", "name": "silencer"}, - {"id": 10903, "synset": "silent_butler.n.01", "name": "silent_butler"}, - {"id": 10904, "synset": "silex.n.02", "name": "Silex"}, - {"id": 10905, "synset": "silk.n.01", "name": "silk"}, - {"id": 10906, "synset": "silks.n.01", "name": "silks"}, - {"id": 10907, "synset": "silver_plate.n.02", "name": "silver_plate"}, - {"id": 10908, "synset": "silverpoint.n.01", "name": "silverpoint"}, - {"id": 10909, "synset": "simple_pendulum.n.01", "name": "simple_pendulum"}, - {"id": 10910, "synset": "simulator.n.01", "name": "simulator"}, - {"id": 10911, "synset": "single_bed.n.01", "name": "single_bed"}, - {"id": 10912, "synset": "single-breasted_jacket.n.01", "name": "single-breasted_jacket"}, - {"id": 10913, "synset": "single-breasted_suit.n.01", "name": "single-breasted_suit"}, - {"id": 10914, "synset": "single_prop.n.01", "name": "single_prop"}, - {"id": 10915, "synset": "single-reed_instrument.n.01", "name": "single-reed_instrument"}, - {"id": 10916, "synset": "single-rotor_helicopter.n.01", "name": "single-rotor_helicopter"}, - {"id": 10917, "synset": "singlestick.n.01", "name": "singlestick"}, - {"id": 10918, "synset": "singlet.n.01", "name": "singlet"}, - {"id": 10919, "synset": "siren.n.04", "name": "siren"}, - {"id": 10920, "synset": "sister_ship.n.01", "name": "sister_ship"}, - {"id": 10921, "synset": "sitar.n.01", "name": "sitar"}, - {"id": 10922, "synset": "sitz_bath.n.01", "name": "sitz_bath"}, - {"id": 10923, "synset": "six-pack.n.01", "name": "six-pack"}, - {"id": 10924, "synset": "skate.n.01", "name": "skate"}, - {"id": 10925, "synset": "skeg.n.01", "name": "skeg"}, - {"id": 10926, "synset": "skein.n.01", "name": "skein"}, - {"id": 10927, "synset": "skeleton.n.04", "name": "skeleton"}, - {"id": 10928, "synset": "skeleton_key.n.01", "name": "skeleton_key"}, - {"id": 10929, "synset": "skep.n.02", "name": "skep"}, - {"id": 10930, "synset": "skep.n.01", "name": "skep"}, - {"id": 10931, "synset": "sketch.n.01", "name": "sketch"}, - {"id": 10932, "synset": "sketcher.n.02", "name": "sketcher"}, - {"id": 10933, "synset": "skew_arch.n.01", "name": "skew_arch"}, - {"id": 10934, "synset": "ski_binding.n.01", "name": "ski_binding"}, - {"id": 10935, "synset": "skibob.n.01", "name": "skibob"}, - {"id": 10936, "synset": "ski_cap.n.01", "name": "ski_cap"}, - {"id": 10937, "synset": "skidder.n.03", "name": "skidder"}, - {"id": 10938, "synset": "skid_lid.n.01", "name": "skid_lid"}, - {"id": 10939, "synset": "skiff.n.01", "name": "skiff"}, - {"id": 10940, "synset": "ski_jump.n.01", "name": "ski_jump"}, - {"id": 10941, "synset": "ski_lodge.n.01", "name": "ski_lodge"}, - {"id": 10942, "synset": "ski_mask.n.01", "name": "ski_mask"}, - {"id": 10943, "synset": "skimmer.n.02", "name": "skimmer"}, - {"id": 10944, "synset": "ski-plane.n.01", "name": "ski-plane"}, - {"id": 10945, "synset": "ski_rack.n.01", "name": "ski_rack"}, - {"id": 10946, "synset": "skirt.n.01", "name": "skirt"}, - {"id": 10947, "synset": "ski_tow.n.01", "name": "ski_tow"}, - {"id": 10948, "synset": "skivvies.n.01", "name": "Skivvies"}, - {"id": 10949, "synset": "skybox.n.01", "name": "skybox"}, - {"id": 10950, "synset": "skyhook.n.02", "name": "skyhook"}, - {"id": 10951, "synset": "skylight.n.01", "name": "skylight"}, - {"id": 10952, "synset": "skysail.n.01", "name": "skysail"}, - {"id": 10953, "synset": "skyscraper.n.01", "name": "skyscraper"}, - {"id": 10954, "synset": "skywalk.n.01", "name": "skywalk"}, - {"id": 10955, "synset": "slacks.n.01", "name": "slacks"}, - {"id": 10956, "synset": "slack_suit.n.01", "name": "slack_suit"}, - {"id": 10957, "synset": "slasher.n.02", "name": "slasher"}, - {"id": 10958, "synset": "slash_pocket.n.01", "name": "slash_pocket"}, - {"id": 10959, "synset": "slat.n.01", "name": "slat"}, - {"id": 10960, "synset": "slate.n.01", "name": "slate"}, - {"id": 10961, "synset": "slate_pencil.n.01", "name": "slate_pencil"}, - {"id": 10962, "synset": "slate_roof.n.01", "name": "slate_roof"}, - {"id": 10963, "synset": "sleeper.n.07", "name": "sleeper"}, - {"id": 10964, "synset": "sleeper.n.06", "name": "sleeper"}, - {"id": 10965, "synset": "sleeping_car.n.01", "name": "sleeping_car"}, - {"id": 10966, "synset": "sleeve.n.01", "name": "sleeve"}, - {"id": 10967, "synset": "sleeve.n.02", "name": "sleeve"}, - {"id": 10968, "synset": "sleigh_bed.n.01", "name": "sleigh_bed"}, - {"id": 10969, "synset": "sleigh_bell.n.01", "name": "sleigh_bell"}, - {"id": 10970, "synset": "slice_bar.n.01", "name": "slice_bar"}, - {"id": 10971, "synset": "slicer.n.03", "name": "slicer"}, - {"id": 10972, "synset": "slicer.n.02", "name": "slicer"}, - {"id": 10973, "synset": "slide.n.04", "name": "slide"}, - {"id": 10974, "synset": "slide_fastener.n.01", "name": "slide_fastener"}, - {"id": 10975, "synset": "slide_projector.n.01", "name": "slide_projector"}, - {"id": 10976, "synset": "slide_rule.n.01", "name": "slide_rule"}, - {"id": 10977, "synset": "slide_valve.n.01", "name": "slide_valve"}, - {"id": 10978, "synset": "sliding_door.n.01", "name": "sliding_door"}, - {"id": 10979, "synset": "sliding_seat.n.01", "name": "sliding_seat"}, - {"id": 10980, "synset": "sliding_window.n.01", "name": "sliding_window"}, - {"id": 10981, "synset": "sling.n.04", "name": "sling"}, - {"id": 10982, "synset": "slingback.n.01", "name": "slingback"}, - {"id": 10983, "synset": "slinger_ring.n.01", "name": "slinger_ring"}, - {"id": 10984, "synset": "slip_clutch.n.01", "name": "slip_clutch"}, - {"id": 10985, "synset": "slipcover.n.01", "name": "slipcover"}, - {"id": 10986, "synset": "slip-joint_pliers.n.01", "name": "slip-joint_pliers"}, - {"id": 10987, "synset": "slipknot.n.01", "name": "slipknot"}, - {"id": 10988, "synset": "slip-on.n.01", "name": "slip-on"}, - {"id": 10989, "synset": "slip_ring.n.01", "name": "slip_ring"}, - {"id": 10990, "synset": "slit_lamp.n.01", "name": "slit_lamp"}, - {"id": 10991, "synset": "slit_trench.n.01", "name": "slit_trench"}, - {"id": 10992, "synset": "sloop.n.01", "name": "sloop"}, - {"id": 10993, "synset": "sloop_of_war.n.01", "name": "sloop_of_war"}, - {"id": 10994, "synset": "slop_basin.n.01", "name": "slop_basin"}, - {"id": 10995, "synset": "slop_pail.n.01", "name": "slop_pail"}, - {"id": 10996, "synset": "slops.n.02", "name": "slops"}, - {"id": 10997, "synset": "slopshop.n.01", "name": "slopshop"}, - {"id": 10998, "synset": "slot.n.07", "name": "slot"}, - {"id": 10999, "synset": "slot_machine.n.01", "name": "slot_machine"}, - {"id": 11000, "synset": "sluice.n.01", "name": "sluice"}, - {"id": 11001, "synset": "smack.n.03", "name": "smack"}, - {"id": 11002, "synset": "small_boat.n.01", "name": "small_boat"}, - { - "id": 11003, - "synset": "small_computer_system_interface.n.01", - "name": "small_computer_system_interface", - }, - {"id": 11004, "synset": "small_ship.n.01", "name": "small_ship"}, - {"id": 11005, "synset": "small_stores.n.01", "name": "small_stores"}, - {"id": 11006, "synset": "smart_bomb.n.01", "name": "smart_bomb"}, - {"id": 11007, "synset": "smelling_bottle.n.01", "name": "smelling_bottle"}, - {"id": 11008, "synset": "smocking.n.01", "name": "smocking"}, - {"id": 11009, "synset": "smoke_bomb.n.01", "name": "smoke_bomb"}, - {"id": 11010, "synset": "smokehouse.n.01", "name": "smokehouse"}, - {"id": 11011, "synset": "smoker.n.03", "name": "smoker"}, - {"id": 11012, "synset": "smoke_screen.n.01", "name": "smoke_screen"}, - {"id": 11013, "synset": "smoking_room.n.01", "name": "smoking_room"}, - {"id": 11014, "synset": "smoothbore.n.01", "name": "smoothbore"}, - {"id": 11015, "synset": "smooth_plane.n.01", "name": "smooth_plane"}, - {"id": 11016, "synset": "snack_bar.n.01", "name": "snack_bar"}, - {"id": 11017, "synset": "snaffle.n.01", "name": "snaffle"}, - {"id": 11018, "synset": "snap.n.10", "name": "snap"}, - {"id": 11019, "synset": "snap_brim.n.01", "name": "snap_brim"}, - {"id": 11020, "synset": "snap-brim_hat.n.01", "name": "snap-brim_hat"}, - {"id": 11021, "synset": "snare.n.05", "name": "snare"}, - {"id": 11022, "synset": "snare_drum.n.01", "name": "snare_drum"}, - {"id": 11023, "synset": "snatch_block.n.01", "name": "snatch_block"}, - {"id": 11024, "synset": "snifter.n.01", "name": "snifter"}, - {"id": 11025, "synset": "sniper_rifle.n.01", "name": "sniper_rifle"}, - {"id": 11026, "synset": "snips.n.01", "name": "snips"}, - {"id": 11027, "synset": "sno-cat.n.01", "name": "Sno-cat"}, - {"id": 11028, "synset": "snood.n.01", "name": "snood"}, - {"id": 11029, "synset": "snorkel.n.02", "name": "snorkel"}, - {"id": 11030, "synset": "snorkel.n.01", "name": "snorkel"}, - {"id": 11031, "synset": "snowbank.n.01", "name": "snowbank"}, - {"id": 11032, "synset": "snowplow.n.01", "name": "snowplow"}, - {"id": 11033, "synset": "snowshoe.n.01", "name": "snowshoe"}, - {"id": 11034, "synset": "snowsuit.n.01", "name": "snowsuit"}, - {"id": 11035, "synset": "snow_thrower.n.01", "name": "snow_thrower"}, - {"id": 11036, "synset": "snuffbox.n.01", "name": "snuffbox"}, - {"id": 11037, "synset": "snuffer.n.01", "name": "snuffer"}, - {"id": 11038, "synset": "snuffers.n.01", "name": "snuffers"}, - {"id": 11039, "synset": "soapbox.n.01", "name": "soapbox"}, - {"id": 11040, "synset": "soap_dish.n.01", "name": "soap_dish"}, - {"id": 11041, "synset": "soap_dispenser.n.01", "name": "soap_dispenser"}, - {"id": 11042, "synset": "soap_pad.n.01", "name": "soap_pad"}, - {"id": 11043, "synset": "socket.n.02", "name": "socket"}, - {"id": 11044, "synset": "socket_wrench.n.01", "name": "socket_wrench"}, - {"id": 11045, "synset": "socle.n.01", "name": "socle"}, - {"id": 11046, "synset": "soda_can.n.01", "name": "soda_can"}, - {"id": 11047, "synset": "soda_fountain.n.02", "name": "soda_fountain"}, - {"id": 11048, "synset": "soda_fountain.n.01", "name": "soda_fountain"}, - {"id": 11049, "synset": "sod_house.n.01", "name": "sod_house"}, - {"id": 11050, "synset": "sodium-vapor_lamp.n.01", "name": "sodium-vapor_lamp"}, - {"id": 11051, "synset": "soffit.n.01", "name": "soffit"}, - {"id": 11052, "synset": "soft_pedal.n.01", "name": "soft_pedal"}, - {"id": 11053, "synset": "soil_pipe.n.01", "name": "soil_pipe"}, - {"id": 11054, "synset": "solar_cell.n.01", "name": "solar_cell"}, - {"id": 11055, "synset": "solar_dish.n.01", "name": "solar_dish"}, - {"id": 11056, "synset": "solar_heater.n.01", "name": "solar_heater"}, - {"id": 11057, "synset": "solar_house.n.01", "name": "solar_house"}, - {"id": 11058, "synset": "solar_telescope.n.01", "name": "solar_telescope"}, - {"id": 11059, "synset": "solar_thermal_system.n.01", "name": "solar_thermal_system"}, - {"id": 11060, "synset": "soldering_iron.n.01", "name": "soldering_iron"}, - {"id": 11061, "synset": "solenoid.n.01", "name": "solenoid"}, - {"id": 11062, "synset": "solleret.n.01", "name": "solleret"}, - {"id": 11063, "synset": "sonic_depth_finder.n.01", "name": "sonic_depth_finder"}, - {"id": 11064, "synset": "sonogram.n.01", "name": "sonogram"}, - {"id": 11065, "synset": "sonograph.n.01", "name": "sonograph"}, - {"id": 11066, "synset": "sorter.n.02", "name": "sorter"}, - {"id": 11067, "synset": "souk.n.01", "name": "souk"}, - {"id": 11068, "synset": "sound_bow.n.01", "name": "sound_bow"}, - {"id": 11069, "synset": "soundbox.n.01", "name": "soundbox"}, - {"id": 11070, "synset": "sound_camera.n.01", "name": "sound_camera"}, - {"id": 11071, "synset": "sounder.n.01", "name": "sounder"}, - {"id": 11072, "synset": "sound_film.n.01", "name": "sound_film"}, - {"id": 11073, "synset": "sounding_board.n.02", "name": "sounding_board"}, - {"id": 11074, "synset": "sounding_rocket.n.01", "name": "sounding_rocket"}, - {"id": 11075, "synset": "sound_recording.n.01", "name": "sound_recording"}, - {"id": 11076, "synset": "sound_spectrograph.n.01", "name": "sound_spectrograph"}, - {"id": 11077, "synset": "soup_ladle.n.01", "name": "soup_ladle"}, - {"id": 11078, "synset": "source_of_illumination.n.01", "name": "source_of_illumination"}, - {"id": 11079, "synset": "sourdine.n.02", "name": "sourdine"}, - {"id": 11080, "synset": "soutache.n.01", "name": "soutache"}, - {"id": 11081, "synset": "soutane.n.01", "name": "soutane"}, - {"id": 11082, "synset": "sou'wester.n.02", "name": "sou'wester"}, - {"id": 11083, "synset": "soybean_future.n.01", "name": "soybean_future"}, - {"id": 11084, "synset": "space_bar.n.01", "name": "space_bar"}, - {"id": 11085, "synset": "space_capsule.n.01", "name": "space_capsule"}, - {"id": 11086, "synset": "spacecraft.n.01", "name": "spacecraft"}, - {"id": 11087, "synset": "space_heater.n.01", "name": "space_heater"}, - {"id": 11088, "synset": "space_helmet.n.01", "name": "space_helmet"}, - {"id": 11089, "synset": "space_rocket.n.01", "name": "space_rocket"}, - {"id": 11090, "synset": "space_station.n.01", "name": "space_station"}, - {"id": 11091, "synset": "spacesuit.n.01", "name": "spacesuit"}, - {"id": 11092, "synset": "spade.n.02", "name": "spade"}, - {"id": 11093, "synset": "spade_bit.n.01", "name": "spade_bit"}, - {"id": 11094, "synset": "spaghetti_junction.n.01", "name": "spaghetti_junction"}, - {"id": 11095, "synset": "spandau.n.01", "name": "Spandau"}, - {"id": 11096, "synset": "spandex.n.01", "name": "spandex"}, - {"id": 11097, "synset": "spandrel.n.01", "name": "spandrel"}, - {"id": 11098, "synset": "spanker.n.02", "name": "spanker"}, - {"id": 11099, "synset": "spar.n.02", "name": "spar"}, - {"id": 11100, "synset": "sparge_pipe.n.01", "name": "sparge_pipe"}, - {"id": 11101, "synset": "spark_arrester.n.02", "name": "spark_arrester"}, - {"id": 11102, "synset": "spark_arrester.n.01", "name": "spark_arrester"}, - {"id": 11103, "synset": "spark_chamber.n.01", "name": "spark_chamber"}, - {"id": 11104, "synset": "spark_coil.n.01", "name": "spark_coil"}, - {"id": 11105, "synset": "spark_gap.n.01", "name": "spark_gap"}, - {"id": 11106, "synset": "spark_lever.n.01", "name": "spark_lever"}, - {"id": 11107, "synset": "spark_plug.n.01", "name": "spark_plug"}, - {"id": 11108, "synset": "sparkplug_wrench.n.01", "name": "sparkplug_wrench"}, - {"id": 11109, "synset": "spark_transmitter.n.01", "name": "spark_transmitter"}, - {"id": 11110, "synset": "spat.n.02", "name": "spat"}, - {"id": 11111, "synset": "spatula.n.01", "name": "spatula"}, - {"id": 11112, "synset": "speakerphone.n.01", "name": "speakerphone"}, - {"id": 11113, "synset": "speaking_trumpet.n.01", "name": "speaking_trumpet"}, - {"id": 11114, "synset": "spear.n.02", "name": "spear"}, - {"id": 11115, "synset": "specialty_store.n.01", "name": "specialty_store"}, - {"id": 11116, "synset": "specimen_bottle.n.01", "name": "specimen_bottle"}, - {"id": 11117, "synset": "spectacle.n.02", "name": "spectacle"}, - {"id": 11118, "synset": "spectator_pump.n.01", "name": "spectator_pump"}, - {"id": 11119, "synset": "spectrograph.n.01", "name": "spectrograph"}, - {"id": 11120, "synset": "spectrophotometer.n.01", "name": "spectrophotometer"}, - {"id": 11121, "synset": "spectroscope.n.01", "name": "spectroscope"}, - {"id": 11122, "synset": "speculum.n.02", "name": "speculum"}, - {"id": 11123, "synset": "speedboat.n.01", "name": "speedboat"}, - {"id": 11124, "synset": "speed_bump.n.01", "name": "speed_bump"}, - {"id": 11125, "synset": "speedometer.n.01", "name": "speedometer"}, - {"id": 11126, "synset": "speed_skate.n.01", "name": "speed_skate"}, - {"id": 11127, "synset": "spherometer.n.01", "name": "spherometer"}, - {"id": 11128, "synset": "sphygmomanometer.n.01", "name": "sphygmomanometer"}, - {"id": 11129, "synset": "spicemill.n.01", "name": "spicemill"}, - {"id": 11130, "synset": "spider.n.03", "name": "spider"}, - {"id": 11131, "synset": "spider_web.n.01", "name": "spider_web"}, - {"id": 11132, "synset": "spike.n.02", "name": "spike"}, - {"id": 11133, "synset": "spike.n.11", "name": "spike"}, - {"id": 11134, "synset": "spindle.n.04", "name": "spindle"}, - {"id": 11135, "synset": "spindle.n.03", "name": "spindle"}, - {"id": 11136, "synset": "spindle.n.02", "name": "spindle"}, - {"id": 11137, "synset": "spin_dryer.n.01", "name": "spin_dryer"}, - {"id": 11138, "synset": "spinet.n.02", "name": "spinet"}, - {"id": 11139, "synset": "spinet.n.01", "name": "spinet"}, - {"id": 11140, "synset": "spinnaker.n.01", "name": "spinnaker"}, - {"id": 11141, "synset": "spinner.n.03", "name": "spinner"}, - {"id": 11142, "synset": "spinning_frame.n.01", "name": "spinning_frame"}, - {"id": 11143, "synset": "spinning_jenny.n.01", "name": "spinning_jenny"}, - {"id": 11144, "synset": "spinning_machine.n.01", "name": "spinning_machine"}, - {"id": 11145, "synset": "spinning_rod.n.01", "name": "spinning_rod"}, - {"id": 11146, "synset": "spinning_wheel.n.01", "name": "spinning_wheel"}, - {"id": 11147, "synset": "spiral_bandage.n.01", "name": "spiral_bandage"}, - { - "id": 11148, - "synset": "spiral_ratchet_screwdriver.n.01", - "name": "spiral_ratchet_screwdriver", - }, - {"id": 11149, "synset": "spiral_spring.n.01", "name": "spiral_spring"}, - {"id": 11150, "synset": "spirit_lamp.n.01", "name": "spirit_lamp"}, - {"id": 11151, "synset": "spirit_stove.n.01", "name": "spirit_stove"}, - {"id": 11152, "synset": "spirometer.n.01", "name": "spirometer"}, - {"id": 11153, "synset": "spit.n.03", "name": "spit"}, - {"id": 11154, "synset": "spittoon.n.01", "name": "spittoon"}, - {"id": 11155, "synset": "splashboard.n.02", "name": "splashboard"}, - {"id": 11156, "synset": "splasher.n.01", "name": "splasher"}, - {"id": 11157, "synset": "splice.n.01", "name": "splice"}, - {"id": 11158, "synset": "splicer.n.03", "name": "splicer"}, - {"id": 11159, "synset": "splint.n.02", "name": "splint"}, - {"id": 11160, "synset": "split_rail.n.01", "name": "split_rail"}, - {"id": 11161, "synset": "spode.n.02", "name": "Spode"}, - {"id": 11162, "synset": "spoiler.n.05", "name": "spoiler"}, - {"id": 11163, "synset": "spoiler.n.04", "name": "spoiler"}, - {"id": 11164, "synset": "spoke.n.01", "name": "spoke"}, - {"id": 11165, "synset": "spokeshave.n.01", "name": "spokeshave"}, - {"id": 11166, "synset": "sponge_cloth.n.01", "name": "sponge_cloth"}, - {"id": 11167, "synset": "sponge_mop.n.01", "name": "sponge_mop"}, - {"id": 11168, "synset": "spoon.n.03", "name": "spoon"}, - {"id": 11169, "synset": "spork.n.01", "name": "Spork"}, - {"id": 11170, "synset": "sporran.n.01", "name": "sporran"}, - {"id": 11171, "synset": "sport_kite.n.01", "name": "sport_kite"}, - {"id": 11172, "synset": "sports_car.n.01", "name": "sports_car"}, - {"id": 11173, "synset": "sports_equipment.n.01", "name": "sports_equipment"}, - {"id": 11174, "synset": "sports_implement.n.01", "name": "sports_implement"}, - {"id": 11175, "synset": "sport_utility.n.01", "name": "sport_utility"}, - {"id": 11176, "synset": "spot.n.07", "name": "spot"}, - {"id": 11177, "synset": "spot_weld.n.01", "name": "spot_weld"}, - {"id": 11178, "synset": "spouter.n.02", "name": "spouter"}, - {"id": 11179, "synset": "sprag.n.01", "name": "sprag"}, - {"id": 11180, "synset": "spray_gun.n.01", "name": "spray_gun"}, - {"id": 11181, "synset": "spray_paint.n.01", "name": "spray_paint"}, - {"id": 11182, "synset": "spreader.n.01", "name": "spreader"}, - {"id": 11183, "synset": "sprig.n.02", "name": "sprig"}, - {"id": 11184, "synset": "spring.n.02", "name": "spring"}, - {"id": 11185, "synset": "spring_balance.n.01", "name": "spring_balance"}, - {"id": 11186, "synset": "springboard.n.01", "name": "springboard"}, - {"id": 11187, "synset": "sprinkler.n.01", "name": "sprinkler"}, - {"id": 11188, "synset": "sprinkler_system.n.01", "name": "sprinkler_system"}, - {"id": 11189, "synset": "sprit.n.01", "name": "sprit"}, - {"id": 11190, "synset": "spritsail.n.01", "name": "spritsail"}, - {"id": 11191, "synset": "sprocket.n.02", "name": "sprocket"}, - {"id": 11192, "synset": "sprocket.n.01", "name": "sprocket"}, - {"id": 11193, "synset": "spun_yarn.n.01", "name": "spun_yarn"}, - {"id": 11194, "synset": "spur.n.04", "name": "spur"}, - {"id": 11195, "synset": "spur_gear.n.01", "name": "spur_gear"}, - {"id": 11196, "synset": "sputnik.n.01", "name": "sputnik"}, - {"id": 11197, "synset": "spy_satellite.n.01", "name": "spy_satellite"}, - {"id": 11198, "synset": "squad_room.n.01", "name": "squad_room"}, - {"id": 11199, "synset": "square.n.08", "name": "square"}, - {"id": 11200, "synset": "square_knot.n.01", "name": "square_knot"}, - {"id": 11201, "synset": "square-rigger.n.01", "name": "square-rigger"}, - {"id": 11202, "synset": "square_sail.n.01", "name": "square_sail"}, - {"id": 11203, "synset": "squash_ball.n.01", "name": "squash_ball"}, - {"id": 11204, "synset": "squash_racket.n.01", "name": "squash_racket"}, - {"id": 11205, "synset": "squawk_box.n.01", "name": "squawk_box"}, - {"id": 11206, "synset": "squeegee.n.01", "name": "squeegee"}, - {"id": 11207, "synset": "squeezer.n.01", "name": "squeezer"}, - {"id": 11208, "synset": "squelch_circuit.n.01", "name": "squelch_circuit"}, - {"id": 11209, "synset": "squinch.n.01", "name": "squinch"}, - {"id": 11210, "synset": "stabilizer.n.03", "name": "stabilizer"}, - {"id": 11211, "synset": "stabilizer.n.02", "name": "stabilizer"}, - {"id": 11212, "synset": "stabilizer_bar.n.01", "name": "stabilizer_bar"}, - {"id": 11213, "synset": "stable.n.01", "name": "stable"}, - {"id": 11214, "synset": "stable_gear.n.01", "name": "stable_gear"}, - {"id": 11215, "synset": "stabling.n.01", "name": "stabling"}, - {"id": 11216, "synset": "stacks.n.02", "name": "stacks"}, - {"id": 11217, "synset": "staddle.n.01", "name": "staddle"}, - {"id": 11218, "synset": "stadium.n.01", "name": "stadium"}, - {"id": 11219, "synset": "stage.n.03", "name": "stage"}, - {"id": 11220, "synset": "stained-glass_window.n.01", "name": "stained-glass_window"}, - {"id": 11221, "synset": "stair-carpet.n.01", "name": "stair-carpet"}, - {"id": 11222, "synset": "stair-rod.n.01", "name": "stair-rod"}, - {"id": 11223, "synset": "stairwell.n.01", "name": "stairwell"}, - {"id": 11224, "synset": "stake.n.05", "name": "stake"}, - {"id": 11225, "synset": "stall.n.03", "name": "stall"}, - {"id": 11226, "synset": "stall.n.01", "name": "stall"}, - {"id": 11227, "synset": "stamp.n.08", "name": "stamp"}, - {"id": 11228, "synset": "stamp_mill.n.01", "name": "stamp_mill"}, - {"id": 11229, "synset": "stamping_machine.n.01", "name": "stamping_machine"}, - {"id": 11230, "synset": "stanchion.n.01", "name": "stanchion"}, - {"id": 11231, "synset": "stand.n.04", "name": "stand"}, - {"id": 11232, "synset": "standard.n.05", "name": "standard"}, - {"id": 11233, "synset": "standard_cell.n.01", "name": "standard_cell"}, - {"id": 11234, "synset": "standard_transmission.n.01", "name": "standard_transmission"}, - {"id": 11235, "synset": "standing_press.n.01", "name": "standing_press"}, - {"id": 11236, "synset": "stanhope.n.01", "name": "stanhope"}, - {"id": 11237, "synset": "stanley_steamer.n.01", "name": "Stanley_Steamer"}, - {"id": 11238, "synset": "staple.n.05", "name": "staple"}, - {"id": 11239, "synset": "staple.n.04", "name": "staple"}, - {"id": 11240, "synset": "staple_gun.n.01", "name": "staple_gun"}, - {"id": 11241, "synset": "starship.n.01", "name": "starship"}, - {"id": 11242, "synset": "starter.n.01", "name": "starter"}, - {"id": 11243, "synset": "starting_gate.n.01", "name": "starting_gate"}, - {"id": 11244, "synset": "stassano_furnace.n.01", "name": "Stassano_furnace"}, - {"id": 11245, "synset": "statehouse.n.01", "name": "Statehouse"}, - {"id": 11246, "synset": "stately_home.n.01", "name": "stately_home"}, - {"id": 11247, "synset": "state_prison.n.01", "name": "state_prison"}, - {"id": 11248, "synset": "stateroom.n.01", "name": "stateroom"}, - {"id": 11249, "synset": "static_tube.n.01", "name": "static_tube"}, - {"id": 11250, "synset": "station.n.01", "name": "station"}, - {"id": 11251, "synset": "stator.n.01", "name": "stator"}, - {"id": 11252, "synset": "stay.n.05", "name": "stay"}, - {"id": 11253, "synset": "staysail.n.01", "name": "staysail"}, - {"id": 11254, "synset": "steakhouse.n.01", "name": "steakhouse"}, - {"id": 11255, "synset": "stealth_aircraft.n.01", "name": "stealth_aircraft"}, - {"id": 11256, "synset": "stealth_bomber.n.01", "name": "stealth_bomber"}, - {"id": 11257, "synset": "stealth_fighter.n.01", "name": "stealth_fighter"}, - {"id": 11258, "synset": "steam_bath.n.01", "name": "steam_bath"}, - {"id": 11259, "synset": "steamboat.n.01", "name": "steamboat"}, - {"id": 11260, "synset": "steam_chest.n.01", "name": "steam_chest"}, - {"id": 11261, "synset": "steam_engine.n.01", "name": "steam_engine"}, - {"id": 11262, "synset": "steamer.n.03", "name": "steamer"}, - {"id": 11263, "synset": "steamer.n.02", "name": "steamer"}, - {"id": 11264, "synset": "steam_iron.n.01", "name": "steam_iron"}, - {"id": 11265, "synset": "steam_locomotive.n.01", "name": "steam_locomotive"}, - {"id": 11266, "synset": "steamroller.n.02", "name": "steamroller"}, - {"id": 11267, "synset": "steam_shovel.n.01", "name": "steam_shovel"}, - {"id": 11268, "synset": "steam_turbine.n.01", "name": "steam_turbine"}, - {"id": 11269, "synset": "steam_whistle.n.01", "name": "steam_whistle"}, - {"id": 11270, "synset": "steel.n.03", "name": "steel"}, - {"id": 11271, "synset": "steel_arch_bridge.n.01", "name": "steel_arch_bridge"}, - {"id": 11272, "synset": "steel_drum.n.01", "name": "steel_drum"}, - {"id": 11273, "synset": "steel_mill.n.01", "name": "steel_mill"}, - {"id": 11274, "synset": "steel-wool_pad.n.01", "name": "steel-wool_pad"}, - {"id": 11275, "synset": "steelyard.n.01", "name": "steelyard"}, - {"id": 11276, "synset": "steeple.n.01", "name": "steeple"}, - {"id": 11277, "synset": "steerage.n.01", "name": "steerage"}, - {"id": 11278, "synset": "steering_gear.n.01", "name": "steering_gear"}, - {"id": 11279, "synset": "steering_linkage.n.01", "name": "steering_linkage"}, - {"id": 11280, "synset": "steering_system.n.01", "name": "steering_system"}, - {"id": 11281, "synset": "stele.n.02", "name": "stele"}, - {"id": 11282, "synset": "stem-winder.n.01", "name": "stem-winder"}, - {"id": 11283, "synset": "stencil.n.01", "name": "stencil"}, - {"id": 11284, "synset": "sten_gun.n.01", "name": "Sten_gun"}, - {"id": 11285, "synset": "stenograph.n.02", "name": "stenograph"}, - {"id": 11286, "synset": "step.n.04", "name": "step"}, - {"id": 11287, "synset": "step-down_transformer.n.01", "name": "step-down_transformer"}, - {"id": 11288, "synset": "step-up_transformer.n.01", "name": "step-up_transformer"}, - {"id": 11289, "synset": "stereoscope.n.01", "name": "stereoscope"}, - {"id": 11290, "synset": "stern_chaser.n.01", "name": "stern_chaser"}, - {"id": 11291, "synset": "sternpost.n.01", "name": "sternpost"}, - {"id": 11292, "synset": "sternwheeler.n.01", "name": "sternwheeler"}, - {"id": 11293, "synset": "stethoscope.n.01", "name": "stethoscope"}, - {"id": 11294, "synset": "stewing_pan.n.01", "name": "stewing_pan"}, - {"id": 11295, "synset": "stick.n.01", "name": "stick"}, - {"id": 11296, "synset": "stick.n.07", "name": "stick"}, - {"id": 11297, "synset": "stick.n.03", "name": "stick"}, - {"id": 11298, "synset": "stick.n.06", "name": "stick"}, - {"id": 11299, "synset": "stile.n.01", "name": "stile"}, - {"id": 11300, "synset": "stiletto.n.01", "name": "stiletto"}, - {"id": 11301, "synset": "still.n.03", "name": "still"}, - {"id": 11302, "synset": "stillroom.n.01", "name": "stillroom"}, - {"id": 11303, "synset": "stillson_wrench.n.01", "name": "Stillson_wrench"}, - {"id": 11304, "synset": "stilt.n.02", "name": "stilt"}, - {"id": 11305, "synset": "stinger.n.03", "name": "Stinger"}, - {"id": 11306, "synset": "stink_bomb.n.01", "name": "stink_bomb"}, - {"id": 11307, "synset": "stirrup_pump.n.01", "name": "stirrup_pump"}, - {"id": 11308, "synset": "stob.n.01", "name": "stob"}, - {"id": 11309, "synset": "stock.n.03", "name": "stock"}, - {"id": 11310, "synset": "stockade.n.01", "name": "stockade"}, - {"id": 11311, "synset": "stockcar.n.01", "name": "stockcar"}, - {"id": 11312, "synset": "stock_car.n.02", "name": "stock_car"}, - {"id": 11313, "synset": "stockinet.n.01", "name": "stockinet"}, - {"id": 11314, "synset": "stocking.n.01", "name": "stocking"}, - {"id": 11315, "synset": "stock-in-trade.n.01", "name": "stock-in-trade"}, - {"id": 11316, "synset": "stockpot.n.01", "name": "stockpot"}, - {"id": 11317, "synset": "stockroom.n.01", "name": "stockroom"}, - {"id": 11318, "synset": "stocks.n.03", "name": "stocks"}, - {"id": 11319, "synset": "stock_saddle.n.01", "name": "stock_saddle"}, - {"id": 11320, "synset": "stockyard.n.01", "name": "stockyard"}, - {"id": 11321, "synset": "stole.n.01", "name": "stole"}, - {"id": 11322, "synset": "stomacher.n.01", "name": "stomacher"}, - {"id": 11323, "synset": "stomach_pump.n.01", "name": "stomach_pump"}, - {"id": 11324, "synset": "stone_wall.n.01", "name": "stone_wall"}, - {"id": 11325, "synset": "stoneware.n.01", "name": "stoneware"}, - {"id": 11326, "synset": "stonework.n.01", "name": "stonework"}, - {"id": 11327, "synset": "stoop.n.03", "name": "stoop"}, - {"id": 11328, "synset": "stop_bath.n.01", "name": "stop_bath"}, - {"id": 11329, "synset": "stopcock.n.01", "name": "stopcock"}, - {"id": 11330, "synset": "stopper_knot.n.01", "name": "stopper_knot"}, - {"id": 11331, "synset": "stopwatch.n.01", "name": "stopwatch"}, - {"id": 11332, "synset": "storage_battery.n.01", "name": "storage_battery"}, - {"id": 11333, "synset": "storage_cell.n.01", "name": "storage_cell"}, - {"id": 11334, "synset": "storage_ring.n.01", "name": "storage_ring"}, - {"id": 11335, "synset": "storage_space.n.01", "name": "storage_space"}, - {"id": 11336, "synset": "storeroom.n.01", "name": "storeroom"}, - {"id": 11337, "synset": "storm_cellar.n.01", "name": "storm_cellar"}, - {"id": 11338, "synset": "storm_door.n.01", "name": "storm_door"}, - {"id": 11339, "synset": "storm_window.n.01", "name": "storm_window"}, - {"id": 11340, "synset": "stoup.n.02", "name": "stoup"}, - {"id": 11341, "synset": "stoup.n.01", "name": "stoup"}, - {"id": 11342, "synset": "stove.n.02", "name": "stove"}, - {"id": 11343, "synset": "stove_bolt.n.01", "name": "stove_bolt"}, - {"id": 11344, "synset": "stovepipe.n.01", "name": "stovepipe"}, - {"id": 11345, "synset": "stovepipe_iron.n.01", "name": "stovepipe_iron"}, - {"id": 11346, "synset": "stradavarius.n.01", "name": "Stradavarius"}, - {"id": 11347, "synset": "straight_chair.n.01", "name": "straight_chair"}, - {"id": 11348, "synset": "straightedge.n.01", "name": "straightedge"}, - {"id": 11349, "synset": "straightener.n.01", "name": "straightener"}, - {"id": 11350, "synset": "straight_flute.n.01", "name": "straight_flute"}, - {"id": 11351, "synset": "straight_pin.n.01", "name": "straight_pin"}, - {"id": 11352, "synset": "straight_razor.n.01", "name": "straight_razor"}, - {"id": 11353, "synset": "straitjacket.n.02", "name": "straitjacket"}, - {"id": 11354, "synset": "strap.n.04", "name": "strap"}, - {"id": 11355, "synset": "strap_hinge.n.01", "name": "strap_hinge"}, - {"id": 11356, "synset": "strapless.n.01", "name": "strapless"}, - {"id": 11357, "synset": "streamer_fly.n.01", "name": "streamer_fly"}, - {"id": 11358, "synset": "streamliner.n.01", "name": "streamliner"}, - {"id": 11359, "synset": "street.n.01", "name": "street"}, - {"id": 11360, "synset": "street.n.02", "name": "street"}, - {"id": 11361, "synset": "streetcar.n.01", "name": "streetcar"}, - {"id": 11362, "synset": "street_clothes.n.01", "name": "street_clothes"}, - {"id": 11363, "synset": "stretcher.n.03", "name": "stretcher"}, - {"id": 11364, "synset": "stretcher.n.01", "name": "stretcher"}, - {"id": 11365, "synset": "stretch_pants.n.01", "name": "stretch_pants"}, - {"id": 11366, "synset": "strickle.n.02", "name": "strickle"}, - {"id": 11367, "synset": "strickle.n.01", "name": "strickle"}, - {"id": 11368, "synset": "stringed_instrument.n.01", "name": "stringed_instrument"}, - {"id": 11369, "synset": "stringer.n.04", "name": "stringer"}, - {"id": 11370, "synset": "stringer.n.03", "name": "stringer"}, - {"id": 11371, "synset": "string_tie.n.01", "name": "string_tie"}, - {"id": 11372, "synset": "strip.n.05", "name": "strip"}, - {"id": 11373, "synset": "strip_lighting.n.01", "name": "strip_lighting"}, - {"id": 11374, "synset": "strip_mall.n.01", "name": "strip_mall"}, - {"id": 11375, "synset": "stroboscope.n.01", "name": "stroboscope"}, - {"id": 11376, "synset": "strongbox.n.01", "name": "strongbox"}, - {"id": 11377, "synset": "stronghold.n.01", "name": "stronghold"}, - {"id": 11378, "synset": "strongroom.n.01", "name": "strongroom"}, - {"id": 11379, "synset": "strop.n.01", "name": "strop"}, - {"id": 11380, "synset": "structural_member.n.01", "name": "structural_member"}, - {"id": 11381, "synset": "structure.n.01", "name": "structure"}, - {"id": 11382, "synset": "student_center.n.01", "name": "student_center"}, - {"id": 11383, "synset": "student_lamp.n.01", "name": "student_lamp"}, - {"id": 11384, "synset": "student_union.n.01", "name": "student_union"}, - {"id": 11385, "synset": "stud_finder.n.01", "name": "stud_finder"}, - {"id": 11386, "synset": "studio_apartment.n.01", "name": "studio_apartment"}, - {"id": 11387, "synset": "studio_couch.n.01", "name": "studio_couch"}, - {"id": 11388, "synset": "study.n.05", "name": "study"}, - {"id": 11389, "synset": "study_hall.n.02", "name": "study_hall"}, - {"id": 11390, "synset": "stuffing_nut.n.01", "name": "stuffing_nut"}, - {"id": 11391, "synset": "stump.n.03", "name": "stump"}, - {"id": 11392, "synset": "stun_gun.n.01", "name": "stun_gun"}, - {"id": 11393, "synset": "stupa.n.01", "name": "stupa"}, - {"id": 11394, "synset": "sty.n.02", "name": "sty"}, - {"id": 11395, "synset": "stylus.n.01", "name": "stylus"}, - {"id": 11396, "synset": "sub-assembly.n.01", "name": "sub-assembly"}, - {"id": 11397, "synset": "subcompact.n.01", "name": "subcompact"}, - {"id": 11398, "synset": "submachine_gun.n.01", "name": "submachine_gun"}, - {"id": 11399, "synset": "submarine.n.01", "name": "submarine"}, - {"id": 11400, "synset": "submarine_torpedo.n.01", "name": "submarine_torpedo"}, - {"id": 11401, "synset": "submersible.n.02", "name": "submersible"}, - {"id": 11402, "synset": "submersible.n.01", "name": "submersible"}, - {"id": 11403, "synset": "subtracter.n.02", "name": "subtracter"}, - {"id": 11404, "synset": "subway_token.n.01", "name": "subway_token"}, - {"id": 11405, "synset": "subway_train.n.01", "name": "subway_train"}, - {"id": 11406, "synset": "suction_cup.n.01", "name": "suction_cup"}, - {"id": 11407, "synset": "suction_pump.n.01", "name": "suction_pump"}, - {"id": 11408, "synset": "sudatorium.n.01", "name": "sudatorium"}, - {"id": 11409, "synset": "suede_cloth.n.01", "name": "suede_cloth"}, - {"id": 11410, "synset": "sugar_refinery.n.01", "name": "sugar_refinery"}, - {"id": 11411, "synset": "sugar_spoon.n.01", "name": "sugar_spoon"}, - {"id": 11412, "synset": "suite.n.02", "name": "suite"}, - {"id": 11413, "synset": "suiting.n.01", "name": "suiting"}, - {"id": 11414, "synset": "sulky.n.01", "name": "sulky"}, - {"id": 11415, "synset": "summer_house.n.01", "name": "summer_house"}, - {"id": 11416, "synset": "sumo_ring.n.01", "name": "sumo_ring"}, - {"id": 11417, "synset": "sump.n.01", "name": "sump"}, - {"id": 11418, "synset": "sump_pump.n.01", "name": "sump_pump"}, - {"id": 11419, "synset": "sunbonnet.n.01", "name": "sunbonnet"}, - {"id": 11420, "synset": "sunday_best.n.01", "name": "Sunday_best"}, - {"id": 11421, "synset": "sun_deck.n.01", "name": "sun_deck"}, - {"id": 11422, "synset": "sundial.n.01", "name": "sundial"}, - {"id": 11423, "synset": "sundress.n.01", "name": "sundress"}, - {"id": 11424, "synset": "sundries.n.01", "name": "sundries"}, - {"id": 11425, "synset": "sun_gear.n.01", "name": "sun_gear"}, - {"id": 11426, "synset": "sunglass.n.01", "name": "sunglass"}, - {"id": 11427, "synset": "sunlamp.n.01", "name": "sunlamp"}, - {"id": 11428, "synset": "sun_parlor.n.01", "name": "sun_parlor"}, - {"id": 11429, "synset": "sunroof.n.01", "name": "sunroof"}, - {"id": 11430, "synset": "sunscreen.n.01", "name": "sunscreen"}, - {"id": 11431, "synset": "sunsuit.n.01", "name": "sunsuit"}, - {"id": 11432, "synset": "supercharger.n.01", "name": "supercharger"}, - {"id": 11433, "synset": "supercomputer.n.01", "name": "supercomputer"}, - { - "id": 11434, - "synset": "superconducting_supercollider.n.01", - "name": "superconducting_supercollider", - }, - {"id": 11435, "synset": "superhighway.n.02", "name": "superhighway"}, - {"id": 11436, "synset": "supermarket.n.01", "name": "supermarket"}, - {"id": 11437, "synset": "superstructure.n.01", "name": "superstructure"}, - {"id": 11438, "synset": "supertanker.n.01", "name": "supertanker"}, - {"id": 11439, "synset": "supper_club.n.01", "name": "supper_club"}, - {"id": 11440, "synset": "supplejack.n.01", "name": "supplejack"}, - {"id": 11441, "synset": "supply_chamber.n.01", "name": "supply_chamber"}, - {"id": 11442, "synset": "supply_closet.n.01", "name": "supply_closet"}, - {"id": 11443, "synset": "support.n.10", "name": "support"}, - {"id": 11444, "synset": "support.n.07", "name": "support"}, - {"id": 11445, "synset": "support_column.n.01", "name": "support_column"}, - {"id": 11446, "synset": "support_hose.n.01", "name": "support_hose"}, - {"id": 11447, "synset": "supporting_structure.n.01", "name": "supporting_structure"}, - {"id": 11448, "synset": "supporting_tower.n.01", "name": "supporting_tower"}, - {"id": 11449, "synset": "surcoat.n.02", "name": "surcoat"}, - {"id": 11450, "synset": "surface_gauge.n.01", "name": "surface_gauge"}, - {"id": 11451, "synset": "surface_lift.n.01", "name": "surface_lift"}, - {"id": 11452, "synset": "surface_search_radar.n.01", "name": "surface_search_radar"}, - {"id": 11453, "synset": "surface_ship.n.01", "name": "surface_ship"}, - {"id": 11454, "synset": "surface-to-air_missile.n.01", "name": "surface-to-air_missile"}, - { - "id": 11455, - "synset": "surface-to-air_missile_system.n.01", - "name": "surface-to-air_missile_system", - }, - {"id": 11456, "synset": "surfboat.n.01", "name": "surfboat"}, - {"id": 11457, "synset": "surcoat.n.01", "name": "surcoat"}, - {"id": 11458, "synset": "surgeon's_knot.n.01", "name": "surgeon's_knot"}, - {"id": 11459, "synset": "surgery.n.02", "name": "surgery"}, - {"id": 11460, "synset": "surge_suppressor.n.01", "name": "surge_suppressor"}, - {"id": 11461, "synset": "surgical_dressing.n.01", "name": "surgical_dressing"}, - {"id": 11462, "synset": "surgical_instrument.n.01", "name": "surgical_instrument"}, - {"id": 11463, "synset": "surgical_knife.n.01", "name": "surgical_knife"}, - {"id": 11464, "synset": "surplice.n.01", "name": "surplice"}, - {"id": 11465, "synset": "surrey.n.02", "name": "surrey"}, - {"id": 11466, "synset": "surtout.n.01", "name": "surtout"}, - {"id": 11467, "synset": "surveillance_system.n.01", "name": "surveillance_system"}, - {"id": 11468, "synset": "surveying_instrument.n.01", "name": "surveying_instrument"}, - {"id": 11469, "synset": "surveyor's_level.n.01", "name": "surveyor's_level"}, - {"id": 11470, "synset": "sushi_bar.n.01", "name": "sushi_bar"}, - {"id": 11471, "synset": "suspension.n.05", "name": "suspension"}, - {"id": 11472, "synset": "suspension_bridge.n.01", "name": "suspension_bridge"}, - {"id": 11473, "synset": "suspensory.n.01", "name": "suspensory"}, - {"id": 11474, "synset": "sustaining_pedal.n.01", "name": "sustaining_pedal"}, - {"id": 11475, "synset": "suture.n.02", "name": "suture"}, - {"id": 11476, "synset": "swab.n.01", "name": "swab"}, - {"id": 11477, "synset": "swaddling_clothes.n.01", "name": "swaddling_clothes"}, - {"id": 11478, "synset": "swag.n.03", "name": "swag"}, - {"id": 11479, "synset": "swage_block.n.01", "name": "swage_block"}, - {"id": 11480, "synset": "swagger_stick.n.01", "name": "swagger_stick"}, - {"id": 11481, "synset": "swallow-tailed_coat.n.01", "name": "swallow-tailed_coat"}, - {"id": 11482, "synset": "swamp_buggy.n.01", "name": "swamp_buggy"}, - {"id": 11483, "synset": "swan's_down.n.01", "name": "swan's_down"}, - {"id": 11484, "synset": "swathe.n.01", "name": "swathe"}, - {"id": 11485, "synset": "swatter.n.01", "name": "swatter"}, - {"id": 11486, "synset": "sweat_bag.n.01", "name": "sweat_bag"}, - {"id": 11487, "synset": "sweatband.n.01", "name": "sweatband"}, - {"id": 11488, "synset": "sweatshop.n.01", "name": "sweatshop"}, - {"id": 11489, "synset": "sweat_suit.n.01", "name": "sweat_suit"}, - {"id": 11490, "synset": "sweep.n.04", "name": "sweep"}, - {"id": 11491, "synset": "sweep_hand.n.01", "name": "sweep_hand"}, - {"id": 11492, "synset": "swimming_trunks.n.01", "name": "swimming_trunks"}, - {"id": 11493, "synset": "swing.n.02", "name": "swing"}, - {"id": 11494, "synset": "swing_door.n.01", "name": "swing_door"}, - {"id": 11495, "synset": "switch.n.01", "name": "switch"}, - {"id": 11496, "synset": "switchblade.n.01", "name": "switchblade"}, - {"id": 11497, "synset": "switch_engine.n.01", "name": "switch_engine"}, - {"id": 11498, "synset": "swivel.n.01", "name": "swivel"}, - {"id": 11499, "synset": "swivel_chair.n.01", "name": "swivel_chair"}, - {"id": 11500, "synset": "swizzle_stick.n.01", "name": "swizzle_stick"}, - {"id": 11501, "synset": "sword_cane.n.01", "name": "sword_cane"}, - {"id": 11502, "synset": "s_wrench.n.01", "name": "S_wrench"}, - {"id": 11503, "synset": "synagogue.n.01", "name": "synagogue"}, - {"id": 11504, "synset": "synchrocyclotron.n.01", "name": "synchrocyclotron"}, - {"id": 11505, "synset": "synchroflash.n.01", "name": "synchroflash"}, - {"id": 11506, "synset": "synchromesh.n.01", "name": "synchromesh"}, - {"id": 11507, "synset": "synchronous_converter.n.01", "name": "synchronous_converter"}, - {"id": 11508, "synset": "synchronous_motor.n.01", "name": "synchronous_motor"}, - {"id": 11509, "synset": "synchrotron.n.01", "name": "synchrotron"}, - {"id": 11510, "synset": "synchroscope.n.01", "name": "synchroscope"}, - {"id": 11511, "synset": "synthesizer.n.02", "name": "synthesizer"}, - {"id": 11512, "synset": "system.n.01", "name": "system"}, - {"id": 11513, "synset": "tabard.n.01", "name": "tabard"}, - {"id": 11514, "synset": "tabernacle.n.02", "name": "Tabernacle"}, - {"id": 11515, "synset": "tabi.n.01", "name": "tabi"}, - {"id": 11516, "synset": "tab_key.n.01", "name": "tab_key"}, - {"id": 11517, "synset": "table.n.03", "name": "table"}, - {"id": 11518, "synset": "tablefork.n.01", "name": "tablefork"}, - {"id": 11519, "synset": "table_knife.n.01", "name": "table_knife"}, - {"id": 11520, "synset": "table_saw.n.01", "name": "table_saw"}, - {"id": 11521, "synset": "tablespoon.n.02", "name": "tablespoon"}, - {"id": 11522, "synset": "tablet-armed_chair.n.01", "name": "tablet-armed_chair"}, - {"id": 11523, "synset": "table-tennis_racquet.n.01", "name": "table-tennis_racquet"}, - {"id": 11524, "synset": "tabletop.n.01", "name": "tabletop"}, - {"id": 11525, "synset": "tableware.n.01", "name": "tableware"}, - {"id": 11526, "synset": "tabor.n.01", "name": "tabor"}, - {"id": 11527, "synset": "taboret.n.01", "name": "taboret"}, - {"id": 11528, "synset": "tachistoscope.n.01", "name": "tachistoscope"}, - {"id": 11529, "synset": "tachograph.n.01", "name": "tachograph"}, - {"id": 11530, "synset": "tachymeter.n.01", "name": "tachymeter"}, - {"id": 11531, "synset": "tack.n.02", "name": "tack"}, - {"id": 11532, "synset": "tack_hammer.n.01", "name": "tack_hammer"}, - {"id": 11533, "synset": "taffeta.n.01", "name": "taffeta"}, - {"id": 11534, "synset": "taffrail.n.01", "name": "taffrail"}, - {"id": 11535, "synset": "tailgate.n.01", "name": "tailgate"}, - {"id": 11536, "synset": "tailor-made.n.01", "name": "tailor-made"}, - {"id": 11537, "synset": "tailor's_chalk.n.01", "name": "tailor's_chalk"}, - {"id": 11538, "synset": "tailpipe.n.01", "name": "tailpipe"}, - {"id": 11539, "synset": "tail_rotor.n.01", "name": "tail_rotor"}, - {"id": 11540, "synset": "tailstock.n.01", "name": "tailstock"}, - {"id": 11541, "synset": "take-up.n.01", "name": "take-up"}, - {"id": 11542, "synset": "talaria.n.01", "name": "talaria"}, - {"id": 11543, "synset": "talcum.n.02", "name": "talcum"}, - {"id": 11544, "synset": "tam.n.01", "name": "tam"}, - {"id": 11545, "synset": "tambour.n.02", "name": "tambour"}, - {"id": 11546, "synset": "tambour.n.01", "name": "tambour"}, - {"id": 11547, "synset": "tammy.n.01", "name": "tammy"}, - {"id": 11548, "synset": "tamp.n.01", "name": "tamp"}, - {"id": 11549, "synset": "tampax.n.01", "name": "Tampax"}, - {"id": 11550, "synset": "tampion.n.01", "name": "tampion"}, - {"id": 11551, "synset": "tampon.n.01", "name": "tampon"}, - {"id": 11552, "synset": "tandoor.n.01", "name": "tandoor"}, - {"id": 11553, "synset": "tangram.n.01", "name": "tangram"}, - {"id": 11554, "synset": "tankard.n.01", "name": "tankard"}, - {"id": 11555, "synset": "tank_car.n.01", "name": "tank_car"}, - {"id": 11556, "synset": "tank_destroyer.n.01", "name": "tank_destroyer"}, - {"id": 11557, "synset": "tank_engine.n.01", "name": "tank_engine"}, - {"id": 11558, "synset": "tanker_plane.n.01", "name": "tanker_plane"}, - {"id": 11559, "synset": "tank_shell.n.01", "name": "tank_shell"}, - {"id": 11560, "synset": "tannoy.n.01", "name": "tannoy"}, - {"id": 11561, "synset": "tap.n.06", "name": "tap"}, - {"id": 11562, "synset": "tapa.n.02", "name": "tapa"}, - {"id": 11563, "synset": "tape.n.02", "name": "tape"}, - {"id": 11564, "synset": "tape_deck.n.01", "name": "tape_deck"}, - {"id": 11565, "synset": "tape_drive.n.01", "name": "tape_drive"}, - {"id": 11566, "synset": "tape_player.n.01", "name": "tape_player"}, - {"id": 11567, "synset": "tape_recorder.n.01", "name": "tape_recorder"}, - {"id": 11568, "synset": "taper_file.n.01", "name": "taper_file"}, - {"id": 11569, "synset": "tappet.n.01", "name": "tappet"}, - {"id": 11570, "synset": "tap_wrench.n.01", "name": "tap_wrench"}, - {"id": 11571, "synset": "tare.n.05", "name": "tare"}, - {"id": 11572, "synset": "target.n.04", "name": "target"}, - {"id": 11573, "synset": "target_acquisition_system.n.01", "name": "target_acquisition_system"}, - {"id": 11574, "synset": "tarmacadam.n.02", "name": "tarmacadam"}, - {"id": 11575, "synset": "tasset.n.01", "name": "tasset"}, - {"id": 11576, "synset": "tattoo.n.02", "name": "tattoo"}, - {"id": 11577, "synset": "tavern.n.01", "name": "tavern"}, - {"id": 11578, "synset": "tawse.n.01", "name": "tawse"}, - {"id": 11579, "synset": "taximeter.n.01", "name": "taximeter"}, - {"id": 11580, "synset": "t-bar_lift.n.01", "name": "T-bar_lift"}, - {"id": 11581, "synset": "tea_bag.n.02", "name": "tea_bag"}, - {"id": 11582, "synset": "tea_ball.n.01", "name": "tea_ball"}, - {"id": 11583, "synset": "tea_cart.n.01", "name": "tea_cart"}, - {"id": 11584, "synset": "tea_chest.n.01", "name": "tea_chest"}, - {"id": 11585, "synset": "teaching_aid.n.01", "name": "teaching_aid"}, - {"id": 11586, "synset": "tea_gown.n.01", "name": "tea_gown"}, - {"id": 11587, "synset": "tea_maker.n.01", "name": "tea_maker"}, - {"id": 11588, "synset": "teashop.n.01", "name": "teashop"}, - {"id": 11589, "synset": "teaspoon.n.02", "name": "teaspoon"}, - {"id": 11590, "synset": "tea-strainer.n.01", "name": "tea-strainer"}, - {"id": 11591, "synset": "tea_table.n.01", "name": "tea_table"}, - {"id": 11592, "synset": "tea_tray.n.01", "name": "tea_tray"}, - {"id": 11593, "synset": "tea_urn.n.01", "name": "tea_urn"}, - {"id": 11594, "synset": "tee.n.03", "name": "tee"}, - {"id": 11595, "synset": "tee_hinge.n.01", "name": "tee_hinge"}, - {"id": 11596, "synset": "telecom_hotel.n.01", "name": "telecom_hotel"}, - {"id": 11597, "synset": "telecommunication_system.n.01", "name": "telecommunication_system"}, - {"id": 11598, "synset": "telegraph.n.01", "name": "telegraph"}, - {"id": 11599, "synset": "telegraph_key.n.01", "name": "telegraph_key"}, - {"id": 11600, "synset": "telemeter.n.01", "name": "telemeter"}, - {"id": 11601, "synset": "telephone_bell.n.01", "name": "telephone_bell"}, - {"id": 11602, "synset": "telephone_cord.n.01", "name": "telephone_cord"}, - {"id": 11603, "synset": "telephone_jack.n.01", "name": "telephone_jack"}, - {"id": 11604, "synset": "telephone_line.n.02", "name": "telephone_line"}, - {"id": 11605, "synset": "telephone_plug.n.01", "name": "telephone_plug"}, - {"id": 11606, "synset": "telephone_receiver.n.01", "name": "telephone_receiver"}, - {"id": 11607, "synset": "telephone_system.n.01", "name": "telephone_system"}, - {"id": 11608, "synset": "telephone_wire.n.01", "name": "telephone_wire"}, - {"id": 11609, "synset": "teleprompter.n.01", "name": "Teleprompter"}, - {"id": 11610, "synset": "telescope.n.01", "name": "telescope"}, - {"id": 11611, "synset": "telescopic_sight.n.01", "name": "telescopic_sight"}, - {"id": 11612, "synset": "telethermometer.n.01", "name": "telethermometer"}, - {"id": 11613, "synset": "teletypewriter.n.01", "name": "teletypewriter"}, - {"id": 11614, "synset": "television.n.02", "name": "television"}, - {"id": 11615, "synset": "television_antenna.n.01", "name": "television_antenna"}, - {"id": 11616, "synset": "television_equipment.n.01", "name": "television_equipment"}, - {"id": 11617, "synset": "television_monitor.n.01", "name": "television_monitor"}, - {"id": 11618, "synset": "television_room.n.01", "name": "television_room"}, - {"id": 11619, "synset": "television_transmitter.n.01", "name": "television_transmitter"}, - {"id": 11620, "synset": "telpher.n.01", "name": "telpher"}, - {"id": 11621, "synset": "telpherage.n.01", "name": "telpherage"}, - {"id": 11622, "synset": "tempera.n.01", "name": "tempera"}, - {"id": 11623, "synset": "temple.n.01", "name": "temple"}, - {"id": 11624, "synset": "temple.n.03", "name": "temple"}, - {"id": 11625, "synset": "temporary_hookup.n.01", "name": "temporary_hookup"}, - {"id": 11626, "synset": "tender.n.06", "name": "tender"}, - {"id": 11627, "synset": "tender.n.05", "name": "tender"}, - {"id": 11628, "synset": "tender.n.04", "name": "tender"}, - {"id": 11629, "synset": "tenement.n.01", "name": "tenement"}, - {"id": 11630, "synset": "tennis_camp.n.01", "name": "tennis_camp"}, - {"id": 11631, "synset": "tenon.n.01", "name": "tenon"}, - {"id": 11632, "synset": "tenor_drum.n.01", "name": "tenor_drum"}, - {"id": 11633, "synset": "tenoroon.n.01", "name": "tenoroon"}, - {"id": 11634, "synset": "tenpenny_nail.n.01", "name": "tenpenny_nail"}, - {"id": 11635, "synset": "tenpin.n.01", "name": "tenpin"}, - {"id": 11636, "synset": "tensimeter.n.01", "name": "tensimeter"}, - {"id": 11637, "synset": "tensiometer.n.03", "name": "tensiometer"}, - {"id": 11638, "synset": "tensiometer.n.02", "name": "tensiometer"}, - {"id": 11639, "synset": "tensiometer.n.01", "name": "tensiometer"}, - {"id": 11640, "synset": "tent.n.01", "name": "tent"}, - {"id": 11641, "synset": "tenter.n.01", "name": "tenter"}, - {"id": 11642, "synset": "tenterhook.n.01", "name": "tenterhook"}, - {"id": 11643, "synset": "tent-fly.n.01", "name": "tent-fly"}, - {"id": 11644, "synset": "tent_peg.n.01", "name": "tent_peg"}, - {"id": 11645, "synset": "tepee.n.01", "name": "tepee"}, - {"id": 11646, "synset": "terminal.n.02", "name": "terminal"}, - {"id": 11647, "synset": "terminal.n.04", "name": "terminal"}, - {"id": 11648, "synset": "terraced_house.n.01", "name": "terraced_house"}, - {"id": 11649, "synset": "terra_cotta.n.01", "name": "terra_cotta"}, - {"id": 11650, "synset": "terrarium.n.01", "name": "terrarium"}, - {"id": 11651, "synset": "terra_sigillata.n.01", "name": "terra_sigillata"}, - {"id": 11652, "synset": "terry.n.02", "name": "terry"}, - {"id": 11653, "synset": "tesla_coil.n.01", "name": "Tesla_coil"}, - {"id": 11654, "synset": "tessera.n.01", "name": "tessera"}, - {"id": 11655, "synset": "test_equipment.n.01", "name": "test_equipment"}, - {"id": 11656, "synset": "test_rocket.n.01", "name": "test_rocket"}, - {"id": 11657, "synset": "test_room.n.01", "name": "test_room"}, - {"id": 11658, "synset": "testudo.n.01", "name": "testudo"}, - {"id": 11659, "synset": "tetraskelion.n.01", "name": "tetraskelion"}, - {"id": 11660, "synset": "tetrode.n.01", "name": "tetrode"}, - {"id": 11661, "synset": "textile_machine.n.01", "name": "textile_machine"}, - {"id": 11662, "synset": "textile_mill.n.01", "name": "textile_mill"}, - {"id": 11663, "synset": "thatch.n.04", "name": "thatch"}, - {"id": 11664, "synset": "theater.n.01", "name": "theater"}, - {"id": 11665, "synset": "theater_curtain.n.01", "name": "theater_curtain"}, - {"id": 11666, "synset": "theater_light.n.01", "name": "theater_light"}, - {"id": 11667, "synset": "theodolite.n.01", "name": "theodolite"}, - {"id": 11668, "synset": "theremin.n.01", "name": "theremin"}, - {"id": 11669, "synset": "thermal_printer.n.01", "name": "thermal_printer"}, - {"id": 11670, "synset": "thermal_reactor.n.01", "name": "thermal_reactor"}, - {"id": 11671, "synset": "thermocouple.n.01", "name": "thermocouple"}, - { - "id": 11672, - "synset": "thermoelectric_thermometer.n.01", - "name": "thermoelectric_thermometer", - }, - {"id": 11673, "synset": "thermograph.n.02", "name": "thermograph"}, - {"id": 11674, "synset": "thermograph.n.01", "name": "thermograph"}, - {"id": 11675, "synset": "thermohydrometer.n.01", "name": "thermohydrometer"}, - {"id": 11676, "synset": "thermojunction.n.01", "name": "thermojunction"}, - {"id": 11677, "synset": "thermonuclear_reactor.n.01", "name": "thermonuclear_reactor"}, - {"id": 11678, "synset": "thermopile.n.01", "name": "thermopile"}, - {"id": 11679, "synset": "thigh_pad.n.01", "name": "thigh_pad"}, - {"id": 11680, "synset": "thill.n.01", "name": "thill"}, - {"id": 11681, "synset": "thinning_shears.n.01", "name": "thinning_shears"}, - {"id": 11682, "synset": "third_base.n.01", "name": "third_base"}, - {"id": 11683, "synset": "third_gear.n.01", "name": "third_gear"}, - {"id": 11684, "synset": "third_rail.n.01", "name": "third_rail"}, - {"id": 11685, "synset": "thong.n.03", "name": "thong"}, - {"id": 11686, "synset": "thong.n.02", "name": "thong"}, - {"id": 11687, "synset": "three-centered_arch.n.01", "name": "three-centered_arch"}, - {"id": 11688, "synset": "three-decker.n.02", "name": "three-decker"}, - {"id": 11689, "synset": "three-dimensional_radar.n.01", "name": "three-dimensional_radar"}, - {"id": 11690, "synset": "three-piece_suit.n.01", "name": "three-piece_suit"}, - {"id": 11691, "synset": "three-quarter_binding.n.01", "name": "three-quarter_binding"}, - {"id": 11692, "synset": "three-way_switch.n.01", "name": "three-way_switch"}, - {"id": 11693, "synset": "thresher.n.01", "name": "thresher"}, - {"id": 11694, "synset": "threshing_floor.n.01", "name": "threshing_floor"}, - {"id": 11695, "synset": "thriftshop.n.01", "name": "thriftshop"}, - {"id": 11696, "synset": "throat_protector.n.01", "name": "throat_protector"}, - {"id": 11697, "synset": "throne.n.01", "name": "throne"}, - {"id": 11698, "synset": "thrust_bearing.n.01", "name": "thrust_bearing"}, - {"id": 11699, "synset": "thruster.n.02", "name": "thruster"}, - {"id": 11700, "synset": "thumb.n.02", "name": "thumb"}, - {"id": 11701, "synset": "thumbhole.n.02", "name": "thumbhole"}, - {"id": 11702, "synset": "thumbscrew.n.02", "name": "thumbscrew"}, - {"id": 11703, "synset": "thumbstall.n.01", "name": "thumbstall"}, - {"id": 11704, "synset": "thunderer.n.02", "name": "thunderer"}, - {"id": 11705, "synset": "thwart.n.01", "name": "thwart"}, - {"id": 11706, "synset": "ticking.n.02", "name": "ticking"}, - {"id": 11707, "synset": "tickler_coil.n.01", "name": "tickler_coil"}, - {"id": 11708, "synset": "tie.n.04", "name": "tie"}, - {"id": 11709, "synset": "tie.n.08", "name": "tie"}, - {"id": 11710, "synset": "tie_rack.n.01", "name": "tie_rack"}, - {"id": 11711, "synset": "tie_rod.n.01", "name": "tie_rod"}, - {"id": 11712, "synset": "tile.n.01", "name": "tile"}, - {"id": 11713, "synset": "tile_cutter.n.01", "name": "tile_cutter"}, - {"id": 11714, "synset": "tile_roof.n.01", "name": "tile_roof"}, - {"id": 11715, "synset": "tiller.n.03", "name": "tiller"}, - {"id": 11716, "synset": "tilter.n.02", "name": "tilter"}, - {"id": 11717, "synset": "tilt-top_table.n.01", "name": "tilt-top_table"}, - {"id": 11718, "synset": "timber.n.02", "name": "timber"}, - {"id": 11719, "synset": "timber.n.03", "name": "timber"}, - {"id": 11720, "synset": "timber_hitch.n.01", "name": "timber_hitch"}, - {"id": 11721, "synset": "timbrel.n.01", "name": "timbrel"}, - {"id": 11722, "synset": "time_bomb.n.02", "name": "time_bomb"}, - {"id": 11723, "synset": "time_capsule.n.01", "name": "time_capsule"}, - {"id": 11724, "synset": "time_clock.n.01", "name": "time_clock"}, - { - "id": 11725, - "synset": "time-delay_measuring_instrument.n.01", - "name": "time-delay_measuring_instrument", - }, - {"id": 11726, "synset": "time-fuse.n.01", "name": "time-fuse"}, - {"id": 11727, "synset": "timepiece.n.01", "name": "timepiece"}, - {"id": 11728, "synset": "timer.n.03", "name": "timer"}, - {"id": 11729, "synset": "time-switch.n.01", "name": "time-switch"}, - {"id": 11730, "synset": "tin.n.02", "name": "tin"}, - {"id": 11731, "synset": "tinderbox.n.02", "name": "tinderbox"}, - {"id": 11732, "synset": "tine.n.01", "name": "tine"}, - {"id": 11733, "synset": "tippet.n.01", "name": "tippet"}, - {"id": 11734, "synset": "tire_chain.n.01", "name": "tire_chain"}, - {"id": 11735, "synset": "tire_iron.n.01", "name": "tire_iron"}, - {"id": 11736, "synset": "titfer.n.01", "name": "titfer"}, - {"id": 11737, "synset": "tithe_barn.n.01", "name": "tithe_barn"}, - {"id": 11738, "synset": "titrator.n.01", "name": "titrator"}, - {"id": 11739, "synset": "toasting_fork.n.01", "name": "toasting_fork"}, - {"id": 11740, "synset": "toastrack.n.01", "name": "toastrack"}, - {"id": 11741, "synset": "tobacco_pouch.n.01", "name": "tobacco_pouch"}, - {"id": 11742, "synset": "tobacco_shop.n.01", "name": "tobacco_shop"}, - {"id": 11743, "synset": "toboggan.n.01", "name": "toboggan"}, - {"id": 11744, "synset": "toby.n.01", "name": "toby"}, - {"id": 11745, "synset": "tocsin.n.02", "name": "tocsin"}, - {"id": 11746, "synset": "toe.n.02", "name": "toe"}, - {"id": 11747, "synset": "toecap.n.01", "name": "toecap"}, - {"id": 11748, "synset": "toehold.n.02", "name": "toehold"}, - {"id": 11749, "synset": "toga.n.01", "name": "toga"}, - {"id": 11750, "synset": "toga_virilis.n.01", "name": "toga_virilis"}, - {"id": 11751, "synset": "toggle.n.03", "name": "toggle"}, - {"id": 11752, "synset": "toggle_bolt.n.01", "name": "toggle_bolt"}, - {"id": 11753, "synset": "toggle_joint.n.01", "name": "toggle_joint"}, - {"id": 11754, "synset": "toggle_switch.n.01", "name": "toggle_switch"}, - {"id": 11755, "synset": "togs.n.01", "name": "togs"}, - {"id": 11756, "synset": "toilet.n.01", "name": "toilet"}, - {"id": 11757, "synset": "toilet_bag.n.01", "name": "toilet_bag"}, - {"id": 11758, "synset": "toilet_bowl.n.01", "name": "toilet_bowl"}, - {"id": 11759, "synset": "toilet_kit.n.01", "name": "toilet_kit"}, - {"id": 11760, "synset": "toilet_powder.n.01", "name": "toilet_powder"}, - {"id": 11761, "synset": "toiletry.n.01", "name": "toiletry"}, - {"id": 11762, "synset": "toilet_seat.n.01", "name": "toilet_seat"}, - {"id": 11763, "synset": "toilet_water.n.01", "name": "toilet_water"}, - {"id": 11764, "synset": "tokamak.n.01", "name": "tokamak"}, - {"id": 11765, "synset": "token.n.03", "name": "token"}, - {"id": 11766, "synset": "tollbooth.n.01", "name": "tollbooth"}, - {"id": 11767, "synset": "toll_bridge.n.01", "name": "toll_bridge"}, - {"id": 11768, "synset": "tollgate.n.01", "name": "tollgate"}, - {"id": 11769, "synset": "toll_line.n.01", "name": "toll_line"}, - {"id": 11770, "synset": "tomahawk.n.01", "name": "tomahawk"}, - {"id": 11771, "synset": "tommy_gun.n.01", "name": "Tommy_gun"}, - {"id": 11772, "synset": "tomograph.n.01", "name": "tomograph"}, - {"id": 11773, "synset": "tone_arm.n.01", "name": "tone_arm"}, - {"id": 11774, "synset": "toner.n.03", "name": "toner"}, - {"id": 11775, "synset": "tongue.n.07", "name": "tongue"}, - {"id": 11776, "synset": "tongue_and_groove_joint.n.01", "name": "tongue_and_groove_joint"}, - {"id": 11777, "synset": "tongue_depressor.n.01", "name": "tongue_depressor"}, - {"id": 11778, "synset": "tonometer.n.01", "name": "tonometer"}, - {"id": 11779, "synset": "tool.n.01", "name": "tool"}, - {"id": 11780, "synset": "tool_bag.n.01", "name": "tool_bag"}, - {"id": 11781, "synset": "toolshed.n.01", "name": "toolshed"}, - {"id": 11782, "synset": "tooth.n.02", "name": "tooth"}, - {"id": 11783, "synset": "tooth.n.05", "name": "tooth"}, - {"id": 11784, "synset": "top.n.10", "name": "top"}, - {"id": 11785, "synset": "topgallant.n.02", "name": "topgallant"}, - {"id": 11786, "synset": "topgallant.n.01", "name": "topgallant"}, - {"id": 11787, "synset": "topiary.n.01", "name": "topiary"}, - {"id": 11788, "synset": "topknot.n.01", "name": "topknot"}, - {"id": 11789, "synset": "topmast.n.01", "name": "topmast"}, - {"id": 11790, "synset": "topper.n.05", "name": "topper"}, - {"id": 11791, "synset": "topsail.n.01", "name": "topsail"}, - {"id": 11792, "synset": "toque.n.01", "name": "toque"}, - {"id": 11793, "synset": "torch.n.01", "name": "torch"}, - {"id": 11794, "synset": "torpedo.n.06", "name": "torpedo"}, - {"id": 11795, "synset": "torpedo.n.05", "name": "torpedo"}, - {"id": 11796, "synset": "torpedo.n.03", "name": "torpedo"}, - {"id": 11797, "synset": "torpedo_boat.n.01", "name": "torpedo_boat"}, - {"id": 11798, "synset": "torpedo-boat_destroyer.n.01", "name": "torpedo-boat_destroyer"}, - {"id": 11799, "synset": "torpedo_tube.n.01", "name": "torpedo_tube"}, - {"id": 11800, "synset": "torque_converter.n.01", "name": "torque_converter"}, - {"id": 11801, "synset": "torque_wrench.n.01", "name": "torque_wrench"}, - {"id": 11802, "synset": "torture_chamber.n.01", "name": "torture_chamber"}, - {"id": 11803, "synset": "totem_pole.n.01", "name": "totem_pole"}, - {"id": 11804, "synset": "touch_screen.n.01", "name": "touch_screen"}, - {"id": 11805, "synset": "toupee.n.01", "name": "toupee"}, - {"id": 11806, "synset": "touring_car.n.01", "name": "touring_car"}, - {"id": 11807, "synset": "tourist_class.n.01", "name": "tourist_class"}, - {"id": 11808, "synset": "toweling.n.01", "name": "toweling"}, - {"id": 11809, "synset": "towel_rail.n.01", "name": "towel_rail"}, - {"id": 11810, "synset": "tower.n.01", "name": "tower"}, - {"id": 11811, "synset": "town_hall.n.01", "name": "town_hall"}, - {"id": 11812, "synset": "towpath.n.01", "name": "towpath"}, - {"id": 11813, "synset": "toy_box.n.01", "name": "toy_box"}, - {"id": 11814, "synset": "toyshop.n.01", "name": "toyshop"}, - {"id": 11815, "synset": "trace_detector.n.01", "name": "trace_detector"}, - {"id": 11816, "synset": "track.n.09", "name": "track"}, - {"id": 11817, "synset": "track.n.08", "name": "track"}, - {"id": 11818, "synset": "trackball.n.01", "name": "trackball"}, - {"id": 11819, "synset": "tracked_vehicle.n.01", "name": "tracked_vehicle"}, - {"id": 11820, "synset": "tract_house.n.01", "name": "tract_house"}, - {"id": 11821, "synset": "tract_housing.n.01", "name": "tract_housing"}, - {"id": 11822, "synset": "traction_engine.n.01", "name": "traction_engine"}, - {"id": 11823, "synset": "tractor.n.02", "name": "tractor"}, - {"id": 11824, "synset": "trailer.n.04", "name": "trailer"}, - {"id": 11825, "synset": "trailer.n.03", "name": "trailer"}, - {"id": 11826, "synset": "trailer_camp.n.01", "name": "trailer_camp"}, - {"id": 11827, "synset": "trailing_edge.n.01", "name": "trailing_edge"}, - {"id": 11828, "synset": "tramline.n.01", "name": "tramline"}, - {"id": 11829, "synset": "trammel.n.02", "name": "trammel"}, - {"id": 11830, "synset": "tramp_steamer.n.01", "name": "tramp_steamer"}, - {"id": 11831, "synset": "tramway.n.01", "name": "tramway"}, - {"id": 11832, "synset": "transdermal_patch.n.01", "name": "transdermal_patch"}, - {"id": 11833, "synset": "transept.n.01", "name": "transept"}, - {"id": 11834, "synset": "transformer.n.01", "name": "transformer"}, - {"id": 11835, "synset": "transistor.n.01", "name": "transistor"}, - {"id": 11836, "synset": "transit_instrument.n.01", "name": "transit_instrument"}, - {"id": 11837, "synset": "transmission.n.05", "name": "transmission"}, - {"id": 11838, "synset": "transmission_shaft.n.01", "name": "transmission_shaft"}, - {"id": 11839, "synset": "transmitter.n.03", "name": "transmitter"}, - {"id": 11840, "synset": "transom.n.02", "name": "transom"}, - {"id": 11841, "synset": "transom.n.01", "name": "transom"}, - {"id": 11842, "synset": "transponder.n.01", "name": "transponder"}, - {"id": 11843, "synset": "transporter.n.02", "name": "transporter"}, - {"id": 11844, "synset": "transporter.n.01", "name": "transporter"}, - {"id": 11845, "synset": "transport_ship.n.01", "name": "transport_ship"}, - {"id": 11846, "synset": "trap.n.01", "name": "trap"}, - {"id": 11847, "synset": "trap_door.n.01", "name": "trap_door"}, - {"id": 11848, "synset": "trapeze.n.01", "name": "trapeze"}, - {"id": 11849, "synset": "trave.n.01", "name": "trave"}, - {"id": 11850, "synset": "travel_iron.n.01", "name": "travel_iron"}, - {"id": 11851, "synset": "trawl.n.02", "name": "trawl"}, - {"id": 11852, "synset": "trawl.n.01", "name": "trawl"}, - {"id": 11853, "synset": "trawler.n.02", "name": "trawler"}, - {"id": 11854, "synset": "tray_cloth.n.01", "name": "tray_cloth"}, - {"id": 11855, "synset": "tread.n.04", "name": "tread"}, - {"id": 11856, "synset": "tread.n.03", "name": "tread"}, - {"id": 11857, "synset": "treadmill.n.02", "name": "treadmill"}, - {"id": 11858, "synset": "treadmill.n.01", "name": "treadmill"}, - {"id": 11859, "synset": "treasure_chest.n.01", "name": "treasure_chest"}, - {"id": 11860, "synset": "treasure_ship.n.01", "name": "treasure_ship"}, - {"id": 11861, "synset": "treenail.n.01", "name": "treenail"}, - {"id": 11862, "synset": "trefoil_arch.n.01", "name": "trefoil_arch"}, - {"id": 11863, "synset": "trellis.n.01", "name": "trellis"}, - {"id": 11864, "synset": "trench.n.01", "name": "trench"}, - {"id": 11865, "synset": "trench_knife.n.01", "name": "trench_knife"}, - {"id": 11866, "synset": "trepan.n.02", "name": "trepan"}, - {"id": 11867, "synset": "trepan.n.01", "name": "trepan"}, - {"id": 11868, "synset": "trestle.n.02", "name": "trestle"}, - {"id": 11869, "synset": "trestle.n.01", "name": "trestle"}, - {"id": 11870, "synset": "trestle_bridge.n.01", "name": "trestle_bridge"}, - {"id": 11871, "synset": "trestle_table.n.01", "name": "trestle_table"}, - {"id": 11872, "synset": "trestlework.n.01", "name": "trestlework"}, - {"id": 11873, "synset": "trews.n.01", "name": "trews"}, - {"id": 11874, "synset": "trial_balloon.n.02", "name": "trial_balloon"}, - {"id": 11875, "synset": "triangle.n.04", "name": "triangle"}, - {"id": 11876, "synset": "triclinium.n.02", "name": "triclinium"}, - {"id": 11877, "synset": "triclinium.n.01", "name": "triclinium"}, - {"id": 11878, "synset": "tricorn.n.01", "name": "tricorn"}, - {"id": 11879, "synset": "tricot.n.01", "name": "tricot"}, - {"id": 11880, "synset": "trident.n.01", "name": "trident"}, - {"id": 11881, "synset": "trigger.n.02", "name": "trigger"}, - {"id": 11882, "synset": "trimaran.n.01", "name": "trimaran"}, - {"id": 11883, "synset": "trimmer.n.02", "name": "trimmer"}, - {"id": 11884, "synset": "trimmer_arch.n.01", "name": "trimmer_arch"}, - {"id": 11885, "synset": "triode.n.01", "name": "triode"}, - {"id": 11886, "synset": "triptych.n.01", "name": "triptych"}, - {"id": 11887, "synset": "trip_wire.n.02", "name": "trip_wire"}, - {"id": 11888, "synset": "trireme.n.01", "name": "trireme"}, - {"id": 11889, "synset": "triskelion.n.01", "name": "triskelion"}, - {"id": 11890, "synset": "triumphal_arch.n.01", "name": "triumphal_arch"}, - {"id": 11891, "synset": "trivet.n.02", "name": "trivet"}, - {"id": 11892, "synset": "trivet.n.01", "name": "trivet"}, - {"id": 11893, "synset": "troika.n.01", "name": "troika"}, - {"id": 11894, "synset": "troll.n.03", "name": "troll"}, - {"id": 11895, "synset": "trolleybus.n.01", "name": "trolleybus"}, - {"id": 11896, "synset": "trombone.n.01", "name": "trombone"}, - {"id": 11897, "synset": "troop_carrier.n.01", "name": "troop_carrier"}, - {"id": 11898, "synset": "troopship.n.01", "name": "troopship"}, - {"id": 11899, "synset": "trophy_case.n.01", "name": "trophy_case"}, - {"id": 11900, "synset": "trough.n.05", "name": "trough"}, - {"id": 11901, "synset": "trouser.n.02", "name": "trouser"}, - {"id": 11902, "synset": "trouser_cuff.n.01", "name": "trouser_cuff"}, - {"id": 11903, "synset": "trouser_press.n.01", "name": "trouser_press"}, - {"id": 11904, "synset": "trousseau.n.01", "name": "trousseau"}, - {"id": 11905, "synset": "trowel.n.01", "name": "trowel"}, - {"id": 11906, "synset": "trumpet_arch.n.01", "name": "trumpet_arch"}, - {"id": 11907, "synset": "truncheon.n.01", "name": "truncheon"}, - {"id": 11908, "synset": "trundle_bed.n.01", "name": "trundle_bed"}, - {"id": 11909, "synset": "trunk_hose.n.01", "name": "trunk_hose"}, - {"id": 11910, "synset": "trunk_lid.n.01", "name": "trunk_lid"}, - {"id": 11911, "synset": "trunk_line.n.02", "name": "trunk_line"}, - {"id": 11912, "synset": "truss.n.02", "name": "truss"}, - {"id": 11913, "synset": "truss_bridge.n.01", "name": "truss_bridge"}, - {"id": 11914, "synset": "try_square.n.01", "name": "try_square"}, - {"id": 11915, "synset": "t-square.n.01", "name": "T-square"}, - {"id": 11916, "synset": "tube.n.02", "name": "tube"}, - {"id": 11917, "synset": "tuck_box.n.01", "name": "tuck_box"}, - {"id": 11918, "synset": "tucker.n.04", "name": "tucker"}, - {"id": 11919, "synset": "tucker-bag.n.01", "name": "tucker-bag"}, - {"id": 11920, "synset": "tuck_shop.n.01", "name": "tuck_shop"}, - {"id": 11921, "synset": "tudor_arch.n.01", "name": "Tudor_arch"}, - {"id": 11922, "synset": "tudung.n.01", "name": "tudung"}, - {"id": 11923, "synset": "tugboat.n.01", "name": "tugboat"}, - {"id": 11924, "synset": "tulle.n.01", "name": "tulle"}, - {"id": 11925, "synset": "tumble-dryer.n.01", "name": "tumble-dryer"}, - {"id": 11926, "synset": "tumbler.n.02", "name": "tumbler"}, - {"id": 11927, "synset": "tumbrel.n.01", "name": "tumbrel"}, - {"id": 11928, "synset": "tun.n.01", "name": "tun"}, - {"id": 11929, "synset": "tunic.n.02", "name": "tunic"}, - {"id": 11930, "synset": "tuning_fork.n.01", "name": "tuning_fork"}, - {"id": 11931, "synset": "tupik.n.01", "name": "tupik"}, - {"id": 11932, "synset": "turbine.n.01", "name": "turbine"}, - {"id": 11933, "synset": "turbogenerator.n.01", "name": "turbogenerator"}, - {"id": 11934, "synset": "tureen.n.01", "name": "tureen"}, - {"id": 11935, "synset": "turkish_bath.n.01", "name": "Turkish_bath"}, - {"id": 11936, "synset": "turkish_towel.n.01", "name": "Turkish_towel"}, - {"id": 11937, "synset": "turk's_head.n.01", "name": "Turk's_head"}, - {"id": 11938, "synset": "turnbuckle.n.01", "name": "turnbuckle"}, - {"id": 11939, "synset": "turner.n.08", "name": "turner"}, - {"id": 11940, "synset": "turnery.n.01", "name": "turnery"}, - {"id": 11941, "synset": "turnpike.n.01", "name": "turnpike"}, - {"id": 11942, "synset": "turnspit.n.01", "name": "turnspit"}, - {"id": 11943, "synset": "turnstile.n.01", "name": "turnstile"}, - {"id": 11944, "synset": "turntable.n.01", "name": "turntable"}, - {"id": 11945, "synset": "turntable.n.02", "name": "turntable"}, - {"id": 11946, "synset": "turret.n.01", "name": "turret"}, - {"id": 11947, "synset": "turret_clock.n.01", "name": "turret_clock"}, - {"id": 11948, "synset": "tweed.n.01", "name": "tweed"}, - {"id": 11949, "synset": "tweeter.n.01", "name": "tweeter"}, - {"id": 11950, "synset": "twenty-two.n.02", "name": "twenty-two"}, - {"id": 11951, "synset": "twenty-two_pistol.n.01", "name": "twenty-two_pistol"}, - {"id": 11952, "synset": "twenty-two_rifle.n.01", "name": "twenty-two_rifle"}, - {"id": 11953, "synset": "twill.n.02", "name": "twill"}, - {"id": 11954, "synset": "twill.n.01", "name": "twill"}, - {"id": 11955, "synset": "twin_bed.n.01", "name": "twin_bed"}, - {"id": 11956, "synset": "twinjet.n.01", "name": "twinjet"}, - {"id": 11957, "synset": "twist_bit.n.01", "name": "twist_bit"}, - {"id": 11958, "synset": "two-by-four.n.01", "name": "two-by-four"}, - {"id": 11959, "synset": "two-man_tent.n.01", "name": "two-man_tent"}, - {"id": 11960, "synset": "two-piece.n.01", "name": "two-piece"}, - {"id": 11961, "synset": "typesetting_machine.n.01", "name": "typesetting_machine"}, - {"id": 11962, "synset": "typewriter_carriage.n.01", "name": "typewriter_carriage"}, - {"id": 11963, "synset": "typewriter_keyboard.n.01", "name": "typewriter_keyboard"}, - {"id": 11964, "synset": "tyrolean.n.02", "name": "tyrolean"}, - {"id": 11965, "synset": "uke.n.01", "name": "uke"}, - {"id": 11966, "synset": "ulster.n.02", "name": "ulster"}, - {"id": 11967, "synset": "ultracentrifuge.n.01", "name": "ultracentrifuge"}, - {"id": 11968, "synset": "ultramicroscope.n.01", "name": "ultramicroscope"}, - {"id": 11969, "synset": "ultrasuede.n.01", "name": "Ultrasuede"}, - {"id": 11970, "synset": "ultraviolet_lamp.n.01", "name": "ultraviolet_lamp"}, - {"id": 11971, "synset": "umbrella_tent.n.01", "name": "umbrella_tent"}, - {"id": 11972, "synset": "undercarriage.n.01", "name": "undercarriage"}, - {"id": 11973, "synset": "undercoat.n.01", "name": "undercoat"}, - {"id": 11974, "synset": "undergarment.n.01", "name": "undergarment"}, - {"id": 11975, "synset": "underpants.n.01", "name": "underpants"}, - {"id": 11976, "synset": "undies.n.01", "name": "undies"}, - {"id": 11977, "synset": "uneven_parallel_bars.n.01", "name": "uneven_parallel_bars"}, - {"id": 11978, "synset": "uniform.n.01", "name": "uniform"}, - {"id": 11979, "synset": "universal_joint.n.01", "name": "universal_joint"}, - {"id": 11980, "synset": "university.n.02", "name": "university"}, - {"id": 11981, "synset": "upholstery.n.01", "name": "upholstery"}, - {"id": 11982, "synset": "upholstery_material.n.01", "name": "upholstery_material"}, - {"id": 11983, "synset": "upholstery_needle.n.01", "name": "upholstery_needle"}, - {"id": 11984, "synset": "uplift.n.02", "name": "uplift"}, - {"id": 11985, "synset": "upper_berth.n.01", "name": "upper_berth"}, - {"id": 11986, "synset": "upright.n.02", "name": "upright"}, - {"id": 11987, "synset": "upset.n.04", "name": "upset"}, - {"id": 11988, "synset": "upstairs.n.01", "name": "upstairs"}, - {"id": 11989, "synset": "urceole.n.01", "name": "urceole"}, - {"id": 11990, "synset": "urn.n.02", "name": "urn"}, - {"id": 11991, "synset": "used-car.n.01", "name": "used-car"}, - {"id": 11992, "synset": "utensil.n.01", "name": "utensil"}, - {"id": 11993, "synset": "uzi.n.01", "name": "Uzi"}, - {"id": 11994, "synset": "vacation_home.n.01", "name": "vacation_home"}, - {"id": 11995, "synset": "vacuum_chamber.n.01", "name": "vacuum_chamber"}, - {"id": 11996, "synset": "vacuum_flask.n.01", "name": "vacuum_flask"}, - {"id": 11997, "synset": "vacuum_gauge.n.01", "name": "vacuum_gauge"}, - {"id": 11998, "synset": "valenciennes.n.02", "name": "Valenciennes"}, - {"id": 11999, "synset": "valise.n.01", "name": "valise"}, - {"id": 12000, "synset": "valve.n.03", "name": "valve"}, - {"id": 12001, "synset": "valve.n.02", "name": "valve"}, - {"id": 12002, "synset": "valve-in-head_engine.n.01", "name": "valve-in-head_engine"}, - {"id": 12003, "synset": "vambrace.n.01", "name": "vambrace"}, - {"id": 12004, "synset": "van.n.05", "name": "van"}, - {"id": 12005, "synset": "van.n.04", "name": "van"}, - {"id": 12006, "synset": "vane.n.02", "name": "vane"}, - {"id": 12007, "synset": "vaporizer.n.01", "name": "vaporizer"}, - {"id": 12008, "synset": "variable-pitch_propeller.n.01", "name": "variable-pitch_propeller"}, - {"id": 12009, "synset": "variometer.n.01", "name": "variometer"}, - {"id": 12010, "synset": "varnish.n.01", "name": "varnish"}, - {"id": 12011, "synset": "vault.n.03", "name": "vault"}, - {"id": 12012, "synset": "vault.n.02", "name": "vault"}, - {"id": 12013, "synset": "vaulting_horse.n.01", "name": "vaulting_horse"}, - {"id": 12014, "synset": "vehicle.n.01", "name": "vehicle"}, - {"id": 12015, "synset": "velcro.n.01", "name": "Velcro"}, - {"id": 12016, "synset": "velocipede.n.01", "name": "velocipede"}, - {"id": 12017, "synset": "velour.n.01", "name": "velour"}, - {"id": 12018, "synset": "velvet.n.01", "name": "velvet"}, - {"id": 12019, "synset": "velveteen.n.01", "name": "velveteen"}, - {"id": 12020, "synset": "veneer.n.01", "name": "veneer"}, - {"id": 12021, "synset": "venetian_blind.n.01", "name": "Venetian_blind"}, - {"id": 12022, "synset": "venn_diagram.n.01", "name": "Venn_diagram"}, - {"id": 12023, "synset": "ventilation.n.02", "name": "ventilation"}, - {"id": 12024, "synset": "ventilation_shaft.n.01", "name": "ventilation_shaft"}, - {"id": 12025, "synset": "ventilator.n.01", "name": "ventilator"}, - {"id": 12026, "synset": "veranda.n.01", "name": "veranda"}, - {"id": 12027, "synset": "verdigris.n.02", "name": "verdigris"}, - {"id": 12028, "synset": "vernier_caliper.n.01", "name": "vernier_caliper"}, - {"id": 12029, "synset": "vernier_scale.n.01", "name": "vernier_scale"}, - {"id": 12030, "synset": "vertical_file.n.01", "name": "vertical_file"}, - {"id": 12031, "synset": "vertical_stabilizer.n.01", "name": "vertical_stabilizer"}, - {"id": 12032, "synset": "vertical_tail.n.01", "name": "vertical_tail"}, - {"id": 12033, "synset": "very_pistol.n.01", "name": "Very_pistol"}, - {"id": 12034, "synset": "vessel.n.02", "name": "vessel"}, - {"id": 12035, "synset": "vessel.n.03", "name": "vessel"}, - {"id": 12036, "synset": "vestiture.n.01", "name": "vestiture"}, - {"id": 12037, "synset": "vestment.n.01", "name": "vestment"}, - {"id": 12038, "synset": "vest_pocket.n.01", "name": "vest_pocket"}, - {"id": 12039, "synset": "vestry.n.02", "name": "vestry"}, - {"id": 12040, "synset": "viaduct.n.01", "name": "viaduct"}, - {"id": 12041, "synset": "vibraphone.n.01", "name": "vibraphone"}, - {"id": 12042, "synset": "vibrator.n.02", "name": "vibrator"}, - {"id": 12043, "synset": "vibrator.n.01", "name": "vibrator"}, - {"id": 12044, "synset": "victrola.n.01", "name": "Victrola"}, - {"id": 12045, "synset": "vicuna.n.02", "name": "vicuna"}, - {"id": 12046, "synset": "videocassette.n.01", "name": "videocassette"}, - {"id": 12047, "synset": "videocassette_recorder.n.01", "name": "videocassette_recorder"}, - {"id": 12048, "synset": "videodisk.n.01", "name": "videodisk"}, - {"id": 12049, "synset": "video_recording.n.01", "name": "video_recording"}, - {"id": 12050, "synset": "videotape.n.02", "name": "videotape"}, - {"id": 12051, "synset": "vigil_light.n.01", "name": "vigil_light"}, - {"id": 12052, "synset": "villa.n.04", "name": "villa"}, - {"id": 12053, "synset": "villa.n.03", "name": "villa"}, - {"id": 12054, "synset": "villa.n.02", "name": "villa"}, - {"id": 12055, "synset": "viol.n.01", "name": "viol"}, - {"id": 12056, "synset": "viola.n.03", "name": "viola"}, - {"id": 12057, "synset": "viola_da_braccio.n.01", "name": "viola_da_braccio"}, - {"id": 12058, "synset": "viola_da_gamba.n.01", "name": "viola_da_gamba"}, - {"id": 12059, "synset": "viola_d'amore.n.01", "name": "viola_d'amore"}, - {"id": 12060, "synset": "virginal.n.01", "name": "virginal"}, - {"id": 12061, "synset": "viscometer.n.01", "name": "viscometer"}, - {"id": 12062, "synset": "viscose_rayon.n.01", "name": "viscose_rayon"}, - {"id": 12063, "synset": "vise.n.01", "name": "vise"}, - {"id": 12064, "synset": "visor.n.01", "name": "visor"}, - {"id": 12065, "synset": "visual_display_unit.n.01", "name": "visual_display_unit"}, - {"id": 12066, "synset": "vivarium.n.01", "name": "vivarium"}, - {"id": 12067, "synset": "viyella.n.01", "name": "Viyella"}, - {"id": 12068, "synset": "voile.n.01", "name": "voile"}, - {"id": 12069, "synset": "volleyball_net.n.01", "name": "volleyball_net"}, - {"id": 12070, "synset": "voltage_regulator.n.01", "name": "voltage_regulator"}, - {"id": 12071, "synset": "voltaic_cell.n.01", "name": "voltaic_cell"}, - {"id": 12072, "synset": "voltaic_pile.n.01", "name": "voltaic_pile"}, - {"id": 12073, "synset": "voltmeter.n.01", "name": "voltmeter"}, - {"id": 12074, "synset": "vomitory.n.01", "name": "vomitory"}, - {"id": 12075, "synset": "von_neumann_machine.n.01", "name": "von_Neumann_machine"}, - {"id": 12076, "synset": "voting_booth.n.01", "name": "voting_booth"}, - {"id": 12077, "synset": "voting_machine.n.01", "name": "voting_machine"}, - {"id": 12078, "synset": "voussoir.n.01", "name": "voussoir"}, - {"id": 12079, "synset": "vox_angelica.n.01", "name": "vox_angelica"}, - {"id": 12080, "synset": "vox_humana.n.01", "name": "vox_humana"}, - {"id": 12081, "synset": "waders.n.01", "name": "waders"}, - {"id": 12082, "synset": "wading_pool.n.01", "name": "wading_pool"}, - {"id": 12083, "synset": "wagon.n.04", "name": "wagon"}, - {"id": 12084, "synset": "wagon_tire.n.01", "name": "wagon_tire"}, - {"id": 12085, "synset": "wain.n.03", "name": "wain"}, - {"id": 12086, "synset": "wainscot.n.02", "name": "wainscot"}, - {"id": 12087, "synset": "wainscoting.n.01", "name": "wainscoting"}, - {"id": 12088, "synset": "waist_pack.n.01", "name": "waist_pack"}, - {"id": 12089, "synset": "walker.n.06", "name": "walker"}, - {"id": 12090, "synset": "walker.n.05", "name": "walker"}, - {"id": 12091, "synset": "walker.n.04", "name": "walker"}, - {"id": 12092, "synset": "walkie-talkie.n.01", "name": "walkie-talkie"}, - {"id": 12093, "synset": "walk-in.n.04", "name": "walk-in"}, - {"id": 12094, "synset": "walking_shoe.n.01", "name": "walking_shoe"}, - {"id": 12095, "synset": "walkman.n.01", "name": "Walkman"}, - {"id": 12096, "synset": "walk-up_apartment.n.01", "name": "walk-up_apartment"}, - {"id": 12097, "synset": "wall.n.01", "name": "wall"}, - {"id": 12098, "synset": "wall.n.07", "name": "wall"}, - {"id": 12099, "synset": "wall_tent.n.01", "name": "wall_tent"}, - {"id": 12100, "synset": "wall_unit.n.01", "name": "wall_unit"}, - {"id": 12101, "synset": "wand.n.01", "name": "wand"}, - {"id": 12102, "synset": "wankel_engine.n.01", "name": "Wankel_engine"}, - {"id": 12103, "synset": "ward.n.03", "name": "ward"}, - {"id": 12104, "synset": "wardroom.n.01", "name": "wardroom"}, - {"id": 12105, "synset": "warehouse.n.01", "name": "warehouse"}, - {"id": 12106, "synset": "warming_pan.n.01", "name": "warming_pan"}, - {"id": 12107, "synset": "war_paint.n.02", "name": "war_paint"}, - {"id": 12108, "synset": "warplane.n.01", "name": "warplane"}, - {"id": 12109, "synset": "war_room.n.01", "name": "war_room"}, - {"id": 12110, "synset": "warship.n.01", "name": "warship"}, - {"id": 12111, "synset": "wash.n.01", "name": "wash"}, - {"id": 12112, "synset": "wash-and-wear.n.01", "name": "wash-and-wear"}, - {"id": 12113, "synset": "washbasin.n.02", "name": "washbasin"}, - {"id": 12114, "synset": "washboard.n.02", "name": "washboard"}, - {"id": 12115, "synset": "washboard.n.01", "name": "washboard"}, - {"id": 12116, "synset": "washer.n.02", "name": "washer"}, - {"id": 12117, "synset": "washhouse.n.01", "name": "washhouse"}, - {"id": 12118, "synset": "washroom.n.01", "name": "washroom"}, - {"id": 12119, "synset": "washstand.n.01", "name": "washstand"}, - {"id": 12120, "synset": "washtub.n.01", "name": "washtub"}, - {"id": 12121, "synset": "wastepaper_basket.n.01", "name": "wastepaper_basket"}, - {"id": 12122, "synset": "watch_cap.n.01", "name": "watch_cap"}, - {"id": 12123, "synset": "watch_case.n.01", "name": "watch_case"}, - {"id": 12124, "synset": "watch_glass.n.01", "name": "watch_glass"}, - {"id": 12125, "synset": "watchtower.n.01", "name": "watchtower"}, - {"id": 12126, "synset": "water-base_paint.n.01", "name": "water-base_paint"}, - {"id": 12127, "synset": "water_bed.n.01", "name": "water_bed"}, - {"id": 12128, "synset": "water_butt.n.01", "name": "water_butt"}, - {"id": 12129, "synset": "water_cart.n.01", "name": "water_cart"}, - {"id": 12130, "synset": "water_chute.n.01", "name": "water_chute"}, - {"id": 12131, "synset": "water_closet.n.01", "name": "water_closet"}, - {"id": 12132, "synset": "watercolor.n.02", "name": "watercolor"}, - {"id": 12133, "synset": "water-cooled_reactor.n.01", "name": "water-cooled_reactor"}, - {"id": 12134, "synset": "water_filter.n.01", "name": "water_filter"}, - {"id": 12135, "synset": "water_gauge.n.01", "name": "water_gauge"}, - {"id": 12136, "synset": "water_glass.n.02", "name": "water_glass"}, - {"id": 12137, "synset": "water_hazard.n.01", "name": "water_hazard"}, - {"id": 12138, "synset": "watering_cart.n.01", "name": "watering_cart"}, - {"id": 12139, "synset": "water_jacket.n.01", "name": "water_jacket"}, - {"id": 12140, "synset": "water_jump.n.01", "name": "water_jump"}, - {"id": 12141, "synset": "water_level.n.04", "name": "water_level"}, - {"id": 12142, "synset": "water_meter.n.01", "name": "water_meter"}, - {"id": 12143, "synset": "water_mill.n.01", "name": "water_mill"}, - {"id": 12144, "synset": "waterproof.n.01", "name": "waterproof"}, - {"id": 12145, "synset": "waterproofing.n.02", "name": "waterproofing"}, - {"id": 12146, "synset": "water_pump.n.01", "name": "water_pump"}, - {"id": 12147, "synset": "waterspout.n.03", "name": "waterspout"}, - {"id": 12148, "synset": "water_wagon.n.01", "name": "water_wagon"}, - {"id": 12149, "synset": "waterwheel.n.02", "name": "waterwheel"}, - {"id": 12150, "synset": "waterwheel.n.01", "name": "waterwheel"}, - {"id": 12151, "synset": "water_wings.n.01", "name": "water_wings"}, - {"id": 12152, "synset": "waterworks.n.02", "name": "waterworks"}, - {"id": 12153, "synset": "wattmeter.n.01", "name": "wattmeter"}, - {"id": 12154, "synset": "waxwork.n.02", "name": "waxwork"}, - {"id": 12155, "synset": "ways.n.01", "name": "ways"}, - {"id": 12156, "synset": "weapon.n.01", "name": "weapon"}, - {"id": 12157, "synset": "weaponry.n.01", "name": "weaponry"}, - {"id": 12158, "synset": "weapons_carrier.n.01", "name": "weapons_carrier"}, - {"id": 12159, "synset": "weathercock.n.01", "name": "weathercock"}, - {"id": 12160, "synset": "weatherglass.n.01", "name": "weatherglass"}, - {"id": 12161, "synset": "weather_satellite.n.01", "name": "weather_satellite"}, - {"id": 12162, "synset": "weather_ship.n.01", "name": "weather_ship"}, - {"id": 12163, "synset": "web.n.02", "name": "web"}, - {"id": 12164, "synset": "web.n.06", "name": "web"}, - {"id": 12165, "synset": "webbing.n.03", "name": "webbing"}, - {"id": 12166, "synset": "wedge.n.06", "name": "wedge"}, - {"id": 12167, "synset": "wedge.n.05", "name": "wedge"}, - {"id": 12168, "synset": "wedgie.n.01", "name": "wedgie"}, - {"id": 12169, "synset": "wedgwood.n.02", "name": "Wedgwood"}, - {"id": 12170, "synset": "weeder.n.02", "name": "weeder"}, - {"id": 12171, "synset": "weeds.n.01", "name": "weeds"}, - {"id": 12172, "synset": "weekender.n.02", "name": "weekender"}, - {"id": 12173, "synset": "weighbridge.n.01", "name": "weighbridge"}, - {"id": 12174, "synset": "weight.n.02", "name": "weight"}, - {"id": 12175, "synset": "weir.n.01", "name": "weir"}, - {"id": 12176, "synset": "weir.n.02", "name": "weir"}, - {"id": 12177, "synset": "welcome_wagon.n.01", "name": "welcome_wagon"}, - {"id": 12178, "synset": "weld.n.03", "name": "weld"}, - {"id": 12179, "synset": "welder's_mask.n.01", "name": "welder's_mask"}, - {"id": 12180, "synset": "weldment.n.01", "name": "weldment"}, - {"id": 12181, "synset": "well.n.02", "name": "well"}, - {"id": 12182, "synset": "wellhead.n.02", "name": "wellhead"}, - {"id": 12183, "synset": "welt.n.02", "name": "welt"}, - {"id": 12184, "synset": "weston_cell.n.01", "name": "Weston_cell"}, - {"id": 12185, "synset": "wet_bar.n.01", "name": "wet_bar"}, - {"id": 12186, "synset": "wet-bulb_thermometer.n.01", "name": "wet-bulb_thermometer"}, - {"id": 12187, "synset": "wet_cell.n.01", "name": "wet_cell"}, - {"id": 12188, "synset": "wet_fly.n.01", "name": "wet_fly"}, - {"id": 12189, "synset": "whaleboat.n.01", "name": "whaleboat"}, - {"id": 12190, "synset": "whaler.n.02", "name": "whaler"}, - {"id": 12191, "synset": "whaling_gun.n.01", "name": "whaling_gun"}, - {"id": 12192, "synset": "wheel.n.04", "name": "wheel"}, - {"id": 12193, "synset": "wheel_and_axle.n.01", "name": "wheel_and_axle"}, - {"id": 12194, "synset": "wheeled_vehicle.n.01", "name": "wheeled_vehicle"}, - {"id": 12195, "synset": "wheelwork.n.01", "name": "wheelwork"}, - {"id": 12196, "synset": "wherry.n.02", "name": "wherry"}, - {"id": 12197, "synset": "wherry.n.01", "name": "wherry"}, - {"id": 12198, "synset": "whetstone.n.01", "name": "whetstone"}, - {"id": 12199, "synset": "whiffletree.n.01", "name": "whiffletree"}, - {"id": 12200, "synset": "whip.n.01", "name": "whip"}, - {"id": 12201, "synset": "whipcord.n.02", "name": "whipcord"}, - {"id": 12202, "synset": "whipping_post.n.01", "name": "whipping_post"}, - {"id": 12203, "synset": "whipstitch.n.01", "name": "whipstitch"}, - {"id": 12204, "synset": "whirler.n.02", "name": "whirler"}, - {"id": 12205, "synset": "whisk.n.02", "name": "whisk"}, - {"id": 12206, "synset": "whisk.n.01", "name": "whisk"}, - {"id": 12207, "synset": "whiskey_bottle.n.01", "name": "whiskey_bottle"}, - {"id": 12208, "synset": "whiskey_jug.n.01", "name": "whiskey_jug"}, - {"id": 12209, "synset": "whispering_gallery.n.01", "name": "whispering_gallery"}, - {"id": 12210, "synset": "whistle.n.04", "name": "whistle"}, - {"id": 12211, "synset": "white.n.11", "name": "white"}, - {"id": 12212, "synset": "white_goods.n.01", "name": "white_goods"}, - {"id": 12213, "synset": "whitewash.n.02", "name": "whitewash"}, - {"id": 12214, "synset": "whorehouse.n.01", "name": "whorehouse"}, - {"id": 12215, "synset": "wick.n.02", "name": "wick"}, - {"id": 12216, "synset": "wicker.n.02", "name": "wicker"}, - {"id": 12217, "synset": "wicker_basket.n.01", "name": "wicker_basket"}, - {"id": 12218, "synset": "wicket.n.02", "name": "wicket"}, - {"id": 12219, "synset": "wicket.n.01", "name": "wicket"}, - {"id": 12220, "synset": "wickiup.n.01", "name": "wickiup"}, - {"id": 12221, "synset": "wide-angle_lens.n.01", "name": "wide-angle_lens"}, - {"id": 12222, "synset": "widebody_aircraft.n.01", "name": "widebody_aircraft"}, - {"id": 12223, "synset": "wide_wale.n.01", "name": "wide_wale"}, - {"id": 12224, "synset": "widow's_walk.n.01", "name": "widow's_walk"}, - {"id": 12225, "synset": "wiffle.n.01", "name": "Wiffle"}, - {"id": 12226, "synset": "wigwam.n.01", "name": "wigwam"}, - {"id": 12227, "synset": "wilton.n.01", "name": "Wilton"}, - {"id": 12228, "synset": "wimple.n.01", "name": "wimple"}, - {"id": 12229, "synset": "wincey.n.01", "name": "wincey"}, - {"id": 12230, "synset": "winceyette.n.01", "name": "winceyette"}, - {"id": 12231, "synset": "winch.n.01", "name": "winch"}, - {"id": 12232, "synset": "winchester.n.02", "name": "Winchester"}, - {"id": 12233, "synset": "windbreak.n.01", "name": "windbreak"}, - {"id": 12234, "synset": "winder.n.02", "name": "winder"}, - {"id": 12235, "synset": "wind_instrument.n.01", "name": "wind_instrument"}, - {"id": 12236, "synset": "windjammer.n.01", "name": "windjammer"}, - {"id": 12237, "synset": "windmill.n.02", "name": "windmill"}, - {"id": 12238, "synset": "window.n.01", "name": "window"}, - {"id": 12239, "synset": "window.n.08", "name": "window"}, - {"id": 12240, "synset": "window_blind.n.01", "name": "window_blind"}, - {"id": 12241, "synset": "window_envelope.n.01", "name": "window_envelope"}, - {"id": 12242, "synset": "window_frame.n.01", "name": "window_frame"}, - {"id": 12243, "synset": "window_screen.n.01", "name": "window_screen"}, - {"id": 12244, "synset": "window_seat.n.01", "name": "window_seat"}, - {"id": 12245, "synset": "window_shade.n.01", "name": "window_shade"}, - {"id": 12246, "synset": "windowsill.n.01", "name": "windowsill"}, - {"id": 12247, "synset": "windshield.n.01", "name": "windshield"}, - {"id": 12248, "synset": "windsor_chair.n.01", "name": "Windsor_chair"}, - {"id": 12249, "synset": "windsor_knot.n.01", "name": "Windsor_knot"}, - {"id": 12250, "synset": "windsor_tie.n.01", "name": "Windsor_tie"}, - {"id": 12251, "synset": "wind_tee.n.01", "name": "wind_tee"}, - {"id": 12252, "synset": "wind_tunnel.n.01", "name": "wind_tunnel"}, - {"id": 12253, "synset": "wind_turbine.n.01", "name": "wind_turbine"}, - {"id": 12254, "synset": "wine_bar.n.01", "name": "wine_bar"}, - {"id": 12255, "synset": "wine_cask.n.01", "name": "wine_cask"}, - {"id": 12256, "synset": "winepress.n.01", "name": "winepress"}, - {"id": 12257, "synset": "winery.n.01", "name": "winery"}, - {"id": 12258, "synset": "wineskin.n.01", "name": "wineskin"}, - {"id": 12259, "synset": "wing.n.02", "name": "wing"}, - {"id": 12260, "synset": "wing_chair.n.01", "name": "wing_chair"}, - {"id": 12261, "synset": "wing_nut.n.02", "name": "wing_nut"}, - {"id": 12262, "synset": "wing_tip.n.02", "name": "wing_tip"}, - {"id": 12263, "synset": "wing_tip.n.01", "name": "wing_tip"}, - {"id": 12264, "synset": "wiper.n.02", "name": "wiper"}, - {"id": 12265, "synset": "wiper_motor.n.01", "name": "wiper_motor"}, - {"id": 12266, "synset": "wire.n.01", "name": "wire"}, - {"id": 12267, "synset": "wire.n.02", "name": "wire"}, - {"id": 12268, "synset": "wire_cloth.n.01", "name": "wire_cloth"}, - {"id": 12269, "synset": "wire_cutter.n.01", "name": "wire_cutter"}, - {"id": 12270, "synset": "wire_gauge.n.01", "name": "wire_gauge"}, - { - "id": 12271, - "synset": "wireless_local_area_network.n.01", - "name": "wireless_local_area_network", - }, - {"id": 12272, "synset": "wire_matrix_printer.n.01", "name": "wire_matrix_printer"}, - {"id": 12273, "synset": "wire_recorder.n.01", "name": "wire_recorder"}, - {"id": 12274, "synset": "wire_stripper.n.01", "name": "wire_stripper"}, - {"id": 12275, "synset": "wirework.n.01", "name": "wirework"}, - {"id": 12276, "synset": "wiring.n.01", "name": "wiring"}, - {"id": 12277, "synset": "wishing_cap.n.01", "name": "wishing_cap"}, - {"id": 12278, "synset": "witness_box.n.01", "name": "witness_box"}, - {"id": 12279, "synset": "woman's_clothing.n.01", "name": "woman's_clothing"}, - {"id": 12280, "synset": "wood.n.08", "name": "wood"}, - {"id": 12281, "synset": "woodcarving.n.01", "name": "woodcarving"}, - {"id": 12282, "synset": "wood_chisel.n.01", "name": "wood_chisel"}, - {"id": 12283, "synset": "woodenware.n.01", "name": "woodenware"}, - {"id": 12284, "synset": "woodscrew.n.01", "name": "woodscrew"}, - {"id": 12285, "synset": "woodshed.n.01", "name": "woodshed"}, - {"id": 12286, "synset": "wood_vise.n.01", "name": "wood_vise"}, - {"id": 12287, "synset": "woodwind.n.01", "name": "woodwind"}, - {"id": 12288, "synset": "woof.n.01", "name": "woof"}, - {"id": 12289, "synset": "woofer.n.01", "name": "woofer"}, - {"id": 12290, "synset": "wool.n.01", "name": "wool"}, - {"id": 12291, "synset": "workbasket.n.01", "name": "workbasket"}, - {"id": 12292, "synset": "workbench.n.01", "name": "workbench"}, - {"id": 12293, "synset": "work-clothing.n.01", "name": "work-clothing"}, - {"id": 12294, "synset": "workhouse.n.02", "name": "workhouse"}, - {"id": 12295, "synset": "workhouse.n.01", "name": "workhouse"}, - {"id": 12296, "synset": "workpiece.n.01", "name": "workpiece"}, - {"id": 12297, "synset": "workroom.n.01", "name": "workroom"}, - {"id": 12298, "synset": "works.n.04", "name": "works"}, - {"id": 12299, "synset": "work-shirt.n.01", "name": "work-shirt"}, - {"id": 12300, "synset": "workstation.n.01", "name": "workstation"}, - {"id": 12301, "synset": "worktable.n.01", "name": "worktable"}, - {"id": 12302, "synset": "workwear.n.01", "name": "workwear"}, - {"id": 12303, "synset": "world_wide_web.n.01", "name": "World_Wide_Web"}, - {"id": 12304, "synset": "worm_fence.n.01", "name": "worm_fence"}, - {"id": 12305, "synset": "worm_gear.n.01", "name": "worm_gear"}, - {"id": 12306, "synset": "worm_wheel.n.01", "name": "worm_wheel"}, - {"id": 12307, "synset": "worsted.n.01", "name": "worsted"}, - {"id": 12308, "synset": "worsted.n.02", "name": "worsted"}, - {"id": 12309, "synset": "wrap.n.01", "name": "wrap"}, - {"id": 12310, "synset": "wraparound.n.01", "name": "wraparound"}, - {"id": 12311, "synset": "wrapping.n.01", "name": "wrapping"}, - {"id": 12312, "synset": "wreck.n.04", "name": "wreck"}, - {"id": 12313, "synset": "wrestling_mat.n.01", "name": "wrestling_mat"}, - {"id": 12314, "synset": "wringer.n.01", "name": "wringer"}, - {"id": 12315, "synset": "wrist_pad.n.01", "name": "wrist_pad"}, - {"id": 12316, "synset": "wrist_pin.n.01", "name": "wrist_pin"}, - {"id": 12317, "synset": "wristwatch.n.01", "name": "wristwatch"}, - {"id": 12318, "synset": "writing_arm.n.01", "name": "writing_arm"}, - {"id": 12319, "synset": "writing_desk.n.02", "name": "writing_desk"}, - {"id": 12320, "synset": "writing_desk.n.01", "name": "writing_desk"}, - {"id": 12321, "synset": "writing_implement.n.01", "name": "writing_implement"}, - {"id": 12322, "synset": "xerographic_printer.n.01", "name": "xerographic_printer"}, - {"id": 12323, "synset": "xerox.n.02", "name": "Xerox"}, - {"id": 12324, "synset": "x-ray_film.n.01", "name": "X-ray_film"}, - {"id": 12325, "synset": "x-ray_machine.n.01", "name": "X-ray_machine"}, - {"id": 12326, "synset": "x-ray_tube.n.01", "name": "X-ray_tube"}, - {"id": 12327, "synset": "yacht_chair.n.01", "name": "yacht_chair"}, - {"id": 12328, "synset": "yagi.n.01", "name": "yagi"}, - {"id": 12329, "synset": "yard.n.09", "name": "yard"}, - {"id": 12330, "synset": "yard.n.08", "name": "yard"}, - {"id": 12331, "synset": "yardarm.n.01", "name": "yardarm"}, - {"id": 12332, "synset": "yard_marker.n.01", "name": "yard_marker"}, - {"id": 12333, "synset": "yardstick.n.02", "name": "yardstick"}, - {"id": 12334, "synset": "yarmulke.n.01", "name": "yarmulke"}, - {"id": 12335, "synset": "yashmak.n.01", "name": "yashmak"}, - {"id": 12336, "synset": "yataghan.n.01", "name": "yataghan"}, - {"id": 12337, "synset": "yawl.n.02", "name": "yawl"}, - {"id": 12338, "synset": "yawl.n.01", "name": "yawl"}, - {"id": 12339, "synset": "yoke.n.01", "name": "yoke"}, - {"id": 12340, "synset": "yoke.n.06", "name": "yoke"}, - {"id": 12341, "synset": "yurt.n.01", "name": "yurt"}, - {"id": 12342, "synset": "zamboni.n.01", "name": "Zamboni"}, - {"id": 12343, "synset": "zero.n.04", "name": "zero"}, - {"id": 12344, "synset": "ziggurat.n.01", "name": "ziggurat"}, - {"id": 12345, "synset": "zill.n.01", "name": "zill"}, - {"id": 12346, "synset": "zip_gun.n.01", "name": "zip_gun"}, - {"id": 12347, "synset": "zither.n.01", "name": "zither"}, - {"id": 12348, "synset": "zoot_suit.n.01", "name": "zoot_suit"}, - {"id": 12349, "synset": "shading.n.01", "name": "shading"}, - {"id": 12350, "synset": "grain.n.10", "name": "grain"}, - {"id": 12351, "synset": "wood_grain.n.01", "name": "wood_grain"}, - {"id": 12352, "synset": "graining.n.01", "name": "graining"}, - {"id": 12353, "synset": "marbleization.n.01", "name": "marbleization"}, - {"id": 12354, "synset": "light.n.07", "name": "light"}, - {"id": 12355, "synset": "aura.n.02", "name": "aura"}, - {"id": 12356, "synset": "sunniness.n.01", "name": "sunniness"}, - {"id": 12357, "synset": "glint.n.02", "name": "glint"}, - {"id": 12358, "synset": "opalescence.n.01", "name": "opalescence"}, - {"id": 12359, "synset": "polish.n.01", "name": "polish"}, - { - "id": 12360, - "synset": "primary_color_for_pigments.n.01", - "name": "primary_color_for_pigments", - }, - {"id": 12361, "synset": "primary_color_for_light.n.01", "name": "primary_color_for_light"}, - {"id": 12362, "synset": "colorlessness.n.01", "name": "colorlessness"}, - {"id": 12363, "synset": "mottle.n.01", "name": "mottle"}, - {"id": 12364, "synset": "achromia.n.01", "name": "achromia"}, - {"id": 12365, "synset": "shade.n.02", "name": "shade"}, - {"id": 12366, "synset": "chromatic_color.n.01", "name": "chromatic_color"}, - {"id": 12367, "synset": "black.n.01", "name": "black"}, - {"id": 12368, "synset": "coal_black.n.01", "name": "coal_black"}, - {"id": 12369, "synset": "alabaster.n.03", "name": "alabaster"}, - {"id": 12370, "synset": "bone.n.03", "name": "bone"}, - {"id": 12371, "synset": "gray.n.01", "name": "gray"}, - {"id": 12372, "synset": "ash_grey.n.01", "name": "ash_grey"}, - {"id": 12373, "synset": "charcoal.n.03", "name": "charcoal"}, - {"id": 12374, "synset": "sanguine.n.01", "name": "sanguine"}, - {"id": 12375, "synset": "turkey_red.n.01", "name": "Turkey_red"}, - {"id": 12376, "synset": "crimson.n.01", "name": "crimson"}, - {"id": 12377, "synset": "dark_red.n.01", "name": "dark_red"}, - {"id": 12378, "synset": "claret.n.01", "name": "claret"}, - {"id": 12379, "synset": "fuschia.n.01", "name": "fuschia"}, - {"id": 12380, "synset": "maroon.n.02", "name": "maroon"}, - {"id": 12381, "synset": "orange.n.02", "name": "orange"}, - {"id": 12382, "synset": "reddish_orange.n.01", "name": "reddish_orange"}, - {"id": 12383, "synset": "yellow.n.01", "name": "yellow"}, - {"id": 12384, "synset": "gamboge.n.02", "name": "gamboge"}, - {"id": 12385, "synset": "pale_yellow.n.01", "name": "pale_yellow"}, - {"id": 12386, "synset": "green.n.01", "name": "green"}, - {"id": 12387, "synset": "greenishness.n.01", "name": "greenishness"}, - {"id": 12388, "synset": "sea_green.n.01", "name": "sea_green"}, - {"id": 12389, "synset": "sage_green.n.01", "name": "sage_green"}, - {"id": 12390, "synset": "bottle_green.n.01", "name": "bottle_green"}, - {"id": 12391, "synset": "emerald.n.03", "name": "emerald"}, - {"id": 12392, "synset": "olive_green.n.01", "name": "olive_green"}, - {"id": 12393, "synset": "jade_green.n.01", "name": "jade_green"}, - {"id": 12394, "synset": "blue.n.01", "name": "blue"}, - {"id": 12395, "synset": "azure.n.01", "name": "azure"}, - {"id": 12396, "synset": "steel_blue.n.01", "name": "steel_blue"}, - {"id": 12397, "synset": "greenish_blue.n.01", "name": "greenish_blue"}, - {"id": 12398, "synset": "purplish_blue.n.01", "name": "purplish_blue"}, - {"id": 12399, "synset": "purple.n.01", "name": "purple"}, - {"id": 12400, "synset": "tyrian_purple.n.02", "name": "Tyrian_purple"}, - {"id": 12401, "synset": "indigo.n.03", "name": "indigo"}, - {"id": 12402, "synset": "lavender.n.02", "name": "lavender"}, - {"id": 12403, "synset": "reddish_purple.n.01", "name": "reddish_purple"}, - {"id": 12404, "synset": "pink.n.01", "name": "pink"}, - {"id": 12405, "synset": "carnation.n.02", "name": "carnation"}, - {"id": 12406, "synset": "rose.n.03", "name": "rose"}, - {"id": 12407, "synset": "chestnut.n.04", "name": "chestnut"}, - {"id": 12408, "synset": "chocolate.n.03", "name": "chocolate"}, - {"id": 12409, "synset": "light_brown.n.01", "name": "light_brown"}, - {"id": 12410, "synset": "tan.n.02", "name": "tan"}, - {"id": 12411, "synset": "beige.n.01", "name": "beige"}, - {"id": 12412, "synset": "reddish_brown.n.01", "name": "reddish_brown"}, - {"id": 12413, "synset": "brick_red.n.01", "name": "brick_red"}, - {"id": 12414, "synset": "copper.n.04", "name": "copper"}, - {"id": 12415, "synset": "indian_red.n.03", "name": "Indian_red"}, - {"id": 12416, "synset": "puce.n.01", "name": "puce"}, - {"id": 12417, "synset": "olive.n.05", "name": "olive"}, - {"id": 12418, "synset": "ultramarine.n.02", "name": "ultramarine"}, - {"id": 12419, "synset": "complementary_color.n.01", "name": "complementary_color"}, - {"id": 12420, "synset": "pigmentation.n.02", "name": "pigmentation"}, - {"id": 12421, "synset": "complexion.n.01", "name": "complexion"}, - {"id": 12422, "synset": "ruddiness.n.01", "name": "ruddiness"}, - {"id": 12423, "synset": "nonsolid_color.n.01", "name": "nonsolid_color"}, - {"id": 12424, "synset": "aposematic_coloration.n.01", "name": "aposematic_coloration"}, - {"id": 12425, "synset": "cryptic_coloration.n.01", "name": "cryptic_coloration"}, - {"id": 12426, "synset": "ring.n.01", "name": "ring"}, - {"id": 12427, "synset": "center_of_curvature.n.01", "name": "center_of_curvature"}, - {"id": 12428, "synset": "cadaver.n.01", "name": "cadaver"}, - {"id": 12429, "synset": "mandibular_notch.n.01", "name": "mandibular_notch"}, - {"id": 12430, "synset": "rib.n.05", "name": "rib"}, - {"id": 12431, "synset": "skin.n.01", "name": "skin"}, - {"id": 12432, "synset": "skin_graft.n.01", "name": "skin_graft"}, - {"id": 12433, "synset": "epidermal_cell.n.01", "name": "epidermal_cell"}, - {"id": 12434, "synset": "melanocyte.n.01", "name": "melanocyte"}, - {"id": 12435, "synset": "prickle_cell.n.01", "name": "prickle_cell"}, - {"id": 12436, "synset": "columnar_cell.n.01", "name": "columnar_cell"}, - {"id": 12437, "synset": "spongioblast.n.01", "name": "spongioblast"}, - {"id": 12438, "synset": "squamous_cell.n.01", "name": "squamous_cell"}, - {"id": 12439, "synset": "amyloid_plaque.n.01", "name": "amyloid_plaque"}, - {"id": 12440, "synset": "dental_plaque.n.01", "name": "dental_plaque"}, - {"id": 12441, "synset": "macule.n.01", "name": "macule"}, - {"id": 12442, "synset": "freckle.n.01", "name": "freckle"}, - {"id": 12443, "synset": "bouffant.n.01", "name": "bouffant"}, - {"id": 12444, "synset": "sausage_curl.n.01", "name": "sausage_curl"}, - {"id": 12445, "synset": "forelock.n.01", "name": "forelock"}, - {"id": 12446, "synset": "spit_curl.n.01", "name": "spit_curl"}, - {"id": 12447, "synset": "pigtail.n.01", "name": "pigtail"}, - {"id": 12448, "synset": "pageboy.n.02", "name": "pageboy"}, - {"id": 12449, "synset": "pompadour.n.02", "name": "pompadour"}, - {"id": 12450, "synset": "thatch.n.01", "name": "thatch"}, - {"id": 12451, "synset": "soup-strainer.n.01", "name": "soup-strainer"}, - {"id": 12452, "synset": "mustachio.n.01", "name": "mustachio"}, - {"id": 12453, "synset": "walrus_mustache.n.01", "name": "walrus_mustache"}, - {"id": 12454, "synset": "stubble.n.02", "name": "stubble"}, - {"id": 12455, "synset": "vandyke_beard.n.01", "name": "vandyke_beard"}, - {"id": 12456, "synset": "soul_patch.n.01", "name": "soul_patch"}, - {"id": 12457, "synset": "esophageal_smear.n.01", "name": "esophageal_smear"}, - {"id": 12458, "synset": "paraduodenal_smear.n.01", "name": "paraduodenal_smear"}, - {"id": 12459, "synset": "specimen.n.02", "name": "specimen"}, - {"id": 12460, "synset": "punctum.n.01", "name": "punctum"}, - {"id": 12461, "synset": "glenoid_fossa.n.02", "name": "glenoid_fossa"}, - {"id": 12462, "synset": "diastema.n.01", "name": "diastema"}, - {"id": 12463, "synset": "marrow.n.01", "name": "marrow"}, - {"id": 12464, "synset": "mouth.n.01", "name": "mouth"}, - {"id": 12465, "synset": "canthus.n.01", "name": "canthus"}, - {"id": 12466, "synset": "milk.n.02", "name": "milk"}, - {"id": 12467, "synset": "mother's_milk.n.01", "name": "mother's_milk"}, - {"id": 12468, "synset": "colostrum.n.01", "name": "colostrum"}, - {"id": 12469, "synset": "vein.n.01", "name": "vein"}, - {"id": 12470, "synset": "ganglion_cell.n.01", "name": "ganglion_cell"}, - {"id": 12471, "synset": "x_chromosome.n.01", "name": "X_chromosome"}, - {"id": 12472, "synset": "embryonic_cell.n.01", "name": "embryonic_cell"}, - {"id": 12473, "synset": "myeloblast.n.01", "name": "myeloblast"}, - {"id": 12474, "synset": "sideroblast.n.01", "name": "sideroblast"}, - {"id": 12475, "synset": "osteocyte.n.01", "name": "osteocyte"}, - {"id": 12476, "synset": "megalocyte.n.01", "name": "megalocyte"}, - {"id": 12477, "synset": "leukocyte.n.01", "name": "leukocyte"}, - {"id": 12478, "synset": "histiocyte.n.01", "name": "histiocyte"}, - {"id": 12479, "synset": "fixed_phagocyte.n.01", "name": "fixed_phagocyte"}, - {"id": 12480, "synset": "lymphocyte.n.01", "name": "lymphocyte"}, - {"id": 12481, "synset": "monoblast.n.01", "name": "monoblast"}, - {"id": 12482, "synset": "neutrophil.n.01", "name": "neutrophil"}, - {"id": 12483, "synset": "microphage.n.01", "name": "microphage"}, - {"id": 12484, "synset": "sickle_cell.n.01", "name": "sickle_cell"}, - {"id": 12485, "synset": "siderocyte.n.01", "name": "siderocyte"}, - {"id": 12486, "synset": "spherocyte.n.01", "name": "spherocyte"}, - {"id": 12487, "synset": "ootid.n.01", "name": "ootid"}, - {"id": 12488, "synset": "oocyte.n.01", "name": "oocyte"}, - {"id": 12489, "synset": "spermatid.n.01", "name": "spermatid"}, - {"id": 12490, "synset": "leydig_cell.n.01", "name": "Leydig_cell"}, - {"id": 12491, "synset": "striated_muscle_cell.n.01", "name": "striated_muscle_cell"}, - {"id": 12492, "synset": "smooth_muscle_cell.n.01", "name": "smooth_muscle_cell"}, - {"id": 12493, "synset": "ranvier's_nodes.n.01", "name": "Ranvier's_nodes"}, - {"id": 12494, "synset": "neuroglia.n.01", "name": "neuroglia"}, - {"id": 12495, "synset": "astrocyte.n.01", "name": "astrocyte"}, - {"id": 12496, "synset": "protoplasmic_astrocyte.n.01", "name": "protoplasmic_astrocyte"}, - {"id": 12497, "synset": "oligodendrocyte.n.01", "name": "oligodendrocyte"}, - {"id": 12498, "synset": "proprioceptor.n.01", "name": "proprioceptor"}, - {"id": 12499, "synset": "dendrite.n.01", "name": "dendrite"}, - {"id": 12500, "synset": "sensory_fiber.n.01", "name": "sensory_fiber"}, - {"id": 12501, "synset": "subarachnoid_space.n.01", "name": "subarachnoid_space"}, - {"id": 12502, "synset": "cerebral_cortex.n.01", "name": "cerebral_cortex"}, - {"id": 12503, "synset": "renal_cortex.n.01", "name": "renal_cortex"}, - {"id": 12504, "synset": "prepuce.n.02", "name": "prepuce"}, - {"id": 12505, "synset": "head.n.01", "name": "head"}, - {"id": 12506, "synset": "scalp.n.01", "name": "scalp"}, - {"id": 12507, "synset": "frontal_eminence.n.01", "name": "frontal_eminence"}, - {"id": 12508, "synset": "suture.n.01", "name": "suture"}, - {"id": 12509, "synset": "foramen_magnum.n.01", "name": "foramen_magnum"}, - {"id": 12510, "synset": "esophagogastric_junction.n.01", "name": "esophagogastric_junction"}, - {"id": 12511, "synset": "heel.n.02", "name": "heel"}, - {"id": 12512, "synset": "cuticle.n.01", "name": "cuticle"}, - {"id": 12513, "synset": "hangnail.n.01", "name": "hangnail"}, - {"id": 12514, "synset": "exoskeleton.n.01", "name": "exoskeleton"}, - {"id": 12515, "synset": "abdominal_wall.n.01", "name": "abdominal_wall"}, - {"id": 12516, "synset": "lemon.n.04", "name": "lemon"}, - {"id": 12517, "synset": "coordinate_axis.n.01", "name": "coordinate_axis"}, - {"id": 12518, "synset": "landscape.n.04", "name": "landscape"}, - {"id": 12519, "synset": "medium.n.01", "name": "medium"}, - {"id": 12520, "synset": "vehicle.n.02", "name": "vehicle"}, - {"id": 12521, "synset": "paper.n.04", "name": "paper"}, - {"id": 12522, "synset": "channel.n.01", "name": "channel"}, - {"id": 12523, "synset": "film.n.02", "name": "film"}, - {"id": 12524, "synset": "silver_screen.n.01", "name": "silver_screen"}, - {"id": 12525, "synset": "free_press.n.01", "name": "free_press"}, - {"id": 12526, "synset": "press.n.02", "name": "press"}, - {"id": 12527, "synset": "print_media.n.01", "name": "print_media"}, - {"id": 12528, "synset": "storage_medium.n.01", "name": "storage_medium"}, - {"id": 12529, "synset": "magnetic_storage_medium.n.01", "name": "magnetic_storage_medium"}, - {"id": 12530, "synset": "journalism.n.01", "name": "journalism"}, - {"id": 12531, "synset": "fleet_street.n.02", "name": "Fleet_Street"}, - {"id": 12532, "synset": "photojournalism.n.01", "name": "photojournalism"}, - {"id": 12533, "synset": "news_photography.n.01", "name": "news_photography"}, - {"id": 12534, "synset": "rotogravure.n.02", "name": "rotogravure"}, - {"id": 12535, "synset": "daily.n.01", "name": "daily"}, - {"id": 12536, "synset": "gazette.n.01", "name": "gazette"}, - {"id": 12537, "synset": "school_newspaper.n.01", "name": "school_newspaper"}, - {"id": 12538, "synset": "tabloid.n.02", "name": "tabloid"}, - {"id": 12539, "synset": "yellow_journalism.n.01", "name": "yellow_journalism"}, - {"id": 12540, "synset": "telecommunication.n.01", "name": "telecommunication"}, - {"id": 12541, "synset": "telephone.n.02", "name": "telephone"}, - {"id": 12542, "synset": "voice_mail.n.01", "name": "voice_mail"}, - {"id": 12543, "synset": "call.n.01", "name": "call"}, - {"id": 12544, "synset": "call-back.n.01", "name": "call-back"}, - {"id": 12545, "synset": "collect_call.n.01", "name": "collect_call"}, - {"id": 12546, "synset": "call_forwarding.n.01", "name": "call_forwarding"}, - {"id": 12547, "synset": "call-in.n.01", "name": "call-in"}, - {"id": 12548, "synset": "call_waiting.n.01", "name": "call_waiting"}, - {"id": 12549, "synset": "crank_call.n.01", "name": "crank_call"}, - {"id": 12550, "synset": "local_call.n.01", "name": "local_call"}, - {"id": 12551, "synset": "long_distance.n.01", "name": "long_distance"}, - {"id": 12552, "synset": "toll_call.n.01", "name": "toll_call"}, - {"id": 12553, "synset": "wake-up_call.n.02", "name": "wake-up_call"}, - {"id": 12554, "synset": "three-way_calling.n.01", "name": "three-way_calling"}, - {"id": 12555, "synset": "telegraphy.n.01", "name": "telegraphy"}, - {"id": 12556, "synset": "cable.n.01", "name": "cable"}, - {"id": 12557, "synset": "wireless.n.02", "name": "wireless"}, - {"id": 12558, "synset": "radiotelegraph.n.01", "name": "radiotelegraph"}, - {"id": 12559, "synset": "radiotelephone.n.01", "name": "radiotelephone"}, - {"id": 12560, "synset": "broadcasting.n.02", "name": "broadcasting"}, - {"id": 12561, "synset": "rediffusion.n.01", "name": "Rediffusion"}, - {"id": 12562, "synset": "multiplex.n.01", "name": "multiplex"}, - {"id": 12563, "synset": "radio.n.01", "name": "radio"}, - {"id": 12564, "synset": "television.n.01", "name": "television"}, - {"id": 12565, "synset": "cable_television.n.01", "name": "cable_television"}, - { - "id": 12566, - "synset": "high-definition_television.n.01", - "name": "high-definition_television", - }, - {"id": 12567, "synset": "reception.n.03", "name": "reception"}, - {"id": 12568, "synset": "signal_detection.n.01", "name": "signal_detection"}, - {"id": 12569, "synset": "hakham.n.01", "name": "Hakham"}, - {"id": 12570, "synset": "web_site.n.01", "name": "web_site"}, - {"id": 12571, "synset": "chat_room.n.01", "name": "chat_room"}, - {"id": 12572, "synset": "portal_site.n.01", "name": "portal_site"}, - {"id": 12573, "synset": "jotter.n.01", "name": "jotter"}, - {"id": 12574, "synset": "breviary.n.01", "name": "breviary"}, - {"id": 12575, "synset": "wordbook.n.01", "name": "wordbook"}, - {"id": 12576, "synset": "desk_dictionary.n.01", "name": "desk_dictionary"}, - {"id": 12577, "synset": "reckoner.n.02", "name": "reckoner"}, - {"id": 12578, "synset": "document.n.01", "name": "document"}, - {"id": 12579, "synset": "album.n.01", "name": "album"}, - {"id": 12580, "synset": "concept_album.n.01", "name": "concept_album"}, - {"id": 12581, "synset": "rock_opera.n.01", "name": "rock_opera"}, - {"id": 12582, "synset": "tribute_album.n.01", "name": "tribute_album"}, - {"id": 12583, "synset": "magazine.n.01", "name": "magazine"}, - {"id": 12584, "synset": "colour_supplement.n.01", "name": "colour_supplement"}, - {"id": 12585, "synset": "news_magazine.n.01", "name": "news_magazine"}, - {"id": 12586, "synset": "pulp.n.04", "name": "pulp"}, - {"id": 12587, "synset": "slick.n.02", "name": "slick"}, - {"id": 12588, "synset": "trade_magazine.n.01", "name": "trade_magazine"}, - {"id": 12589, "synset": "movie.n.01", "name": "movie"}, - {"id": 12590, "synset": "outtake.n.01", "name": "outtake"}, - {"id": 12591, "synset": "shoot-'em-up.n.01", "name": "shoot-'em-up"}, - {"id": 12592, "synset": "spaghetti_western.n.01", "name": "spaghetti_Western"}, - {"id": 12593, "synset": "encyclical.n.01", "name": "encyclical"}, - {"id": 12594, "synset": "crossword_puzzle.n.01", "name": "crossword_puzzle"}, - {"id": 12595, "synset": "sign.n.02", "name": "sign"}, - {"id": 12596, "synset": "swastika.n.01", "name": "swastika"}, - {"id": 12597, "synset": "concert.n.01", "name": "concert"}, - {"id": 12598, "synset": "artwork.n.01", "name": "artwork"}, - {"id": 12599, "synset": "lobe.n.03", "name": "lobe"}, - {"id": 12600, "synset": "book_jacket.n.01", "name": "book_jacket"}, - {"id": 12601, "synset": "cairn.n.01", "name": "cairn"}, - {"id": 12602, "synset": "three-day_event.n.01", "name": "three-day_event"}, - {"id": 12603, "synset": "comfort_food.n.01", "name": "comfort_food"}, - {"id": 12604, "synset": "comestible.n.01", "name": "comestible"}, - {"id": 12605, "synset": "tuck.n.01", "name": "tuck"}, - {"id": 12606, "synset": "course.n.07", "name": "course"}, - {"id": 12607, "synset": "dainty.n.01", "name": "dainty"}, - {"id": 12608, "synset": "dish.n.02", "name": "dish"}, - {"id": 12609, "synset": "fast_food.n.01", "name": "fast_food"}, - {"id": 12610, "synset": "finger_food.n.01", "name": "finger_food"}, - {"id": 12611, "synset": "ingesta.n.01", "name": "ingesta"}, - {"id": 12612, "synset": "kosher.n.01", "name": "kosher"}, - {"id": 12613, "synset": "fare.n.04", "name": "fare"}, - {"id": 12614, "synset": "diet.n.03", "name": "diet"}, - {"id": 12615, "synset": "diet.n.01", "name": "diet"}, - {"id": 12616, "synset": "dietary.n.01", "name": "dietary"}, - {"id": 12617, "synset": "balanced_diet.n.01", "name": "balanced_diet"}, - {"id": 12618, "synset": "bland_diet.n.01", "name": "bland_diet"}, - {"id": 12619, "synset": "clear_liquid_diet.n.01", "name": "clear_liquid_diet"}, - {"id": 12620, "synset": "diabetic_diet.n.01", "name": "diabetic_diet"}, - {"id": 12621, "synset": "dietary_supplement.n.01", "name": "dietary_supplement"}, - {"id": 12622, "synset": "carbohydrate_loading.n.01", "name": "carbohydrate_loading"}, - {"id": 12623, "synset": "fad_diet.n.01", "name": "fad_diet"}, - {"id": 12624, "synset": "gluten-free_diet.n.01", "name": "gluten-free_diet"}, - {"id": 12625, "synset": "high-protein_diet.n.01", "name": "high-protein_diet"}, - {"id": 12626, "synset": "high-vitamin_diet.n.01", "name": "high-vitamin_diet"}, - {"id": 12627, "synset": "light_diet.n.01", "name": "light_diet"}, - {"id": 12628, "synset": "liquid_diet.n.01", "name": "liquid_diet"}, - {"id": 12629, "synset": "low-calorie_diet.n.01", "name": "low-calorie_diet"}, - {"id": 12630, "synset": "low-fat_diet.n.01", "name": "low-fat_diet"}, - {"id": 12631, "synset": "low-sodium_diet.n.01", "name": "low-sodium_diet"}, - {"id": 12632, "synset": "macrobiotic_diet.n.01", "name": "macrobiotic_diet"}, - {"id": 12633, "synset": "reducing_diet.n.01", "name": "reducing_diet"}, - {"id": 12634, "synset": "soft_diet.n.01", "name": "soft_diet"}, - {"id": 12635, "synset": "vegetarianism.n.01", "name": "vegetarianism"}, - {"id": 12636, "synset": "menu.n.02", "name": "menu"}, - {"id": 12637, "synset": "chow.n.02", "name": "chow"}, - {"id": 12638, "synset": "board.n.04", "name": "board"}, - {"id": 12639, "synset": "mess.n.04", "name": "mess"}, - {"id": 12640, "synset": "ration.n.01", "name": "ration"}, - {"id": 12641, "synset": "field_ration.n.01", "name": "field_ration"}, - {"id": 12642, "synset": "k_ration.n.01", "name": "K_ration"}, - {"id": 12643, "synset": "c-ration.n.01", "name": "C-ration"}, - {"id": 12644, "synset": "foodstuff.n.02", "name": "foodstuff"}, - {"id": 12645, "synset": "starches.n.01", "name": "starches"}, - {"id": 12646, "synset": "breadstuff.n.02", "name": "breadstuff"}, - {"id": 12647, "synset": "coloring.n.01", "name": "coloring"}, - {"id": 12648, "synset": "concentrate.n.02", "name": "concentrate"}, - {"id": 12649, "synset": "tomato_concentrate.n.01", "name": "tomato_concentrate"}, - {"id": 12650, "synset": "meal.n.03", "name": "meal"}, - {"id": 12651, "synset": "kibble.n.01", "name": "kibble"}, - {"id": 12652, "synset": "farina.n.01", "name": "farina"}, - {"id": 12653, "synset": "matzo_meal.n.01", "name": "matzo_meal"}, - {"id": 12654, "synset": "oatmeal.n.02", "name": "oatmeal"}, - {"id": 12655, "synset": "pea_flour.n.01", "name": "pea_flour"}, - {"id": 12656, "synset": "roughage.n.01", "name": "roughage"}, - {"id": 12657, "synset": "bran.n.02", "name": "bran"}, - {"id": 12658, "synset": "flour.n.01", "name": "flour"}, - {"id": 12659, "synset": "plain_flour.n.01", "name": "plain_flour"}, - {"id": 12660, "synset": "wheat_flour.n.01", "name": "wheat_flour"}, - {"id": 12661, "synset": "whole_wheat_flour.n.01", "name": "whole_wheat_flour"}, - {"id": 12662, "synset": "soybean_meal.n.01", "name": "soybean_meal"}, - {"id": 12663, "synset": "semolina.n.01", "name": "semolina"}, - {"id": 12664, "synset": "corn_gluten_feed.n.01", "name": "corn_gluten_feed"}, - {"id": 12665, "synset": "nutriment.n.01", "name": "nutriment"}, - {"id": 12666, "synset": "commissariat.n.01", "name": "commissariat"}, - {"id": 12667, "synset": "larder.n.01", "name": "larder"}, - {"id": 12668, "synset": "frozen_food.n.01", "name": "frozen_food"}, - {"id": 12669, "synset": "canned_food.n.01", "name": "canned_food"}, - {"id": 12670, "synset": "canned_meat.n.01", "name": "canned_meat"}, - {"id": 12671, "synset": "spam.n.01", "name": "Spam"}, - {"id": 12672, "synset": "dehydrated_food.n.01", "name": "dehydrated_food"}, - {"id": 12673, "synset": "square_meal.n.01", "name": "square_meal"}, - {"id": 12674, "synset": "meal.n.01", "name": "meal"}, - {"id": 12675, "synset": "potluck.n.01", "name": "potluck"}, - {"id": 12676, "synset": "refection.n.01", "name": "refection"}, - {"id": 12677, "synset": "refreshment.n.01", "name": "refreshment"}, - {"id": 12678, "synset": "breakfast.n.01", "name": "breakfast"}, - {"id": 12679, "synset": "continental_breakfast.n.01", "name": "continental_breakfast"}, - {"id": 12680, "synset": "brunch.n.01", "name": "brunch"}, - {"id": 12681, "synset": "lunch.n.01", "name": "lunch"}, - {"id": 12682, "synset": "business_lunch.n.01", "name": "business_lunch"}, - {"id": 12683, "synset": "high_tea.n.01", "name": "high_tea"}, - {"id": 12684, "synset": "tea.n.02", "name": "tea"}, - {"id": 12685, "synset": "dinner.n.01", "name": "dinner"}, - {"id": 12686, "synset": "supper.n.01", "name": "supper"}, - {"id": 12687, "synset": "buffet.n.02", "name": "buffet"}, - {"id": 12688, "synset": "picnic.n.03", "name": "picnic"}, - {"id": 12689, "synset": "cookout.n.01", "name": "cookout"}, - {"id": 12690, "synset": "barbecue.n.02", "name": "barbecue"}, - {"id": 12691, "synset": "clambake.n.01", "name": "clambake"}, - {"id": 12692, "synset": "fish_fry.n.01", "name": "fish_fry"}, - {"id": 12693, "synset": "bite.n.04", "name": "bite"}, - {"id": 12694, "synset": "nosh.n.01", "name": "nosh"}, - {"id": 12695, "synset": "nosh-up.n.01", "name": "nosh-up"}, - {"id": 12696, "synset": "ploughman's_lunch.n.01", "name": "ploughman's_lunch"}, - {"id": 12697, "synset": "coffee_break.n.01", "name": "coffee_break"}, - {"id": 12698, "synset": "banquet.n.02", "name": "banquet"}, - {"id": 12699, "synset": "entree.n.01", "name": "entree"}, - {"id": 12700, "synset": "piece_de_resistance.n.02", "name": "piece_de_resistance"}, - {"id": 12701, "synset": "plate.n.08", "name": "plate"}, - {"id": 12702, "synset": "adobo.n.01", "name": "adobo"}, - {"id": 12703, "synset": "side_dish.n.01", "name": "side_dish"}, - {"id": 12704, "synset": "special.n.02", "name": "special"}, - {"id": 12705, "synset": "chicken_casserole.n.01", "name": "chicken_casserole"}, - {"id": 12706, "synset": "chicken_cacciatore.n.01", "name": "chicken_cacciatore"}, - {"id": 12707, "synset": "antipasto.n.01", "name": "antipasto"}, - {"id": 12708, "synset": "appetizer.n.01", "name": "appetizer"}, - {"id": 12709, "synset": "canape.n.01", "name": "canape"}, - {"id": 12710, "synset": "cocktail.n.02", "name": "cocktail"}, - {"id": 12711, "synset": "fruit_cocktail.n.01", "name": "fruit_cocktail"}, - {"id": 12712, "synset": "crab_cocktail.n.01", "name": "crab_cocktail"}, - {"id": 12713, "synset": "shrimp_cocktail.n.01", "name": "shrimp_cocktail"}, - {"id": 12714, "synset": "hors_d'oeuvre.n.01", "name": "hors_d'oeuvre"}, - {"id": 12715, "synset": "relish.n.02", "name": "relish"}, - {"id": 12716, "synset": "dip.n.04", "name": "dip"}, - {"id": 12717, "synset": "bean_dip.n.01", "name": "bean_dip"}, - {"id": 12718, "synset": "cheese_dip.n.01", "name": "cheese_dip"}, - {"id": 12719, "synset": "clam_dip.n.01", "name": "clam_dip"}, - {"id": 12720, "synset": "guacamole.n.01", "name": "guacamole"}, - {"id": 12721, "synset": "soup_du_jour.n.01", "name": "soup_du_jour"}, - {"id": 12722, "synset": "alphabet_soup.n.02", "name": "alphabet_soup"}, - {"id": 12723, "synset": "consomme.n.01", "name": "consomme"}, - {"id": 12724, "synset": "madrilene.n.01", "name": "madrilene"}, - {"id": 12725, "synset": "bisque.n.01", "name": "bisque"}, - {"id": 12726, "synset": "borsch.n.01", "name": "borsch"}, - {"id": 12727, "synset": "broth.n.02", "name": "broth"}, - {"id": 12728, "synset": "barley_water.n.01", "name": "barley_water"}, - {"id": 12729, "synset": "bouillon.n.01", "name": "bouillon"}, - {"id": 12730, "synset": "beef_broth.n.01", "name": "beef_broth"}, - {"id": 12731, "synset": "chicken_broth.n.01", "name": "chicken_broth"}, - {"id": 12732, "synset": "broth.n.01", "name": "broth"}, - {"id": 12733, "synset": "stock_cube.n.01", "name": "stock_cube"}, - {"id": 12734, "synset": "chicken_soup.n.01", "name": "chicken_soup"}, - {"id": 12735, "synset": "cock-a-leekie.n.01", "name": "cock-a-leekie"}, - {"id": 12736, "synset": "gazpacho.n.01", "name": "gazpacho"}, - {"id": 12737, "synset": "gumbo.n.04", "name": "gumbo"}, - {"id": 12738, "synset": "julienne.n.02", "name": "julienne"}, - {"id": 12739, "synset": "marmite.n.01", "name": "marmite"}, - {"id": 12740, "synset": "mock_turtle_soup.n.01", "name": "mock_turtle_soup"}, - {"id": 12741, "synset": "mulligatawny.n.01", "name": "mulligatawny"}, - {"id": 12742, "synset": "oxtail_soup.n.01", "name": "oxtail_soup"}, - {"id": 12743, "synset": "pea_soup.n.01", "name": "pea_soup"}, - {"id": 12744, "synset": "pepper_pot.n.01", "name": "pepper_pot"}, - {"id": 12745, "synset": "petite_marmite.n.01", "name": "petite_marmite"}, - {"id": 12746, "synset": "potage.n.01", "name": "potage"}, - {"id": 12747, "synset": "pottage.n.01", "name": "pottage"}, - {"id": 12748, "synset": "turtle_soup.n.01", "name": "turtle_soup"}, - {"id": 12749, "synset": "eggdrop_soup.n.01", "name": "eggdrop_soup"}, - {"id": 12750, "synset": "chowder.n.01", "name": "chowder"}, - {"id": 12751, "synset": "corn_chowder.n.01", "name": "corn_chowder"}, - {"id": 12752, "synset": "clam_chowder.n.01", "name": "clam_chowder"}, - {"id": 12753, "synset": "manhattan_clam_chowder.n.01", "name": "Manhattan_clam_chowder"}, - {"id": 12754, "synset": "new_england_clam_chowder.n.01", "name": "New_England_clam_chowder"}, - {"id": 12755, "synset": "fish_chowder.n.01", "name": "fish_chowder"}, - {"id": 12756, "synset": "won_ton.n.02", "name": "won_ton"}, - {"id": 12757, "synset": "split-pea_soup.n.01", "name": "split-pea_soup"}, - {"id": 12758, "synset": "green_pea_soup.n.01", "name": "green_pea_soup"}, - {"id": 12759, "synset": "lentil_soup.n.01", "name": "lentil_soup"}, - {"id": 12760, "synset": "scotch_broth.n.01", "name": "Scotch_broth"}, - {"id": 12761, "synset": "vichyssoise.n.01", "name": "vichyssoise"}, - {"id": 12762, "synset": "bigos.n.01", "name": "bigos"}, - {"id": 12763, "synset": "brunswick_stew.n.01", "name": "Brunswick_stew"}, - {"id": 12764, "synset": "burgoo.n.03", "name": "burgoo"}, - {"id": 12765, "synset": "burgoo.n.02", "name": "burgoo"}, - {"id": 12766, "synset": "olla_podrida.n.01", "name": "olla_podrida"}, - {"id": 12767, "synset": "mulligan_stew.n.01", "name": "mulligan_stew"}, - {"id": 12768, "synset": "purloo.n.01", "name": "purloo"}, - {"id": 12769, "synset": "goulash.n.01", "name": "goulash"}, - {"id": 12770, "synset": "hotchpotch.n.02", "name": "hotchpotch"}, - {"id": 12771, "synset": "hot_pot.n.01", "name": "hot_pot"}, - {"id": 12772, "synset": "beef_goulash.n.01", "name": "beef_goulash"}, - {"id": 12773, "synset": "pork-and-veal_goulash.n.01", "name": "pork-and-veal_goulash"}, - {"id": 12774, "synset": "porkholt.n.01", "name": "porkholt"}, - {"id": 12775, "synset": "irish_stew.n.01", "name": "Irish_stew"}, - {"id": 12776, "synset": "oyster_stew.n.01", "name": "oyster_stew"}, - {"id": 12777, "synset": "lobster_stew.n.01", "name": "lobster_stew"}, - {"id": 12778, "synset": "lobscouse.n.01", "name": "lobscouse"}, - {"id": 12779, "synset": "fish_stew.n.01", "name": "fish_stew"}, - {"id": 12780, "synset": "bouillabaisse.n.01", "name": "bouillabaisse"}, - {"id": 12781, "synset": "matelote.n.01", "name": "matelote"}, - {"id": 12782, "synset": "paella.n.01", "name": "paella"}, - {"id": 12783, "synset": "fricassee.n.01", "name": "fricassee"}, - {"id": 12784, "synset": "chicken_stew.n.01", "name": "chicken_stew"}, - {"id": 12785, "synset": "turkey_stew.n.01", "name": "turkey_stew"}, - {"id": 12786, "synset": "beef_stew.n.01", "name": "beef_stew"}, - {"id": 12787, "synset": "ragout.n.01", "name": "ragout"}, - {"id": 12788, "synset": "ratatouille.n.01", "name": "ratatouille"}, - {"id": 12789, "synset": "salmi.n.01", "name": "salmi"}, - {"id": 12790, "synset": "pot-au-feu.n.01", "name": "pot-au-feu"}, - {"id": 12791, "synset": "slumgullion.n.01", "name": "slumgullion"}, - {"id": 12792, "synset": "smorgasbord.n.02", "name": "smorgasbord"}, - {"id": 12793, "synset": "viand.n.01", "name": "viand"}, - {"id": 12794, "synset": "ready-mix.n.01", "name": "ready-mix"}, - {"id": 12795, "synset": "brownie_mix.n.01", "name": "brownie_mix"}, - {"id": 12796, "synset": "cake_mix.n.01", "name": "cake_mix"}, - {"id": 12797, "synset": "lemonade_mix.n.01", "name": "lemonade_mix"}, - {"id": 12798, "synset": "self-rising_flour.n.01", "name": "self-rising_flour"}, - {"id": 12799, "synset": "choice_morsel.n.01", "name": "choice_morsel"}, - {"id": 12800, "synset": "savory.n.04", "name": "savory"}, - {"id": 12801, "synset": "calf's-foot_jelly.n.01", "name": "calf's-foot_jelly"}, - {"id": 12802, "synset": "caramel.n.02", "name": "caramel"}, - {"id": 12803, "synset": "lump_sugar.n.01", "name": "lump_sugar"}, - {"id": 12804, "synset": "cane_sugar.n.02", "name": "cane_sugar"}, - {"id": 12805, "synset": "castor_sugar.n.01", "name": "castor_sugar"}, - {"id": 12806, "synset": "powdered_sugar.n.01", "name": "powdered_sugar"}, - {"id": 12807, "synset": "granulated_sugar.n.01", "name": "granulated_sugar"}, - {"id": 12808, "synset": "icing_sugar.n.01", "name": "icing_sugar"}, - {"id": 12809, "synset": "corn_sugar.n.02", "name": "corn_sugar"}, - {"id": 12810, "synset": "brown_sugar.n.01", "name": "brown_sugar"}, - {"id": 12811, "synset": "demerara.n.05", "name": "demerara"}, - {"id": 12812, "synset": "sweet.n.03", "name": "sweet"}, - {"id": 12813, "synset": "confectionery.n.01", "name": "confectionery"}, - {"id": 12814, "synset": "confiture.n.01", "name": "confiture"}, - {"id": 12815, "synset": "sweetmeat.n.01", "name": "sweetmeat"}, - {"id": 12816, "synset": "candy.n.01", "name": "candy"}, - {"id": 12817, "synset": "carob_bar.n.01", "name": "carob_bar"}, - {"id": 12818, "synset": "hardbake.n.01", "name": "hardbake"}, - {"id": 12819, "synset": "hard_candy.n.01", "name": "hard_candy"}, - {"id": 12820, "synset": "barley-sugar.n.01", "name": "barley-sugar"}, - {"id": 12821, "synset": "brandyball.n.01", "name": "brandyball"}, - {"id": 12822, "synset": "jawbreaker.n.01", "name": "jawbreaker"}, - {"id": 12823, "synset": "lemon_drop.n.01", "name": "lemon_drop"}, - {"id": 12824, "synset": "sourball.n.01", "name": "sourball"}, - {"id": 12825, "synset": "patty.n.03", "name": "patty"}, - {"id": 12826, "synset": "peppermint_patty.n.01", "name": "peppermint_patty"}, - {"id": 12827, "synset": "bonbon.n.01", "name": "bonbon"}, - {"id": 12828, "synset": "brittle.n.01", "name": "brittle"}, - {"id": 12829, "synset": "peanut_brittle.n.01", "name": "peanut_brittle"}, - {"id": 12830, "synset": "chewing_gum.n.01", "name": "chewing_gum"}, - {"id": 12831, "synset": "gum_ball.n.01", "name": "gum_ball"}, - {"id": 12832, "synset": "butterscotch.n.01", "name": "butterscotch"}, - {"id": 12833, "synset": "candied_fruit.n.01", "name": "candied_fruit"}, - {"id": 12834, "synset": "candied_apple.n.01", "name": "candied_apple"}, - {"id": 12835, "synset": "crystallized_ginger.n.01", "name": "crystallized_ginger"}, - {"id": 12836, "synset": "grapefruit_peel.n.01", "name": "grapefruit_peel"}, - {"id": 12837, "synset": "lemon_peel.n.02", "name": "lemon_peel"}, - {"id": 12838, "synset": "orange_peel.n.02", "name": "orange_peel"}, - {"id": 12839, "synset": "candied_citrus_peel.n.01", "name": "candied_citrus_peel"}, - {"id": 12840, "synset": "candy_corn.n.01", "name": "candy_corn"}, - {"id": 12841, "synset": "caramel.n.01", "name": "caramel"}, - {"id": 12842, "synset": "center.n.14", "name": "center"}, - {"id": 12843, "synset": "comfit.n.01", "name": "comfit"}, - {"id": 12844, "synset": "cotton_candy.n.01", "name": "cotton_candy"}, - {"id": 12845, "synset": "dragee.n.02", "name": "dragee"}, - {"id": 12846, "synset": "dragee.n.01", "name": "dragee"}, - {"id": 12847, "synset": "fondant.n.01", "name": "fondant"}, - {"id": 12848, "synset": "chocolate_fudge.n.01", "name": "chocolate_fudge"}, - {"id": 12849, "synset": "divinity.n.03", "name": "divinity"}, - {"id": 12850, "synset": "penuche.n.01", "name": "penuche"}, - {"id": 12851, "synset": "gumdrop.n.01", "name": "gumdrop"}, - {"id": 12852, "synset": "jujube.n.03", "name": "jujube"}, - {"id": 12853, "synset": "honey_crisp.n.01", "name": "honey_crisp"}, - {"id": 12854, "synset": "horehound.n.02", "name": "horehound"}, - {"id": 12855, "synset": "peppermint.n.03", "name": "peppermint"}, - {"id": 12856, "synset": "kiss.n.03", "name": "kiss"}, - {"id": 12857, "synset": "molasses_kiss.n.01", "name": "molasses_kiss"}, - {"id": 12858, "synset": "meringue_kiss.n.01", "name": "meringue_kiss"}, - {"id": 12859, "synset": "chocolate_kiss.n.01", "name": "chocolate_kiss"}, - {"id": 12860, "synset": "licorice.n.02", "name": "licorice"}, - {"id": 12861, "synset": "life_saver.n.01", "name": "Life_Saver"}, - {"id": 12862, "synset": "lozenge.n.01", "name": "lozenge"}, - {"id": 12863, "synset": "cachou.n.01", "name": "cachou"}, - {"id": 12864, "synset": "cough_drop.n.01", "name": "cough_drop"}, - {"id": 12865, "synset": "marshmallow.n.01", "name": "marshmallow"}, - {"id": 12866, "synset": "marzipan.n.01", "name": "marzipan"}, - {"id": 12867, "synset": "nougat.n.01", "name": "nougat"}, - {"id": 12868, "synset": "nougat_bar.n.01", "name": "nougat_bar"}, - {"id": 12869, "synset": "nut_bar.n.01", "name": "nut_bar"}, - {"id": 12870, "synset": "peanut_bar.n.01", "name": "peanut_bar"}, - {"id": 12871, "synset": "popcorn_ball.n.01", "name": "popcorn_ball"}, - {"id": 12872, "synset": "praline.n.01", "name": "praline"}, - {"id": 12873, "synset": "rock_candy.n.02", "name": "rock_candy"}, - {"id": 12874, "synset": "rock_candy.n.01", "name": "rock_candy"}, - {"id": 12875, "synset": "sugar_candy.n.01", "name": "sugar_candy"}, - {"id": 12876, "synset": "sugarplum.n.01", "name": "sugarplum"}, - {"id": 12877, "synset": "taffy.n.01", "name": "taffy"}, - {"id": 12878, "synset": "molasses_taffy.n.01", "name": "molasses_taffy"}, - {"id": 12879, "synset": "turkish_delight.n.01", "name": "Turkish_Delight"}, - {"id": 12880, "synset": "dessert.n.01", "name": "dessert"}, - {"id": 12881, "synset": "ambrosia.n.04", "name": "ambrosia"}, - {"id": 12882, "synset": "ambrosia.n.03", "name": "ambrosia"}, - {"id": 12883, "synset": "baked_alaska.n.01", "name": "baked_Alaska"}, - {"id": 12884, "synset": "blancmange.n.01", "name": "blancmange"}, - {"id": 12885, "synset": "charlotte.n.02", "name": "charlotte"}, - {"id": 12886, "synset": "compote.n.01", "name": "compote"}, - {"id": 12887, "synset": "dumpling.n.02", "name": "dumpling"}, - {"id": 12888, "synset": "flan.n.01", "name": "flan"}, - {"id": 12889, "synset": "frozen_dessert.n.01", "name": "frozen_dessert"}, - {"id": 12890, "synset": "junket.n.01", "name": "junket"}, - {"id": 12891, "synset": "mousse.n.02", "name": "mousse"}, - {"id": 12892, "synset": "mousse.n.01", "name": "mousse"}, - {"id": 12893, "synset": "pavlova.n.02", "name": "pavlova"}, - {"id": 12894, "synset": "peach_melba.n.01", "name": "peach_melba"}, - {"id": 12895, "synset": "whip.n.03", "name": "whip"}, - {"id": 12896, "synset": "prune_whip.n.01", "name": "prune_whip"}, - {"id": 12897, "synset": "pudding.n.03", "name": "pudding"}, - {"id": 12898, "synset": "pudding.n.02", "name": "pudding"}, - {"id": 12899, "synset": "syllabub.n.02", "name": "syllabub"}, - {"id": 12900, "synset": "tiramisu.n.01", "name": "tiramisu"}, - {"id": 12901, "synset": "trifle.n.01", "name": "trifle"}, - {"id": 12902, "synset": "tipsy_cake.n.01", "name": "tipsy_cake"}, - {"id": 12903, "synset": "jello.n.01", "name": "jello"}, - {"id": 12904, "synset": "apple_dumpling.n.01", "name": "apple_dumpling"}, - {"id": 12905, "synset": "ice.n.05", "name": "ice"}, - {"id": 12906, "synset": "water_ice.n.02", "name": "water_ice"}, - {"id": 12907, "synset": "ice-cream_cone.n.01", "name": "ice-cream_cone"}, - {"id": 12908, "synset": "chocolate_ice_cream.n.01", "name": "chocolate_ice_cream"}, - {"id": 12909, "synset": "neapolitan_ice_cream.n.01", "name": "Neapolitan_ice_cream"}, - {"id": 12910, "synset": "peach_ice_cream.n.01", "name": "peach_ice_cream"}, - {"id": 12911, "synset": "strawberry_ice_cream.n.01", "name": "strawberry_ice_cream"}, - {"id": 12912, "synset": "tutti-frutti.n.01", "name": "tutti-frutti"}, - {"id": 12913, "synset": "vanilla_ice_cream.n.01", "name": "vanilla_ice_cream"}, - {"id": 12914, "synset": "ice_milk.n.01", "name": "ice_milk"}, - {"id": 12915, "synset": "frozen_yogurt.n.01", "name": "frozen_yogurt"}, - {"id": 12916, "synset": "snowball.n.03", "name": "snowball"}, - {"id": 12917, "synset": "snowball.n.02", "name": "snowball"}, - {"id": 12918, "synset": "parfait.n.01", "name": "parfait"}, - {"id": 12919, "synset": "ice-cream_sundae.n.01", "name": "ice-cream_sundae"}, - {"id": 12920, "synset": "split.n.07", "name": "split"}, - {"id": 12921, "synset": "banana_split.n.01", "name": "banana_split"}, - {"id": 12922, "synset": "frozen_pudding.n.01", "name": "frozen_pudding"}, - {"id": 12923, "synset": "frozen_custard.n.01", "name": "frozen_custard"}, - {"id": 12924, "synset": "flummery.n.01", "name": "flummery"}, - {"id": 12925, "synset": "fish_mousse.n.01", "name": "fish_mousse"}, - {"id": 12926, "synset": "chicken_mousse.n.01", "name": "chicken_mousse"}, - {"id": 12927, "synset": "plum_pudding.n.01", "name": "plum_pudding"}, - {"id": 12928, "synset": "carrot_pudding.n.01", "name": "carrot_pudding"}, - {"id": 12929, "synset": "corn_pudding.n.01", "name": "corn_pudding"}, - {"id": 12930, "synset": "steamed_pudding.n.01", "name": "steamed_pudding"}, - {"id": 12931, "synset": "duff.n.01", "name": "duff"}, - {"id": 12932, "synset": "vanilla_pudding.n.01", "name": "vanilla_pudding"}, - {"id": 12933, "synset": "chocolate_pudding.n.01", "name": "chocolate_pudding"}, - {"id": 12934, "synset": "brown_betty.n.01", "name": "brown_Betty"}, - {"id": 12935, "synset": "nesselrode.n.01", "name": "Nesselrode"}, - {"id": 12936, "synset": "pease_pudding.n.01", "name": "pease_pudding"}, - {"id": 12937, "synset": "custard.n.01", "name": "custard"}, - {"id": 12938, "synset": "creme_caramel.n.01", "name": "creme_caramel"}, - {"id": 12939, "synset": "creme_anglais.n.01", "name": "creme_anglais"}, - {"id": 12940, "synset": "creme_brulee.n.01", "name": "creme_brulee"}, - {"id": 12941, "synset": "fruit_custard.n.01", "name": "fruit_custard"}, - {"id": 12942, "synset": "tapioca.n.01", "name": "tapioca"}, - {"id": 12943, "synset": "tapioca_pudding.n.01", "name": "tapioca_pudding"}, - {"id": 12944, "synset": "roly-poly.n.02", "name": "roly-poly"}, - {"id": 12945, "synset": "suet_pudding.n.01", "name": "suet_pudding"}, - {"id": 12946, "synset": "bavarian_cream.n.01", "name": "Bavarian_cream"}, - {"id": 12947, "synset": "maraschino.n.02", "name": "maraschino"}, - {"id": 12948, "synset": "nonpareil.n.02", "name": "nonpareil"}, - {"id": 12949, "synset": "zabaglione.n.01", "name": "zabaglione"}, - {"id": 12950, "synset": "garnish.n.01", "name": "garnish"}, - {"id": 12951, "synset": "pastry.n.01", "name": "pastry"}, - {"id": 12952, "synset": "turnover.n.02", "name": "turnover"}, - {"id": 12953, "synset": "apple_turnover.n.01", "name": "apple_turnover"}, - {"id": 12954, "synset": "knish.n.01", "name": "knish"}, - {"id": 12955, "synset": "pirogi.n.01", "name": "pirogi"}, - {"id": 12956, "synset": "samosa.n.01", "name": "samosa"}, - {"id": 12957, "synset": "timbale.n.01", "name": "timbale"}, - {"id": 12958, "synset": "puff_paste.n.01", "name": "puff_paste"}, - {"id": 12959, "synset": "phyllo.n.01", "name": "phyllo"}, - {"id": 12960, "synset": "puff_batter.n.01", "name": "puff_batter"}, - {"id": 12961, "synset": "ice-cream_cake.n.01", "name": "ice-cream_cake"}, - {"id": 12962, "synset": "fish_cake.n.01", "name": "fish_cake"}, - {"id": 12963, "synset": "fish_stick.n.01", "name": "fish_stick"}, - {"id": 12964, "synset": "conserve.n.01", "name": "conserve"}, - {"id": 12965, "synset": "apple_butter.n.01", "name": "apple_butter"}, - {"id": 12966, "synset": "chowchow.n.02", "name": "chowchow"}, - {"id": 12967, "synset": "lemon_curd.n.01", "name": "lemon_curd"}, - {"id": 12968, "synset": "strawberry_jam.n.01", "name": "strawberry_jam"}, - {"id": 12969, "synset": "jelly.n.02", "name": "jelly"}, - {"id": 12970, "synset": "apple_jelly.n.01", "name": "apple_jelly"}, - {"id": 12971, "synset": "crabapple_jelly.n.01", "name": "crabapple_jelly"}, - {"id": 12972, "synset": "grape_jelly.n.01", "name": "grape_jelly"}, - {"id": 12973, "synset": "marmalade.n.01", "name": "marmalade"}, - {"id": 12974, "synset": "orange_marmalade.n.01", "name": "orange_marmalade"}, - {"id": 12975, "synset": "gelatin_dessert.n.01", "name": "gelatin_dessert"}, - {"id": 12976, "synset": "buffalo_wing.n.01", "name": "buffalo_wing"}, - {"id": 12977, "synset": "barbecued_wing.n.01", "name": "barbecued_wing"}, - {"id": 12978, "synset": "mess.n.03", "name": "mess"}, - {"id": 12979, "synset": "mince.n.01", "name": "mince"}, - {"id": 12980, "synset": "puree.n.01", "name": "puree"}, - {"id": 12981, "synset": "barbecue.n.01", "name": "barbecue"}, - {"id": 12982, "synset": "biryani.n.01", "name": "biryani"}, - {"id": 12983, "synset": "escalope_de_veau_orloff.n.01", "name": "escalope_de_veau_Orloff"}, - {"id": 12984, "synset": "saute.n.01", "name": "saute"}, - {"id": 12985, "synset": "veal_parmesan.n.01", "name": "veal_parmesan"}, - {"id": 12986, "synset": "veal_cordon_bleu.n.01", "name": "veal_cordon_bleu"}, - {"id": 12987, "synset": "margarine.n.01", "name": "margarine"}, - {"id": 12988, "synset": "mincemeat.n.01", "name": "mincemeat"}, - {"id": 12989, "synset": "stuffing.n.01", "name": "stuffing"}, - {"id": 12990, "synset": "turkey_stuffing.n.01", "name": "turkey_stuffing"}, - {"id": 12991, "synset": "oyster_stuffing.n.01", "name": "oyster_stuffing"}, - {"id": 12992, "synset": "forcemeat.n.01", "name": "forcemeat"}, - {"id": 12993, "synset": "anadama_bread.n.01", "name": "anadama_bread"}, - {"id": 12994, "synset": "bap.n.01", "name": "bap"}, - {"id": 12995, "synset": "barmbrack.n.01", "name": "barmbrack"}, - {"id": 12996, "synset": "breadstick.n.01", "name": "breadstick"}, - {"id": 12997, "synset": "grissino.n.01", "name": "grissino"}, - {"id": 12998, "synset": "brown_bread.n.02", "name": "brown_bread"}, - {"id": 12999, "synset": "tea_bread.n.01", "name": "tea_bread"}, - {"id": 13000, "synset": "caraway_seed_bread.n.01", "name": "caraway_seed_bread"}, - {"id": 13001, "synset": "challah.n.01", "name": "challah"}, - {"id": 13002, "synset": "cinnamon_bread.n.01", "name": "cinnamon_bread"}, - {"id": 13003, "synset": "cracked-wheat_bread.n.01", "name": "cracked-wheat_bread"}, - {"id": 13004, "synset": "dark_bread.n.01", "name": "dark_bread"}, - {"id": 13005, "synset": "english_muffin.n.01", "name": "English_muffin"}, - {"id": 13006, "synset": "flatbread.n.01", "name": "flatbread"}, - {"id": 13007, "synset": "garlic_bread.n.01", "name": "garlic_bread"}, - {"id": 13008, "synset": "gluten_bread.n.01", "name": "gluten_bread"}, - {"id": 13009, "synset": "graham_bread.n.01", "name": "graham_bread"}, - {"id": 13010, "synset": "host.n.09", "name": "Host"}, - {"id": 13011, "synset": "flatbrod.n.01", "name": "flatbrod"}, - {"id": 13012, "synset": "bannock.n.01", "name": "bannock"}, - {"id": 13013, "synset": "chapatti.n.01", "name": "chapatti"}, - {"id": 13014, "synset": "loaf_of_bread.n.01", "name": "loaf_of_bread"}, - {"id": 13015, "synset": "french_loaf.n.01", "name": "French_loaf"}, - {"id": 13016, "synset": "matzo.n.01", "name": "matzo"}, - {"id": 13017, "synset": "nan.n.04", "name": "nan"}, - {"id": 13018, "synset": "onion_bread.n.01", "name": "onion_bread"}, - {"id": 13019, "synset": "raisin_bread.n.01", "name": "raisin_bread"}, - {"id": 13020, "synset": "quick_bread.n.01", "name": "quick_bread"}, - {"id": 13021, "synset": "banana_bread.n.01", "name": "banana_bread"}, - {"id": 13022, "synset": "date_bread.n.01", "name": "date_bread"}, - {"id": 13023, "synset": "date-nut_bread.n.01", "name": "date-nut_bread"}, - {"id": 13024, "synset": "nut_bread.n.01", "name": "nut_bread"}, - {"id": 13025, "synset": "oatcake.n.01", "name": "oatcake"}, - {"id": 13026, "synset": "irish_soda_bread.n.01", "name": "Irish_soda_bread"}, - {"id": 13027, "synset": "skillet_bread.n.01", "name": "skillet_bread"}, - {"id": 13028, "synset": "rye_bread.n.01", "name": "rye_bread"}, - {"id": 13029, "synset": "black_bread.n.01", "name": "black_bread"}, - {"id": 13030, "synset": "jewish_rye_bread.n.01", "name": "Jewish_rye_bread"}, - {"id": 13031, "synset": "limpa.n.01", "name": "limpa"}, - {"id": 13032, "synset": "swedish_rye_bread.n.01", "name": "Swedish_rye_bread"}, - {"id": 13033, "synset": "salt-rising_bread.n.01", "name": "salt-rising_bread"}, - {"id": 13034, "synset": "simnel.n.01", "name": "simnel"}, - {"id": 13035, "synset": "sour_bread.n.01", "name": "sour_bread"}, - {"id": 13036, "synset": "wafer.n.03", "name": "wafer"}, - {"id": 13037, "synset": "white_bread.n.01", "name": "white_bread"}, - {"id": 13038, "synset": "french_bread.n.01", "name": "French_bread"}, - {"id": 13039, "synset": "italian_bread.n.01", "name": "Italian_bread"}, - {"id": 13040, "synset": "corn_cake.n.01", "name": "corn_cake"}, - {"id": 13041, "synset": "skillet_corn_bread.n.01", "name": "skillet_corn_bread"}, - {"id": 13042, "synset": "ashcake.n.01", "name": "ashcake"}, - {"id": 13043, "synset": "hoecake.n.01", "name": "hoecake"}, - {"id": 13044, "synset": "cornpone.n.01", "name": "cornpone"}, - {"id": 13045, "synset": "corn_dab.n.01", "name": "corn_dab"}, - {"id": 13046, "synset": "hush_puppy.n.01", "name": "hush_puppy"}, - {"id": 13047, "synset": "johnnycake.n.01", "name": "johnnycake"}, - {"id": 13048, "synset": "shawnee_cake.n.01", "name": "Shawnee_cake"}, - {"id": 13049, "synset": "spoon_bread.n.01", "name": "spoon_bread"}, - {"id": 13050, "synset": "cinnamon_toast.n.01", "name": "cinnamon_toast"}, - {"id": 13051, "synset": "orange_toast.n.01", "name": "orange_toast"}, - {"id": 13052, "synset": "melba_toast.n.01", "name": "Melba_toast"}, - {"id": 13053, "synset": "zwieback.n.01", "name": "zwieback"}, - {"id": 13054, "synset": "frankfurter_bun.n.01", "name": "frankfurter_bun"}, - {"id": 13055, "synset": "hamburger_bun.n.01", "name": "hamburger_bun"}, - {"id": 13056, "synset": "bran_muffin.n.01", "name": "bran_muffin"}, - {"id": 13057, "synset": "corn_muffin.n.01", "name": "corn_muffin"}, - {"id": 13058, "synset": "yorkshire_pudding.n.01", "name": "Yorkshire_pudding"}, - {"id": 13059, "synset": "popover.n.01", "name": "popover"}, - {"id": 13060, "synset": "scone.n.01", "name": "scone"}, - {"id": 13061, "synset": "drop_scone.n.01", "name": "drop_scone"}, - {"id": 13062, "synset": "cross_bun.n.01", "name": "cross_bun"}, - {"id": 13063, "synset": "brioche.n.01", "name": "brioche"}, - {"id": 13064, "synset": "hard_roll.n.01", "name": "hard_roll"}, - {"id": 13065, "synset": "soft_roll.n.01", "name": "soft_roll"}, - {"id": 13066, "synset": "kaiser_roll.n.01", "name": "kaiser_roll"}, - {"id": 13067, "synset": "parker_house_roll.n.01", "name": "Parker_House_roll"}, - {"id": 13068, "synset": "clover-leaf_roll.n.01", "name": "clover-leaf_roll"}, - {"id": 13069, "synset": "onion_roll.n.01", "name": "onion_roll"}, - {"id": 13070, "synset": "bialy.n.01", "name": "bialy"}, - {"id": 13071, "synset": "sweet_roll.n.01", "name": "sweet_roll"}, - {"id": 13072, "synset": "bear_claw.n.01", "name": "bear_claw"}, - {"id": 13073, "synset": "cinnamon_roll.n.01", "name": "cinnamon_roll"}, - {"id": 13074, "synset": "honey_bun.n.01", "name": "honey_bun"}, - {"id": 13075, "synset": "pinwheel_roll.n.01", "name": "pinwheel_roll"}, - {"id": 13076, "synset": "danish.n.02", "name": "danish"}, - {"id": 13077, "synset": "onion_bagel.n.01", "name": "onion_bagel"}, - {"id": 13078, "synset": "biscuit.n.01", "name": "biscuit"}, - {"id": 13079, "synset": "rolled_biscuit.n.01", "name": "rolled_biscuit"}, - {"id": 13080, "synset": "baking-powder_biscuit.n.01", "name": "baking-powder_biscuit"}, - {"id": 13081, "synset": "buttermilk_biscuit.n.01", "name": "buttermilk_biscuit"}, - {"id": 13082, "synset": "shortcake.n.01", "name": "shortcake"}, - {"id": 13083, "synset": "hardtack.n.01", "name": "hardtack"}, - {"id": 13084, "synset": "saltine.n.01", "name": "saltine"}, - {"id": 13085, "synset": "soda_cracker.n.01", "name": "soda_cracker"}, - {"id": 13086, "synset": "oyster_cracker.n.01", "name": "oyster_cracker"}, - {"id": 13087, "synset": "water_biscuit.n.01", "name": "water_biscuit"}, - {"id": 13088, "synset": "graham_cracker.n.01", "name": "graham_cracker"}, - {"id": 13089, "synset": "soft_pretzel.n.01", "name": "soft_pretzel"}, - {"id": 13090, "synset": "sandwich_plate.n.01", "name": "sandwich_plate"}, - {"id": 13091, "synset": "butty.n.01", "name": "butty"}, - {"id": 13092, "synset": "ham_sandwich.n.01", "name": "ham_sandwich"}, - {"id": 13093, "synset": "chicken_sandwich.n.01", "name": "chicken_sandwich"}, - {"id": 13094, "synset": "club_sandwich.n.01", "name": "club_sandwich"}, - {"id": 13095, "synset": "open-face_sandwich.n.01", "name": "open-face_sandwich"}, - {"id": 13096, "synset": "cheeseburger.n.01", "name": "cheeseburger"}, - {"id": 13097, "synset": "tunaburger.n.01", "name": "tunaburger"}, - {"id": 13098, "synset": "hotdog.n.02", "name": "hotdog"}, - {"id": 13099, "synset": "sloppy_joe.n.01", "name": "Sloppy_Joe"}, - {"id": 13100, "synset": "bomber.n.03", "name": "bomber"}, - {"id": 13101, "synset": "gyro.n.01", "name": "gyro"}, - { - "id": 13102, - "synset": "bacon-lettuce-tomato_sandwich.n.01", - "name": "bacon-lettuce-tomato_sandwich", - }, - {"id": 13103, "synset": "reuben.n.02", "name": "Reuben"}, - {"id": 13104, "synset": "western.n.02", "name": "western"}, - {"id": 13105, "synset": "wrap.n.02", "name": "wrap"}, - {"id": 13106, "synset": "spaghetti.n.01", "name": "spaghetti"}, - {"id": 13107, "synset": "hasty_pudding.n.01", "name": "hasty_pudding"}, - {"id": 13108, "synset": "gruel.n.01", "name": "gruel"}, - {"id": 13109, "synset": "congee.n.01", "name": "congee"}, - {"id": 13110, "synset": "skilly.n.01", "name": "skilly"}, - {"id": 13111, "synset": "edible_fruit.n.01", "name": "edible_fruit"}, - {"id": 13112, "synset": "vegetable.n.01", "name": "vegetable"}, - {"id": 13113, "synset": "julienne.n.01", "name": "julienne"}, - {"id": 13114, "synset": "raw_vegetable.n.01", "name": "raw_vegetable"}, - {"id": 13115, "synset": "crudites.n.01", "name": "crudites"}, - {"id": 13116, "synset": "celery_stick.n.01", "name": "celery_stick"}, - {"id": 13117, "synset": "legume.n.03", "name": "legume"}, - {"id": 13118, "synset": "pulse.n.04", "name": "pulse"}, - {"id": 13119, "synset": "potherb.n.01", "name": "potherb"}, - {"id": 13120, "synset": "greens.n.01", "name": "greens"}, - {"id": 13121, "synset": "chop-suey_greens.n.02", "name": "chop-suey_greens"}, - {"id": 13122, "synset": "solanaceous_vegetable.n.01", "name": "solanaceous_vegetable"}, - {"id": 13123, "synset": "root_vegetable.n.01", "name": "root_vegetable"}, - {"id": 13124, "synset": "baked_potato.n.01", "name": "baked_potato"}, - {"id": 13125, "synset": "french_fries.n.01", "name": "french_fries"}, - {"id": 13126, "synset": "home_fries.n.01", "name": "home_fries"}, - {"id": 13127, "synset": "jacket_potato.n.01", "name": "jacket_potato"}, - {"id": 13128, "synset": "potato_skin.n.01", "name": "potato_skin"}, - {"id": 13129, "synset": "uruguay_potato.n.02", "name": "Uruguay_potato"}, - {"id": 13130, "synset": "yam.n.04", "name": "yam"}, - {"id": 13131, "synset": "yam.n.03", "name": "yam"}, - {"id": 13132, "synset": "snack_food.n.01", "name": "snack_food"}, - {"id": 13133, "synset": "corn_chip.n.01", "name": "corn_chip"}, - {"id": 13134, "synset": "tortilla_chip.n.01", "name": "tortilla_chip"}, - {"id": 13135, "synset": "nacho.n.01", "name": "nacho"}, - {"id": 13136, "synset": "pieplant.n.01", "name": "pieplant"}, - {"id": 13137, "synset": "cruciferous_vegetable.n.01", "name": "cruciferous_vegetable"}, - {"id": 13138, "synset": "mustard.n.03", "name": "mustard"}, - {"id": 13139, "synset": "cabbage.n.01", "name": "cabbage"}, - {"id": 13140, "synset": "kale.n.03", "name": "kale"}, - {"id": 13141, "synset": "collards.n.01", "name": "collards"}, - {"id": 13142, "synset": "chinese_cabbage.n.02", "name": "Chinese_cabbage"}, - {"id": 13143, "synset": "bok_choy.n.02", "name": "bok_choy"}, - {"id": 13144, "synset": "head_cabbage.n.02", "name": "head_cabbage"}, - {"id": 13145, "synset": "red_cabbage.n.02", "name": "red_cabbage"}, - {"id": 13146, "synset": "savoy_cabbage.n.02", "name": "savoy_cabbage"}, - {"id": 13147, "synset": "broccoli.n.02", "name": "broccoli"}, - {"id": 13148, "synset": "broccoli_rabe.n.02", "name": "broccoli_rabe"}, - {"id": 13149, "synset": "squash.n.02", "name": "squash"}, - {"id": 13150, "synset": "summer_squash.n.02", "name": "summer_squash"}, - {"id": 13151, "synset": "yellow_squash.n.02", "name": "yellow_squash"}, - {"id": 13152, "synset": "crookneck.n.01", "name": "crookneck"}, - {"id": 13153, "synset": "marrow.n.04", "name": "marrow"}, - {"id": 13154, "synset": "cocozelle.n.02", "name": "cocozelle"}, - {"id": 13155, "synset": "pattypan_squash.n.02", "name": "pattypan_squash"}, - {"id": 13156, "synset": "spaghetti_squash.n.02", "name": "spaghetti_squash"}, - {"id": 13157, "synset": "winter_squash.n.02", "name": "winter_squash"}, - {"id": 13158, "synset": "acorn_squash.n.02", "name": "acorn_squash"}, - {"id": 13159, "synset": "butternut_squash.n.02", "name": "butternut_squash"}, - {"id": 13160, "synset": "hubbard_squash.n.02", "name": "hubbard_squash"}, - {"id": 13161, "synset": "turban_squash.n.02", "name": "turban_squash"}, - {"id": 13162, "synset": "buttercup_squash.n.02", "name": "buttercup_squash"}, - {"id": 13163, "synset": "cushaw.n.02", "name": "cushaw"}, - {"id": 13164, "synset": "winter_crookneck_squash.n.02", "name": "winter_crookneck_squash"}, - {"id": 13165, "synset": "gherkin.n.02", "name": "gherkin"}, - {"id": 13166, "synset": "artichoke_heart.n.01", "name": "artichoke_heart"}, - {"id": 13167, "synset": "jerusalem_artichoke.n.03", "name": "Jerusalem_artichoke"}, - {"id": 13168, "synset": "bamboo_shoot.n.01", "name": "bamboo_shoot"}, - {"id": 13169, "synset": "sprout.n.02", "name": "sprout"}, - {"id": 13170, "synset": "bean_sprout.n.01", "name": "bean_sprout"}, - {"id": 13171, "synset": "alfalfa_sprout.n.01", "name": "alfalfa_sprout"}, - {"id": 13172, "synset": "beet.n.02", "name": "beet"}, - {"id": 13173, "synset": "beet_green.n.01", "name": "beet_green"}, - {"id": 13174, "synset": "sugar_beet.n.02", "name": "sugar_beet"}, - {"id": 13175, "synset": "mangel-wurzel.n.02", "name": "mangel-wurzel"}, - {"id": 13176, "synset": "chard.n.02", "name": "chard"}, - {"id": 13177, "synset": "pepper.n.04", "name": "pepper"}, - {"id": 13178, "synset": "sweet_pepper.n.02", "name": "sweet_pepper"}, - {"id": 13179, "synset": "green_pepper.n.01", "name": "green_pepper"}, - {"id": 13180, "synset": "globe_pepper.n.01", "name": "globe_pepper"}, - {"id": 13181, "synset": "pimento.n.02", "name": "pimento"}, - {"id": 13182, "synset": "hot_pepper.n.02", "name": "hot_pepper"}, - {"id": 13183, "synset": "jalapeno.n.02", "name": "jalapeno"}, - {"id": 13184, "synset": "chipotle.n.01", "name": "chipotle"}, - {"id": 13185, "synset": "cayenne.n.03", "name": "cayenne"}, - {"id": 13186, "synset": "tabasco.n.03", "name": "tabasco"}, - {"id": 13187, "synset": "onion.n.03", "name": "onion"}, - {"id": 13188, "synset": "bermuda_onion.n.01", "name": "Bermuda_onion"}, - {"id": 13189, "synset": "vidalia_onion.n.01", "name": "Vidalia_onion"}, - {"id": 13190, "synset": "spanish_onion.n.01", "name": "Spanish_onion"}, - {"id": 13191, "synset": "purple_onion.n.01", "name": "purple_onion"}, - {"id": 13192, "synset": "leek.n.02", "name": "leek"}, - {"id": 13193, "synset": "shallot.n.03", "name": "shallot"}, - {"id": 13194, "synset": "salad_green.n.01", "name": "salad_green"}, - {"id": 13195, "synset": "lettuce.n.03", "name": "lettuce"}, - {"id": 13196, "synset": "butterhead_lettuce.n.01", "name": "butterhead_lettuce"}, - {"id": 13197, "synset": "buttercrunch.n.01", "name": "buttercrunch"}, - {"id": 13198, "synset": "bibb_lettuce.n.01", "name": "Bibb_lettuce"}, - {"id": 13199, "synset": "boston_lettuce.n.01", "name": "Boston_lettuce"}, - {"id": 13200, "synset": "crisphead_lettuce.n.01", "name": "crisphead_lettuce"}, - {"id": 13201, "synset": "cos.n.02", "name": "cos"}, - {"id": 13202, "synset": "leaf_lettuce.n.02", "name": "leaf_lettuce"}, - {"id": 13203, "synset": "celtuce.n.02", "name": "celtuce"}, - {"id": 13204, "synset": "bean.n.01", "name": "bean"}, - {"id": 13205, "synset": "goa_bean.n.02", "name": "goa_bean"}, - {"id": 13206, "synset": "lentil.n.01", "name": "lentil"}, - {"id": 13207, "synset": "green_pea.n.01", "name": "green_pea"}, - {"id": 13208, "synset": "marrowfat_pea.n.01", "name": "marrowfat_pea"}, - {"id": 13209, "synset": "snow_pea.n.02", "name": "snow_pea"}, - {"id": 13210, "synset": "sugar_snap_pea.n.02", "name": "sugar_snap_pea"}, - {"id": 13211, "synset": "split-pea.n.01", "name": "split-pea"}, - {"id": 13212, "synset": "chickpea.n.03", "name": "chickpea"}, - {"id": 13213, "synset": "cajan_pea.n.02", "name": "cajan_pea"}, - {"id": 13214, "synset": "field_pea.n.03", "name": "field_pea"}, - {"id": 13215, "synset": "mushy_peas.n.01", "name": "mushy_peas"}, - {"id": 13216, "synset": "black-eyed_pea.n.03", "name": "black-eyed_pea"}, - {"id": 13217, "synset": "common_bean.n.02", "name": "common_bean"}, - {"id": 13218, "synset": "kidney_bean.n.02", "name": "kidney_bean"}, - {"id": 13219, "synset": "navy_bean.n.01", "name": "navy_bean"}, - {"id": 13220, "synset": "pinto_bean.n.01", "name": "pinto_bean"}, - {"id": 13221, "synset": "frijole.n.02", "name": "frijole"}, - {"id": 13222, "synset": "black_bean.n.01", "name": "black_bean"}, - {"id": 13223, "synset": "fresh_bean.n.01", "name": "fresh_bean"}, - {"id": 13224, "synset": "flageolet.n.01", "name": "flageolet"}, - {"id": 13225, "synset": "green_bean.n.01", "name": "green_bean"}, - {"id": 13226, "synset": "snap_bean.n.01", "name": "snap_bean"}, - {"id": 13227, "synset": "string_bean.n.01", "name": "string_bean"}, - {"id": 13228, "synset": "kentucky_wonder.n.01", "name": "Kentucky_wonder"}, - {"id": 13229, "synset": "scarlet_runner.n.03", "name": "scarlet_runner"}, - {"id": 13230, "synset": "haricot_vert.n.01", "name": "haricot_vert"}, - {"id": 13231, "synset": "wax_bean.n.02", "name": "wax_bean"}, - {"id": 13232, "synset": "shell_bean.n.02", "name": "shell_bean"}, - {"id": 13233, "synset": "lima_bean.n.03", "name": "lima_bean"}, - {"id": 13234, "synset": "fordhooks.n.01", "name": "Fordhooks"}, - {"id": 13235, "synset": "sieva_bean.n.02", "name": "sieva_bean"}, - {"id": 13236, "synset": "fava_bean.n.02", "name": "fava_bean"}, - {"id": 13237, "synset": "soy.n.04", "name": "soy"}, - {"id": 13238, "synset": "green_soybean.n.01", "name": "green_soybean"}, - {"id": 13239, "synset": "field_soybean.n.01", "name": "field_soybean"}, - {"id": 13240, "synset": "cardoon.n.02", "name": "cardoon"}, - {"id": 13241, "synset": "carrot.n.03", "name": "carrot"}, - {"id": 13242, "synset": "carrot_stick.n.01", "name": "carrot_stick"}, - {"id": 13243, "synset": "celery.n.02", "name": "celery"}, - {"id": 13244, "synset": "pascal_celery.n.01", "name": "pascal_celery"}, - {"id": 13245, "synset": "celeriac.n.02", "name": "celeriac"}, - {"id": 13246, "synset": "chicory.n.04", "name": "chicory"}, - {"id": 13247, "synset": "radicchio.n.01", "name": "radicchio"}, - {"id": 13248, "synset": "coffee_substitute.n.01", "name": "coffee_substitute"}, - {"id": 13249, "synset": "chicory.n.03", "name": "chicory"}, - {"id": 13250, "synset": "postum.n.01", "name": "Postum"}, - {"id": 13251, "synset": "chicory_escarole.n.01", "name": "chicory_escarole"}, - {"id": 13252, "synset": "belgian_endive.n.01", "name": "Belgian_endive"}, - {"id": 13253, "synset": "sweet_corn.n.02", "name": "sweet_corn"}, - {"id": 13254, "synset": "hominy.n.01", "name": "hominy"}, - {"id": 13255, "synset": "lye_hominy.n.01", "name": "lye_hominy"}, - {"id": 13256, "synset": "pearl_hominy.n.01", "name": "pearl_hominy"}, - {"id": 13257, "synset": "popcorn.n.02", "name": "popcorn"}, - {"id": 13258, "synset": "cress.n.02", "name": "cress"}, - {"id": 13259, "synset": "watercress.n.02", "name": "watercress"}, - {"id": 13260, "synset": "garden_cress.n.01", "name": "garden_cress"}, - {"id": 13261, "synset": "winter_cress.n.02", "name": "winter_cress"}, - {"id": 13262, "synset": "dandelion_green.n.02", "name": "dandelion_green"}, - {"id": 13263, "synset": "gumbo.n.03", "name": "gumbo"}, - {"id": 13264, "synset": "kohlrabi.n.02", "name": "kohlrabi"}, - {"id": 13265, "synset": "lamb's-quarter.n.01", "name": "lamb's-quarter"}, - {"id": 13266, "synset": "wild_spinach.n.03", "name": "wild_spinach"}, - {"id": 13267, "synset": "beefsteak_tomato.n.01", "name": "beefsteak_tomato"}, - {"id": 13268, "synset": "cherry_tomato.n.02", "name": "cherry_tomato"}, - {"id": 13269, "synset": "plum_tomato.n.02", "name": "plum_tomato"}, - {"id": 13270, "synset": "tomatillo.n.03", "name": "tomatillo"}, - {"id": 13271, "synset": "mushroom.n.05", "name": "mushroom"}, - {"id": 13272, "synset": "stuffed_mushroom.n.01", "name": "stuffed_mushroom"}, - {"id": 13273, "synset": "salsify.n.03", "name": "salsify"}, - {"id": 13274, "synset": "oyster_plant.n.03", "name": "oyster_plant"}, - {"id": 13275, "synset": "scorzonera.n.02", "name": "scorzonera"}, - {"id": 13276, "synset": "parsnip.n.03", "name": "parsnip"}, - {"id": 13277, "synset": "radish.n.01", "name": "radish"}, - {"id": 13278, "synset": "turnip.n.02", "name": "turnip"}, - {"id": 13279, "synset": "white_turnip.n.02", "name": "white_turnip"}, - {"id": 13280, "synset": "rutabaga.n.01", "name": "rutabaga"}, - {"id": 13281, "synset": "turnip_greens.n.01", "name": "turnip_greens"}, - {"id": 13282, "synset": "sorrel.n.04", "name": "sorrel"}, - {"id": 13283, "synset": "french_sorrel.n.02", "name": "French_sorrel"}, - {"id": 13284, "synset": "spinach.n.02", "name": "spinach"}, - {"id": 13285, "synset": "taro.n.03", "name": "taro"}, - {"id": 13286, "synset": "truffle.n.02", "name": "truffle"}, - {"id": 13287, "synset": "edible_nut.n.01", "name": "edible_nut"}, - {"id": 13288, "synset": "bunya_bunya.n.02", "name": "bunya_bunya"}, - {"id": 13289, "synset": "peanut.n.04", "name": "peanut"}, - {"id": 13290, "synset": "freestone.n.01", "name": "freestone"}, - {"id": 13291, "synset": "cling.n.01", "name": "cling"}, - {"id": 13292, "synset": "windfall.n.01", "name": "windfall"}, - {"id": 13293, "synset": "crab_apple.n.03", "name": "crab_apple"}, - {"id": 13294, "synset": "eating_apple.n.01", "name": "eating_apple"}, - {"id": 13295, "synset": "baldwin.n.03", "name": "Baldwin"}, - {"id": 13296, "synset": "cortland.n.01", "name": "Cortland"}, - {"id": 13297, "synset": "cox's_orange_pippin.n.01", "name": "Cox's_Orange_Pippin"}, - {"id": 13298, "synset": "delicious.n.01", "name": "Delicious"}, - {"id": 13299, "synset": "golden_delicious.n.01", "name": "Golden_Delicious"}, - {"id": 13300, "synset": "red_delicious.n.01", "name": "Red_Delicious"}, - {"id": 13301, "synset": "empire.n.05", "name": "Empire"}, - {"id": 13302, "synset": "grimes'_golden.n.01", "name": "Grimes'_golden"}, - {"id": 13303, "synset": "jonathan.n.01", "name": "Jonathan"}, - {"id": 13304, "synset": "mcintosh.n.01", "name": "McIntosh"}, - {"id": 13305, "synset": "macoun.n.01", "name": "Macoun"}, - {"id": 13306, "synset": "northern_spy.n.01", "name": "Northern_Spy"}, - {"id": 13307, "synset": "pearmain.n.01", "name": "Pearmain"}, - {"id": 13308, "synset": "pippin.n.01", "name": "Pippin"}, - {"id": 13309, "synset": "prima.n.01", "name": "Prima"}, - {"id": 13310, "synset": "stayman.n.01", "name": "Stayman"}, - {"id": 13311, "synset": "winesap.n.01", "name": "Winesap"}, - {"id": 13312, "synset": "stayman_winesap.n.01", "name": "Stayman_Winesap"}, - {"id": 13313, "synset": "cooking_apple.n.01", "name": "cooking_apple"}, - {"id": 13314, "synset": "bramley's_seedling.n.01", "name": "Bramley's_Seedling"}, - {"id": 13315, "synset": "granny_smith.n.01", "name": "Granny_Smith"}, - {"id": 13316, "synset": "lane's_prince_albert.n.01", "name": "Lane's_Prince_Albert"}, - {"id": 13317, "synset": "newtown_wonder.n.01", "name": "Newtown_Wonder"}, - {"id": 13318, "synset": "rome_beauty.n.01", "name": "Rome_Beauty"}, - {"id": 13319, "synset": "berry.n.01", "name": "berry"}, - {"id": 13320, "synset": "bilberry.n.03", "name": "bilberry"}, - {"id": 13321, "synset": "huckleberry.n.03", "name": "huckleberry"}, - {"id": 13322, "synset": "wintergreen.n.03", "name": "wintergreen"}, - {"id": 13323, "synset": "cranberry.n.02", "name": "cranberry"}, - {"id": 13324, "synset": "lingonberry.n.02", "name": "lingonberry"}, - {"id": 13325, "synset": "currant.n.01", "name": "currant"}, - {"id": 13326, "synset": "gooseberry.n.02", "name": "gooseberry"}, - {"id": 13327, "synset": "black_currant.n.02", "name": "black_currant"}, - {"id": 13328, "synset": "red_currant.n.02", "name": "red_currant"}, - {"id": 13329, "synset": "boysenberry.n.02", "name": "boysenberry"}, - {"id": 13330, "synset": "dewberry.n.02", "name": "dewberry"}, - {"id": 13331, "synset": "loganberry.n.02", "name": "loganberry"}, - {"id": 13332, "synset": "saskatoon.n.02", "name": "saskatoon"}, - {"id": 13333, "synset": "sugarberry.n.02", "name": "sugarberry"}, - {"id": 13334, "synset": "acerola.n.02", "name": "acerola"}, - {"id": 13335, "synset": "carambola.n.02", "name": "carambola"}, - {"id": 13336, "synset": "ceriman.n.02", "name": "ceriman"}, - {"id": 13337, "synset": "carissa_plum.n.01", "name": "carissa_plum"}, - {"id": 13338, "synset": "citrus.n.01", "name": "citrus"}, - {"id": 13339, "synset": "temple_orange.n.02", "name": "temple_orange"}, - {"id": 13340, "synset": "clementine.n.02", "name": "clementine"}, - {"id": 13341, "synset": "satsuma.n.02", "name": "satsuma"}, - {"id": 13342, "synset": "tangerine.n.02", "name": "tangerine"}, - {"id": 13343, "synset": "tangelo.n.02", "name": "tangelo"}, - {"id": 13344, "synset": "bitter_orange.n.02", "name": "bitter_orange"}, - {"id": 13345, "synset": "sweet_orange.n.01", "name": "sweet_orange"}, - {"id": 13346, "synset": "jaffa_orange.n.01", "name": "Jaffa_orange"}, - {"id": 13347, "synset": "navel_orange.n.01", "name": "navel_orange"}, - {"id": 13348, "synset": "valencia_orange.n.01", "name": "Valencia_orange"}, - {"id": 13349, "synset": "kumquat.n.02", "name": "kumquat"}, - {"id": 13350, "synset": "key_lime.n.01", "name": "key_lime"}, - {"id": 13351, "synset": "grapefruit.n.02", "name": "grapefruit"}, - {"id": 13352, "synset": "pomelo.n.02", "name": "pomelo"}, - {"id": 13353, "synset": "citrange.n.02", "name": "citrange"}, - {"id": 13354, "synset": "citron.n.01", "name": "citron"}, - {"id": 13355, "synset": "jordan_almond.n.02", "name": "Jordan_almond"}, - {"id": 13356, "synset": "nectarine.n.02", "name": "nectarine"}, - {"id": 13357, "synset": "pitahaya.n.02", "name": "pitahaya"}, - {"id": 13358, "synset": "plum.n.02", "name": "plum"}, - {"id": 13359, "synset": "damson.n.01", "name": "damson"}, - {"id": 13360, "synset": "greengage.n.01", "name": "greengage"}, - {"id": 13361, "synset": "beach_plum.n.02", "name": "beach_plum"}, - {"id": 13362, "synset": "sloe.n.03", "name": "sloe"}, - {"id": 13363, "synset": "victoria_plum.n.01", "name": "Victoria_plum"}, - {"id": 13364, "synset": "dried_fruit.n.01", "name": "dried_fruit"}, - {"id": 13365, "synset": "dried_apricot.n.01", "name": "dried_apricot"}, - {"id": 13366, "synset": "raisin.n.01", "name": "raisin"}, - {"id": 13367, "synset": "seedless_raisin.n.01", "name": "seedless_raisin"}, - {"id": 13368, "synset": "seeded_raisin.n.01", "name": "seeded_raisin"}, - {"id": 13369, "synset": "currant.n.03", "name": "currant"}, - {"id": 13370, "synset": "anchovy_pear.n.02", "name": "anchovy_pear"}, - {"id": 13371, "synset": "passion_fruit.n.01", "name": "passion_fruit"}, - {"id": 13372, "synset": "granadilla.n.04", "name": "granadilla"}, - {"id": 13373, "synset": "sweet_calabash.n.02", "name": "sweet_calabash"}, - {"id": 13374, "synset": "bell_apple.n.01", "name": "bell_apple"}, - {"id": 13375, "synset": "breadfruit.n.02", "name": "breadfruit"}, - {"id": 13376, "synset": "jackfruit.n.02", "name": "jackfruit"}, - {"id": 13377, "synset": "cacao_bean.n.01", "name": "cacao_bean"}, - {"id": 13378, "synset": "cocoa.n.02", "name": "cocoa"}, - {"id": 13379, "synset": "canistel.n.02", "name": "canistel"}, - {"id": 13380, "synset": "melon_ball.n.01", "name": "melon_ball"}, - {"id": 13381, "synset": "muskmelon.n.02", "name": "muskmelon"}, - {"id": 13382, "synset": "winter_melon.n.02", "name": "winter_melon"}, - {"id": 13383, "synset": "honeydew.n.01", "name": "honeydew"}, - {"id": 13384, "synset": "persian_melon.n.02", "name": "Persian_melon"}, - {"id": 13385, "synset": "net_melon.n.02", "name": "net_melon"}, - {"id": 13386, "synset": "casaba.n.01", "name": "casaba"}, - {"id": 13387, "synset": "sweet_cherry.n.02", "name": "sweet_cherry"}, - {"id": 13388, "synset": "bing_cherry.n.01", "name": "bing_cherry"}, - {"id": 13389, "synset": "heart_cherry.n.02", "name": "heart_cherry"}, - {"id": 13390, "synset": "blackheart.n.02", "name": "blackheart"}, - {"id": 13391, "synset": "capulin.n.02", "name": "capulin"}, - {"id": 13392, "synset": "sour_cherry.n.03", "name": "sour_cherry"}, - {"id": 13393, "synset": "amarelle.n.02", "name": "amarelle"}, - {"id": 13394, "synset": "morello.n.02", "name": "morello"}, - {"id": 13395, "synset": "cocoa_plum.n.02", "name": "cocoa_plum"}, - {"id": 13396, "synset": "gherkin.n.01", "name": "gherkin"}, - {"id": 13397, "synset": "fox_grape.n.02", "name": "fox_grape"}, - {"id": 13398, "synset": "concord_grape.n.01", "name": "Concord_grape"}, - {"id": 13399, "synset": "catawba.n.02", "name": "Catawba"}, - {"id": 13400, "synset": "muscadine.n.02", "name": "muscadine"}, - {"id": 13401, "synset": "scuppernong.n.01", "name": "scuppernong"}, - {"id": 13402, "synset": "slipskin_grape.n.01", "name": "slipskin_grape"}, - {"id": 13403, "synset": "vinifera_grape.n.02", "name": "vinifera_grape"}, - {"id": 13404, "synset": "emperor.n.02", "name": "emperor"}, - {"id": 13405, "synset": "muscat.n.04", "name": "muscat"}, - {"id": 13406, "synset": "ribier.n.01", "name": "ribier"}, - {"id": 13407, "synset": "sultana.n.01", "name": "sultana"}, - {"id": 13408, "synset": "tokay.n.02", "name": "Tokay"}, - {"id": 13409, "synset": "flame_tokay.n.01", "name": "flame_tokay"}, - {"id": 13410, "synset": "thompson_seedless.n.01", "name": "Thompson_Seedless"}, - {"id": 13411, "synset": "custard_apple.n.02", "name": "custard_apple"}, - {"id": 13412, "synset": "cherimoya.n.02", "name": "cherimoya"}, - {"id": 13413, "synset": "soursop.n.02", "name": "soursop"}, - {"id": 13414, "synset": "sweetsop.n.02", "name": "sweetsop"}, - {"id": 13415, "synset": "ilama.n.02", "name": "ilama"}, - {"id": 13416, "synset": "pond_apple.n.02", "name": "pond_apple"}, - {"id": 13417, "synset": "papaw.n.02", "name": "papaw"}, - {"id": 13418, "synset": "kai_apple.n.01", "name": "kai_apple"}, - {"id": 13419, "synset": "ketembilla.n.02", "name": "ketembilla"}, - {"id": 13420, "synset": "ackee.n.01", "name": "ackee"}, - {"id": 13421, "synset": "durian.n.02", "name": "durian"}, - {"id": 13422, "synset": "feijoa.n.02", "name": "feijoa"}, - {"id": 13423, "synset": "genip.n.02", "name": "genip"}, - {"id": 13424, "synset": "genipap.n.01", "name": "genipap"}, - {"id": 13425, "synset": "loquat.n.02", "name": "loquat"}, - {"id": 13426, "synset": "mangosteen.n.02", "name": "mangosteen"}, - {"id": 13427, "synset": "mango.n.02", "name": "mango"}, - {"id": 13428, "synset": "sapodilla.n.02", "name": "sapodilla"}, - {"id": 13429, "synset": "sapote.n.02", "name": "sapote"}, - {"id": 13430, "synset": "tamarind.n.02", "name": "tamarind"}, - {"id": 13431, "synset": "elderberry.n.02", "name": "elderberry"}, - {"id": 13432, "synset": "guava.n.03", "name": "guava"}, - {"id": 13433, "synset": "mombin.n.02", "name": "mombin"}, - {"id": 13434, "synset": "hog_plum.n.04", "name": "hog_plum"}, - {"id": 13435, "synset": "hog_plum.n.03", "name": "hog_plum"}, - {"id": 13436, "synset": "jaboticaba.n.02", "name": "jaboticaba"}, - {"id": 13437, "synset": "jujube.n.02", "name": "jujube"}, - {"id": 13438, "synset": "litchi.n.02", "name": "litchi"}, - {"id": 13439, "synset": "longanberry.n.02", "name": "longanberry"}, - {"id": 13440, "synset": "mamey.n.02", "name": "mamey"}, - {"id": 13441, "synset": "marang.n.02", "name": "marang"}, - {"id": 13442, "synset": "medlar.n.04", "name": "medlar"}, - {"id": 13443, "synset": "medlar.n.03", "name": "medlar"}, - {"id": 13444, "synset": "mulberry.n.02", "name": "mulberry"}, - {"id": 13445, "synset": "olive.n.04", "name": "olive"}, - {"id": 13446, "synset": "black_olive.n.01", "name": "black_olive"}, - {"id": 13447, "synset": "green_olive.n.01", "name": "green_olive"}, - {"id": 13448, "synset": "bosc.n.01", "name": "bosc"}, - {"id": 13449, "synset": "anjou.n.02", "name": "anjou"}, - {"id": 13450, "synset": "bartlett.n.03", "name": "bartlett"}, - {"id": 13451, "synset": "seckel.n.01", "name": "seckel"}, - {"id": 13452, "synset": "plantain.n.03", "name": "plantain"}, - {"id": 13453, "synset": "plumcot.n.02", "name": "plumcot"}, - {"id": 13454, "synset": "pomegranate.n.02", "name": "pomegranate"}, - {"id": 13455, "synset": "prickly_pear.n.02", "name": "prickly_pear"}, - {"id": 13456, "synset": "barbados_gooseberry.n.02", "name": "Barbados_gooseberry"}, - {"id": 13457, "synset": "quandong.n.04", "name": "quandong"}, - {"id": 13458, "synset": "quandong_nut.n.01", "name": "quandong_nut"}, - {"id": 13459, "synset": "quince.n.02", "name": "quince"}, - {"id": 13460, "synset": "rambutan.n.02", "name": "rambutan"}, - {"id": 13461, "synset": "pulasan.n.02", "name": "pulasan"}, - {"id": 13462, "synset": "rose_apple.n.02", "name": "rose_apple"}, - {"id": 13463, "synset": "sorb.n.01", "name": "sorb"}, - {"id": 13464, "synset": "sour_gourd.n.02", "name": "sour_gourd"}, - {"id": 13465, "synset": "edible_seed.n.01", "name": "edible_seed"}, - {"id": 13466, "synset": "pumpkin_seed.n.01", "name": "pumpkin_seed"}, - {"id": 13467, "synset": "betel_nut.n.01", "name": "betel_nut"}, - {"id": 13468, "synset": "beechnut.n.01", "name": "beechnut"}, - {"id": 13469, "synset": "walnut.n.01", "name": "walnut"}, - {"id": 13470, "synset": "black_walnut.n.02", "name": "black_walnut"}, - {"id": 13471, "synset": "english_walnut.n.02", "name": "English_walnut"}, - {"id": 13472, "synset": "brazil_nut.n.02", "name": "brazil_nut"}, - {"id": 13473, "synset": "butternut.n.02", "name": "butternut"}, - {"id": 13474, "synset": "souari_nut.n.02", "name": "souari_nut"}, - {"id": 13475, "synset": "cashew.n.02", "name": "cashew"}, - {"id": 13476, "synset": "chestnut.n.03", "name": "chestnut"}, - {"id": 13477, "synset": "chincapin.n.01", "name": "chincapin"}, - {"id": 13478, "synset": "hazelnut.n.02", "name": "hazelnut"}, - {"id": 13479, "synset": "coconut_milk.n.02", "name": "coconut_milk"}, - {"id": 13480, "synset": "grugru_nut.n.01", "name": "grugru_nut"}, - {"id": 13481, "synset": "hickory_nut.n.01", "name": "hickory_nut"}, - {"id": 13482, "synset": "cola_extract.n.01", "name": "cola_extract"}, - {"id": 13483, "synset": "macadamia_nut.n.02", "name": "macadamia_nut"}, - {"id": 13484, "synset": "pecan.n.03", "name": "pecan"}, - {"id": 13485, "synset": "pine_nut.n.01", "name": "pine_nut"}, - {"id": 13486, "synset": "pistachio.n.02", "name": "pistachio"}, - {"id": 13487, "synset": "sunflower_seed.n.01", "name": "sunflower_seed"}, - {"id": 13488, "synset": "anchovy_paste.n.01", "name": "anchovy_paste"}, - {"id": 13489, "synset": "rollmops.n.01", "name": "rollmops"}, - {"id": 13490, "synset": "feed.n.01", "name": "feed"}, - {"id": 13491, "synset": "cattle_cake.n.01", "name": "cattle_cake"}, - {"id": 13492, "synset": "creep_feed.n.01", "name": "creep_feed"}, - {"id": 13493, "synset": "fodder.n.02", "name": "fodder"}, - {"id": 13494, "synset": "feed_grain.n.01", "name": "feed_grain"}, - {"id": 13495, "synset": "eatage.n.01", "name": "eatage"}, - {"id": 13496, "synset": "silage.n.01", "name": "silage"}, - {"id": 13497, "synset": "oil_cake.n.01", "name": "oil_cake"}, - {"id": 13498, "synset": "oil_meal.n.01", "name": "oil_meal"}, - {"id": 13499, "synset": "alfalfa.n.02", "name": "alfalfa"}, - {"id": 13500, "synset": "broad_bean.n.03", "name": "broad_bean"}, - {"id": 13501, "synset": "hay.n.01", "name": "hay"}, - {"id": 13502, "synset": "timothy.n.03", "name": "timothy"}, - {"id": 13503, "synset": "stover.n.01", "name": "stover"}, - {"id": 13504, "synset": "grain.n.02", "name": "grain"}, - {"id": 13505, "synset": "grist.n.01", "name": "grist"}, - {"id": 13506, "synset": "groats.n.01", "name": "groats"}, - {"id": 13507, "synset": "millet.n.03", "name": "millet"}, - {"id": 13508, "synset": "barley.n.01", "name": "barley"}, - {"id": 13509, "synset": "pearl_barley.n.01", "name": "pearl_barley"}, - {"id": 13510, "synset": "buckwheat.n.02", "name": "buckwheat"}, - {"id": 13511, "synset": "bulgur.n.01", "name": "bulgur"}, - {"id": 13512, "synset": "wheat.n.02", "name": "wheat"}, - {"id": 13513, "synset": "cracked_wheat.n.01", "name": "cracked_wheat"}, - {"id": 13514, "synset": "stodge.n.01", "name": "stodge"}, - {"id": 13515, "synset": "wheat_germ.n.01", "name": "wheat_germ"}, - {"id": 13516, "synset": "oat.n.02", "name": "oat"}, - {"id": 13517, "synset": "rice.n.01", "name": "rice"}, - {"id": 13518, "synset": "brown_rice.n.01", "name": "brown_rice"}, - {"id": 13519, "synset": "white_rice.n.01", "name": "white_rice"}, - {"id": 13520, "synset": "wild_rice.n.02", "name": "wild_rice"}, - {"id": 13521, "synset": "paddy.n.03", "name": "paddy"}, - {"id": 13522, "synset": "slop.n.01", "name": "slop"}, - {"id": 13523, "synset": "mash.n.02", "name": "mash"}, - {"id": 13524, "synset": "chicken_feed.n.01", "name": "chicken_feed"}, - {"id": 13525, "synset": "cud.n.01", "name": "cud"}, - {"id": 13526, "synset": "bird_feed.n.01", "name": "bird_feed"}, - {"id": 13527, "synset": "petfood.n.01", "name": "petfood"}, - {"id": 13528, "synset": "dog_food.n.01", "name": "dog_food"}, - {"id": 13529, "synset": "cat_food.n.01", "name": "cat_food"}, - {"id": 13530, "synset": "canary_seed.n.01", "name": "canary_seed"}, - {"id": 13531, "synset": "tossed_salad.n.01", "name": "tossed_salad"}, - {"id": 13532, "synset": "green_salad.n.01", "name": "green_salad"}, - {"id": 13533, "synset": "caesar_salad.n.01", "name": "Caesar_salad"}, - {"id": 13534, "synset": "salmagundi.n.02", "name": "salmagundi"}, - {"id": 13535, "synset": "salad_nicoise.n.01", "name": "salad_nicoise"}, - {"id": 13536, "synset": "combination_salad.n.01", "name": "combination_salad"}, - {"id": 13537, "synset": "chef's_salad.n.01", "name": "chef's_salad"}, - {"id": 13538, "synset": "potato_salad.n.01", "name": "potato_salad"}, - {"id": 13539, "synset": "pasta_salad.n.01", "name": "pasta_salad"}, - {"id": 13540, "synset": "macaroni_salad.n.01", "name": "macaroni_salad"}, - {"id": 13541, "synset": "fruit_salad.n.01", "name": "fruit_salad"}, - {"id": 13542, "synset": "waldorf_salad.n.01", "name": "Waldorf_salad"}, - {"id": 13543, "synset": "crab_louis.n.01", "name": "crab_Louis"}, - {"id": 13544, "synset": "herring_salad.n.01", "name": "herring_salad"}, - {"id": 13545, "synset": "tuna_fish_salad.n.01", "name": "tuna_fish_salad"}, - {"id": 13546, "synset": "chicken_salad.n.01", "name": "chicken_salad"}, - {"id": 13547, "synset": "aspic.n.01", "name": "aspic"}, - {"id": 13548, "synset": "molded_salad.n.01", "name": "molded_salad"}, - {"id": 13549, "synset": "tabbouleh.n.01", "name": "tabbouleh"}, - {"id": 13550, "synset": "ingredient.n.03", "name": "ingredient"}, - {"id": 13551, "synset": "flavorer.n.01", "name": "flavorer"}, - {"id": 13552, "synset": "bouillon_cube.n.01", "name": "bouillon_cube"}, - {"id": 13553, "synset": "herb.n.02", "name": "herb"}, - {"id": 13554, "synset": "fines_herbes.n.01", "name": "fines_herbes"}, - {"id": 13555, "synset": "spice.n.02", "name": "spice"}, - {"id": 13556, "synset": "spearmint_oil.n.01", "name": "spearmint_oil"}, - {"id": 13557, "synset": "lemon_oil.n.01", "name": "lemon_oil"}, - {"id": 13558, "synset": "wintergreen_oil.n.01", "name": "wintergreen_oil"}, - {"id": 13559, "synset": "salt.n.02", "name": "salt"}, - {"id": 13560, "synset": "celery_salt.n.01", "name": "celery_salt"}, - {"id": 13561, "synset": "onion_salt.n.01", "name": "onion_salt"}, - {"id": 13562, "synset": "seasoned_salt.n.01", "name": "seasoned_salt"}, - {"id": 13563, "synset": "sour_salt.n.01", "name": "sour_salt"}, - {"id": 13564, "synset": "five_spice_powder.n.01", "name": "five_spice_powder"}, - {"id": 13565, "synset": "allspice.n.03", "name": "allspice"}, - {"id": 13566, "synset": "cinnamon.n.03", "name": "cinnamon"}, - {"id": 13567, "synset": "stick_cinnamon.n.01", "name": "stick_cinnamon"}, - {"id": 13568, "synset": "clove.n.04", "name": "clove"}, - {"id": 13569, "synset": "cumin.n.02", "name": "cumin"}, - {"id": 13570, "synset": "fennel.n.04", "name": "fennel"}, - {"id": 13571, "synset": "ginger.n.02", "name": "ginger"}, - {"id": 13572, "synset": "mace.n.03", "name": "mace"}, - {"id": 13573, "synset": "nutmeg.n.02", "name": "nutmeg"}, - {"id": 13574, "synset": "black_pepper.n.02", "name": "black_pepper"}, - {"id": 13575, "synset": "white_pepper.n.02", "name": "white_pepper"}, - {"id": 13576, "synset": "sassafras.n.02", "name": "sassafras"}, - {"id": 13577, "synset": "basil.n.03", "name": "basil"}, - {"id": 13578, "synset": "bay_leaf.n.01", "name": "bay_leaf"}, - {"id": 13579, "synset": "borage.n.02", "name": "borage"}, - {"id": 13580, "synset": "hyssop.n.02", "name": "hyssop"}, - {"id": 13581, "synset": "caraway.n.02", "name": "caraway"}, - {"id": 13582, "synset": "chervil.n.02", "name": "chervil"}, - {"id": 13583, "synset": "chives.n.02", "name": "chives"}, - {"id": 13584, "synset": "comfrey.n.02", "name": "comfrey"}, - {"id": 13585, "synset": "coriander.n.03", "name": "coriander"}, - {"id": 13586, "synset": "coriander.n.02", "name": "coriander"}, - {"id": 13587, "synset": "costmary.n.02", "name": "costmary"}, - {"id": 13588, "synset": "fennel.n.03", "name": "fennel"}, - {"id": 13589, "synset": "fennel.n.02", "name": "fennel"}, - {"id": 13590, "synset": "fennel_seed.n.01", "name": "fennel_seed"}, - {"id": 13591, "synset": "fenugreek.n.02", "name": "fenugreek"}, - {"id": 13592, "synset": "clove.n.03", "name": "clove"}, - {"id": 13593, "synset": "garlic_chive.n.02", "name": "garlic_chive"}, - {"id": 13594, "synset": "lemon_balm.n.02", "name": "lemon_balm"}, - {"id": 13595, "synset": "lovage.n.02", "name": "lovage"}, - {"id": 13596, "synset": "marjoram.n.02", "name": "marjoram"}, - {"id": 13597, "synset": "mint.n.04", "name": "mint"}, - {"id": 13598, "synset": "mustard_seed.n.01", "name": "mustard_seed"}, - {"id": 13599, "synset": "mustard.n.02", "name": "mustard"}, - {"id": 13600, "synset": "chinese_mustard.n.02", "name": "Chinese_mustard"}, - {"id": 13601, "synset": "nasturtium.n.03", "name": "nasturtium"}, - {"id": 13602, "synset": "parsley.n.02", "name": "parsley"}, - {"id": 13603, "synset": "salad_burnet.n.02", "name": "salad_burnet"}, - {"id": 13604, "synset": "rosemary.n.02", "name": "rosemary"}, - {"id": 13605, "synset": "rue.n.02", "name": "rue"}, - {"id": 13606, "synset": "sage.n.02", "name": "sage"}, - {"id": 13607, "synset": "clary_sage.n.02", "name": "clary_sage"}, - {"id": 13608, "synset": "savory.n.03", "name": "savory"}, - {"id": 13609, "synset": "summer_savory.n.02", "name": "summer_savory"}, - {"id": 13610, "synset": "winter_savory.n.02", "name": "winter_savory"}, - {"id": 13611, "synset": "sweet_woodruff.n.02", "name": "sweet_woodruff"}, - {"id": 13612, "synset": "sweet_cicely.n.03", "name": "sweet_cicely"}, - {"id": 13613, "synset": "tarragon.n.02", "name": "tarragon"}, - {"id": 13614, "synset": "thyme.n.02", "name": "thyme"}, - {"id": 13615, "synset": "turmeric.n.02", "name": "turmeric"}, - {"id": 13616, "synset": "caper.n.02", "name": "caper"}, - {"id": 13617, "synset": "catsup.n.01", "name": "catsup"}, - {"id": 13618, "synset": "cardamom.n.02", "name": "cardamom"}, - {"id": 13619, "synset": "chili_powder.n.01", "name": "chili_powder"}, - {"id": 13620, "synset": "chili_sauce.n.01", "name": "chili_sauce"}, - {"id": 13621, "synset": "chutney.n.01", "name": "chutney"}, - {"id": 13622, "synset": "steak_sauce.n.01", "name": "steak_sauce"}, - {"id": 13623, "synset": "taco_sauce.n.01", "name": "taco_sauce"}, - {"id": 13624, "synset": "mint_sauce.n.01", "name": "mint_sauce"}, - {"id": 13625, "synset": "cranberry_sauce.n.01", "name": "cranberry_sauce"}, - {"id": 13626, "synset": "curry_powder.n.01", "name": "curry_powder"}, - {"id": 13627, "synset": "curry.n.01", "name": "curry"}, - {"id": 13628, "synset": "lamb_curry.n.01", "name": "lamb_curry"}, - {"id": 13629, "synset": "duck_sauce.n.01", "name": "duck_sauce"}, - {"id": 13630, "synset": "horseradish.n.03", "name": "horseradish"}, - {"id": 13631, "synset": "marinade.n.01", "name": "marinade"}, - {"id": 13632, "synset": "paprika.n.02", "name": "paprika"}, - {"id": 13633, "synset": "spanish_paprika.n.01", "name": "Spanish_paprika"}, - {"id": 13634, "synset": "dill_pickle.n.01", "name": "dill_pickle"}, - {"id": 13635, "synset": "bread_and_butter_pickle.n.01", "name": "bread_and_butter_pickle"}, - {"id": 13636, "synset": "pickle_relish.n.01", "name": "pickle_relish"}, - {"id": 13637, "synset": "piccalilli.n.01", "name": "piccalilli"}, - {"id": 13638, "synset": "sweet_pickle.n.01", "name": "sweet_pickle"}, - {"id": 13639, "synset": "soy_sauce.n.01", "name": "soy_sauce"}, - {"id": 13640, "synset": "tomato_paste.n.01", "name": "tomato_paste"}, - {"id": 13641, "synset": "angelica.n.03", "name": "angelica"}, - {"id": 13642, "synset": "angelica.n.02", "name": "angelica"}, - {"id": 13643, "synset": "almond_extract.n.01", "name": "almond_extract"}, - {"id": 13644, "synset": "anise.n.02", "name": "anise"}, - {"id": 13645, "synset": "chinese_anise.n.02", "name": "Chinese_anise"}, - {"id": 13646, "synset": "juniper_berries.n.01", "name": "juniper_berries"}, - {"id": 13647, "synset": "saffron.n.02", "name": "saffron"}, - {"id": 13648, "synset": "sesame_seed.n.01", "name": "sesame_seed"}, - {"id": 13649, "synset": "caraway_seed.n.01", "name": "caraway_seed"}, - {"id": 13650, "synset": "poppy_seed.n.01", "name": "poppy_seed"}, - {"id": 13651, "synset": "dill.n.02", "name": "dill"}, - {"id": 13652, "synset": "dill_seed.n.01", "name": "dill_seed"}, - {"id": 13653, "synset": "celery_seed.n.01", "name": "celery_seed"}, - {"id": 13654, "synset": "lemon_extract.n.01", "name": "lemon_extract"}, - {"id": 13655, "synset": "monosodium_glutamate.n.01", "name": "monosodium_glutamate"}, - {"id": 13656, "synset": "vanilla_bean.n.01", "name": "vanilla_bean"}, - {"id": 13657, "synset": "cider_vinegar.n.01", "name": "cider_vinegar"}, - {"id": 13658, "synset": "wine_vinegar.n.01", "name": "wine_vinegar"}, - {"id": 13659, "synset": "sauce.n.01", "name": "sauce"}, - {"id": 13660, "synset": "anchovy_sauce.n.01", "name": "anchovy_sauce"}, - {"id": 13661, "synset": "hard_sauce.n.01", "name": "hard_sauce"}, - {"id": 13662, "synset": "horseradish_sauce.n.01", "name": "horseradish_sauce"}, - {"id": 13663, "synset": "bolognese_pasta_sauce.n.01", "name": "bolognese_pasta_sauce"}, - {"id": 13664, "synset": "carbonara.n.01", "name": "carbonara"}, - {"id": 13665, "synset": "tomato_sauce.n.01", "name": "tomato_sauce"}, - {"id": 13666, "synset": "tartare_sauce.n.01", "name": "tartare_sauce"}, - {"id": 13667, "synset": "wine_sauce.n.01", "name": "wine_sauce"}, - {"id": 13668, "synset": "marchand_de_vin.n.01", "name": "marchand_de_vin"}, - {"id": 13669, "synset": "bread_sauce.n.01", "name": "bread_sauce"}, - {"id": 13670, "synset": "plum_sauce.n.01", "name": "plum_sauce"}, - {"id": 13671, "synset": "peach_sauce.n.01", "name": "peach_sauce"}, - {"id": 13672, "synset": "apricot_sauce.n.01", "name": "apricot_sauce"}, - {"id": 13673, "synset": "pesto.n.01", "name": "pesto"}, - {"id": 13674, "synset": "ravigote.n.01", "name": "ravigote"}, - {"id": 13675, "synset": "remoulade_sauce.n.01", "name": "remoulade_sauce"}, - {"id": 13676, "synset": "dressing.n.01", "name": "dressing"}, - {"id": 13677, "synset": "sauce_louis.n.01", "name": "sauce_Louis"}, - {"id": 13678, "synset": "bleu_cheese_dressing.n.01", "name": "bleu_cheese_dressing"}, - {"id": 13679, "synset": "blue_cheese_dressing.n.01", "name": "blue_cheese_dressing"}, - {"id": 13680, "synset": "french_dressing.n.01", "name": "French_dressing"}, - {"id": 13681, "synset": "lorenzo_dressing.n.01", "name": "Lorenzo_dressing"}, - {"id": 13682, "synset": "anchovy_dressing.n.01", "name": "anchovy_dressing"}, - {"id": 13683, "synset": "italian_dressing.n.01", "name": "Italian_dressing"}, - {"id": 13684, "synset": "half-and-half_dressing.n.01", "name": "half-and-half_dressing"}, - {"id": 13685, "synset": "mayonnaise.n.01", "name": "mayonnaise"}, - {"id": 13686, "synset": "green_mayonnaise.n.01", "name": "green_mayonnaise"}, - {"id": 13687, "synset": "aioli.n.01", "name": "aioli"}, - {"id": 13688, "synset": "russian_dressing.n.01", "name": "Russian_dressing"}, - {"id": 13689, "synset": "salad_cream.n.01", "name": "salad_cream"}, - {"id": 13690, "synset": "thousand_island_dressing.n.01", "name": "Thousand_Island_dressing"}, - {"id": 13691, "synset": "barbecue_sauce.n.01", "name": "barbecue_sauce"}, - {"id": 13692, "synset": "hollandaise.n.01", "name": "hollandaise"}, - {"id": 13693, "synset": "bearnaise.n.01", "name": "bearnaise"}, - {"id": 13694, "synset": "bercy.n.01", "name": "Bercy"}, - {"id": 13695, "synset": "bordelaise.n.01", "name": "bordelaise"}, - {"id": 13696, "synset": "bourguignon.n.01", "name": "bourguignon"}, - {"id": 13697, "synset": "brown_sauce.n.02", "name": "brown_sauce"}, - {"id": 13698, "synset": "espagnole.n.01", "name": "Espagnole"}, - {"id": 13699, "synset": "chinese_brown_sauce.n.01", "name": "Chinese_brown_sauce"}, - {"id": 13700, "synset": "blanc.n.01", "name": "blanc"}, - {"id": 13701, "synset": "cheese_sauce.n.01", "name": "cheese_sauce"}, - {"id": 13702, "synset": "chocolate_sauce.n.01", "name": "chocolate_sauce"}, - {"id": 13703, "synset": "hot-fudge_sauce.n.01", "name": "hot-fudge_sauce"}, - {"id": 13704, "synset": "cocktail_sauce.n.01", "name": "cocktail_sauce"}, - {"id": 13705, "synset": "colbert.n.01", "name": "Colbert"}, - {"id": 13706, "synset": "white_sauce.n.01", "name": "white_sauce"}, - {"id": 13707, "synset": "cream_sauce.n.01", "name": "cream_sauce"}, - {"id": 13708, "synset": "mornay_sauce.n.01", "name": "Mornay_sauce"}, - {"id": 13709, "synset": "demiglace.n.01", "name": "demiglace"}, - {"id": 13710, "synset": "gravy.n.02", "name": "gravy"}, - {"id": 13711, "synset": "gravy.n.01", "name": "gravy"}, - {"id": 13712, "synset": "spaghetti_sauce.n.01", "name": "spaghetti_sauce"}, - {"id": 13713, "synset": "marinara.n.01", "name": "marinara"}, - {"id": 13714, "synset": "mole.n.03", "name": "mole"}, - {"id": 13715, "synset": "hunter's_sauce.n.01", "name": "hunter's_sauce"}, - {"id": 13716, "synset": "mushroom_sauce.n.01", "name": "mushroom_sauce"}, - {"id": 13717, "synset": "mustard_sauce.n.01", "name": "mustard_sauce"}, - {"id": 13718, "synset": "nantua.n.01", "name": "Nantua"}, - {"id": 13719, "synset": "hungarian_sauce.n.01", "name": "Hungarian_sauce"}, - {"id": 13720, "synset": "pepper_sauce.n.01", "name": "pepper_sauce"}, - {"id": 13721, "synset": "roux.n.01", "name": "roux"}, - {"id": 13722, "synset": "smitane.n.01", "name": "Smitane"}, - {"id": 13723, "synset": "soubise.n.01", "name": "Soubise"}, - {"id": 13724, "synset": "lyonnaise_sauce.n.01", "name": "Lyonnaise_sauce"}, - {"id": 13725, "synset": "veloute.n.01", "name": "veloute"}, - {"id": 13726, "synset": "allemande.n.01", "name": "allemande"}, - {"id": 13727, "synset": "caper_sauce.n.01", "name": "caper_sauce"}, - {"id": 13728, "synset": "poulette.n.01", "name": "poulette"}, - {"id": 13729, "synset": "curry_sauce.n.01", "name": "curry_sauce"}, - {"id": 13730, "synset": "worcester_sauce.n.01", "name": "Worcester_sauce"}, - {"id": 13731, "synset": "coconut_milk.n.01", "name": "coconut_milk"}, - {"id": 13732, "synset": "egg_white.n.01", "name": "egg_white"}, - {"id": 13733, "synset": "hard-boiled_egg.n.01", "name": "hard-boiled_egg"}, - {"id": 13734, "synset": "easter_egg.n.02", "name": "Easter_egg"}, - {"id": 13735, "synset": "easter_egg.n.01", "name": "Easter_egg"}, - {"id": 13736, "synset": "chocolate_egg.n.01", "name": "chocolate_egg"}, - {"id": 13737, "synset": "candy_egg.n.01", "name": "candy_egg"}, - {"id": 13738, "synset": "poached_egg.n.01", "name": "poached_egg"}, - {"id": 13739, "synset": "scrambled_eggs.n.01", "name": "scrambled_eggs"}, - {"id": 13740, "synset": "deviled_egg.n.01", "name": "deviled_egg"}, - {"id": 13741, "synset": "shirred_egg.n.01", "name": "shirred_egg"}, - {"id": 13742, "synset": "firm_omelet.n.01", "name": "firm_omelet"}, - {"id": 13743, "synset": "french_omelet.n.01", "name": "French_omelet"}, - {"id": 13744, "synset": "fluffy_omelet.n.01", "name": "fluffy_omelet"}, - {"id": 13745, "synset": "western_omelet.n.01", "name": "western_omelet"}, - {"id": 13746, "synset": "souffle.n.01", "name": "souffle"}, - {"id": 13747, "synset": "fried_egg.n.01", "name": "fried_egg"}, - {"id": 13748, "synset": "dairy_product.n.01", "name": "dairy_product"}, - {"id": 13749, "synset": "milk.n.04", "name": "milk"}, - {"id": 13750, "synset": "sour_milk.n.01", "name": "sour_milk"}, - {"id": 13751, "synset": "formula.n.06", "name": "formula"}, - {"id": 13752, "synset": "pasteurized_milk.n.01", "name": "pasteurized_milk"}, - {"id": 13753, "synset": "cows'_milk.n.01", "name": "cows'_milk"}, - {"id": 13754, "synset": "yak's_milk.n.01", "name": "yak's_milk"}, - {"id": 13755, "synset": "goats'_milk.n.01", "name": "goats'_milk"}, - {"id": 13756, "synset": "acidophilus_milk.n.01", "name": "acidophilus_milk"}, - {"id": 13757, "synset": "raw_milk.n.01", "name": "raw_milk"}, - {"id": 13758, "synset": "scalded_milk.n.01", "name": "scalded_milk"}, - {"id": 13759, "synset": "homogenized_milk.n.01", "name": "homogenized_milk"}, - {"id": 13760, "synset": "certified_milk.n.01", "name": "certified_milk"}, - {"id": 13761, "synset": "powdered_milk.n.01", "name": "powdered_milk"}, - {"id": 13762, "synset": "nonfat_dry_milk.n.01", "name": "nonfat_dry_milk"}, - {"id": 13763, "synset": "evaporated_milk.n.01", "name": "evaporated_milk"}, - {"id": 13764, "synset": "condensed_milk.n.01", "name": "condensed_milk"}, - {"id": 13765, "synset": "skim_milk.n.01", "name": "skim_milk"}, - {"id": 13766, "synset": "semi-skimmed_milk.n.01", "name": "semi-skimmed_milk"}, - {"id": 13767, "synset": "whole_milk.n.01", "name": "whole_milk"}, - {"id": 13768, "synset": "low-fat_milk.n.01", "name": "low-fat_milk"}, - {"id": 13769, "synset": "buttermilk.n.01", "name": "buttermilk"}, - {"id": 13770, "synset": "cream.n.02", "name": "cream"}, - {"id": 13771, "synset": "clotted_cream.n.01", "name": "clotted_cream"}, - {"id": 13772, "synset": "double_creme.n.01", "name": "double_creme"}, - {"id": 13773, "synset": "half-and-half.n.01", "name": "half-and-half"}, - {"id": 13774, "synset": "heavy_cream.n.01", "name": "heavy_cream"}, - {"id": 13775, "synset": "light_cream.n.01", "name": "light_cream"}, - {"id": 13776, "synset": "whipping_cream.n.01", "name": "whipping_cream"}, - {"id": 13777, "synset": "clarified_butter.n.01", "name": "clarified_butter"}, - {"id": 13778, "synset": "ghee.n.01", "name": "ghee"}, - {"id": 13779, "synset": "brown_butter.n.01", "name": "brown_butter"}, - {"id": 13780, "synset": "meuniere_butter.n.01", "name": "Meuniere_butter"}, - {"id": 13781, "synset": "blueberry_yogurt.n.01", "name": "blueberry_yogurt"}, - {"id": 13782, "synset": "raita.n.01", "name": "raita"}, - {"id": 13783, "synset": "whey.n.02", "name": "whey"}, - {"id": 13784, "synset": "curd.n.02", "name": "curd"}, - {"id": 13785, "synset": "curd.n.01", "name": "curd"}, - {"id": 13786, "synset": "clabber.n.01", "name": "clabber"}, - {"id": 13787, "synset": "cheese.n.01", "name": "cheese"}, - {"id": 13788, "synset": "paring.n.02", "name": "paring"}, - {"id": 13789, "synset": "cream_cheese.n.01", "name": "cream_cheese"}, - {"id": 13790, "synset": "double_cream.n.01", "name": "double_cream"}, - {"id": 13791, "synset": "mascarpone.n.01", "name": "mascarpone"}, - {"id": 13792, "synset": "triple_cream.n.01", "name": "triple_cream"}, - {"id": 13793, "synset": "cottage_cheese.n.01", "name": "cottage_cheese"}, - {"id": 13794, "synset": "process_cheese.n.01", "name": "process_cheese"}, - {"id": 13795, "synset": "bleu.n.01", "name": "bleu"}, - {"id": 13796, "synset": "stilton.n.01", "name": "Stilton"}, - {"id": 13797, "synset": "roquefort.n.01", "name": "Roquefort"}, - {"id": 13798, "synset": "gorgonzola.n.01", "name": "gorgonzola"}, - {"id": 13799, "synset": "danish_blue.n.01", "name": "Danish_blue"}, - {"id": 13800, "synset": "bavarian_blue.n.01", "name": "Bavarian_blue"}, - {"id": 13801, "synset": "brie.n.01", "name": "Brie"}, - {"id": 13802, "synset": "brick_cheese.n.01", "name": "brick_cheese"}, - {"id": 13803, "synset": "camembert.n.01", "name": "Camembert"}, - {"id": 13804, "synset": "cheddar.n.02", "name": "cheddar"}, - {"id": 13805, "synset": "rat_cheese.n.01", "name": "rat_cheese"}, - {"id": 13806, "synset": "cheshire_cheese.n.01", "name": "Cheshire_cheese"}, - {"id": 13807, "synset": "double_gloucester.n.01", "name": "double_Gloucester"}, - {"id": 13808, "synset": "edam.n.01", "name": "Edam"}, - {"id": 13809, "synset": "goat_cheese.n.01", "name": "goat_cheese"}, - {"id": 13810, "synset": "gouda.n.01", "name": "Gouda"}, - {"id": 13811, "synset": "grated_cheese.n.01", "name": "grated_cheese"}, - {"id": 13812, "synset": "hand_cheese.n.01", "name": "hand_cheese"}, - {"id": 13813, "synset": "liederkranz.n.01", "name": "Liederkranz"}, - {"id": 13814, "synset": "limburger.n.01", "name": "Limburger"}, - {"id": 13815, "synset": "mozzarella.n.01", "name": "mozzarella"}, - {"id": 13816, "synset": "muenster.n.01", "name": "Muenster"}, - {"id": 13817, "synset": "parmesan.n.01", "name": "Parmesan"}, - {"id": 13818, "synset": "quark_cheese.n.01", "name": "quark_cheese"}, - {"id": 13819, "synset": "ricotta.n.01", "name": "ricotta"}, - {"id": 13820, "synset": "swiss_cheese.n.01", "name": "Swiss_cheese"}, - {"id": 13821, "synset": "emmenthal.n.01", "name": "Emmenthal"}, - {"id": 13822, "synset": "gruyere.n.01", "name": "Gruyere"}, - {"id": 13823, "synset": "sapsago.n.01", "name": "sapsago"}, - {"id": 13824, "synset": "velveeta.n.01", "name": "Velveeta"}, - {"id": 13825, "synset": "nut_butter.n.01", "name": "nut_butter"}, - {"id": 13826, "synset": "marshmallow_fluff.n.01", "name": "marshmallow_fluff"}, - {"id": 13827, "synset": "onion_butter.n.01", "name": "onion_butter"}, - {"id": 13828, "synset": "pimento_butter.n.01", "name": "pimento_butter"}, - {"id": 13829, "synset": "shrimp_butter.n.01", "name": "shrimp_butter"}, - {"id": 13830, "synset": "lobster_butter.n.01", "name": "lobster_butter"}, - {"id": 13831, "synset": "yak_butter.n.01", "name": "yak_butter"}, - {"id": 13832, "synset": "spread.n.05", "name": "spread"}, - {"id": 13833, "synset": "cheese_spread.n.01", "name": "cheese_spread"}, - {"id": 13834, "synset": "anchovy_butter.n.01", "name": "anchovy_butter"}, - {"id": 13835, "synset": "fishpaste.n.01", "name": "fishpaste"}, - {"id": 13836, "synset": "garlic_butter.n.01", "name": "garlic_butter"}, - {"id": 13837, "synset": "miso.n.01", "name": "miso"}, - {"id": 13838, "synset": "wasabi.n.02", "name": "wasabi"}, - {"id": 13839, "synset": "snail_butter.n.01", "name": "snail_butter"}, - {"id": 13840, "synset": "pate.n.01", "name": "pate"}, - {"id": 13841, "synset": "duck_pate.n.01", "name": "duck_pate"}, - {"id": 13842, "synset": "foie_gras.n.01", "name": "foie_gras"}, - {"id": 13843, "synset": "tapenade.n.01", "name": "tapenade"}, - {"id": 13844, "synset": "tahini.n.01", "name": "tahini"}, - {"id": 13845, "synset": "sweetening.n.01", "name": "sweetening"}, - {"id": 13846, "synset": "aspartame.n.01", "name": "aspartame"}, - {"id": 13847, "synset": "saccharin.n.01", "name": "saccharin"}, - {"id": 13848, "synset": "sugar.n.01", "name": "sugar"}, - {"id": 13849, "synset": "syrup.n.01", "name": "syrup"}, - {"id": 13850, "synset": "sugar_syrup.n.01", "name": "sugar_syrup"}, - {"id": 13851, "synset": "molasses.n.01", "name": "molasses"}, - {"id": 13852, "synset": "sorghum.n.03", "name": "sorghum"}, - {"id": 13853, "synset": "treacle.n.01", "name": "treacle"}, - {"id": 13854, "synset": "grenadine.n.01", "name": "grenadine"}, - {"id": 13855, "synset": "maple_syrup.n.01", "name": "maple_syrup"}, - {"id": 13856, "synset": "corn_syrup.n.01", "name": "corn_syrup"}, - {"id": 13857, "synset": "miraculous_food.n.01", "name": "miraculous_food"}, - {"id": 13858, "synset": "dough.n.01", "name": "dough"}, - {"id": 13859, "synset": "bread_dough.n.01", "name": "bread_dough"}, - {"id": 13860, "synset": "pancake_batter.n.01", "name": "pancake_batter"}, - {"id": 13861, "synset": "fritter_batter.n.01", "name": "fritter_batter"}, - {"id": 13862, "synset": "coq_au_vin.n.01", "name": "coq_au_vin"}, - {"id": 13863, "synset": "chicken_provencale.n.01", "name": "chicken_provencale"}, - {"id": 13864, "synset": "chicken_and_rice.n.01", "name": "chicken_and_rice"}, - {"id": 13865, "synset": "moo_goo_gai_pan.n.01", "name": "moo_goo_gai_pan"}, - {"id": 13866, "synset": "arroz_con_pollo.n.01", "name": "arroz_con_pollo"}, - {"id": 13867, "synset": "bacon_and_eggs.n.02", "name": "bacon_and_eggs"}, - {"id": 13868, "synset": "barbecued_spareribs.n.01", "name": "barbecued_spareribs"}, - {"id": 13869, "synset": "beef_bourguignonne.n.01", "name": "beef_Bourguignonne"}, - {"id": 13870, "synset": "beef_wellington.n.01", "name": "beef_Wellington"}, - {"id": 13871, "synset": "bitok.n.01", "name": "bitok"}, - {"id": 13872, "synset": "boiled_dinner.n.01", "name": "boiled_dinner"}, - {"id": 13873, "synset": "boston_baked_beans.n.01", "name": "Boston_baked_beans"}, - {"id": 13874, "synset": "bubble_and_squeak.n.01", "name": "bubble_and_squeak"}, - {"id": 13875, "synset": "pasta.n.01", "name": "pasta"}, - {"id": 13876, "synset": "cannelloni.n.01", "name": "cannelloni"}, - {"id": 13877, "synset": "carbonnade_flamande.n.01", "name": "carbonnade_flamande"}, - {"id": 13878, "synset": "cheese_souffle.n.01", "name": "cheese_souffle"}, - {"id": 13879, "synset": "chicken_marengo.n.01", "name": "chicken_Marengo"}, - {"id": 13880, "synset": "chicken_cordon_bleu.n.01", "name": "chicken_cordon_bleu"}, - {"id": 13881, "synset": "maryland_chicken.n.01", "name": "Maryland_chicken"}, - {"id": 13882, "synset": "chicken_paprika.n.01", "name": "chicken_paprika"}, - {"id": 13883, "synset": "chicken_tetrazzini.n.01", "name": "chicken_Tetrazzini"}, - {"id": 13884, "synset": "tetrazzini.n.01", "name": "Tetrazzini"}, - {"id": 13885, "synset": "chicken_kiev.n.01", "name": "chicken_Kiev"}, - {"id": 13886, "synset": "chili.n.01", "name": "chili"}, - {"id": 13887, "synset": "chili_dog.n.01", "name": "chili_dog"}, - {"id": 13888, "synset": "chop_suey.n.01", "name": "chop_suey"}, - {"id": 13889, "synset": "chow_mein.n.01", "name": "chow_mein"}, - {"id": 13890, "synset": "codfish_ball.n.01", "name": "codfish_ball"}, - {"id": 13891, "synset": "coquille.n.01", "name": "coquille"}, - {"id": 13892, "synset": "coquilles_saint-jacques.n.01", "name": "coquilles_Saint-Jacques"}, - {"id": 13893, "synset": "croquette.n.01", "name": "croquette"}, - {"id": 13894, "synset": "cottage_pie.n.01", "name": "cottage_pie"}, - {"id": 13895, "synset": "rissole.n.01", "name": "rissole"}, - {"id": 13896, "synset": "dolmas.n.01", "name": "dolmas"}, - {"id": 13897, "synset": "egg_foo_yong.n.01", "name": "egg_foo_yong"}, - {"id": 13898, "synset": "eggs_benedict.n.01", "name": "eggs_Benedict"}, - {"id": 13899, "synset": "enchilada.n.01", "name": "enchilada"}, - {"id": 13900, "synset": "falafel.n.01", "name": "falafel"}, - {"id": 13901, "synset": "fish_and_chips.n.01", "name": "fish_and_chips"}, - {"id": 13902, "synset": "fondue.n.02", "name": "fondue"}, - {"id": 13903, "synset": "cheese_fondue.n.01", "name": "cheese_fondue"}, - {"id": 13904, "synset": "chocolate_fondue.n.01", "name": "chocolate_fondue"}, - {"id": 13905, "synset": "fondue.n.01", "name": "fondue"}, - {"id": 13906, "synset": "beef_fondue.n.01", "name": "beef_fondue"}, - {"id": 13907, "synset": "fried_rice.n.01", "name": "fried_rice"}, - {"id": 13908, "synset": "frittata.n.01", "name": "frittata"}, - {"id": 13909, "synset": "frog_legs.n.01", "name": "frog_legs"}, - {"id": 13910, "synset": "galantine.n.01", "name": "galantine"}, - {"id": 13911, "synset": "gefilte_fish.n.01", "name": "gefilte_fish"}, - {"id": 13912, "synset": "haggis.n.01", "name": "haggis"}, - {"id": 13913, "synset": "ham_and_eggs.n.01", "name": "ham_and_eggs"}, - {"id": 13914, "synset": "hash.n.01", "name": "hash"}, - {"id": 13915, "synset": "corned_beef_hash.n.01", "name": "corned_beef_hash"}, - {"id": 13916, "synset": "jambalaya.n.01", "name": "jambalaya"}, - {"id": 13917, "synset": "kabob.n.01", "name": "kabob"}, - {"id": 13918, "synset": "kedgeree.n.01", "name": "kedgeree"}, - {"id": 13919, "synset": "souvlaki.n.01", "name": "souvlaki"}, - {"id": 13920, "synset": "seafood_newburg.n.01", "name": "seafood_Newburg"}, - {"id": 13921, "synset": "lobster_newburg.n.01", "name": "lobster_Newburg"}, - {"id": 13922, "synset": "shrimp_newburg.n.01", "name": "shrimp_Newburg"}, - {"id": 13923, "synset": "newburg_sauce.n.01", "name": "Newburg_sauce"}, - {"id": 13924, "synset": "lobster_thermidor.n.01", "name": "lobster_thermidor"}, - {"id": 13925, "synset": "lutefisk.n.01", "name": "lutefisk"}, - {"id": 13926, "synset": "macaroni_and_cheese.n.01", "name": "macaroni_and_cheese"}, - {"id": 13927, "synset": "macedoine.n.01", "name": "macedoine"}, - {"id": 13928, "synset": "porcupine_ball.n.01", "name": "porcupine_ball"}, - {"id": 13929, "synset": "swedish_meatball.n.01", "name": "Swedish_meatball"}, - {"id": 13930, "synset": "meat_loaf.n.01", "name": "meat_loaf"}, - {"id": 13931, "synset": "moussaka.n.01", "name": "moussaka"}, - {"id": 13932, "synset": "osso_buco.n.01", "name": "osso_buco"}, - {"id": 13933, "synset": "marrow.n.03", "name": "marrow"}, - {"id": 13934, "synset": "pheasant_under_glass.n.01", "name": "pheasant_under_glass"}, - {"id": 13935, "synset": "pigs_in_blankets.n.01", "name": "pigs_in_blankets"}, - {"id": 13936, "synset": "pilaf.n.01", "name": "pilaf"}, - {"id": 13937, "synset": "bulgur_pilaf.n.01", "name": "bulgur_pilaf"}, - {"id": 13938, "synset": "sausage_pizza.n.01", "name": "sausage_pizza"}, - {"id": 13939, "synset": "pepperoni_pizza.n.01", "name": "pepperoni_pizza"}, - {"id": 13940, "synset": "cheese_pizza.n.01", "name": "cheese_pizza"}, - {"id": 13941, "synset": "anchovy_pizza.n.01", "name": "anchovy_pizza"}, - {"id": 13942, "synset": "sicilian_pizza.n.01", "name": "Sicilian_pizza"}, - {"id": 13943, "synset": "poi.n.01", "name": "poi"}, - {"id": 13944, "synset": "pork_and_beans.n.01", "name": "pork_and_beans"}, - {"id": 13945, "synset": "porridge.n.01", "name": "porridge"}, - {"id": 13946, "synset": "oatmeal.n.01", "name": "oatmeal"}, - {"id": 13947, "synset": "loblolly.n.01", "name": "loblolly"}, - {"id": 13948, "synset": "potpie.n.01", "name": "potpie"}, - {"id": 13949, "synset": "rijsttaffel.n.01", "name": "rijsttaffel"}, - {"id": 13950, "synset": "risotto.n.01", "name": "risotto"}, - {"id": 13951, "synset": "roulade.n.01", "name": "roulade"}, - {"id": 13952, "synset": "fish_loaf.n.01", "name": "fish_loaf"}, - {"id": 13953, "synset": "salmon_loaf.n.01", "name": "salmon_loaf"}, - {"id": 13954, "synset": "salisbury_steak.n.01", "name": "Salisbury_steak"}, - {"id": 13955, "synset": "sauerbraten.n.01", "name": "sauerbraten"}, - {"id": 13956, "synset": "sauerkraut.n.01", "name": "sauerkraut"}, - {"id": 13957, "synset": "scallopine.n.01", "name": "scallopine"}, - {"id": 13958, "synset": "veal_scallopini.n.01", "name": "veal_scallopini"}, - {"id": 13959, "synset": "scampi.n.01", "name": "scampi"}, - {"id": 13960, "synset": "scotch_egg.n.01", "name": "Scotch_egg"}, - {"id": 13961, "synset": "scotch_woodcock.n.01", "name": "Scotch_woodcock"}, - {"id": 13962, "synset": "scrapple.n.01", "name": "scrapple"}, - {"id": 13963, "synset": "spaghetti_and_meatballs.n.01", "name": "spaghetti_and_meatballs"}, - {"id": 13964, "synset": "spanish_rice.n.01", "name": "Spanish_rice"}, - {"id": 13965, "synset": "steak_tartare.n.01", "name": "steak_tartare"}, - {"id": 13966, "synset": "pepper_steak.n.02", "name": "pepper_steak"}, - {"id": 13967, "synset": "steak_au_poivre.n.01", "name": "steak_au_poivre"}, - {"id": 13968, "synset": "beef_stroganoff.n.01", "name": "beef_Stroganoff"}, - {"id": 13969, "synset": "stuffed_cabbage.n.01", "name": "stuffed_cabbage"}, - {"id": 13970, "synset": "kishke.n.01", "name": "kishke"}, - {"id": 13971, "synset": "stuffed_peppers.n.01", "name": "stuffed_peppers"}, - {"id": 13972, "synset": "stuffed_tomato.n.02", "name": "stuffed_tomato"}, - {"id": 13973, "synset": "stuffed_tomato.n.01", "name": "stuffed_tomato"}, - {"id": 13974, "synset": "succotash.n.01", "name": "succotash"}, - {"id": 13975, "synset": "sukiyaki.n.01", "name": "sukiyaki"}, - {"id": 13976, "synset": "sashimi.n.01", "name": "sashimi"}, - {"id": 13977, "synset": "swiss_steak.n.01", "name": "Swiss_steak"}, - {"id": 13978, "synset": "tamale.n.02", "name": "tamale"}, - {"id": 13979, "synset": "tamale_pie.n.01", "name": "tamale_pie"}, - {"id": 13980, "synset": "tempura.n.01", "name": "tempura"}, - {"id": 13981, "synset": "teriyaki.n.01", "name": "teriyaki"}, - {"id": 13982, "synset": "terrine.n.01", "name": "terrine"}, - {"id": 13983, "synset": "welsh_rarebit.n.01", "name": "Welsh_rarebit"}, - {"id": 13984, "synset": "schnitzel.n.01", "name": "schnitzel"}, - {"id": 13985, "synset": "chicken_taco.n.01", "name": "chicken_taco"}, - {"id": 13986, "synset": "beef_burrito.n.01", "name": "beef_burrito"}, - {"id": 13987, "synset": "tostada.n.01", "name": "tostada"}, - {"id": 13988, "synset": "bean_tostada.n.01", "name": "bean_tostada"}, - {"id": 13989, "synset": "refried_beans.n.01", "name": "refried_beans"}, - {"id": 13990, "synset": "beverage.n.01", "name": "beverage"}, - {"id": 13991, "synset": "wish-wash.n.01", "name": "wish-wash"}, - {"id": 13992, "synset": "concoction.n.01", "name": "concoction"}, - {"id": 13993, "synset": "mix.n.01", "name": "mix"}, - {"id": 13994, "synset": "filling.n.03", "name": "filling"}, - {"id": 13995, "synset": "lekvar.n.01", "name": "lekvar"}, - {"id": 13996, "synset": "potion.n.01", "name": "potion"}, - {"id": 13997, "synset": "elixir.n.03", "name": "elixir"}, - {"id": 13998, "synset": "elixir_of_life.n.01", "name": "elixir_of_life"}, - {"id": 13999, "synset": "philter.n.01", "name": "philter"}, - {"id": 14000, "synset": "proof_spirit.n.01", "name": "proof_spirit"}, - {"id": 14001, "synset": "home_brew.n.01", "name": "home_brew"}, - {"id": 14002, "synset": "hooch.n.01", "name": "hooch"}, - {"id": 14003, "synset": "kava.n.01", "name": "kava"}, - {"id": 14004, "synset": "aperitif.n.01", "name": "aperitif"}, - {"id": 14005, "synset": "brew.n.01", "name": "brew"}, - {"id": 14006, "synset": "beer.n.01", "name": "beer"}, - {"id": 14007, "synset": "draft_beer.n.01", "name": "draft_beer"}, - {"id": 14008, "synset": "suds.n.02", "name": "suds"}, - {"id": 14009, "synset": "munich_beer.n.01", "name": "Munich_beer"}, - {"id": 14010, "synset": "bock.n.01", "name": "bock"}, - {"id": 14011, "synset": "lager.n.02", "name": "lager"}, - {"id": 14012, "synset": "light_beer.n.01", "name": "light_beer"}, - {"id": 14013, "synset": "oktoberfest.n.01", "name": "Oktoberfest"}, - {"id": 14014, "synset": "pilsner.n.01", "name": "Pilsner"}, - {"id": 14015, "synset": "shebeen.n.01", "name": "shebeen"}, - {"id": 14016, "synset": "weissbier.n.01", "name": "Weissbier"}, - {"id": 14017, "synset": "weizenbock.n.01", "name": "Weizenbock"}, - {"id": 14018, "synset": "malt.n.03", "name": "malt"}, - {"id": 14019, "synset": "wort.n.02", "name": "wort"}, - {"id": 14020, "synset": "malt.n.02", "name": "malt"}, - {"id": 14021, "synset": "ale.n.01", "name": "ale"}, - {"id": 14022, "synset": "bitter.n.01", "name": "bitter"}, - {"id": 14023, "synset": "burton.n.03", "name": "Burton"}, - {"id": 14024, "synset": "pale_ale.n.01", "name": "pale_ale"}, - {"id": 14025, "synset": "porter.n.07", "name": "porter"}, - {"id": 14026, "synset": "stout.n.01", "name": "stout"}, - {"id": 14027, "synset": "guinness.n.02", "name": "Guinness"}, - {"id": 14028, "synset": "kvass.n.01", "name": "kvass"}, - {"id": 14029, "synset": "mead.n.03", "name": "mead"}, - {"id": 14030, "synset": "metheglin.n.01", "name": "metheglin"}, - {"id": 14031, "synset": "hydromel.n.01", "name": "hydromel"}, - {"id": 14032, "synset": "oenomel.n.01", "name": "oenomel"}, - {"id": 14033, "synset": "near_beer.n.01", "name": "near_beer"}, - {"id": 14034, "synset": "ginger_beer.n.01", "name": "ginger_beer"}, - {"id": 14035, "synset": "sake.n.02", "name": "sake"}, - {"id": 14036, "synset": "wine.n.01", "name": "wine"}, - {"id": 14037, "synset": "vintage.n.01", "name": "vintage"}, - {"id": 14038, "synset": "red_wine.n.01", "name": "red_wine"}, - {"id": 14039, "synset": "white_wine.n.01", "name": "white_wine"}, - {"id": 14040, "synset": "blush_wine.n.01", "name": "blush_wine"}, - {"id": 14041, "synset": "altar_wine.n.01", "name": "altar_wine"}, - {"id": 14042, "synset": "sparkling_wine.n.01", "name": "sparkling_wine"}, - {"id": 14043, "synset": "champagne.n.01", "name": "champagne"}, - {"id": 14044, "synset": "cold_duck.n.01", "name": "cold_duck"}, - {"id": 14045, "synset": "burgundy.n.02", "name": "Burgundy"}, - {"id": 14046, "synset": "beaujolais.n.01", "name": "Beaujolais"}, - {"id": 14047, "synset": "medoc.n.01", "name": "Medoc"}, - {"id": 14048, "synset": "canary_wine.n.01", "name": "Canary_wine"}, - {"id": 14049, "synset": "chablis.n.02", "name": "Chablis"}, - {"id": 14050, "synset": "montrachet.n.01", "name": "Montrachet"}, - {"id": 14051, "synset": "chardonnay.n.02", "name": "Chardonnay"}, - {"id": 14052, "synset": "pinot_noir.n.02", "name": "Pinot_noir"}, - {"id": 14053, "synset": "pinot_blanc.n.02", "name": "Pinot_blanc"}, - {"id": 14054, "synset": "bordeaux.n.02", "name": "Bordeaux"}, - {"id": 14055, "synset": "claret.n.02", "name": "claret"}, - {"id": 14056, "synset": "chianti.n.01", "name": "Chianti"}, - {"id": 14057, "synset": "cabernet.n.01", "name": "Cabernet"}, - {"id": 14058, "synset": "merlot.n.02", "name": "Merlot"}, - {"id": 14059, "synset": "sauvignon_blanc.n.02", "name": "Sauvignon_blanc"}, - {"id": 14060, "synset": "california_wine.n.01", "name": "California_wine"}, - {"id": 14061, "synset": "cotes_de_provence.n.01", "name": "Cotes_de_Provence"}, - {"id": 14062, "synset": "dessert_wine.n.01", "name": "dessert_wine"}, - {"id": 14063, "synset": "dubonnet.n.01", "name": "Dubonnet"}, - {"id": 14064, "synset": "jug_wine.n.01", "name": "jug_wine"}, - {"id": 14065, "synset": "macon.n.02", "name": "macon"}, - {"id": 14066, "synset": "moselle.n.01", "name": "Moselle"}, - {"id": 14067, "synset": "muscadet.n.02", "name": "Muscadet"}, - {"id": 14068, "synset": "plonk.n.01", "name": "plonk"}, - {"id": 14069, "synset": "retsina.n.01", "name": "retsina"}, - {"id": 14070, "synset": "rhine_wine.n.01", "name": "Rhine_wine"}, - {"id": 14071, "synset": "riesling.n.02", "name": "Riesling"}, - {"id": 14072, "synset": "liebfraumilch.n.01", "name": "liebfraumilch"}, - {"id": 14073, "synset": "rhone_wine.n.01", "name": "Rhone_wine"}, - {"id": 14074, "synset": "rioja.n.01", "name": "Rioja"}, - {"id": 14075, "synset": "sack.n.04", "name": "sack"}, - {"id": 14076, "synset": "saint_emilion.n.01", "name": "Saint_Emilion"}, - {"id": 14077, "synset": "soave.n.01", "name": "Soave"}, - {"id": 14078, "synset": "zinfandel.n.02", "name": "zinfandel"}, - {"id": 14079, "synset": "sauterne.n.01", "name": "Sauterne"}, - {"id": 14080, "synset": "straw_wine.n.01", "name": "straw_wine"}, - {"id": 14081, "synset": "table_wine.n.01", "name": "table_wine"}, - {"id": 14082, "synset": "tokay.n.01", "name": "Tokay"}, - {"id": 14083, "synset": "vin_ordinaire.n.01", "name": "vin_ordinaire"}, - {"id": 14084, "synset": "vermouth.n.01", "name": "vermouth"}, - {"id": 14085, "synset": "sweet_vermouth.n.01", "name": "sweet_vermouth"}, - {"id": 14086, "synset": "dry_vermouth.n.01", "name": "dry_vermouth"}, - {"id": 14087, "synset": "chenin_blanc.n.02", "name": "Chenin_blanc"}, - {"id": 14088, "synset": "verdicchio.n.02", "name": "Verdicchio"}, - {"id": 14089, "synset": "vouvray.n.01", "name": "Vouvray"}, - {"id": 14090, "synset": "yquem.n.01", "name": "Yquem"}, - {"id": 14091, "synset": "generic.n.01", "name": "generic"}, - {"id": 14092, "synset": "varietal.n.01", "name": "varietal"}, - {"id": 14093, "synset": "fortified_wine.n.01", "name": "fortified_wine"}, - {"id": 14094, "synset": "madeira.n.03", "name": "Madeira"}, - {"id": 14095, "synset": "malmsey.n.01", "name": "malmsey"}, - {"id": 14096, "synset": "port.n.02", "name": "port"}, - {"id": 14097, "synset": "sherry.n.01", "name": "sherry"}, - {"id": 14098, "synset": "marsala.n.01", "name": "Marsala"}, - {"id": 14099, "synset": "muscat.n.03", "name": "muscat"}, - {"id": 14100, "synset": "neutral_spirits.n.01", "name": "neutral_spirits"}, - {"id": 14101, "synset": "aqua_vitae.n.01", "name": "aqua_vitae"}, - {"id": 14102, "synset": "eau_de_vie.n.01", "name": "eau_de_vie"}, - {"id": 14103, "synset": "moonshine.n.02", "name": "moonshine"}, - {"id": 14104, "synset": "bathtub_gin.n.01", "name": "bathtub_gin"}, - {"id": 14105, "synset": "aquavit.n.01", "name": "aquavit"}, - {"id": 14106, "synset": "arrack.n.01", "name": "arrack"}, - {"id": 14107, "synset": "bitters.n.01", "name": "bitters"}, - {"id": 14108, "synset": "brandy.n.01", "name": "brandy"}, - {"id": 14109, "synset": "applejack.n.01", "name": "applejack"}, - {"id": 14110, "synset": "calvados.n.01", "name": "Calvados"}, - {"id": 14111, "synset": "armagnac.n.01", "name": "Armagnac"}, - {"id": 14112, "synset": "cognac.n.01", "name": "Cognac"}, - {"id": 14113, "synset": "grappa.n.01", "name": "grappa"}, - {"id": 14114, "synset": "kirsch.n.01", "name": "kirsch"}, - {"id": 14115, "synset": "slivovitz.n.01", "name": "slivovitz"}, - {"id": 14116, "synset": "gin.n.01", "name": "gin"}, - {"id": 14117, "synset": "sloe_gin.n.01", "name": "sloe_gin"}, - {"id": 14118, "synset": "geneva.n.02", "name": "geneva"}, - {"id": 14119, "synset": "grog.n.01", "name": "grog"}, - {"id": 14120, "synset": "ouzo.n.01", "name": "ouzo"}, - {"id": 14121, "synset": "rum.n.01", "name": "rum"}, - {"id": 14122, "synset": "demerara.n.04", "name": "demerara"}, - {"id": 14123, "synset": "jamaica_rum.n.01", "name": "Jamaica_rum"}, - {"id": 14124, "synset": "schnapps.n.01", "name": "schnapps"}, - {"id": 14125, "synset": "pulque.n.01", "name": "pulque"}, - {"id": 14126, "synset": "mescal.n.02", "name": "mescal"}, - {"id": 14127, "synset": "whiskey.n.01", "name": "whiskey"}, - {"id": 14128, "synset": "blended_whiskey.n.01", "name": "blended_whiskey"}, - {"id": 14129, "synset": "bourbon.n.02", "name": "bourbon"}, - {"id": 14130, "synset": "corn_whiskey.n.01", "name": "corn_whiskey"}, - {"id": 14131, "synset": "firewater.n.01", "name": "firewater"}, - {"id": 14132, "synset": "irish.n.02", "name": "Irish"}, - {"id": 14133, "synset": "poteen.n.01", "name": "poteen"}, - {"id": 14134, "synset": "rye.n.03", "name": "rye"}, - {"id": 14135, "synset": "scotch.n.02", "name": "Scotch"}, - {"id": 14136, "synset": "sour_mash.n.02", "name": "sour_mash"}, - {"id": 14137, "synset": "liqueur.n.01", "name": "liqueur"}, - {"id": 14138, "synset": "absinth.n.01", "name": "absinth"}, - {"id": 14139, "synset": "amaretto.n.01", "name": "amaretto"}, - {"id": 14140, "synset": "anisette.n.01", "name": "anisette"}, - {"id": 14141, "synset": "benedictine.n.02", "name": "benedictine"}, - {"id": 14142, "synset": "chartreuse.n.01", "name": "Chartreuse"}, - {"id": 14143, "synset": "coffee_liqueur.n.01", "name": "coffee_liqueur"}, - {"id": 14144, "synset": "creme_de_cacao.n.01", "name": "creme_de_cacao"}, - {"id": 14145, "synset": "creme_de_menthe.n.01", "name": "creme_de_menthe"}, - {"id": 14146, "synset": "creme_de_fraise.n.01", "name": "creme_de_fraise"}, - {"id": 14147, "synset": "drambuie.n.01", "name": "Drambuie"}, - {"id": 14148, "synset": "galliano.n.01", "name": "Galliano"}, - {"id": 14149, "synset": "orange_liqueur.n.01", "name": "orange_liqueur"}, - {"id": 14150, "synset": "curacao.n.02", "name": "curacao"}, - {"id": 14151, "synset": "triple_sec.n.01", "name": "triple_sec"}, - {"id": 14152, "synset": "grand_marnier.n.01", "name": "Grand_Marnier"}, - {"id": 14153, "synset": "kummel.n.01", "name": "kummel"}, - {"id": 14154, "synset": "maraschino.n.01", "name": "maraschino"}, - {"id": 14155, "synset": "pastis.n.01", "name": "pastis"}, - {"id": 14156, "synset": "pernod.n.01", "name": "Pernod"}, - {"id": 14157, "synset": "pousse-cafe.n.01", "name": "pousse-cafe"}, - {"id": 14158, "synset": "kahlua.n.01", "name": "Kahlua"}, - {"id": 14159, "synset": "ratafia.n.01", "name": "ratafia"}, - {"id": 14160, "synset": "sambuca.n.01", "name": "sambuca"}, - {"id": 14161, "synset": "mixed_drink.n.01", "name": "mixed_drink"}, - {"id": 14162, "synset": "cocktail.n.01", "name": "cocktail"}, - {"id": 14163, "synset": "dom_pedro.n.01", "name": "Dom_Pedro"}, - {"id": 14164, "synset": "highball.n.01", "name": "highball"}, - {"id": 14165, "synset": "mixer.n.02", "name": "mixer"}, - {"id": 14166, "synset": "bishop.n.02", "name": "bishop"}, - {"id": 14167, "synset": "bloody_mary.n.02", "name": "Bloody_Mary"}, - {"id": 14168, "synset": "virgin_mary.n.02", "name": "Virgin_Mary"}, - {"id": 14169, "synset": "bullshot.n.01", "name": "bullshot"}, - {"id": 14170, "synset": "cobbler.n.02", "name": "cobbler"}, - {"id": 14171, "synset": "collins.n.02", "name": "collins"}, - {"id": 14172, "synset": "cooler.n.02", "name": "cooler"}, - {"id": 14173, "synset": "refresher.n.02", "name": "refresher"}, - {"id": 14174, "synset": "daiquiri.n.01", "name": "daiquiri"}, - {"id": 14175, "synset": "strawberry_daiquiri.n.01", "name": "strawberry_daiquiri"}, - {"id": 14176, "synset": "nada_daiquiri.n.01", "name": "NADA_daiquiri"}, - {"id": 14177, "synset": "spritzer.n.01", "name": "spritzer"}, - {"id": 14178, "synset": "flip.n.02", "name": "flip"}, - {"id": 14179, "synset": "gimlet.n.01", "name": "gimlet"}, - {"id": 14180, "synset": "gin_and_tonic.n.01", "name": "gin_and_tonic"}, - {"id": 14181, "synset": "grasshopper.n.02", "name": "grasshopper"}, - {"id": 14182, "synset": "harvey_wallbanger.n.01", "name": "Harvey_Wallbanger"}, - {"id": 14183, "synset": "julep.n.01", "name": "julep"}, - {"id": 14184, "synset": "manhattan.n.02", "name": "manhattan"}, - {"id": 14185, "synset": "rob_roy.n.02", "name": "Rob_Roy"}, - {"id": 14186, "synset": "margarita.n.01", "name": "margarita"}, - {"id": 14187, "synset": "gin_and_it.n.01", "name": "gin_and_it"}, - {"id": 14188, "synset": "vodka_martini.n.01", "name": "vodka_martini"}, - {"id": 14189, "synset": "old_fashioned.n.01", "name": "old_fashioned"}, - {"id": 14190, "synset": "pink_lady.n.01", "name": "pink_lady"}, - {"id": 14191, "synset": "sazerac.n.01", "name": "Sazerac"}, - {"id": 14192, "synset": "screwdriver.n.02", "name": "screwdriver"}, - {"id": 14193, "synset": "sidecar.n.01", "name": "sidecar"}, - {"id": 14194, "synset": "scotch_and_soda.n.01", "name": "Scotch_and_soda"}, - {"id": 14195, "synset": "sling.n.01", "name": "sling"}, - {"id": 14196, "synset": "brandy_sling.n.01", "name": "brandy_sling"}, - {"id": 14197, "synset": "gin_sling.n.01", "name": "gin_sling"}, - {"id": 14198, "synset": "rum_sling.n.01", "name": "rum_sling"}, - {"id": 14199, "synset": "sour.n.01", "name": "sour"}, - {"id": 14200, "synset": "whiskey_sour.n.01", "name": "whiskey_sour"}, - {"id": 14201, "synset": "stinger.n.01", "name": "stinger"}, - {"id": 14202, "synset": "swizzle.n.01", "name": "swizzle"}, - {"id": 14203, "synset": "hot_toddy.n.01", "name": "hot_toddy"}, - {"id": 14204, "synset": "zombie.n.05", "name": "zombie"}, - {"id": 14205, "synset": "fizz.n.01", "name": "fizz"}, - {"id": 14206, "synset": "irish_coffee.n.01", "name": "Irish_coffee"}, - {"id": 14207, "synset": "cafe_au_lait.n.01", "name": "cafe_au_lait"}, - {"id": 14208, "synset": "cafe_noir.n.01", "name": "cafe_noir"}, - {"id": 14209, "synset": "decaffeinated_coffee.n.01", "name": "decaffeinated_coffee"}, - {"id": 14210, "synset": "drip_coffee.n.01", "name": "drip_coffee"}, - {"id": 14211, "synset": "espresso.n.01", "name": "espresso"}, - {"id": 14212, "synset": "caffe_latte.n.01", "name": "caffe_latte"}, - {"id": 14213, "synset": "iced_coffee.n.01", "name": "iced_coffee"}, - {"id": 14214, "synset": "instant_coffee.n.01", "name": "instant_coffee"}, - {"id": 14215, "synset": "mocha.n.03", "name": "mocha"}, - {"id": 14216, "synset": "mocha.n.02", "name": "mocha"}, - {"id": 14217, "synset": "cassareep.n.01", "name": "cassareep"}, - {"id": 14218, "synset": "turkish_coffee.n.01", "name": "Turkish_coffee"}, - {"id": 14219, "synset": "hard_cider.n.01", "name": "hard_cider"}, - {"id": 14220, "synset": "scrumpy.n.01", "name": "scrumpy"}, - {"id": 14221, "synset": "sweet_cider.n.01", "name": "sweet_cider"}, - {"id": 14222, "synset": "mulled_cider.n.01", "name": "mulled_cider"}, - {"id": 14223, "synset": "perry.n.04", "name": "perry"}, - {"id": 14224, "synset": "rotgut.n.01", "name": "rotgut"}, - {"id": 14225, "synset": "slug.n.05", "name": "slug"}, - {"id": 14226, "synset": "criollo.n.02", "name": "criollo"}, - {"id": 14227, "synset": "juice.n.01", "name": "juice"}, - {"id": 14228, "synset": "nectar.n.02", "name": "nectar"}, - {"id": 14229, "synset": "apple_juice.n.01", "name": "apple_juice"}, - {"id": 14230, "synset": "cranberry_juice.n.01", "name": "cranberry_juice"}, - {"id": 14231, "synset": "grape_juice.n.01", "name": "grape_juice"}, - {"id": 14232, "synset": "must.n.02", "name": "must"}, - {"id": 14233, "synset": "grapefruit_juice.n.01", "name": "grapefruit_juice"}, - {"id": 14234, "synset": "frozen_orange_juice.n.01", "name": "frozen_orange_juice"}, - {"id": 14235, "synset": "pineapple_juice.n.01", "name": "pineapple_juice"}, - {"id": 14236, "synset": "lemon_juice.n.01", "name": "lemon_juice"}, - {"id": 14237, "synset": "lime_juice.n.01", "name": "lime_juice"}, - {"id": 14238, "synset": "papaya_juice.n.01", "name": "papaya_juice"}, - {"id": 14239, "synset": "tomato_juice.n.01", "name": "tomato_juice"}, - {"id": 14240, "synset": "carrot_juice.n.01", "name": "carrot_juice"}, - {"id": 14241, "synset": "v-8_juice.n.01", "name": "V-8_juice"}, - {"id": 14242, "synset": "koumiss.n.01", "name": "koumiss"}, - {"id": 14243, "synset": "fruit_drink.n.01", "name": "fruit_drink"}, - {"id": 14244, "synset": "limeade.n.01", "name": "limeade"}, - {"id": 14245, "synset": "orangeade.n.01", "name": "orangeade"}, - {"id": 14246, "synset": "malted_milk.n.02", "name": "malted_milk"}, - {"id": 14247, "synset": "mate.n.09", "name": "mate"}, - {"id": 14248, "synset": "mulled_wine.n.01", "name": "mulled_wine"}, - {"id": 14249, "synset": "negus.n.01", "name": "negus"}, - {"id": 14250, "synset": "soft_drink.n.01", "name": "soft_drink"}, - {"id": 14251, "synset": "birch_beer.n.01", "name": "birch_beer"}, - {"id": 14252, "synset": "bitter_lemon.n.01", "name": "bitter_lemon"}, - {"id": 14253, "synset": "cola.n.02", "name": "cola"}, - {"id": 14254, "synset": "cream_soda.n.01", "name": "cream_soda"}, - {"id": 14255, "synset": "egg_cream.n.01", "name": "egg_cream"}, - {"id": 14256, "synset": "ginger_ale.n.01", "name": "ginger_ale"}, - {"id": 14257, "synset": "orange_soda.n.01", "name": "orange_soda"}, - {"id": 14258, "synset": "phosphate.n.02", "name": "phosphate"}, - {"id": 14259, "synset": "coca_cola.n.01", "name": "Coca_Cola"}, - {"id": 14260, "synset": "pepsi.n.01", "name": "Pepsi"}, - {"id": 14261, "synset": "sarsaparilla.n.02", "name": "sarsaparilla"}, - {"id": 14262, "synset": "tonic.n.01", "name": "tonic"}, - {"id": 14263, "synset": "coffee_bean.n.01", "name": "coffee_bean"}, - {"id": 14264, "synset": "coffee.n.01", "name": "coffee"}, - {"id": 14265, "synset": "cafe_royale.n.01", "name": "cafe_royale"}, - {"id": 14266, "synset": "fruit_punch.n.01", "name": "fruit_punch"}, - {"id": 14267, "synset": "milk_punch.n.01", "name": "milk_punch"}, - {"id": 14268, "synset": "mimosa.n.03", "name": "mimosa"}, - {"id": 14269, "synset": "pina_colada.n.01", "name": "pina_colada"}, - {"id": 14270, "synset": "punch.n.02", "name": "punch"}, - {"id": 14271, "synset": "cup.n.06", "name": "cup"}, - {"id": 14272, "synset": "champagne_cup.n.01", "name": "champagne_cup"}, - {"id": 14273, "synset": "claret_cup.n.01", "name": "claret_cup"}, - {"id": 14274, "synset": "wassail.n.01", "name": "wassail"}, - {"id": 14275, "synset": "planter's_punch.n.01", "name": "planter's_punch"}, - {"id": 14276, "synset": "white_russian.n.02", "name": "White_Russian"}, - {"id": 14277, "synset": "fish_house_punch.n.01", "name": "fish_house_punch"}, - {"id": 14278, "synset": "may_wine.n.01", "name": "May_wine"}, - {"id": 14279, "synset": "eggnog.n.01", "name": "eggnog"}, - {"id": 14280, "synset": "cassiri.n.01", "name": "cassiri"}, - {"id": 14281, "synset": "spruce_beer.n.01", "name": "spruce_beer"}, - {"id": 14282, "synset": "rickey.n.01", "name": "rickey"}, - {"id": 14283, "synset": "gin_rickey.n.01", "name": "gin_rickey"}, - {"id": 14284, "synset": "tea.n.05", "name": "tea"}, - {"id": 14285, "synset": "tea.n.01", "name": "tea"}, - {"id": 14286, "synset": "tea-like_drink.n.01", "name": "tea-like_drink"}, - {"id": 14287, "synset": "cambric_tea.n.01", "name": "cambric_tea"}, - {"id": 14288, "synset": "cuppa.n.01", "name": "cuppa"}, - {"id": 14289, "synset": "herb_tea.n.01", "name": "herb_tea"}, - {"id": 14290, "synset": "tisane.n.01", "name": "tisane"}, - {"id": 14291, "synset": "camomile_tea.n.01", "name": "camomile_tea"}, - {"id": 14292, "synset": "ice_tea.n.01", "name": "ice_tea"}, - {"id": 14293, "synset": "sun_tea.n.01", "name": "sun_tea"}, - {"id": 14294, "synset": "black_tea.n.01", "name": "black_tea"}, - {"id": 14295, "synset": "congou.n.01", "name": "congou"}, - {"id": 14296, "synset": "darjeeling.n.01", "name": "Darjeeling"}, - {"id": 14297, "synset": "orange_pekoe.n.01", "name": "orange_pekoe"}, - {"id": 14298, "synset": "souchong.n.01", "name": "souchong"}, - {"id": 14299, "synset": "green_tea.n.01", "name": "green_tea"}, - {"id": 14300, "synset": "hyson.n.01", "name": "hyson"}, - {"id": 14301, "synset": "oolong.n.01", "name": "oolong"}, - {"id": 14302, "synset": "water.n.06", "name": "water"}, - {"id": 14303, "synset": "bottled_water.n.01", "name": "bottled_water"}, - {"id": 14304, "synset": "branch_water.n.01", "name": "branch_water"}, - {"id": 14305, "synset": "spring_water.n.02", "name": "spring_water"}, - {"id": 14306, "synset": "sugar_water.n.01", "name": "sugar_water"}, - {"id": 14307, "synset": "drinking_water.n.01", "name": "drinking_water"}, - {"id": 14308, "synset": "ice_water.n.01", "name": "ice_water"}, - {"id": 14309, "synset": "soda_water.n.01", "name": "soda_water"}, - {"id": 14310, "synset": "mineral_water.n.01", "name": "mineral_water"}, - {"id": 14311, "synset": "seltzer.n.01", "name": "seltzer"}, - {"id": 14312, "synset": "vichy_water.n.01", "name": "Vichy_water"}, - {"id": 14313, "synset": "perishable.n.01", "name": "perishable"}, - {"id": 14314, "synset": "couscous.n.01", "name": "couscous"}, - {"id": 14315, "synset": "ramekin.n.01", "name": "ramekin"}, - {"id": 14316, "synset": "multivitamin.n.01", "name": "multivitamin"}, - {"id": 14317, "synset": "vitamin_pill.n.01", "name": "vitamin_pill"}, - {"id": 14318, "synset": "soul_food.n.01", "name": "soul_food"}, - {"id": 14319, "synset": "mold.n.06", "name": "mold"}, - {"id": 14320, "synset": "people.n.01", "name": "people"}, - {"id": 14321, "synset": "collection.n.01", "name": "collection"}, - {"id": 14322, "synset": "book.n.07", "name": "book"}, - {"id": 14323, "synset": "library.n.02", "name": "library"}, - {"id": 14324, "synset": "baseball_club.n.01", "name": "baseball_club"}, - {"id": 14325, "synset": "crowd.n.01", "name": "crowd"}, - {"id": 14326, "synset": "class.n.02", "name": "class"}, - {"id": 14327, "synset": "core.n.01", "name": "core"}, - {"id": 14328, "synset": "concert_band.n.01", "name": "concert_band"}, - {"id": 14329, "synset": "dance.n.02", "name": "dance"}, - {"id": 14330, "synset": "wedding.n.03", "name": "wedding"}, - {"id": 14331, "synset": "chain.n.01", "name": "chain"}, - {"id": 14332, "synset": "power_breakfast.n.01", "name": "power_breakfast"}, - {"id": 14333, "synset": "aerie.n.02", "name": "aerie"}, - {"id": 14334, "synset": "agora.n.02", "name": "agora"}, - {"id": 14335, "synset": "amusement_park.n.01", "name": "amusement_park"}, - {"id": 14336, "synset": "aphelion.n.01", "name": "aphelion"}, - {"id": 14337, "synset": "apron.n.02", "name": "apron"}, - {"id": 14338, "synset": "interplanetary_space.n.01", "name": "interplanetary_space"}, - {"id": 14339, "synset": "interstellar_space.n.01", "name": "interstellar_space"}, - {"id": 14340, "synset": "intergalactic_space.n.01", "name": "intergalactic_space"}, - {"id": 14341, "synset": "bush.n.02", "name": "bush"}, - {"id": 14342, "synset": "semidesert.n.01", "name": "semidesert"}, - {"id": 14343, "synset": "beam-ends.n.01", "name": "beam-ends"}, - {"id": 14344, "synset": "bridgehead.n.02", "name": "bridgehead"}, - {"id": 14345, "synset": "bus_stop.n.01", "name": "bus_stop"}, - {"id": 14346, "synset": "campsite.n.01", "name": "campsite"}, - {"id": 14347, "synset": "detention_basin.n.01", "name": "detention_basin"}, - {"id": 14348, "synset": "cemetery.n.01", "name": "cemetery"}, - {"id": 14349, "synset": "trichion.n.01", "name": "trichion"}, - {"id": 14350, "synset": "city.n.01", "name": "city"}, - {"id": 14351, "synset": "business_district.n.01", "name": "business_district"}, - {"id": 14352, "synset": "outskirts.n.01", "name": "outskirts"}, - {"id": 14353, "synset": "borough.n.01", "name": "borough"}, - {"id": 14354, "synset": "cow_pasture.n.01", "name": "cow_pasture"}, - {"id": 14355, "synset": "crest.n.01", "name": "crest"}, - {"id": 14356, "synset": "eparchy.n.02", "name": "eparchy"}, - {"id": 14357, "synset": "suburb.n.01", "name": "suburb"}, - {"id": 14358, "synset": "stockbroker_belt.n.01", "name": "stockbroker_belt"}, - {"id": 14359, "synset": "crawlspace.n.01", "name": "crawlspace"}, - {"id": 14360, "synset": "sheikdom.n.01", "name": "sheikdom"}, - {"id": 14361, "synset": "residence.n.01", "name": "residence"}, - {"id": 14362, "synset": "domicile.n.01", "name": "domicile"}, - {"id": 14363, "synset": "dude_ranch.n.01", "name": "dude_ranch"}, - {"id": 14364, "synset": "farmland.n.01", "name": "farmland"}, - {"id": 14365, "synset": "midfield.n.01", "name": "midfield"}, - {"id": 14366, "synset": "firebreak.n.01", "name": "firebreak"}, - {"id": 14367, "synset": "flea_market.n.01", "name": "flea_market"}, - {"id": 14368, "synset": "battlefront.n.01", "name": "battlefront"}, - {"id": 14369, "synset": "garbage_heap.n.01", "name": "garbage_heap"}, - {"id": 14370, "synset": "benthos.n.01", "name": "benthos"}, - {"id": 14371, "synset": "goldfield.n.01", "name": "goldfield"}, - {"id": 14372, "synset": "grainfield.n.01", "name": "grainfield"}, - {"id": 14373, "synset": "half-mast.n.01", "name": "half-mast"}, - {"id": 14374, "synset": "hemline.n.01", "name": "hemline"}, - {"id": 14375, "synset": "heronry.n.01", "name": "heronry"}, - {"id": 14376, "synset": "hipline.n.02", "name": "hipline"}, - {"id": 14377, "synset": "hipline.n.01", "name": "hipline"}, - {"id": 14378, "synset": "hole-in-the-wall.n.01", "name": "hole-in-the-wall"}, - {"id": 14379, "synset": "junkyard.n.01", "name": "junkyard"}, - {"id": 14380, "synset": "isoclinic_line.n.01", "name": "isoclinic_line"}, - {"id": 14381, "synset": "littoral.n.01", "name": "littoral"}, - {"id": 14382, "synset": "magnetic_pole.n.01", "name": "magnetic_pole"}, - {"id": 14383, "synset": "grassland.n.01", "name": "grassland"}, - {"id": 14384, "synset": "mecca.n.02", "name": "mecca"}, - {"id": 14385, "synset": "observer's_meridian.n.01", "name": "observer's_meridian"}, - {"id": 14386, "synset": "prime_meridian.n.01", "name": "prime_meridian"}, - {"id": 14387, "synset": "nombril.n.01", "name": "nombril"}, - {"id": 14388, "synset": "no-parking_zone.n.01", "name": "no-parking_zone"}, - {"id": 14389, "synset": "outdoors.n.01", "name": "outdoors"}, - {"id": 14390, "synset": "fairground.n.01", "name": "fairground"}, - {"id": 14391, "synset": "pasture.n.01", "name": "pasture"}, - {"id": 14392, "synset": "perihelion.n.01", "name": "perihelion"}, - {"id": 14393, "synset": "periselene.n.01", "name": "periselene"}, - {"id": 14394, "synset": "locus_of_infection.n.01", "name": "locus_of_infection"}, - {"id": 14395, "synset": "kasbah.n.01", "name": "kasbah"}, - {"id": 14396, "synset": "waterfront.n.01", "name": "waterfront"}, - {"id": 14397, "synset": "resort.n.01", "name": "resort"}, - {"id": 14398, "synset": "resort_area.n.01", "name": "resort_area"}, - {"id": 14399, "synset": "rough.n.01", "name": "rough"}, - {"id": 14400, "synset": "ashram.n.02", "name": "ashram"}, - {"id": 14401, "synset": "harborage.n.01", "name": "harborage"}, - {"id": 14402, "synset": "scrubland.n.01", "name": "scrubland"}, - {"id": 14403, "synset": "weald.n.01", "name": "weald"}, - {"id": 14404, "synset": "wold.n.01", "name": "wold"}, - {"id": 14405, "synset": "schoolyard.n.01", "name": "schoolyard"}, - {"id": 14406, "synset": "showplace.n.01", "name": "showplace"}, - {"id": 14407, "synset": "bedside.n.01", "name": "bedside"}, - {"id": 14408, "synset": "sideline.n.01", "name": "sideline"}, - {"id": 14409, "synset": "ski_resort.n.01", "name": "ski_resort"}, - {"id": 14410, "synset": "soil_horizon.n.01", "name": "soil_horizon"}, - {"id": 14411, "synset": "geological_horizon.n.01", "name": "geological_horizon"}, - {"id": 14412, "synset": "coal_seam.n.01", "name": "coal_seam"}, - {"id": 14413, "synset": "coalface.n.01", "name": "coalface"}, - {"id": 14414, "synset": "field.n.14", "name": "field"}, - {"id": 14415, "synset": "oilfield.n.01", "name": "oilfield"}, - {"id": 14416, "synset": "temperate_zone.n.01", "name": "Temperate_Zone"}, - {"id": 14417, "synset": "terreplein.n.01", "name": "terreplein"}, - {"id": 14418, "synset": "three-mile_limit.n.01", "name": "three-mile_limit"}, - {"id": 14419, "synset": "desktop.n.01", "name": "desktop"}, - {"id": 14420, "synset": "top.n.01", "name": "top"}, - {"id": 14421, "synset": "kampong.n.01", "name": "kampong"}, - {"id": 14422, "synset": "subtropics.n.01", "name": "subtropics"}, - {"id": 14423, "synset": "barrio.n.02", "name": "barrio"}, - {"id": 14424, "synset": "veld.n.01", "name": "veld"}, - {"id": 14425, "synset": "vertex.n.02", "name": "vertex"}, - {"id": 14426, "synset": "waterline.n.01", "name": "waterline"}, - {"id": 14427, "synset": "high-water_mark.n.01", "name": "high-water_mark"}, - {"id": 14428, "synset": "low-water_mark.n.02", "name": "low-water_mark"}, - {"id": 14429, "synset": "continental_divide.n.01", "name": "continental_divide"}, - {"id": 14430, "synset": "zodiac.n.01", "name": "zodiac"}, - {"id": 14431, "synset": "aegean_island.n.01", "name": "Aegean_island"}, - {"id": 14432, "synset": "sultanate.n.01", "name": "sultanate"}, - {"id": 14433, "synset": "swiss_canton.n.01", "name": "Swiss_canton"}, - {"id": 14434, "synset": "abyssal_zone.n.01", "name": "abyssal_zone"}, - {"id": 14435, "synset": "aerie.n.01", "name": "aerie"}, - {"id": 14436, "synset": "air_bubble.n.01", "name": "air_bubble"}, - {"id": 14437, "synset": "alluvial_flat.n.01", "name": "alluvial_flat"}, - {"id": 14438, "synset": "alp.n.01", "name": "alp"}, - {"id": 14439, "synset": "alpine_glacier.n.01", "name": "Alpine_glacier"}, - {"id": 14440, "synset": "anthill.n.01", "name": "anthill"}, - {"id": 14441, "synset": "aquifer.n.01", "name": "aquifer"}, - {"id": 14442, "synset": "archipelago.n.01", "name": "archipelago"}, - {"id": 14443, "synset": "arete.n.01", "name": "arete"}, - {"id": 14444, "synset": "arroyo.n.01", "name": "arroyo"}, - {"id": 14445, "synset": "ascent.n.01", "name": "ascent"}, - {"id": 14446, "synset": "asterism.n.02", "name": "asterism"}, - {"id": 14447, "synset": "asthenosphere.n.01", "name": "asthenosphere"}, - {"id": 14448, "synset": "atoll.n.01", "name": "atoll"}, - {"id": 14449, "synset": "bank.n.03", "name": "bank"}, - {"id": 14450, "synset": "bank.n.01", "name": "bank"}, - {"id": 14451, "synset": "bar.n.08", "name": "bar"}, - {"id": 14452, "synset": "barbecue_pit.n.01", "name": "barbecue_pit"}, - {"id": 14453, "synset": "barrier_reef.n.01", "name": "barrier_reef"}, - {"id": 14454, "synset": "baryon.n.01", "name": "baryon"}, - {"id": 14455, "synset": "basin.n.03", "name": "basin"}, - {"id": 14456, "synset": "beach.n.01", "name": "beach"}, - {"id": 14457, "synset": "honeycomb.n.01", "name": "honeycomb"}, - {"id": 14458, "synset": "belay.n.01", "name": "belay"}, - {"id": 14459, "synset": "ben.n.01", "name": "ben"}, - {"id": 14460, "synset": "berm.n.01", "name": "berm"}, - {"id": 14461, "synset": "bladder_stone.n.01", "name": "bladder_stone"}, - {"id": 14462, "synset": "bluff.n.01", "name": "bluff"}, - {"id": 14463, "synset": "borrow_pit.n.01", "name": "borrow_pit"}, - {"id": 14464, "synset": "brae.n.01", "name": "brae"}, - {"id": 14465, "synset": "bubble.n.01", "name": "bubble"}, - {"id": 14466, "synset": "burrow.n.01", "name": "burrow"}, - {"id": 14467, "synset": "butte.n.01", "name": "butte"}, - {"id": 14468, "synset": "caldera.n.01", "name": "caldera"}, - {"id": 14469, "synset": "canyon.n.01", "name": "canyon"}, - {"id": 14470, "synset": "canyonside.n.01", "name": "canyonside"}, - {"id": 14471, "synset": "cave.n.01", "name": "cave"}, - {"id": 14472, "synset": "cavern.n.02", "name": "cavern"}, - {"id": 14473, "synset": "chasm.n.01", "name": "chasm"}, - {"id": 14474, "synset": "cirque.n.01", "name": "cirque"}, - {"id": 14475, "synset": "cliff.n.01", "name": "cliff"}, - {"id": 14476, "synset": "cloud.n.02", "name": "cloud"}, - {"id": 14477, "synset": "coast.n.02", "name": "coast"}, - {"id": 14478, "synset": "coastland.n.01", "name": "coastland"}, - {"id": 14479, "synset": "col.n.01", "name": "col"}, - {"id": 14480, "synset": "collector.n.03", "name": "collector"}, - {"id": 14481, "synset": "comet.n.01", "name": "comet"}, - {"id": 14482, "synset": "continental_glacier.n.01", "name": "continental_glacier"}, - {"id": 14483, "synset": "coral_reef.n.01", "name": "coral_reef"}, - {"id": 14484, "synset": "cove.n.02", "name": "cove"}, - {"id": 14485, "synset": "crag.n.01", "name": "crag"}, - {"id": 14486, "synset": "crater.n.03", "name": "crater"}, - {"id": 14487, "synset": "cultivated_land.n.01", "name": "cultivated_land"}, - {"id": 14488, "synset": "dale.n.01", "name": "dale"}, - {"id": 14489, "synset": "defile.n.01", "name": "defile"}, - {"id": 14490, "synset": "delta.n.01", "name": "delta"}, - {"id": 14491, "synset": "descent.n.05", "name": "descent"}, - {"id": 14492, "synset": "diapir.n.01", "name": "diapir"}, - {"id": 14493, "synset": "divot.n.02", "name": "divot"}, - {"id": 14494, "synset": "divot.n.01", "name": "divot"}, - {"id": 14495, "synset": "down.n.04", "name": "down"}, - {"id": 14496, "synset": "downhill.n.01", "name": "downhill"}, - {"id": 14497, "synset": "draw.n.01", "name": "draw"}, - {"id": 14498, "synset": "drey.n.01", "name": "drey"}, - {"id": 14499, "synset": "drumlin.n.01", "name": "drumlin"}, - {"id": 14500, "synset": "dune.n.01", "name": "dune"}, - {"id": 14501, "synset": "escarpment.n.01", "name": "escarpment"}, - {"id": 14502, "synset": "esker.n.01", "name": "esker"}, - {"id": 14503, "synset": "fireball.n.03", "name": "fireball"}, - {"id": 14504, "synset": "flare_star.n.01", "name": "flare_star"}, - {"id": 14505, "synset": "floor.n.04", "name": "floor"}, - {"id": 14506, "synset": "fomite.n.01", "name": "fomite"}, - {"id": 14507, "synset": "foothill.n.01", "name": "foothill"}, - {"id": 14508, "synset": "footwall.n.01", "name": "footwall"}, - {"id": 14509, "synset": "foreland.n.02", "name": "foreland"}, - {"id": 14510, "synset": "foreshore.n.01", "name": "foreshore"}, - {"id": 14511, "synset": "gauge_boson.n.01", "name": "gauge_boson"}, - {"id": 14512, "synset": "geological_formation.n.01", "name": "geological_formation"}, - {"id": 14513, "synset": "geyser.n.01", "name": "geyser"}, - {"id": 14514, "synset": "glacier.n.01", "name": "glacier"}, - {"id": 14515, "synset": "glen.n.01", "name": "glen"}, - {"id": 14516, "synset": "gopher_hole.n.01", "name": "gopher_hole"}, - {"id": 14517, "synset": "gorge.n.01", "name": "gorge"}, - {"id": 14518, "synset": "grotto.n.01", "name": "grotto"}, - {"id": 14519, "synset": "growler.n.02", "name": "growler"}, - {"id": 14520, "synset": "gulch.n.01", "name": "gulch"}, - {"id": 14521, "synset": "gully.n.01", "name": "gully"}, - {"id": 14522, "synset": "hail.n.02", "name": "hail"}, - {"id": 14523, "synset": "highland.n.01", "name": "highland"}, - {"id": 14524, "synset": "hill.n.01", "name": "hill"}, - {"id": 14525, "synset": "hillside.n.01", "name": "hillside"}, - {"id": 14526, "synset": "hole.n.05", "name": "hole"}, - {"id": 14527, "synset": "hollow.n.02", "name": "hollow"}, - {"id": 14528, "synset": "hot_spring.n.01", "name": "hot_spring"}, - {"id": 14529, "synset": "iceberg.n.01", "name": "iceberg"}, - {"id": 14530, "synset": "icecap.n.01", "name": "icecap"}, - {"id": 14531, "synset": "ice_field.n.01", "name": "ice_field"}, - {"id": 14532, "synset": "ice_floe.n.01", "name": "ice_floe"}, - {"id": 14533, "synset": "ice_mass.n.01", "name": "ice_mass"}, - {"id": 14534, "synset": "inclined_fault.n.01", "name": "inclined_fault"}, - {"id": 14535, "synset": "ion.n.01", "name": "ion"}, - {"id": 14536, "synset": "isthmus.n.01", "name": "isthmus"}, - {"id": 14537, "synset": "kidney_stone.n.01", "name": "kidney_stone"}, - {"id": 14538, "synset": "knoll.n.01", "name": "knoll"}, - {"id": 14539, "synset": "kopje.n.01", "name": "kopje"}, - {"id": 14540, "synset": "kuiper_belt.n.01", "name": "Kuiper_belt"}, - {"id": 14541, "synset": "lake_bed.n.01", "name": "lake_bed"}, - {"id": 14542, "synset": "lakefront.n.01", "name": "lakefront"}, - {"id": 14543, "synset": "lakeside.n.01", "name": "lakeside"}, - {"id": 14544, "synset": "landfall.n.01", "name": "landfall"}, - {"id": 14545, "synset": "landfill.n.01", "name": "landfill"}, - {"id": 14546, "synset": "lather.n.04", "name": "lather"}, - {"id": 14547, "synset": "leak.n.01", "name": "leak"}, - {"id": 14548, "synset": "ledge.n.01", "name": "ledge"}, - {"id": 14549, "synset": "lepton.n.02", "name": "lepton"}, - {"id": 14550, "synset": "lithosphere.n.01", "name": "lithosphere"}, - {"id": 14551, "synset": "lowland.n.01", "name": "lowland"}, - {"id": 14552, "synset": "lunar_crater.n.01", "name": "lunar_crater"}, - {"id": 14553, "synset": "maar.n.01", "name": "maar"}, - {"id": 14554, "synset": "massif.n.01", "name": "massif"}, - {"id": 14555, "synset": "meander.n.01", "name": "meander"}, - {"id": 14556, "synset": "mesa.n.01", "name": "mesa"}, - {"id": 14557, "synset": "meteorite.n.01", "name": "meteorite"}, - {"id": 14558, "synset": "microfossil.n.01", "name": "microfossil"}, - {"id": 14559, "synset": "midstream.n.01", "name": "midstream"}, - {"id": 14560, "synset": "molehill.n.01", "name": "molehill"}, - {"id": 14561, "synset": "monocline.n.01", "name": "monocline"}, - {"id": 14562, "synset": "mountain.n.01", "name": "mountain"}, - {"id": 14563, "synset": "mountainside.n.01", "name": "mountainside"}, - {"id": 14564, "synset": "mouth.n.04", "name": "mouth"}, - {"id": 14565, "synset": "mull.n.01", "name": "mull"}, - {"id": 14566, "synset": "natural_depression.n.01", "name": "natural_depression"}, - {"id": 14567, "synset": "natural_elevation.n.01", "name": "natural_elevation"}, - {"id": 14568, "synset": "nullah.n.01", "name": "nullah"}, - {"id": 14569, "synset": "ocean.n.01", "name": "ocean"}, - {"id": 14570, "synset": "ocean_floor.n.01", "name": "ocean_floor"}, - {"id": 14571, "synset": "oceanfront.n.01", "name": "oceanfront"}, - {"id": 14572, "synset": "outcrop.n.01", "name": "outcrop"}, - {"id": 14573, "synset": "oxbow.n.01", "name": "oxbow"}, - {"id": 14574, "synset": "pallasite.n.01", "name": "pallasite"}, - {"id": 14575, "synset": "perforation.n.02", "name": "perforation"}, - {"id": 14576, "synset": "photosphere.n.01", "name": "photosphere"}, - {"id": 14577, "synset": "piedmont.n.02", "name": "piedmont"}, - {"id": 14578, "synset": "piedmont_glacier.n.01", "name": "Piedmont_glacier"}, - {"id": 14579, "synset": "pinetum.n.01", "name": "pinetum"}, - {"id": 14580, "synset": "plage.n.01", "name": "plage"}, - {"id": 14581, "synset": "plain.n.01", "name": "plain"}, - {"id": 14582, "synset": "point.n.11", "name": "point"}, - {"id": 14583, "synset": "polar_glacier.n.01", "name": "polar_glacier"}, - {"id": 14584, "synset": "pothole.n.01", "name": "pothole"}, - {"id": 14585, "synset": "precipice.n.01", "name": "precipice"}, - {"id": 14586, "synset": "promontory.n.01", "name": "promontory"}, - {"id": 14587, "synset": "ptyalith.n.01", "name": "ptyalith"}, - {"id": 14588, "synset": "pulsar.n.01", "name": "pulsar"}, - {"id": 14589, "synset": "quicksand.n.02", "name": "quicksand"}, - {"id": 14590, "synset": "rabbit_burrow.n.01", "name": "rabbit_burrow"}, - {"id": 14591, "synset": "radiator.n.01", "name": "radiator"}, - {"id": 14592, "synset": "rainbow.n.01", "name": "rainbow"}, - {"id": 14593, "synset": "range.n.04", "name": "range"}, - {"id": 14594, "synset": "rangeland.n.01", "name": "rangeland"}, - {"id": 14595, "synset": "ravine.n.01", "name": "ravine"}, - {"id": 14596, "synset": "reef.n.01", "name": "reef"}, - {"id": 14597, "synset": "ridge.n.01", "name": "ridge"}, - {"id": 14598, "synset": "ridge.n.04", "name": "ridge"}, - {"id": 14599, "synset": "rift_valley.n.01", "name": "rift_valley"}, - {"id": 14600, "synset": "riparian_forest.n.01", "name": "riparian_forest"}, - {"id": 14601, "synset": "ripple_mark.n.01", "name": "ripple_mark"}, - {"id": 14602, "synset": "riverbank.n.01", "name": "riverbank"}, - {"id": 14603, "synset": "riverbed.n.01", "name": "riverbed"}, - {"id": 14604, "synset": "rock.n.01", "name": "rock"}, - {"id": 14605, "synset": "roof.n.03", "name": "roof"}, - {"id": 14606, "synset": "saltpan.n.01", "name": "saltpan"}, - {"id": 14607, "synset": "sandbank.n.01", "name": "sandbank"}, - {"id": 14608, "synset": "sandbar.n.01", "name": "sandbar"}, - {"id": 14609, "synset": "sandpit.n.01", "name": "sandpit"}, - {"id": 14610, "synset": "sanitary_landfill.n.01", "name": "sanitary_landfill"}, - {"id": 14611, "synset": "sawpit.n.01", "name": "sawpit"}, - {"id": 14612, "synset": "scablands.n.01", "name": "scablands"}, - {"id": 14613, "synset": "seashore.n.01", "name": "seashore"}, - {"id": 14614, "synset": "seaside.n.01", "name": "seaside"}, - {"id": 14615, "synset": "seif_dune.n.01", "name": "seif_dune"}, - {"id": 14616, "synset": "shell.n.06", "name": "shell"}, - {"id": 14617, "synset": "shiner.n.02", "name": "shiner"}, - {"id": 14618, "synset": "shoal.n.01", "name": "shoal"}, - {"id": 14619, "synset": "shore.n.01", "name": "shore"}, - {"id": 14620, "synset": "shoreline.n.01", "name": "shoreline"}, - {"id": 14621, "synset": "sinkhole.n.01", "name": "sinkhole"}, - {"id": 14622, "synset": "ski_slope.n.01", "name": "ski_slope"}, - {"id": 14623, "synset": "sky.n.01", "name": "sky"}, - {"id": 14624, "synset": "slope.n.01", "name": "slope"}, - {"id": 14625, "synset": "snowcap.n.01", "name": "snowcap"}, - {"id": 14626, "synset": "snowdrift.n.01", "name": "snowdrift"}, - {"id": 14627, "synset": "snowfield.n.01", "name": "snowfield"}, - {"id": 14628, "synset": "soapsuds.n.01", "name": "soapsuds"}, - {"id": 14629, "synset": "spit.n.01", "name": "spit"}, - {"id": 14630, "synset": "spoor.n.01", "name": "spoor"}, - {"id": 14631, "synset": "spume.n.01", "name": "spume"}, - {"id": 14632, "synset": "star.n.03", "name": "star"}, - {"id": 14633, "synset": "steep.n.01", "name": "steep"}, - {"id": 14634, "synset": "steppe.n.01", "name": "steppe"}, - {"id": 14635, "synset": "strand.n.05", "name": "strand"}, - {"id": 14636, "synset": "streambed.n.01", "name": "streambed"}, - {"id": 14637, "synset": "sun.n.01", "name": "sun"}, - {"id": 14638, "synset": "supernova.n.01", "name": "supernova"}, - {"id": 14639, "synset": "swale.n.01", "name": "swale"}, - {"id": 14640, "synset": "swamp.n.01", "name": "swamp"}, - {"id": 14641, "synset": "swell.n.02", "name": "swell"}, - {"id": 14642, "synset": "tableland.n.01", "name": "tableland"}, - {"id": 14643, "synset": "talus.n.01", "name": "talus"}, - {"id": 14644, "synset": "tangle.n.01", "name": "tangle"}, - {"id": 14645, "synset": "tar_pit.n.01", "name": "tar_pit"}, - {"id": 14646, "synset": "terrace.n.02", "name": "terrace"}, - {"id": 14647, "synset": "tidal_basin.n.01", "name": "tidal_basin"}, - {"id": 14648, "synset": "tideland.n.01", "name": "tideland"}, - {"id": 14649, "synset": "tor.n.02", "name": "tor"}, - {"id": 14650, "synset": "tor.n.01", "name": "tor"}, - {"id": 14651, "synset": "trapezium.n.02", "name": "Trapezium"}, - {"id": 14652, "synset": "troposphere.n.01", "name": "troposphere"}, - {"id": 14653, "synset": "tundra.n.01", "name": "tundra"}, - {"id": 14654, "synset": "twinkler.n.01", "name": "twinkler"}, - {"id": 14655, "synset": "uphill.n.01", "name": "uphill"}, - {"id": 14656, "synset": "urolith.n.01", "name": "urolith"}, - {"id": 14657, "synset": "valley.n.01", "name": "valley"}, - { - "id": 14658, - "synset": "vehicle-borne_transmission.n.01", - "name": "vehicle-borne_transmission", - }, - {"id": 14659, "synset": "vein.n.04", "name": "vein"}, - {"id": 14660, "synset": "volcanic_crater.n.01", "name": "volcanic_crater"}, - {"id": 14661, "synset": "volcano.n.02", "name": "volcano"}, - {"id": 14662, "synset": "wadi.n.01", "name": "wadi"}, - {"id": 14663, "synset": "wall.n.05", "name": "wall"}, - {"id": 14664, "synset": "warren.n.03", "name": "warren"}, - {"id": 14665, "synset": "wasp's_nest.n.01", "name": "wasp's_nest"}, - {"id": 14666, "synset": "watercourse.n.01", "name": "watercourse"}, - {"id": 14667, "synset": "waterside.n.01", "name": "waterside"}, - {"id": 14668, "synset": "water_table.n.01", "name": "water_table"}, - {"id": 14669, "synset": "whinstone.n.01", "name": "whinstone"}, - {"id": 14670, "synset": "wormcast.n.02", "name": "wormcast"}, - {"id": 14671, "synset": "xenolith.n.01", "name": "xenolith"}, - {"id": 14672, "synset": "circe.n.01", "name": "Circe"}, - {"id": 14673, "synset": "gryphon.n.01", "name": "gryphon"}, - {"id": 14674, "synset": "spiritual_leader.n.01", "name": "spiritual_leader"}, - {"id": 14675, "synset": "messiah.n.01", "name": "messiah"}, - {"id": 14676, "synset": "rhea_silvia.n.01", "name": "Rhea_Silvia"}, - {"id": 14677, "synset": "number_one.n.01", "name": "number_one"}, - {"id": 14678, "synset": "adventurer.n.01", "name": "adventurer"}, - {"id": 14679, "synset": "anomaly.n.02", "name": "anomaly"}, - {"id": 14680, "synset": "appointee.n.02", "name": "appointee"}, - {"id": 14681, "synset": "argonaut.n.01", "name": "argonaut"}, - {"id": 14682, "synset": "ashkenazi.n.01", "name": "Ashkenazi"}, - {"id": 14683, "synset": "benefactor.n.01", "name": "benefactor"}, - {"id": 14684, "synset": "color-blind_person.n.01", "name": "color-blind_person"}, - {"id": 14685, "synset": "commoner.n.01", "name": "commoner"}, - {"id": 14686, "synset": "conservator.n.02", "name": "conservator"}, - {"id": 14687, "synset": "contrarian.n.01", "name": "contrarian"}, - {"id": 14688, "synset": "contadino.n.01", "name": "contadino"}, - {"id": 14689, "synset": "contestant.n.01", "name": "contestant"}, - {"id": 14690, "synset": "cosigner.n.01", "name": "cosigner"}, - {"id": 14691, "synset": "discussant.n.01", "name": "discussant"}, - {"id": 14692, "synset": "enologist.n.01", "name": "enologist"}, - {"id": 14693, "synset": "entertainer.n.01", "name": "entertainer"}, - {"id": 14694, "synset": "eulogist.n.01", "name": "eulogist"}, - {"id": 14695, "synset": "ex-gambler.n.01", "name": "ex-gambler"}, - {"id": 14696, "synset": "experimenter.n.01", "name": "experimenter"}, - {"id": 14697, "synset": "experimenter.n.02", "name": "experimenter"}, - {"id": 14698, "synset": "exponent.n.02", "name": "exponent"}, - {"id": 14699, "synset": "ex-president.n.01", "name": "ex-president"}, - {"id": 14700, "synset": "face.n.05", "name": "face"}, - {"id": 14701, "synset": "female.n.02", "name": "female"}, - {"id": 14702, "synset": "finisher.n.04", "name": "finisher"}, - {"id": 14703, "synset": "inhabitant.n.01", "name": "inhabitant"}, - {"id": 14704, "synset": "native.n.01", "name": "native"}, - {"id": 14705, "synset": "native.n.02", "name": "native"}, - {"id": 14706, "synset": "juvenile.n.01", "name": "juvenile"}, - {"id": 14707, "synset": "lover.n.01", "name": "lover"}, - {"id": 14708, "synset": "male.n.02", "name": "male"}, - {"id": 14709, "synset": "mediator.n.01", "name": "mediator"}, - {"id": 14710, "synset": "mediatrix.n.01", "name": "mediatrix"}, - {"id": 14711, "synset": "national.n.01", "name": "national"}, - {"id": 14712, "synset": "peer.n.01", "name": "peer"}, - {"id": 14713, "synset": "prize_winner.n.01", "name": "prize_winner"}, - {"id": 14714, "synset": "recipient.n.01", "name": "recipient"}, - {"id": 14715, "synset": "religionist.n.01", "name": "religionist"}, - {"id": 14716, "synset": "sensualist.n.01", "name": "sensualist"}, - {"id": 14717, "synset": "traveler.n.01", "name": "traveler"}, - {"id": 14718, "synset": "unwelcome_person.n.01", "name": "unwelcome_person"}, - {"id": 14719, "synset": "unskilled_person.n.01", "name": "unskilled_person"}, - {"id": 14720, "synset": "worker.n.01", "name": "worker"}, - {"id": 14721, "synset": "wrongdoer.n.01", "name": "wrongdoer"}, - {"id": 14722, "synset": "black_african.n.01", "name": "Black_African"}, - {"id": 14723, "synset": "afrikaner.n.01", "name": "Afrikaner"}, - {"id": 14724, "synset": "aryan.n.01", "name": "Aryan"}, - {"id": 14725, "synset": "black.n.05", "name": "Black"}, - {"id": 14726, "synset": "black_woman.n.01", "name": "Black_woman"}, - {"id": 14727, "synset": "mulatto.n.01", "name": "mulatto"}, - {"id": 14728, "synset": "white.n.01", "name": "White"}, - {"id": 14729, "synset": "circassian.n.01", "name": "Circassian"}, - {"id": 14730, "synset": "semite.n.01", "name": "Semite"}, - {"id": 14731, "synset": "chaldean.n.02", "name": "Chaldean"}, - {"id": 14732, "synset": "elamite.n.01", "name": "Elamite"}, - {"id": 14733, "synset": "white_man.n.01", "name": "white_man"}, - {"id": 14734, "synset": "wasp.n.01", "name": "WASP"}, - {"id": 14735, "synset": "gook.n.02", "name": "gook"}, - {"id": 14736, "synset": "mongol.n.01", "name": "Mongol"}, - {"id": 14737, "synset": "tatar.n.01", "name": "Tatar"}, - {"id": 14738, "synset": "nahuatl.n.01", "name": "Nahuatl"}, - {"id": 14739, "synset": "aztec.n.01", "name": "Aztec"}, - {"id": 14740, "synset": "olmec.n.01", "name": "Olmec"}, - {"id": 14741, "synset": "biloxi.n.01", "name": "Biloxi"}, - {"id": 14742, "synset": "blackfoot.n.01", "name": "Blackfoot"}, - {"id": 14743, "synset": "brule.n.01", "name": "Brule"}, - {"id": 14744, "synset": "caddo.n.01", "name": "Caddo"}, - {"id": 14745, "synset": "cheyenne.n.03", "name": "Cheyenne"}, - {"id": 14746, "synset": "chickasaw.n.01", "name": "Chickasaw"}, - {"id": 14747, "synset": "cocopa.n.01", "name": "Cocopa"}, - {"id": 14748, "synset": "comanche.n.01", "name": "Comanche"}, - {"id": 14749, "synset": "creek.n.02", "name": "Creek"}, - {"id": 14750, "synset": "delaware.n.02", "name": "Delaware"}, - {"id": 14751, "synset": "diegueno.n.01", "name": "Diegueno"}, - {"id": 14752, "synset": "esselen.n.01", "name": "Esselen"}, - {"id": 14753, "synset": "eyeish.n.01", "name": "Eyeish"}, - {"id": 14754, "synset": "havasupai.n.01", "name": "Havasupai"}, - {"id": 14755, "synset": "hunkpapa.n.01", "name": "Hunkpapa"}, - {"id": 14756, "synset": "iowa.n.01", "name": "Iowa"}, - {"id": 14757, "synset": "kalapooia.n.01", "name": "Kalapooia"}, - {"id": 14758, "synset": "kamia.n.01", "name": "Kamia"}, - {"id": 14759, "synset": "kekchi.n.01", "name": "Kekchi"}, - {"id": 14760, "synset": "kichai.n.01", "name": "Kichai"}, - {"id": 14761, "synset": "kickapoo.n.01", "name": "Kickapoo"}, - {"id": 14762, "synset": "kiliwa.n.01", "name": "Kiliwa"}, - {"id": 14763, "synset": "malecite.n.01", "name": "Malecite"}, - {"id": 14764, "synset": "maricopa.n.01", "name": "Maricopa"}, - {"id": 14765, "synset": "mohican.n.01", "name": "Mohican"}, - {"id": 14766, "synset": "muskhogean.n.01", "name": "Muskhogean"}, - {"id": 14767, "synset": "navaho.n.01", "name": "Navaho"}, - {"id": 14768, "synset": "nootka.n.01", "name": "Nootka"}, - {"id": 14769, "synset": "oglala.n.01", "name": "Oglala"}, - {"id": 14770, "synset": "osage.n.01", "name": "Osage"}, - {"id": 14771, "synset": "oneida.n.01", "name": "Oneida"}, - {"id": 14772, "synset": "paiute.n.01", "name": "Paiute"}, - {"id": 14773, "synset": "passamaquody.n.01", "name": "Passamaquody"}, - {"id": 14774, "synset": "penobscot.n.01", "name": "Penobscot"}, - {"id": 14775, "synset": "penutian.n.02", "name": "Penutian"}, - {"id": 14776, "synset": "potawatomi.n.01", "name": "Potawatomi"}, - {"id": 14777, "synset": "powhatan.n.02", "name": "Powhatan"}, - {"id": 14778, "synset": "kachina.n.02", "name": "kachina"}, - {"id": 14779, "synset": "salish.n.02", "name": "Salish"}, - {"id": 14780, "synset": "shahaptian.n.01", "name": "Shahaptian"}, - {"id": 14781, "synset": "shasta.n.01", "name": "Shasta"}, - {"id": 14782, "synset": "shawnee.n.01", "name": "Shawnee"}, - {"id": 14783, "synset": "sihasapa.n.01", "name": "Sihasapa"}, - {"id": 14784, "synset": "teton.n.01", "name": "Teton"}, - {"id": 14785, "synset": "taracahitian.n.01", "name": "Taracahitian"}, - {"id": 14786, "synset": "tarahumara.n.01", "name": "Tarahumara"}, - {"id": 14787, "synset": "tuscarora.n.01", "name": "Tuscarora"}, - {"id": 14788, "synset": "tutelo.n.01", "name": "Tutelo"}, - {"id": 14789, "synset": "yana.n.01", "name": "Yana"}, - {"id": 14790, "synset": "yavapai.n.01", "name": "Yavapai"}, - {"id": 14791, "synset": "yokuts.n.02", "name": "Yokuts"}, - {"id": 14792, "synset": "yuma.n.01", "name": "Yuma"}, - {"id": 14793, "synset": "gadaba.n.01", "name": "Gadaba"}, - {"id": 14794, "synset": "kolam.n.01", "name": "Kolam"}, - {"id": 14795, "synset": "kui.n.01", "name": "Kui"}, - {"id": 14796, "synset": "toda.n.01", "name": "Toda"}, - {"id": 14797, "synset": "tulu.n.01", "name": "Tulu"}, - {"id": 14798, "synset": "gujarati.n.01", "name": "Gujarati"}, - {"id": 14799, "synset": "kashmiri.n.01", "name": "Kashmiri"}, - {"id": 14800, "synset": "punjabi.n.01", "name": "Punjabi"}, - {"id": 14801, "synset": "slav.n.01", "name": "Slav"}, - {"id": 14802, "synset": "anabaptist.n.01", "name": "Anabaptist"}, - {"id": 14803, "synset": "adventist.n.01", "name": "Adventist"}, - {"id": 14804, "synset": "gentile.n.03", "name": "gentile"}, - {"id": 14805, "synset": "gentile.n.02", "name": "gentile"}, - {"id": 14806, "synset": "catholic.n.01", "name": "Catholic"}, - {"id": 14807, "synset": "old_catholic.n.01", "name": "Old_Catholic"}, - {"id": 14808, "synset": "uniat.n.01", "name": "Uniat"}, - {"id": 14809, "synset": "copt.n.02", "name": "Copt"}, - {"id": 14810, "synset": "jewess.n.01", "name": "Jewess"}, - {"id": 14811, "synset": "jihadist.n.01", "name": "Jihadist"}, - {"id": 14812, "synset": "buddhist.n.01", "name": "Buddhist"}, - {"id": 14813, "synset": "zen_buddhist.n.01", "name": "Zen_Buddhist"}, - {"id": 14814, "synset": "mahayanist.n.01", "name": "Mahayanist"}, - {"id": 14815, "synset": "swami.n.01", "name": "swami"}, - {"id": 14816, "synset": "hare_krishna.n.01", "name": "Hare_Krishna"}, - {"id": 14817, "synset": "shintoist.n.01", "name": "Shintoist"}, - {"id": 14818, "synset": "eurafrican.n.01", "name": "Eurafrican"}, - {"id": 14819, "synset": "eurasian.n.01", "name": "Eurasian"}, - {"id": 14820, "synset": "gael.n.01", "name": "Gael"}, - {"id": 14821, "synset": "frank.n.01", "name": "Frank"}, - {"id": 14822, "synset": "afghan.n.02", "name": "Afghan"}, - {"id": 14823, "synset": "albanian.n.01", "name": "Albanian"}, - {"id": 14824, "synset": "algerian.n.01", "name": "Algerian"}, - {"id": 14825, "synset": "altaic.n.01", "name": "Altaic"}, - {"id": 14826, "synset": "andorran.n.01", "name": "Andorran"}, - {"id": 14827, "synset": "angolan.n.01", "name": "Angolan"}, - {"id": 14828, "synset": "anguillan.n.01", "name": "Anguillan"}, - {"id": 14829, "synset": "austrian.n.01", "name": "Austrian"}, - {"id": 14830, "synset": "bahamian.n.01", "name": "Bahamian"}, - {"id": 14831, "synset": "bahraini.n.01", "name": "Bahraini"}, - {"id": 14832, "synset": "basotho.n.01", "name": "Basotho"}, - {"id": 14833, "synset": "herero.n.01", "name": "Herero"}, - {"id": 14834, "synset": "luba.n.01", "name": "Luba"}, - {"id": 14835, "synset": "barbadian.n.01", "name": "Barbadian"}, - {"id": 14836, "synset": "bolivian.n.01", "name": "Bolivian"}, - {"id": 14837, "synset": "bornean.n.01", "name": "Bornean"}, - {"id": 14838, "synset": "carioca.n.01", "name": "Carioca"}, - {"id": 14839, "synset": "tupi.n.01", "name": "Tupi"}, - {"id": 14840, "synset": "bruneian.n.01", "name": "Bruneian"}, - {"id": 14841, "synset": "bulgarian.n.01", "name": "Bulgarian"}, - {"id": 14842, "synset": "byelorussian.n.01", "name": "Byelorussian"}, - {"id": 14843, "synset": "cameroonian.n.01", "name": "Cameroonian"}, - {"id": 14844, "synset": "canadian.n.01", "name": "Canadian"}, - {"id": 14845, "synset": "french_canadian.n.01", "name": "French_Canadian"}, - {"id": 14846, "synset": "central_american.n.01", "name": "Central_American"}, - {"id": 14847, "synset": "chilean.n.01", "name": "Chilean"}, - {"id": 14848, "synset": "congolese.n.01", "name": "Congolese"}, - {"id": 14849, "synset": "cypriot.n.01", "name": "Cypriot"}, - {"id": 14850, "synset": "dane.n.01", "name": "Dane"}, - {"id": 14851, "synset": "djiboutian.n.01", "name": "Djiboutian"}, - {"id": 14852, "synset": "britisher.n.01", "name": "Britisher"}, - {"id": 14853, "synset": "english_person.n.01", "name": "English_person"}, - {"id": 14854, "synset": "englishwoman.n.01", "name": "Englishwoman"}, - {"id": 14855, "synset": "anglo-saxon.n.02", "name": "Anglo-Saxon"}, - {"id": 14856, "synset": "angle.n.03", "name": "Angle"}, - {"id": 14857, "synset": "west_saxon.n.01", "name": "West_Saxon"}, - {"id": 14858, "synset": "lombard.n.01", "name": "Lombard"}, - {"id": 14859, "synset": "limey.n.01", "name": "limey"}, - {"id": 14860, "synset": "cantabrigian.n.01", "name": "Cantabrigian"}, - {"id": 14861, "synset": "cornishman.n.01", "name": "Cornishman"}, - {"id": 14862, "synset": "cornishwoman.n.01", "name": "Cornishwoman"}, - {"id": 14863, "synset": "lancastrian.n.02", "name": "Lancastrian"}, - {"id": 14864, "synset": "lancastrian.n.01", "name": "Lancastrian"}, - {"id": 14865, "synset": "geordie.n.01", "name": "Geordie"}, - {"id": 14866, "synset": "oxonian.n.01", "name": "Oxonian"}, - {"id": 14867, "synset": "ethiopian.n.01", "name": "Ethiopian"}, - {"id": 14868, "synset": "amhara.n.01", "name": "Amhara"}, - {"id": 14869, "synset": "eritrean.n.01", "name": "Eritrean"}, - {"id": 14870, "synset": "finn.n.01", "name": "Finn"}, - {"id": 14871, "synset": "komi.n.01", "name": "Komi"}, - {"id": 14872, "synset": "livonian.n.01", "name": "Livonian"}, - {"id": 14873, "synset": "lithuanian.n.01", "name": "Lithuanian"}, - {"id": 14874, "synset": "selkup.n.01", "name": "Selkup"}, - {"id": 14875, "synset": "parisian.n.01", "name": "Parisian"}, - {"id": 14876, "synset": "parisienne.n.01", "name": "Parisienne"}, - {"id": 14877, "synset": "creole.n.02", "name": "Creole"}, - {"id": 14878, "synset": "creole.n.01", "name": "Creole"}, - {"id": 14879, "synset": "gabonese.n.01", "name": "Gabonese"}, - {"id": 14880, "synset": "greek.n.02", "name": "Greek"}, - {"id": 14881, "synset": "dorian.n.01", "name": "Dorian"}, - {"id": 14882, "synset": "athenian.n.01", "name": "Athenian"}, - {"id": 14883, "synset": "laconian.n.01", "name": "Laconian"}, - {"id": 14884, "synset": "guyanese.n.01", "name": "Guyanese"}, - {"id": 14885, "synset": "haitian.n.01", "name": "Haitian"}, - {"id": 14886, "synset": "malay.n.01", "name": "Malay"}, - {"id": 14887, "synset": "moro.n.01", "name": "Moro"}, - {"id": 14888, "synset": "netherlander.n.01", "name": "Netherlander"}, - {"id": 14889, "synset": "icelander.n.01", "name": "Icelander"}, - {"id": 14890, "synset": "iraqi.n.01", "name": "Iraqi"}, - {"id": 14891, "synset": "irishman.n.01", "name": "Irishman"}, - {"id": 14892, "synset": "irishwoman.n.01", "name": "Irishwoman"}, - {"id": 14893, "synset": "dubliner.n.01", "name": "Dubliner"}, - {"id": 14894, "synset": "italian.n.01", "name": "Italian"}, - {"id": 14895, "synset": "roman.n.01", "name": "Roman"}, - {"id": 14896, "synset": "sabine.n.02", "name": "Sabine"}, - {"id": 14897, "synset": "japanese.n.01", "name": "Japanese"}, - {"id": 14898, "synset": "jordanian.n.01", "name": "Jordanian"}, - {"id": 14899, "synset": "korean.n.01", "name": "Korean"}, - {"id": 14900, "synset": "kenyan.n.01", "name": "Kenyan"}, - {"id": 14901, "synset": "lao.n.01", "name": "Lao"}, - {"id": 14902, "synset": "lapp.n.01", "name": "Lapp"}, - {"id": 14903, "synset": "latin_american.n.01", "name": "Latin_American"}, - {"id": 14904, "synset": "lebanese.n.01", "name": "Lebanese"}, - {"id": 14905, "synset": "levantine.n.01", "name": "Levantine"}, - {"id": 14906, "synset": "liberian.n.01", "name": "Liberian"}, - {"id": 14907, "synset": "luxemburger.n.01", "name": "Luxemburger"}, - {"id": 14908, "synset": "macedonian.n.01", "name": "Macedonian"}, - {"id": 14909, "synset": "sabahan.n.01", "name": "Sabahan"}, - {"id": 14910, "synset": "mexican.n.01", "name": "Mexican"}, - {"id": 14911, "synset": "chicano.n.01", "name": "Chicano"}, - {"id": 14912, "synset": "mexican-american.n.01", "name": "Mexican-American"}, - {"id": 14913, "synset": "namibian.n.01", "name": "Namibian"}, - {"id": 14914, "synset": "nauruan.n.01", "name": "Nauruan"}, - {"id": 14915, "synset": "gurkha.n.02", "name": "Gurkha"}, - {"id": 14916, "synset": "new_zealander.n.01", "name": "New_Zealander"}, - {"id": 14917, "synset": "nicaraguan.n.01", "name": "Nicaraguan"}, - {"id": 14918, "synset": "nigerian.n.01", "name": "Nigerian"}, - {"id": 14919, "synset": "hausa.n.01", "name": "Hausa"}, - {"id": 14920, "synset": "north_american.n.01", "name": "North_American"}, - {"id": 14921, "synset": "nova_scotian.n.01", "name": "Nova_Scotian"}, - {"id": 14922, "synset": "omani.n.01", "name": "Omani"}, - {"id": 14923, "synset": "pakistani.n.01", "name": "Pakistani"}, - {"id": 14924, "synset": "brahui.n.01", "name": "Brahui"}, - {"id": 14925, "synset": "south_american_indian.n.01", "name": "South_American_Indian"}, - {"id": 14926, "synset": "carib.n.01", "name": "Carib"}, - {"id": 14927, "synset": "filipino.n.01", "name": "Filipino"}, - {"id": 14928, "synset": "polynesian.n.01", "name": "Polynesian"}, - {"id": 14929, "synset": "qatari.n.01", "name": "Qatari"}, - {"id": 14930, "synset": "romanian.n.01", "name": "Romanian"}, - {"id": 14931, "synset": "muscovite.n.02", "name": "Muscovite"}, - {"id": 14932, "synset": "georgian.n.02", "name": "Georgian"}, - {"id": 14933, "synset": "sarawakian.n.01", "name": "Sarawakian"}, - {"id": 14934, "synset": "scandinavian.n.01", "name": "Scandinavian"}, - {"id": 14935, "synset": "senegalese.n.01", "name": "Senegalese"}, - {"id": 14936, "synset": "slovene.n.01", "name": "Slovene"}, - {"id": 14937, "synset": "south_african.n.01", "name": "South_African"}, - {"id": 14938, "synset": "south_american.n.01", "name": "South_American"}, - {"id": 14939, "synset": "sudanese.n.01", "name": "Sudanese"}, - {"id": 14940, "synset": "syrian.n.01", "name": "Syrian"}, - {"id": 14941, "synset": "tahitian.n.01", "name": "Tahitian"}, - {"id": 14942, "synset": "tanzanian.n.01", "name": "Tanzanian"}, - {"id": 14943, "synset": "tibetan.n.02", "name": "Tibetan"}, - {"id": 14944, "synset": "togolese.n.01", "name": "Togolese"}, - {"id": 14945, "synset": "tuareg.n.01", "name": "Tuareg"}, - {"id": 14946, "synset": "turki.n.01", "name": "Turki"}, - {"id": 14947, "synset": "chuvash.n.01", "name": "Chuvash"}, - {"id": 14948, "synset": "turkoman.n.01", "name": "Turkoman"}, - {"id": 14949, "synset": "uzbek.n.01", "name": "Uzbek"}, - {"id": 14950, "synset": "ugandan.n.01", "name": "Ugandan"}, - {"id": 14951, "synset": "ukranian.n.01", "name": "Ukranian"}, - {"id": 14952, "synset": "yakut.n.01", "name": "Yakut"}, - {"id": 14953, "synset": "tungus.n.01", "name": "Tungus"}, - {"id": 14954, "synset": "igbo.n.01", "name": "Igbo"}, - {"id": 14955, "synset": "american.n.03", "name": "American"}, - {"id": 14956, "synset": "anglo-american.n.01", "name": "Anglo-American"}, - {"id": 14957, "synset": "alaska_native.n.01", "name": "Alaska_Native"}, - {"id": 14958, "synset": "arkansan.n.01", "name": "Arkansan"}, - {"id": 14959, "synset": "carolinian.n.01", "name": "Carolinian"}, - {"id": 14960, "synset": "coloradan.n.01", "name": "Coloradan"}, - {"id": 14961, "synset": "connecticuter.n.01", "name": "Connecticuter"}, - {"id": 14962, "synset": "delawarean.n.01", "name": "Delawarean"}, - {"id": 14963, "synset": "floridian.n.01", "name": "Floridian"}, - {"id": 14964, "synset": "german_american.n.01", "name": "German_American"}, - {"id": 14965, "synset": "illinoisan.n.01", "name": "Illinoisan"}, - {"id": 14966, "synset": "mainer.n.01", "name": "Mainer"}, - {"id": 14967, "synset": "marylander.n.01", "name": "Marylander"}, - {"id": 14968, "synset": "minnesotan.n.01", "name": "Minnesotan"}, - {"id": 14969, "synset": "nebraskan.n.01", "name": "Nebraskan"}, - {"id": 14970, "synset": "new_hampshirite.n.01", "name": "New_Hampshirite"}, - {"id": 14971, "synset": "new_jerseyan.n.01", "name": "New_Jerseyan"}, - {"id": 14972, "synset": "new_yorker.n.01", "name": "New_Yorker"}, - {"id": 14973, "synset": "north_carolinian.n.01", "name": "North_Carolinian"}, - {"id": 14974, "synset": "oregonian.n.01", "name": "Oregonian"}, - {"id": 14975, "synset": "pennsylvanian.n.02", "name": "Pennsylvanian"}, - {"id": 14976, "synset": "texan.n.01", "name": "Texan"}, - {"id": 14977, "synset": "utahan.n.01", "name": "Utahan"}, - {"id": 14978, "synset": "uruguayan.n.01", "name": "Uruguayan"}, - {"id": 14979, "synset": "vietnamese.n.01", "name": "Vietnamese"}, - {"id": 14980, "synset": "gambian.n.01", "name": "Gambian"}, - {"id": 14981, "synset": "east_german.n.01", "name": "East_German"}, - {"id": 14982, "synset": "berliner.n.01", "name": "Berliner"}, - {"id": 14983, "synset": "prussian.n.01", "name": "Prussian"}, - {"id": 14984, "synset": "ghanian.n.01", "name": "Ghanian"}, - {"id": 14985, "synset": "guinean.n.01", "name": "Guinean"}, - {"id": 14986, "synset": "papuan.n.01", "name": "Papuan"}, - {"id": 14987, "synset": "walloon.n.01", "name": "Walloon"}, - {"id": 14988, "synset": "yemeni.n.01", "name": "Yemeni"}, - {"id": 14989, "synset": "yugoslav.n.01", "name": "Yugoslav"}, - {"id": 14990, "synset": "serbian.n.01", "name": "Serbian"}, - {"id": 14991, "synset": "xhosa.n.01", "name": "Xhosa"}, - {"id": 14992, "synset": "zairese.n.01", "name": "Zairese"}, - {"id": 14993, "synset": "zimbabwean.n.01", "name": "Zimbabwean"}, - {"id": 14994, "synset": "zulu.n.01", "name": "Zulu"}, - {"id": 14995, "synset": "gemini.n.01", "name": "Gemini"}, - {"id": 14996, "synset": "sagittarius.n.01", "name": "Sagittarius"}, - {"id": 14997, "synset": "pisces.n.02", "name": "Pisces"}, - {"id": 14998, "synset": "abbe.n.01", "name": "abbe"}, - {"id": 14999, "synset": "abbess.n.01", "name": "abbess"}, - {"id": 15000, "synset": "abnegator.n.01", "name": "abnegator"}, - {"id": 15001, "synset": "abridger.n.01", "name": "abridger"}, - {"id": 15002, "synset": "abstractor.n.01", "name": "abstractor"}, - {"id": 15003, "synset": "absconder.n.01", "name": "absconder"}, - {"id": 15004, "synset": "absolver.n.01", "name": "absolver"}, - {"id": 15005, "synset": "abecedarian.n.01", "name": "abecedarian"}, - {"id": 15006, "synset": "aberrant.n.01", "name": "aberrant"}, - {"id": 15007, "synset": "abettor.n.01", "name": "abettor"}, - {"id": 15008, "synset": "abhorrer.n.01", "name": "abhorrer"}, - {"id": 15009, "synset": "abomination.n.01", "name": "abomination"}, - {"id": 15010, "synset": "abseiler.n.01", "name": "abseiler"}, - {"id": 15011, "synset": "abstainer.n.01", "name": "abstainer"}, - {"id": 15012, "synset": "academic_administrator.n.01", "name": "academic_administrator"}, - {"id": 15013, "synset": "academician.n.01", "name": "academician"}, - {"id": 15014, "synset": "accessory_before_the_fact.n.01", "name": "accessory_before_the_fact"}, - {"id": 15015, "synset": "companion.n.03", "name": "companion"}, - {"id": 15016, "synset": "accompanist.n.01", "name": "accompanist"}, - {"id": 15017, "synset": "accomplice.n.01", "name": "accomplice"}, - {"id": 15018, "synset": "account_executive.n.01", "name": "account_executive"}, - {"id": 15019, "synset": "accused.n.01", "name": "accused"}, - {"id": 15020, "synset": "accuser.n.01", "name": "accuser"}, - {"id": 15021, "synset": "acid_head.n.01", "name": "acid_head"}, - {"id": 15022, "synset": "acquaintance.n.03", "name": "acquaintance"}, - {"id": 15023, "synset": "acquirer.n.01", "name": "acquirer"}, - {"id": 15024, "synset": "aerialist.n.01", "name": "aerialist"}, - {"id": 15025, "synset": "action_officer.n.01", "name": "action_officer"}, - {"id": 15026, "synset": "active.n.03", "name": "active"}, - {"id": 15027, "synset": "active_citizen.n.01", "name": "active_citizen"}, - {"id": 15028, "synset": "actor.n.01", "name": "actor"}, - {"id": 15029, "synset": "actor.n.02", "name": "actor"}, - {"id": 15030, "synset": "addict.n.01", "name": "addict"}, - {"id": 15031, "synset": "adducer.n.01", "name": "adducer"}, - {"id": 15032, "synset": "adjuster.n.01", "name": "adjuster"}, - {"id": 15033, "synset": "adjutant.n.01", "name": "adjutant"}, - {"id": 15034, "synset": "adjutant_general.n.01", "name": "adjutant_general"}, - {"id": 15035, "synset": "admirer.n.03", "name": "admirer"}, - {"id": 15036, "synset": "adoptee.n.01", "name": "adoptee"}, - {"id": 15037, "synset": "adulterer.n.01", "name": "adulterer"}, - {"id": 15038, "synset": "adulteress.n.01", "name": "adulteress"}, - {"id": 15039, "synset": "advertiser.n.01", "name": "advertiser"}, - {"id": 15040, "synset": "advisee.n.01", "name": "advisee"}, - {"id": 15041, "synset": "advocate.n.01", "name": "advocate"}, - {"id": 15042, "synset": "aeronautical_engineer.n.01", "name": "aeronautical_engineer"}, - {"id": 15043, "synset": "affiliate.n.01", "name": "affiliate"}, - {"id": 15044, "synset": "affluent.n.01", "name": "affluent"}, - {"id": 15045, "synset": "aficionado.n.02", "name": "aficionado"}, - {"id": 15046, "synset": "buck_sergeant.n.01", "name": "buck_sergeant"}, - {"id": 15047, "synset": "agent-in-place.n.01", "name": "agent-in-place"}, - {"id": 15048, "synset": "aggravator.n.01", "name": "aggravator"}, - {"id": 15049, "synset": "agitator.n.01", "name": "agitator"}, - {"id": 15050, "synset": "agnostic.n.02", "name": "agnostic"}, - {"id": 15051, "synset": "agnostic.n.01", "name": "agnostic"}, - {"id": 15052, "synset": "agonist.n.02", "name": "agonist"}, - {"id": 15053, "synset": "agony_aunt.n.01", "name": "agony_aunt"}, - {"id": 15054, "synset": "agriculturist.n.01", "name": "agriculturist"}, - {"id": 15055, "synset": "air_attache.n.01", "name": "air_attache"}, - {"id": 15056, "synset": "air_force_officer.n.01", "name": "air_force_officer"}, - {"id": 15057, "synset": "airhead.n.01", "name": "airhead"}, - {"id": 15058, "synset": "air_traveler.n.01", "name": "air_traveler"}, - {"id": 15059, "synset": "alarmist.n.01", "name": "alarmist"}, - {"id": 15060, "synset": "albino.n.01", "name": "albino"}, - {"id": 15061, "synset": "alcoholic.n.01", "name": "alcoholic"}, - {"id": 15062, "synset": "alderman.n.01", "name": "alderman"}, - {"id": 15063, "synset": "alexic.n.01", "name": "alexic"}, - {"id": 15064, "synset": "alienee.n.01", "name": "alienee"}, - {"id": 15065, "synset": "alienor.n.01", "name": "alienor"}, - {"id": 15066, "synset": "aliterate.n.01", "name": "aliterate"}, - {"id": 15067, "synset": "algebraist.n.01", "name": "algebraist"}, - {"id": 15068, "synset": "allegorizer.n.01", "name": "allegorizer"}, - {"id": 15069, "synset": "alliterator.n.01", "name": "alliterator"}, - {"id": 15070, "synset": "almoner.n.01", "name": "almoner"}, - {"id": 15071, "synset": "alpinist.n.01", "name": "alpinist"}, - {"id": 15072, "synset": "altar_boy.n.01", "name": "altar_boy"}, - {"id": 15073, "synset": "alto.n.01", "name": "alto"}, - {"id": 15074, "synset": "ambassador.n.01", "name": "ambassador"}, - {"id": 15075, "synset": "ambassador.n.02", "name": "ambassador"}, - {"id": 15076, "synset": "ambusher.n.01", "name": "ambusher"}, - {"id": 15077, "synset": "amicus_curiae.n.01", "name": "amicus_curiae"}, - {"id": 15078, "synset": "amoralist.n.01", "name": "amoralist"}, - {"id": 15079, "synset": "amputee.n.01", "name": "amputee"}, - {"id": 15080, "synset": "analogist.n.01", "name": "analogist"}, - {"id": 15081, "synset": "analphabet.n.01", "name": "analphabet"}, - {"id": 15082, "synset": "analyst.n.01", "name": "analyst"}, - {"id": 15083, "synset": "industry_analyst.n.01", "name": "industry_analyst"}, - {"id": 15084, "synset": "market_strategist.n.01", "name": "market_strategist"}, - {"id": 15085, "synset": "anarchist.n.01", "name": "anarchist"}, - {"id": 15086, "synset": "anathema.n.01", "name": "anathema"}, - {"id": 15087, "synset": "ancestor.n.01", "name": "ancestor"}, - {"id": 15088, "synset": "anchor.n.03", "name": "anchor"}, - {"id": 15089, "synset": "ancient.n.02", "name": "ancient"}, - {"id": 15090, "synset": "anecdotist.n.01", "name": "anecdotist"}, - {"id": 15091, "synset": "angler.n.02", "name": "angler"}, - {"id": 15092, "synset": "animator.n.02", "name": "animator"}, - {"id": 15093, "synset": "animist.n.01", "name": "animist"}, - {"id": 15094, "synset": "annotator.n.01", "name": "annotator"}, - {"id": 15095, "synset": "announcer.n.02", "name": "announcer"}, - {"id": 15096, "synset": "announcer.n.01", "name": "announcer"}, - {"id": 15097, "synset": "anti.n.01", "name": "anti"}, - {"id": 15098, "synset": "anti-american.n.01", "name": "anti-American"}, - {"id": 15099, "synset": "anti-semite.n.01", "name": "anti-Semite"}, - {"id": 15100, "synset": "anzac.n.01", "name": "Anzac"}, - {"id": 15101, "synset": "ape-man.n.02", "name": "ape-man"}, - {"id": 15102, "synset": "aphakic.n.01", "name": "aphakic"}, - {"id": 15103, "synset": "appellant.n.01", "name": "appellant"}, - {"id": 15104, "synset": "appointee.n.01", "name": "appointee"}, - {"id": 15105, "synset": "apprehender.n.02", "name": "apprehender"}, - {"id": 15106, "synset": "april_fool.n.01", "name": "April_fool"}, - {"id": 15107, "synset": "aspirant.n.01", "name": "aspirant"}, - {"id": 15108, "synset": "appreciator.n.01", "name": "appreciator"}, - {"id": 15109, "synset": "appropriator.n.01", "name": "appropriator"}, - {"id": 15110, "synset": "arabist.n.01", "name": "Arabist"}, - {"id": 15111, "synset": "archaist.n.01", "name": "archaist"}, - {"id": 15112, "synset": "archbishop.n.01", "name": "archbishop"}, - {"id": 15113, "synset": "archer.n.01", "name": "archer"}, - {"id": 15114, "synset": "architect.n.01", "name": "architect"}, - {"id": 15115, "synset": "archivist.n.01", "name": "archivist"}, - {"id": 15116, "synset": "archpriest.n.01", "name": "archpriest"}, - {"id": 15117, "synset": "aristotelian.n.01", "name": "Aristotelian"}, - {"id": 15118, "synset": "armiger.n.02", "name": "armiger"}, - {"id": 15119, "synset": "army_attache.n.01", "name": "army_attache"}, - {"id": 15120, "synset": "army_engineer.n.01", "name": "army_engineer"}, - {"id": 15121, "synset": "army_officer.n.01", "name": "army_officer"}, - {"id": 15122, "synset": "arranger.n.02", "name": "arranger"}, - {"id": 15123, "synset": "arrival.n.03", "name": "arrival"}, - {"id": 15124, "synset": "arthritic.n.01", "name": "arthritic"}, - {"id": 15125, "synset": "articulator.n.01", "name": "articulator"}, - {"id": 15126, "synset": "artilleryman.n.01", "name": "artilleryman"}, - {"id": 15127, "synset": "artist's_model.n.01", "name": "artist's_model"}, - {"id": 15128, "synset": "assayer.n.01", "name": "assayer"}, - {"id": 15129, "synset": "assemblyman.n.01", "name": "assemblyman"}, - {"id": 15130, "synset": "assemblywoman.n.01", "name": "assemblywoman"}, - {"id": 15131, "synset": "assenter.n.01", "name": "assenter"}, - {"id": 15132, "synset": "asserter.n.01", "name": "asserter"}, - {"id": 15133, "synset": "assignee.n.01", "name": "assignee"}, - {"id": 15134, "synset": "assistant.n.01", "name": "assistant"}, - {"id": 15135, "synset": "assistant_professor.n.01", "name": "assistant_professor"}, - {"id": 15136, "synset": "associate.n.01", "name": "associate"}, - {"id": 15137, "synset": "associate.n.03", "name": "associate"}, - {"id": 15138, "synset": "associate_professor.n.01", "name": "associate_professor"}, - {"id": 15139, "synset": "astronaut.n.01", "name": "astronaut"}, - {"id": 15140, "synset": "cosmographer.n.01", "name": "cosmographer"}, - {"id": 15141, "synset": "atheist.n.01", "name": "atheist"}, - {"id": 15142, "synset": "athlete.n.01", "name": "athlete"}, - {"id": 15143, "synset": "attendant.n.01", "name": "attendant"}, - {"id": 15144, "synset": "attorney_general.n.01", "name": "attorney_general"}, - {"id": 15145, "synset": "auditor.n.02", "name": "auditor"}, - {"id": 15146, "synset": "augur.n.01", "name": "augur"}, - {"id": 15147, "synset": "aunt.n.01", "name": "aunt"}, - {"id": 15148, "synset": "au_pair_girl.n.01", "name": "au_pair_girl"}, - {"id": 15149, "synset": "authoritarian.n.01", "name": "authoritarian"}, - {"id": 15150, "synset": "authority.n.02", "name": "authority"}, - {"id": 15151, "synset": "authorizer.n.01", "name": "authorizer"}, - {"id": 15152, "synset": "automobile_mechanic.n.01", "name": "automobile_mechanic"}, - {"id": 15153, "synset": "aviator.n.01", "name": "aviator"}, - {"id": 15154, "synset": "aviatrix.n.01", "name": "aviatrix"}, - {"id": 15155, "synset": "ayah.n.01", "name": "ayah"}, - {"id": 15156, "synset": "babu.n.01", "name": "babu"}, - {"id": 15157, "synset": "baby.n.05", "name": "baby"}, - {"id": 15158, "synset": "baby.n.04", "name": "baby"}, - {"id": 15159, "synset": "baby_boomer.n.01", "name": "baby_boomer"}, - {"id": 15160, "synset": "baby_farmer.n.01", "name": "baby_farmer"}, - {"id": 15161, "synset": "back.n.04", "name": "back"}, - {"id": 15162, "synset": "backbencher.n.01", "name": "backbencher"}, - {"id": 15163, "synset": "backpacker.n.01", "name": "backpacker"}, - {"id": 15164, "synset": "backroom_boy.n.01", "name": "backroom_boy"}, - {"id": 15165, "synset": "backscratcher.n.01", "name": "backscratcher"}, - {"id": 15166, "synset": "bad_person.n.01", "name": "bad_person"}, - {"id": 15167, "synset": "baggage.n.02", "name": "baggage"}, - {"id": 15168, "synset": "bag_lady.n.01", "name": "bag_lady"}, - {"id": 15169, "synset": "bailee.n.01", "name": "bailee"}, - {"id": 15170, "synset": "bailiff.n.01", "name": "bailiff"}, - {"id": 15171, "synset": "bailor.n.01", "name": "bailor"}, - {"id": 15172, "synset": "bairn.n.01", "name": "bairn"}, - {"id": 15173, "synset": "baker.n.02", "name": "baker"}, - {"id": 15174, "synset": "balancer.n.01", "name": "balancer"}, - {"id": 15175, "synset": "balker.n.01", "name": "balker"}, - {"id": 15176, "synset": "ball-buster.n.01", "name": "ball-buster"}, - {"id": 15177, "synset": "ball_carrier.n.01", "name": "ball_carrier"}, - {"id": 15178, "synset": "ballet_dancer.n.01", "name": "ballet_dancer"}, - {"id": 15179, "synset": "ballet_master.n.01", "name": "ballet_master"}, - {"id": 15180, "synset": "ballet_mistress.n.01", "name": "ballet_mistress"}, - {"id": 15181, "synset": "balletomane.n.01", "name": "balletomane"}, - {"id": 15182, "synset": "ball_hawk.n.01", "name": "ball_hawk"}, - {"id": 15183, "synset": "balloonist.n.01", "name": "balloonist"}, - {"id": 15184, "synset": "ballplayer.n.01", "name": "ballplayer"}, - {"id": 15185, "synset": "bullfighter.n.01", "name": "bullfighter"}, - {"id": 15186, "synset": "banderillero.n.01", "name": "banderillero"}, - {"id": 15187, "synset": "matador.n.01", "name": "matador"}, - {"id": 15188, "synset": "picador.n.01", "name": "picador"}, - {"id": 15189, "synset": "bandsman.n.01", "name": "bandsman"}, - {"id": 15190, "synset": "banker.n.02", "name": "banker"}, - {"id": 15191, "synset": "bank_robber.n.01", "name": "bank_robber"}, - {"id": 15192, "synset": "bankrupt.n.01", "name": "bankrupt"}, - {"id": 15193, "synset": "bantamweight.n.01", "name": "bantamweight"}, - {"id": 15194, "synset": "barmaid.n.01", "name": "barmaid"}, - {"id": 15195, "synset": "baron.n.03", "name": "baron"}, - {"id": 15196, "synset": "baron.n.02", "name": "baron"}, - {"id": 15197, "synset": "baron.n.01", "name": "baron"}, - {"id": 15198, "synset": "bartender.n.01", "name": "bartender"}, - {"id": 15199, "synset": "baseball_coach.n.01", "name": "baseball_coach"}, - {"id": 15200, "synset": "base_runner.n.01", "name": "base_runner"}, - {"id": 15201, "synset": "basketball_player.n.01", "name": "basketball_player"}, - {"id": 15202, "synset": "basketweaver.n.01", "name": "basketweaver"}, - {"id": 15203, "synset": "basket_maker.n.01", "name": "Basket_Maker"}, - {"id": 15204, "synset": "bass.n.03", "name": "bass"}, - {"id": 15205, "synset": "bastard.n.02", "name": "bastard"}, - {"id": 15206, "synset": "bat_boy.n.01", "name": "bat_boy"}, - {"id": 15207, "synset": "bather.n.02", "name": "bather"}, - {"id": 15208, "synset": "batman.n.01", "name": "batman"}, - {"id": 15209, "synset": "baton_twirler.n.01", "name": "baton_twirler"}, - {"id": 15210, "synset": "bavarian.n.01", "name": "Bavarian"}, - {"id": 15211, "synset": "beadsman.n.01", "name": "beadsman"}, - {"id": 15212, "synset": "beard.n.03", "name": "beard"}, - {"id": 15213, "synset": "beatnik.n.01", "name": "beatnik"}, - {"id": 15214, "synset": "beauty_consultant.n.01", "name": "beauty_consultant"}, - {"id": 15215, "synset": "bedouin.n.01", "name": "Bedouin"}, - {"id": 15216, "synset": "bedwetter.n.01", "name": "bedwetter"}, - {"id": 15217, "synset": "beekeeper.n.01", "name": "beekeeper"}, - {"id": 15218, "synset": "beer_drinker.n.01", "name": "beer_drinker"}, - {"id": 15219, "synset": "beggarman.n.01", "name": "beggarman"}, - {"id": 15220, "synset": "beggarwoman.n.01", "name": "beggarwoman"}, - {"id": 15221, "synset": "beldam.n.02", "name": "beldam"}, - {"id": 15222, "synset": "theist.n.01", "name": "theist"}, - {"id": 15223, "synset": "believer.n.01", "name": "believer"}, - {"id": 15224, "synset": "bell_founder.n.01", "name": "bell_founder"}, - {"id": 15225, "synset": "benedick.n.01", "name": "benedick"}, - {"id": 15226, "synset": "berserker.n.01", "name": "berserker"}, - {"id": 15227, "synset": "besieger.n.01", "name": "besieger"}, - {"id": 15228, "synset": "best.n.02", "name": "best"}, - {"id": 15229, "synset": "betrothed.n.01", "name": "betrothed"}, - {"id": 15230, "synset": "big_brother.n.01", "name": "Big_Brother"}, - {"id": 15231, "synset": "bigot.n.01", "name": "bigot"}, - {"id": 15232, "synset": "big_shot.n.01", "name": "big_shot"}, - {"id": 15233, "synset": "big_sister.n.01", "name": "big_sister"}, - {"id": 15234, "synset": "billiard_player.n.01", "name": "billiard_player"}, - {"id": 15235, "synset": "biochemist.n.01", "name": "biochemist"}, - {"id": 15236, "synset": "biographer.n.01", "name": "biographer"}, - {"id": 15237, "synset": "bird_fancier.n.01", "name": "bird_fancier"}, - {"id": 15238, "synset": "birth.n.05", "name": "birth"}, - {"id": 15239, "synset": "birth-control_campaigner.n.01", "name": "birth-control_campaigner"}, - {"id": 15240, "synset": "bisexual.n.01", "name": "bisexual"}, - {"id": 15241, "synset": "black_belt.n.01", "name": "black_belt"}, - {"id": 15242, "synset": "blackmailer.n.01", "name": "blackmailer"}, - {"id": 15243, "synset": "black_muslim.n.01", "name": "Black_Muslim"}, - {"id": 15244, "synset": "blacksmith.n.01", "name": "blacksmith"}, - {"id": 15245, "synset": "blade.n.02", "name": "blade"}, - {"id": 15246, "synset": "blind_date.n.01", "name": "blind_date"}, - {"id": 15247, "synset": "bluecoat.n.01", "name": "bluecoat"}, - {"id": 15248, "synset": "bluestocking.n.01", "name": "bluestocking"}, - {"id": 15249, "synset": "boatbuilder.n.01", "name": "boatbuilder"}, - {"id": 15250, "synset": "boatman.n.01", "name": "boatman"}, - {"id": 15251, "synset": "boatswain.n.01", "name": "boatswain"}, - {"id": 15252, "synset": "bobby.n.01", "name": "bobby"}, - {"id": 15253, "synset": "bodyguard.n.01", "name": "bodyguard"}, - {"id": 15254, "synset": "boffin.n.01", "name": "boffin"}, - {"id": 15255, "synset": "bolshevik.n.01", "name": "Bolshevik"}, - {"id": 15256, "synset": "bolshevik.n.02", "name": "Bolshevik"}, - {"id": 15257, "synset": "bombshell.n.01", "name": "bombshell"}, - {"id": 15258, "synset": "bondman.n.01", "name": "bondman"}, - {"id": 15259, "synset": "bondwoman.n.02", "name": "bondwoman"}, - {"id": 15260, "synset": "bondwoman.n.01", "name": "bondwoman"}, - {"id": 15261, "synset": "bond_servant.n.01", "name": "bond_servant"}, - {"id": 15262, "synset": "book_agent.n.01", "name": "book_agent"}, - {"id": 15263, "synset": "bookbinder.n.01", "name": "bookbinder"}, - {"id": 15264, "synset": "bookkeeper.n.01", "name": "bookkeeper"}, - {"id": 15265, "synset": "bookmaker.n.01", "name": "bookmaker"}, - {"id": 15266, "synset": "bookworm.n.02", "name": "bookworm"}, - {"id": 15267, "synset": "booster.n.03", "name": "booster"}, - {"id": 15268, "synset": "bootblack.n.01", "name": "bootblack"}, - {"id": 15269, "synset": "bootlegger.n.01", "name": "bootlegger"}, - {"id": 15270, "synset": "bootmaker.n.01", "name": "bootmaker"}, - {"id": 15271, "synset": "borderer.n.01", "name": "borderer"}, - {"id": 15272, "synset": "border_patrolman.n.01", "name": "border_patrolman"}, - {"id": 15273, "synset": "botanist.n.01", "name": "botanist"}, - {"id": 15274, "synset": "bottom_feeder.n.01", "name": "bottom_feeder"}, - {"id": 15275, "synset": "boulevardier.n.01", "name": "boulevardier"}, - {"id": 15276, "synset": "bounty_hunter.n.02", "name": "bounty_hunter"}, - {"id": 15277, "synset": "bounty_hunter.n.01", "name": "bounty_hunter"}, - {"id": 15278, "synset": "bourbon.n.03", "name": "Bourbon"}, - {"id": 15279, "synset": "bowler.n.01", "name": "bowler"}, - {"id": 15280, "synset": "slugger.n.02", "name": "slugger"}, - {"id": 15281, "synset": "cub.n.02", "name": "cub"}, - {"id": 15282, "synset": "boy_scout.n.01", "name": "Boy_Scout"}, - {"id": 15283, "synset": "boy_scout.n.02", "name": "boy_scout"}, - {"id": 15284, "synset": "boy_wonder.n.01", "name": "boy_wonder"}, - {"id": 15285, "synset": "bragger.n.01", "name": "bragger"}, - {"id": 15286, "synset": "brahman.n.02", "name": "brahman"}, - {"id": 15287, "synset": "brawler.n.01", "name": "brawler"}, - {"id": 15288, "synset": "breadwinner.n.01", "name": "breadwinner"}, - {"id": 15289, "synset": "breaststroker.n.01", "name": "breaststroker"}, - {"id": 15290, "synset": "breeder.n.01", "name": "breeder"}, - {"id": 15291, "synset": "brick.n.02", "name": "brick"}, - {"id": 15292, "synset": "bride.n.03", "name": "bride"}, - {"id": 15293, "synset": "bridesmaid.n.01", "name": "bridesmaid"}, - {"id": 15294, "synset": "bridge_agent.n.01", "name": "bridge_agent"}, - {"id": 15295, "synset": "broadcast_journalist.n.01", "name": "broadcast_journalist"}, - {"id": 15296, "synset": "brother.n.05", "name": "Brother"}, - {"id": 15297, "synset": "brother-in-law.n.01", "name": "brother-in-law"}, - {"id": 15298, "synset": "browser.n.01", "name": "browser"}, - {"id": 15299, "synset": "brummie.n.01", "name": "Brummie"}, - {"id": 15300, "synset": "buddy.n.01", "name": "buddy"}, - {"id": 15301, "synset": "bull.n.06", "name": "bull"}, - {"id": 15302, "synset": "bully.n.02", "name": "bully"}, - {"id": 15303, "synset": "bunny.n.01", "name": "bunny"}, - {"id": 15304, "synset": "burglar.n.01", "name": "burglar"}, - {"id": 15305, "synset": "bursar.n.01", "name": "bursar"}, - {"id": 15306, "synset": "busboy.n.01", "name": "busboy"}, - {"id": 15307, "synset": "business_editor.n.01", "name": "business_editor"}, - {"id": 15308, "synset": "business_traveler.n.01", "name": "business_traveler"}, - {"id": 15309, "synset": "buster.n.04", "name": "buster"}, - {"id": 15310, "synset": "busybody.n.01", "name": "busybody"}, - {"id": 15311, "synset": "buttinsky.n.01", "name": "buttinsky"}, - {"id": 15312, "synset": "cabinetmaker.n.01", "name": "cabinetmaker"}, - {"id": 15313, "synset": "caddie.n.01", "name": "caddie"}, - {"id": 15314, "synset": "cadet.n.01", "name": "cadet"}, - {"id": 15315, "synset": "caller.n.04", "name": "caller"}, - {"id": 15316, "synset": "call_girl.n.01", "name": "call_girl"}, - {"id": 15317, "synset": "calligrapher.n.01", "name": "calligrapher"}, - {"id": 15318, "synset": "campaigner.n.01", "name": "campaigner"}, - {"id": 15319, "synset": "camper.n.01", "name": "camper"}, - {"id": 15320, "synset": "camp_follower.n.02", "name": "camp_follower"}, - {"id": 15321, "synset": "candidate.n.02", "name": "candidate"}, - {"id": 15322, "synset": "canonist.n.01", "name": "canonist"}, - {"id": 15323, "synset": "capitalist.n.01", "name": "capitalist"}, - {"id": 15324, "synset": "captain.n.07", "name": "captain"}, - {"id": 15325, "synset": "captain.n.06", "name": "captain"}, - {"id": 15326, "synset": "captain.n.01", "name": "captain"}, - {"id": 15327, "synset": "captain.n.05", "name": "captain"}, - {"id": 15328, "synset": "captive.n.02", "name": "captive"}, - {"id": 15329, "synset": "captive.n.03", "name": "captive"}, - {"id": 15330, "synset": "cardinal.n.01", "name": "cardinal"}, - {"id": 15331, "synset": "cardiologist.n.01", "name": "cardiologist"}, - {"id": 15332, "synset": "card_player.n.01", "name": "card_player"}, - {"id": 15333, "synset": "cardsharp.n.01", "name": "cardsharp"}, - {"id": 15334, "synset": "careerist.n.01", "name": "careerist"}, - {"id": 15335, "synset": "career_man.n.01", "name": "career_man"}, - {"id": 15336, "synset": "caregiver.n.02", "name": "caregiver"}, - {"id": 15337, "synset": "caretaker.n.01", "name": "caretaker"}, - {"id": 15338, "synset": "caretaker.n.02", "name": "caretaker"}, - {"id": 15339, "synset": "caricaturist.n.01", "name": "caricaturist"}, - {"id": 15340, "synset": "carillonneur.n.01", "name": "carillonneur"}, - {"id": 15341, "synset": "caroler.n.01", "name": "caroler"}, - {"id": 15342, "synset": "carpenter.n.01", "name": "carpenter"}, - {"id": 15343, "synset": "carper.n.01", "name": "carper"}, - {"id": 15344, "synset": "cartesian.n.01", "name": "Cartesian"}, - {"id": 15345, "synset": "cashier.n.02", "name": "cashier"}, - {"id": 15346, "synset": "casualty.n.02", "name": "casualty"}, - {"id": 15347, "synset": "casualty.n.01", "name": "casualty"}, - {"id": 15348, "synset": "casuist.n.01", "name": "casuist"}, - {"id": 15349, "synset": "catechist.n.01", "name": "catechist"}, - {"id": 15350, "synset": "catechumen.n.01", "name": "catechumen"}, - {"id": 15351, "synset": "caterer.n.01", "name": "caterer"}, - {"id": 15352, "synset": "catholicos.n.01", "name": "Catholicos"}, - {"id": 15353, "synset": "cat_fancier.n.01", "name": "cat_fancier"}, - {"id": 15354, "synset": "cavalier.n.02", "name": "Cavalier"}, - {"id": 15355, "synset": "cavalryman.n.02", "name": "cavalryman"}, - {"id": 15356, "synset": "caveman.n.01", "name": "caveman"}, - {"id": 15357, "synset": "celebrant.n.02", "name": "celebrant"}, - {"id": 15358, "synset": "celebrant.n.01", "name": "celebrant"}, - {"id": 15359, "synset": "celebrity.n.01", "name": "celebrity"}, - {"id": 15360, "synset": "cellist.n.01", "name": "cellist"}, - {"id": 15361, "synset": "censor.n.02", "name": "censor"}, - {"id": 15362, "synset": "censor.n.01", "name": "censor"}, - {"id": 15363, "synset": "centenarian.n.01", "name": "centenarian"}, - {"id": 15364, "synset": "centrist.n.01", "name": "centrist"}, - {"id": 15365, "synset": "centurion.n.01", "name": "centurion"}, - { - "id": 15366, - "synset": "certified_public_accountant.n.01", - "name": "certified_public_accountant", - }, - {"id": 15367, "synset": "chachka.n.01", "name": "chachka"}, - {"id": 15368, "synset": "chambermaid.n.01", "name": "chambermaid"}, - {"id": 15369, "synset": "chameleon.n.01", "name": "chameleon"}, - {"id": 15370, "synset": "champion.n.01", "name": "champion"}, - {"id": 15371, "synset": "chandler.n.02", "name": "chandler"}, - {"id": 15372, "synset": "prison_chaplain.n.01", "name": "prison_chaplain"}, - {"id": 15373, "synset": "charcoal_burner.n.01", "name": "charcoal_burner"}, - {"id": 15374, "synset": "charge_d'affaires.n.01", "name": "charge_d'affaires"}, - {"id": 15375, "synset": "charioteer.n.01", "name": "charioteer"}, - {"id": 15376, "synset": "charmer.n.02", "name": "charmer"}, - {"id": 15377, "synset": "chartered_accountant.n.01", "name": "chartered_accountant"}, - {"id": 15378, "synset": "chartist.n.02", "name": "chartist"}, - {"id": 15379, "synset": "charwoman.n.01", "name": "charwoman"}, - {"id": 15380, "synset": "male_chauvinist.n.01", "name": "male_chauvinist"}, - {"id": 15381, "synset": "cheapskate.n.01", "name": "cheapskate"}, - {"id": 15382, "synset": "chechen.n.01", "name": "Chechen"}, - {"id": 15383, "synset": "checker.n.02", "name": "checker"}, - {"id": 15384, "synset": "cheerer.n.01", "name": "cheerer"}, - {"id": 15385, "synset": "cheerleader.n.02", "name": "cheerleader"}, - {"id": 15386, "synset": "cheerleader.n.01", "name": "cheerleader"}, - {"id": 15387, "synset": "cheops.n.01", "name": "Cheops"}, - {"id": 15388, "synset": "chess_master.n.01", "name": "chess_master"}, - {"id": 15389, "synset": "chief_executive_officer.n.01", "name": "chief_executive_officer"}, - {"id": 15390, "synset": "chief_of_staff.n.01", "name": "chief_of_staff"}, - {"id": 15391, "synset": "chief_petty_officer.n.01", "name": "chief_petty_officer"}, - {"id": 15392, "synset": "chief_secretary.n.01", "name": "Chief_Secretary"}, - {"id": 15393, "synset": "child.n.01", "name": "child"}, - {"id": 15394, "synset": "child.n.02", "name": "child"}, - {"id": 15395, "synset": "child.n.03", "name": "child"}, - {"id": 15396, "synset": "child_prodigy.n.01", "name": "child_prodigy"}, - {"id": 15397, "synset": "chimneysweeper.n.01", "name": "chimneysweeper"}, - {"id": 15398, "synset": "chiropractor.n.01", "name": "chiropractor"}, - {"id": 15399, "synset": "chit.n.01", "name": "chit"}, - {"id": 15400, "synset": "choker.n.02", "name": "choker"}, - {"id": 15401, "synset": "choragus.n.01", "name": "choragus"}, - {"id": 15402, "synset": "choreographer.n.01", "name": "choreographer"}, - {"id": 15403, "synset": "chorus_girl.n.01", "name": "chorus_girl"}, - {"id": 15404, "synset": "chosen.n.01", "name": "chosen"}, - {"id": 15405, "synset": "cicerone.n.01", "name": "cicerone"}, - {"id": 15406, "synset": "cigar_smoker.n.01", "name": "cigar_smoker"}, - {"id": 15407, "synset": "cipher.n.04", "name": "cipher"}, - {"id": 15408, "synset": "circus_acrobat.n.01", "name": "circus_acrobat"}, - {"id": 15409, "synset": "citizen.n.01", "name": "citizen"}, - {"id": 15410, "synset": "city_editor.n.01", "name": "city_editor"}, - {"id": 15411, "synset": "city_father.n.01", "name": "city_father"}, - {"id": 15412, "synset": "city_man.n.01", "name": "city_man"}, - {"id": 15413, "synset": "city_slicker.n.01", "name": "city_slicker"}, - {"id": 15414, "synset": "civic_leader.n.01", "name": "civic_leader"}, - {"id": 15415, "synset": "civil_rights_leader.n.01", "name": "civil_rights_leader"}, - {"id": 15416, "synset": "cleaner.n.03", "name": "cleaner"}, - {"id": 15417, "synset": "clergyman.n.01", "name": "clergyman"}, - {"id": 15418, "synset": "cleric.n.01", "name": "cleric"}, - {"id": 15419, "synset": "clerk.n.01", "name": "clerk"}, - {"id": 15420, "synset": "clever_dick.n.01", "name": "clever_Dick"}, - {"id": 15421, "synset": "climatologist.n.01", "name": "climatologist"}, - {"id": 15422, "synset": "climber.n.04", "name": "climber"}, - {"id": 15423, "synset": "clinician.n.01", "name": "clinician"}, - {"id": 15424, "synset": "closer.n.02", "name": "closer"}, - {"id": 15425, "synset": "closet_queen.n.01", "name": "closet_queen"}, - {"id": 15426, "synset": "clown.n.02", "name": "clown"}, - {"id": 15427, "synset": "clown.n.01", "name": "clown"}, - {"id": 15428, "synset": "coach.n.02", "name": "coach"}, - {"id": 15429, "synset": "coach.n.01", "name": "coach"}, - {"id": 15430, "synset": "pitching_coach.n.01", "name": "pitching_coach"}, - {"id": 15431, "synset": "coachman.n.01", "name": "coachman"}, - {"id": 15432, "synset": "coal_miner.n.01", "name": "coal_miner"}, - {"id": 15433, "synset": "coastguardsman.n.01", "name": "coastguardsman"}, - {"id": 15434, "synset": "cobber.n.01", "name": "cobber"}, - {"id": 15435, "synset": "cobbler.n.01", "name": "cobbler"}, - {"id": 15436, "synset": "codger.n.01", "name": "codger"}, - {"id": 15437, "synset": "co-beneficiary.n.01", "name": "co-beneficiary"}, - {"id": 15438, "synset": "cog.n.01", "name": "cog"}, - {"id": 15439, "synset": "cognitive_neuroscientist.n.01", "name": "cognitive_neuroscientist"}, - {"id": 15440, "synset": "coiffeur.n.01", "name": "coiffeur"}, - {"id": 15441, "synset": "coiner.n.02", "name": "coiner"}, - {"id": 15442, "synset": "collaborator.n.03", "name": "collaborator"}, - {"id": 15443, "synset": "colleen.n.01", "name": "colleen"}, - {"id": 15444, "synset": "college_student.n.01", "name": "college_student"}, - {"id": 15445, "synset": "collegian.n.01", "name": "collegian"}, - {"id": 15446, "synset": "colonial.n.01", "name": "colonial"}, - {"id": 15447, "synset": "colonialist.n.01", "name": "colonialist"}, - {"id": 15448, "synset": "colonizer.n.01", "name": "colonizer"}, - {"id": 15449, "synset": "coloratura.n.01", "name": "coloratura"}, - {"id": 15450, "synset": "color_guard.n.01", "name": "color_guard"}, - {"id": 15451, "synset": "colossus.n.02", "name": "colossus"}, - {"id": 15452, "synset": "comedian.n.02", "name": "comedian"}, - {"id": 15453, "synset": "comedienne.n.02", "name": "comedienne"}, - {"id": 15454, "synset": "comer.n.01", "name": "comer"}, - {"id": 15455, "synset": "commander.n.03", "name": "commander"}, - {"id": 15456, "synset": "commander_in_chief.n.01", "name": "commander_in_chief"}, - {"id": 15457, "synset": "commanding_officer.n.01", "name": "commanding_officer"}, - {"id": 15458, "synset": "commissar.n.01", "name": "commissar"}, - {"id": 15459, "synset": "commissioned_officer.n.01", "name": "commissioned_officer"}, - { - "id": 15460, - "synset": "commissioned_military_officer.n.01", - "name": "commissioned_military_officer", - }, - {"id": 15461, "synset": "commissioner.n.01", "name": "commissioner"}, - {"id": 15462, "synset": "commissioner.n.02", "name": "commissioner"}, - {"id": 15463, "synset": "committee_member.n.01", "name": "committee_member"}, - {"id": 15464, "synset": "committeewoman.n.01", "name": "committeewoman"}, - {"id": 15465, "synset": "commodore.n.01", "name": "commodore"}, - {"id": 15466, "synset": "communicant.n.01", "name": "communicant"}, - {"id": 15467, "synset": "communist.n.02", "name": "communist"}, - {"id": 15468, "synset": "communist.n.01", "name": "Communist"}, - {"id": 15469, "synset": "commuter.n.02", "name": "commuter"}, - {"id": 15470, "synset": "compere.n.01", "name": "compere"}, - {"id": 15471, "synset": "complexifier.n.01", "name": "complexifier"}, - {"id": 15472, "synset": "compulsive.n.01", "name": "compulsive"}, - {"id": 15473, "synset": "computational_linguist.n.01", "name": "computational_linguist"}, - {"id": 15474, "synset": "computer_scientist.n.01", "name": "computer_scientist"}, - {"id": 15475, "synset": "computer_user.n.01", "name": "computer_user"}, - {"id": 15476, "synset": "comrade.n.02", "name": "Comrade"}, - {"id": 15477, "synset": "concert-goer.n.01", "name": "concert-goer"}, - {"id": 15478, "synset": "conciliator.n.01", "name": "conciliator"}, - {"id": 15479, "synset": "conductor.n.03", "name": "conductor"}, - {"id": 15480, "synset": "confectioner.n.01", "name": "confectioner"}, - {"id": 15481, "synset": "confederate.n.01", "name": "Confederate"}, - {"id": 15482, "synset": "confessor.n.01", "name": "confessor"}, - {"id": 15483, "synset": "confidant.n.01", "name": "confidant"}, - {"id": 15484, "synset": "confucian.n.01", "name": "Confucian"}, - {"id": 15485, "synset": "rep.n.01", "name": "rep"}, - {"id": 15486, "synset": "conqueror.n.01", "name": "conqueror"}, - {"id": 15487, "synset": "conservative.n.02", "name": "Conservative"}, - {"id": 15488, "synset": "nonconformist.n.01", "name": "Nonconformist"}, - {"id": 15489, "synset": "anglican.n.01", "name": "Anglican"}, - {"id": 15490, "synset": "consignee.n.01", "name": "consignee"}, - {"id": 15491, "synset": "consigner.n.01", "name": "consigner"}, - {"id": 15492, "synset": "constable.n.01", "name": "constable"}, - {"id": 15493, "synset": "constructivist.n.01", "name": "constructivist"}, - {"id": 15494, "synset": "contractor.n.01", "name": "contractor"}, - {"id": 15495, "synset": "contralto.n.01", "name": "contralto"}, - {"id": 15496, "synset": "contributor.n.02", "name": "contributor"}, - {"id": 15497, "synset": "control_freak.n.01", "name": "control_freak"}, - {"id": 15498, "synset": "convalescent.n.01", "name": "convalescent"}, - {"id": 15499, "synset": "convener.n.01", "name": "convener"}, - {"id": 15500, "synset": "convict.n.01", "name": "convict"}, - {"id": 15501, "synset": "copilot.n.01", "name": "copilot"}, - {"id": 15502, "synset": "copycat.n.01", "name": "copycat"}, - {"id": 15503, "synset": "coreligionist.n.01", "name": "coreligionist"}, - {"id": 15504, "synset": "cornerback.n.01", "name": "cornerback"}, - {"id": 15505, "synset": "corporatist.n.01", "name": "corporatist"}, - {"id": 15506, "synset": "correspondent.n.01", "name": "correspondent"}, - {"id": 15507, "synset": "cosmetician.n.01", "name": "cosmetician"}, - {"id": 15508, "synset": "cosmopolitan.n.01", "name": "cosmopolitan"}, - {"id": 15509, "synset": "cossack.n.01", "name": "Cossack"}, - {"id": 15510, "synset": "cost_accountant.n.01", "name": "cost_accountant"}, - {"id": 15511, "synset": "co-star.n.01", "name": "co-star"}, - {"id": 15512, "synset": "costumier.n.01", "name": "costumier"}, - {"id": 15513, "synset": "cotter.n.02", "name": "cotter"}, - {"id": 15514, "synset": "cotter.n.01", "name": "cotter"}, - {"id": 15515, "synset": "counselor.n.01", "name": "counselor"}, - {"id": 15516, "synset": "counterterrorist.n.01", "name": "counterterrorist"}, - {"id": 15517, "synset": "counterspy.n.01", "name": "counterspy"}, - {"id": 15518, "synset": "countess.n.01", "name": "countess"}, - {"id": 15519, "synset": "compromiser.n.01", "name": "compromiser"}, - {"id": 15520, "synset": "countrywoman.n.01", "name": "countrywoman"}, - {"id": 15521, "synset": "county_agent.n.01", "name": "county_agent"}, - {"id": 15522, "synset": "courtier.n.01", "name": "courtier"}, - {"id": 15523, "synset": "cousin.n.01", "name": "cousin"}, - {"id": 15524, "synset": "cover_girl.n.01", "name": "cover_girl"}, - {"id": 15525, "synset": "cow.n.03", "name": "cow"}, - {"id": 15526, "synset": "craftsman.n.03", "name": "craftsman"}, - {"id": 15527, "synset": "craftsman.n.02", "name": "craftsman"}, - {"id": 15528, "synset": "crapshooter.n.01", "name": "crapshooter"}, - {"id": 15529, "synset": "crazy.n.01", "name": "crazy"}, - {"id": 15530, "synset": "creature.n.02", "name": "creature"}, - {"id": 15531, "synset": "creditor.n.01", "name": "creditor"}, - {"id": 15532, "synset": "creep.n.01", "name": "creep"}, - {"id": 15533, "synset": "criminologist.n.01", "name": "criminologist"}, - {"id": 15534, "synset": "critic.n.02", "name": "critic"}, - {"id": 15535, "synset": "croesus.n.02", "name": "Croesus"}, - {"id": 15536, "synset": "cross-examiner.n.01", "name": "cross-examiner"}, - {"id": 15537, "synset": "crossover_voter.n.01", "name": "crossover_voter"}, - {"id": 15538, "synset": "croupier.n.01", "name": "croupier"}, - {"id": 15539, "synset": "crown_prince.n.01", "name": "crown_prince"}, - {"id": 15540, "synset": "crown_princess.n.01", "name": "crown_princess"}, - {"id": 15541, "synset": "cryptanalyst.n.01", "name": "cryptanalyst"}, - {"id": 15542, "synset": "cub_scout.n.01", "name": "Cub_Scout"}, - {"id": 15543, "synset": "cuckold.n.01", "name": "cuckold"}, - {"id": 15544, "synset": "cultist.n.02", "name": "cultist"}, - {"id": 15545, "synset": "curandera.n.01", "name": "curandera"}, - {"id": 15546, "synset": "curate.n.01", "name": "curate"}, - {"id": 15547, "synset": "curator.n.01", "name": "curator"}, - {"id": 15548, "synset": "customer_agent.n.01", "name": "customer_agent"}, - {"id": 15549, "synset": "cutter.n.02", "name": "cutter"}, - {"id": 15550, "synset": "cyberpunk.n.02", "name": "cyberpunk"}, - {"id": 15551, "synset": "cyborg.n.01", "name": "cyborg"}, - {"id": 15552, "synset": "cymbalist.n.01", "name": "cymbalist"}, - {"id": 15553, "synset": "cynic.n.02", "name": "Cynic"}, - {"id": 15554, "synset": "cytogeneticist.n.01", "name": "cytogeneticist"}, - {"id": 15555, "synset": "cytologist.n.01", "name": "cytologist"}, - {"id": 15556, "synset": "czar.n.02", "name": "czar"}, - {"id": 15557, "synset": "czar.n.01", "name": "czar"}, - {"id": 15558, "synset": "dad.n.01", "name": "dad"}, - {"id": 15559, "synset": "dairyman.n.02", "name": "dairyman"}, - {"id": 15560, "synset": "dalai_lama.n.01", "name": "Dalai_Lama"}, - {"id": 15561, "synset": "dallier.n.01", "name": "dallier"}, - {"id": 15562, "synset": "dancer.n.01", "name": "dancer"}, - {"id": 15563, "synset": "dancer.n.02", "name": "dancer"}, - {"id": 15564, "synset": "clog_dancer.n.01", "name": "clog_dancer"}, - {"id": 15565, "synset": "dancing-master.n.01", "name": "dancing-master"}, - {"id": 15566, "synset": "dark_horse.n.01", "name": "dark_horse"}, - {"id": 15567, "synset": "darling.n.01", "name": "darling"}, - {"id": 15568, "synset": "date.n.02", "name": "date"}, - {"id": 15569, "synset": "daughter.n.01", "name": "daughter"}, - {"id": 15570, "synset": "dawdler.n.01", "name": "dawdler"}, - {"id": 15571, "synset": "day_boarder.n.01", "name": "day_boarder"}, - {"id": 15572, "synset": "day_laborer.n.01", "name": "day_laborer"}, - {"id": 15573, "synset": "deacon.n.01", "name": "deacon"}, - {"id": 15574, "synset": "deaconess.n.01", "name": "deaconess"}, - {"id": 15575, "synset": "deadeye.n.01", "name": "deadeye"}, - {"id": 15576, "synset": "deipnosophist.n.01", "name": "deipnosophist"}, - {"id": 15577, "synset": "dropout.n.02", "name": "dropout"}, - {"id": 15578, "synset": "deadhead.n.01", "name": "deadhead"}, - {"id": 15579, "synset": "deaf_person.n.01", "name": "deaf_person"}, - {"id": 15580, "synset": "debtor.n.01", "name": "debtor"}, - {"id": 15581, "synset": "deckhand.n.01", "name": "deckhand"}, - {"id": 15582, "synset": "defamer.n.01", "name": "defamer"}, - {"id": 15583, "synset": "defense_contractor.n.01", "name": "defense_contractor"}, - {"id": 15584, "synset": "deist.n.01", "name": "deist"}, - {"id": 15585, "synset": "delegate.n.01", "name": "delegate"}, - {"id": 15586, "synset": "deliveryman.n.01", "name": "deliveryman"}, - {"id": 15587, "synset": "demagogue.n.01", "name": "demagogue"}, - {"id": 15588, "synset": "demigod.n.01", "name": "demigod"}, - {"id": 15589, "synset": "demographer.n.01", "name": "demographer"}, - {"id": 15590, "synset": "demonstrator.n.03", "name": "demonstrator"}, - {"id": 15591, "synset": "den_mother.n.02", "name": "den_mother"}, - {"id": 15592, "synset": "department_head.n.01", "name": "department_head"}, - {"id": 15593, "synset": "depositor.n.01", "name": "depositor"}, - {"id": 15594, "synset": "deputy.n.03", "name": "deputy"}, - {"id": 15595, "synset": "dermatologist.n.01", "name": "dermatologist"}, - {"id": 15596, "synset": "descender.n.01", "name": "descender"}, - {"id": 15597, "synset": "designated_hitter.n.01", "name": "designated_hitter"}, - {"id": 15598, "synset": "designer.n.04", "name": "designer"}, - {"id": 15599, "synset": "desk_clerk.n.01", "name": "desk_clerk"}, - {"id": 15600, "synset": "desk_officer.n.01", "name": "desk_officer"}, - {"id": 15601, "synset": "desk_sergeant.n.01", "name": "desk_sergeant"}, - {"id": 15602, "synset": "detainee.n.01", "name": "detainee"}, - {"id": 15603, "synset": "detective.n.01", "name": "detective"}, - {"id": 15604, "synset": "detective.n.02", "name": "detective"}, - {"id": 15605, "synset": "detractor.n.01", "name": "detractor"}, - {"id": 15606, "synset": "developer.n.01", "name": "developer"}, - {"id": 15607, "synset": "deviationist.n.01", "name": "deviationist"}, - {"id": 15608, "synset": "devisee.n.01", "name": "devisee"}, - {"id": 15609, "synset": "devisor.n.01", "name": "devisor"}, - {"id": 15610, "synset": "devourer.n.01", "name": "devourer"}, - {"id": 15611, "synset": "dialectician.n.01", "name": "dialectician"}, - {"id": 15612, "synset": "diarist.n.01", "name": "diarist"}, - {"id": 15613, "synset": "dietician.n.01", "name": "dietician"}, - {"id": 15614, "synset": "diocesan.n.01", "name": "diocesan"}, - {"id": 15615, "synset": "director.n.03", "name": "director"}, - {"id": 15616, "synset": "director.n.02", "name": "director"}, - {"id": 15617, "synset": "dirty_old_man.n.01", "name": "dirty_old_man"}, - {"id": 15618, "synset": "disbeliever.n.01", "name": "disbeliever"}, - {"id": 15619, "synset": "disk_jockey.n.01", "name": "disk_jockey"}, - {"id": 15620, "synset": "dispatcher.n.02", "name": "dispatcher"}, - {"id": 15621, "synset": "distortionist.n.01", "name": "distortionist"}, - {"id": 15622, "synset": "distributor.n.01", "name": "distributor"}, - {"id": 15623, "synset": "district_attorney.n.01", "name": "district_attorney"}, - {"id": 15624, "synset": "district_manager.n.01", "name": "district_manager"}, - {"id": 15625, "synset": "diver.n.02", "name": "diver"}, - {"id": 15626, "synset": "divorcee.n.01", "name": "divorcee"}, - {"id": 15627, "synset": "ex-wife.n.01", "name": "ex-wife"}, - {"id": 15628, "synset": "divorce_lawyer.n.01", "name": "divorce_lawyer"}, - {"id": 15629, "synset": "docent.n.01", "name": "docent"}, - {"id": 15630, "synset": "doctor.n.01", "name": "doctor"}, - {"id": 15631, "synset": "dodo.n.01", "name": "dodo"}, - {"id": 15632, "synset": "doge.n.01", "name": "doge"}, - {"id": 15633, "synset": "dog_in_the_manger.n.01", "name": "dog_in_the_manger"}, - {"id": 15634, "synset": "dogmatist.n.01", "name": "dogmatist"}, - {"id": 15635, "synset": "dolichocephalic.n.01", "name": "dolichocephalic"}, - {"id": 15636, "synset": "domestic_partner.n.01", "name": "domestic_partner"}, - {"id": 15637, "synset": "dominican.n.02", "name": "Dominican"}, - {"id": 15638, "synset": "dominus.n.01", "name": "dominus"}, - {"id": 15639, "synset": "don.n.03", "name": "don"}, - {"id": 15640, "synset": "donatist.n.01", "name": "Donatist"}, - {"id": 15641, "synset": "donna.n.01", "name": "donna"}, - {"id": 15642, "synset": "dosser.n.01", "name": "dosser"}, - {"id": 15643, "synset": "double.n.03", "name": "double"}, - {"id": 15644, "synset": "double-crosser.n.01", "name": "double-crosser"}, - {"id": 15645, "synset": "down-and-out.n.01", "name": "down-and-out"}, - {"id": 15646, "synset": "doyenne.n.01", "name": "doyenne"}, - {"id": 15647, "synset": "draftsman.n.02", "name": "draftsman"}, - {"id": 15648, "synset": "dramatist.n.01", "name": "dramatist"}, - {"id": 15649, "synset": "dreamer.n.01", "name": "dreamer"}, - {"id": 15650, "synset": "dressmaker.n.01", "name": "dressmaker"}, - {"id": 15651, "synset": "dressmaker's_model.n.01", "name": "dressmaker's_model"}, - {"id": 15652, "synset": "dribbler.n.02", "name": "dribbler"}, - {"id": 15653, "synset": "dribbler.n.01", "name": "dribbler"}, - {"id": 15654, "synset": "drinker.n.02", "name": "drinker"}, - {"id": 15655, "synset": "drinker.n.01", "name": "drinker"}, - {"id": 15656, "synset": "drug_addict.n.01", "name": "drug_addict"}, - {"id": 15657, "synset": "drug_user.n.01", "name": "drug_user"}, - {"id": 15658, "synset": "druid.n.01", "name": "Druid"}, - {"id": 15659, "synset": "drum_majorette.n.02", "name": "drum_majorette"}, - {"id": 15660, "synset": "drummer.n.01", "name": "drummer"}, - {"id": 15661, "synset": "drunk.n.02", "name": "drunk"}, - {"id": 15662, "synset": "drunkard.n.01", "name": "drunkard"}, - {"id": 15663, "synset": "druze.n.01", "name": "Druze"}, - {"id": 15664, "synset": "dry.n.01", "name": "dry"}, - {"id": 15665, "synset": "dry_nurse.n.01", "name": "dry_nurse"}, - {"id": 15666, "synset": "duchess.n.01", "name": "duchess"}, - {"id": 15667, "synset": "duke.n.01", "name": "duke"}, - {"id": 15668, "synset": "duffer.n.01", "name": "duffer"}, - {"id": 15669, "synset": "dunker.n.02", "name": "dunker"}, - {"id": 15670, "synset": "dutch_uncle.n.01", "name": "Dutch_uncle"}, - {"id": 15671, "synset": "dyspeptic.n.01", "name": "dyspeptic"}, - {"id": 15672, "synset": "eager_beaver.n.01", "name": "eager_beaver"}, - {"id": 15673, "synset": "earl.n.01", "name": "earl"}, - {"id": 15674, "synset": "earner.n.01", "name": "earner"}, - {"id": 15675, "synset": "eavesdropper.n.01", "name": "eavesdropper"}, - {"id": 15676, "synset": "eccentric.n.01", "name": "eccentric"}, - {"id": 15677, "synset": "eclectic.n.01", "name": "eclectic"}, - {"id": 15678, "synset": "econometrician.n.01", "name": "econometrician"}, - {"id": 15679, "synset": "economist.n.01", "name": "economist"}, - {"id": 15680, "synset": "ectomorph.n.01", "name": "ectomorph"}, - {"id": 15681, "synset": "editor.n.01", "name": "editor"}, - {"id": 15682, "synset": "egocentric.n.01", "name": "egocentric"}, - {"id": 15683, "synset": "egotist.n.01", "name": "egotist"}, - {"id": 15684, "synset": "ejaculator.n.01", "name": "ejaculator"}, - {"id": 15685, "synset": "elder.n.03", "name": "elder"}, - {"id": 15686, "synset": "elder_statesman.n.01", "name": "elder_statesman"}, - {"id": 15687, "synset": "elected_official.n.01", "name": "elected_official"}, - {"id": 15688, "synset": "electrician.n.01", "name": "electrician"}, - {"id": 15689, "synset": "elegist.n.01", "name": "elegist"}, - {"id": 15690, "synset": "elocutionist.n.01", "name": "elocutionist"}, - {"id": 15691, "synset": "emancipator.n.01", "name": "emancipator"}, - {"id": 15692, "synset": "embryologist.n.01", "name": "embryologist"}, - {"id": 15693, "synset": "emeritus.n.01", "name": "emeritus"}, - {"id": 15694, "synset": "emigrant.n.01", "name": "emigrant"}, - {"id": 15695, "synset": "emissary.n.01", "name": "emissary"}, - {"id": 15696, "synset": "empress.n.01", "name": "empress"}, - {"id": 15697, "synset": "employee.n.01", "name": "employee"}, - {"id": 15698, "synset": "employer.n.01", "name": "employer"}, - {"id": 15699, "synset": "enchantress.n.02", "name": "enchantress"}, - {"id": 15700, "synset": "enchantress.n.01", "name": "enchantress"}, - {"id": 15701, "synset": "encyclopedist.n.01", "name": "encyclopedist"}, - {"id": 15702, "synset": "endomorph.n.01", "name": "endomorph"}, - {"id": 15703, "synset": "enemy.n.02", "name": "enemy"}, - {"id": 15704, "synset": "energizer.n.01", "name": "energizer"}, - {"id": 15705, "synset": "end_man.n.02", "name": "end_man"}, - {"id": 15706, "synset": "end_man.n.01", "name": "end_man"}, - {"id": 15707, "synset": "endorser.n.02", "name": "endorser"}, - {"id": 15708, "synset": "enjoyer.n.01", "name": "enjoyer"}, - {"id": 15709, "synset": "enlisted_woman.n.01", "name": "enlisted_woman"}, - {"id": 15710, "synset": "enophile.n.01", "name": "enophile"}, - {"id": 15711, "synset": "entrant.n.04", "name": "entrant"}, - {"id": 15712, "synset": "entrant.n.03", "name": "entrant"}, - {"id": 15713, "synset": "entrepreneur.n.01", "name": "entrepreneur"}, - {"id": 15714, "synset": "envoy.n.01", "name": "envoy"}, - {"id": 15715, "synset": "enzymologist.n.01", "name": "enzymologist"}, - {"id": 15716, "synset": "eparch.n.01", "name": "eparch"}, - {"id": 15717, "synset": "epidemiologist.n.01", "name": "epidemiologist"}, - {"id": 15718, "synset": "epigone.n.01", "name": "epigone"}, - {"id": 15719, "synset": "epileptic.n.01", "name": "epileptic"}, - {"id": 15720, "synset": "episcopalian.n.01", "name": "Episcopalian"}, - {"id": 15721, "synset": "equerry.n.02", "name": "equerry"}, - {"id": 15722, "synset": "equerry.n.01", "name": "equerry"}, - {"id": 15723, "synset": "erotic.n.01", "name": "erotic"}, - {"id": 15724, "synset": "escapee.n.01", "name": "escapee"}, - {"id": 15725, "synset": "escapist.n.01", "name": "escapist"}, - {"id": 15726, "synset": "eskimo.n.01", "name": "Eskimo"}, - {"id": 15727, "synset": "espionage_agent.n.01", "name": "espionage_agent"}, - {"id": 15728, "synset": "esthetician.n.01", "name": "esthetician"}, - {"id": 15729, "synset": "etcher.n.01", "name": "etcher"}, - {"id": 15730, "synset": "ethnologist.n.01", "name": "ethnologist"}, - {"id": 15731, "synset": "etonian.n.01", "name": "Etonian"}, - {"id": 15732, "synset": "etymologist.n.01", "name": "etymologist"}, - {"id": 15733, "synset": "evangelist.n.01", "name": "evangelist"}, - {"id": 15734, "synset": "evangelist.n.02", "name": "Evangelist"}, - {"id": 15735, "synset": "event_planner.n.01", "name": "event_planner"}, - {"id": 15736, "synset": "examiner.n.02", "name": "examiner"}, - {"id": 15737, "synset": "examiner.n.01", "name": "examiner"}, - {"id": 15738, "synset": "exarch.n.03", "name": "exarch"}, - {"id": 15739, "synset": "executant.n.01", "name": "executant"}, - {"id": 15740, "synset": "executive_secretary.n.01", "name": "executive_secretary"}, - {"id": 15741, "synset": "executive_vice_president.n.01", "name": "executive_vice_president"}, - {"id": 15742, "synset": "executrix.n.01", "name": "executrix"}, - {"id": 15743, "synset": "exegete.n.01", "name": "exegete"}, - {"id": 15744, "synset": "exhibitor.n.01", "name": "exhibitor"}, - {"id": 15745, "synset": "exhibitionist.n.02", "name": "exhibitionist"}, - {"id": 15746, "synset": "exile.n.01", "name": "exile"}, - {"id": 15747, "synset": "existentialist.n.01", "name": "existentialist"}, - {"id": 15748, "synset": "exorcist.n.02", "name": "exorcist"}, - {"id": 15749, "synset": "ex-spouse.n.01", "name": "ex-spouse"}, - {"id": 15750, "synset": "extern.n.01", "name": "extern"}, - {"id": 15751, "synset": "extremist.n.01", "name": "extremist"}, - {"id": 15752, "synset": "extrovert.n.01", "name": "extrovert"}, - {"id": 15753, "synset": "eyewitness.n.01", "name": "eyewitness"}, - {"id": 15754, "synset": "facilitator.n.01", "name": "facilitator"}, - {"id": 15755, "synset": "fairy_godmother.n.01", "name": "fairy_godmother"}, - {"id": 15756, "synset": "falangist.n.01", "name": "falangist"}, - {"id": 15757, "synset": "falconer.n.01", "name": "falconer"}, - {"id": 15758, "synset": "falsifier.n.01", "name": "falsifier"}, - {"id": 15759, "synset": "familiar.n.01", "name": "familiar"}, - {"id": 15760, "synset": "fan.n.03", "name": "fan"}, - {"id": 15761, "synset": "fanatic.n.01", "name": "fanatic"}, - {"id": 15762, "synset": "fancier.n.01", "name": "fancier"}, - {"id": 15763, "synset": "farm_boy.n.01", "name": "farm_boy"}, - {"id": 15764, "synset": "farmer.n.01", "name": "farmer"}, - {"id": 15765, "synset": "farmhand.n.01", "name": "farmhand"}, - {"id": 15766, "synset": "fascist.n.01", "name": "fascist"}, - {"id": 15767, "synset": "fascista.n.01", "name": "fascista"}, - {"id": 15768, "synset": "fatalist.n.01", "name": "fatalist"}, - {"id": 15769, "synset": "father.n.01", "name": "father"}, - {"id": 15770, "synset": "father.n.03", "name": "Father"}, - {"id": 15771, "synset": "father-figure.n.01", "name": "father-figure"}, - {"id": 15772, "synset": "father-in-law.n.01", "name": "father-in-law"}, - {"id": 15773, "synset": "fauntleroy.n.01", "name": "Fauntleroy"}, - {"id": 15774, "synset": "fauve.n.01", "name": "Fauve"}, - {"id": 15775, "synset": "favorite_son.n.01", "name": "favorite_son"}, - {"id": 15776, "synset": "featherweight.n.03", "name": "featherweight"}, - {"id": 15777, "synset": "federalist.n.02", "name": "federalist"}, - {"id": 15778, "synset": "fellow_traveler.n.01", "name": "fellow_traveler"}, - {"id": 15779, "synset": "female_aristocrat.n.01", "name": "female_aristocrat"}, - {"id": 15780, "synset": "female_offspring.n.01", "name": "female_offspring"}, - {"id": 15781, "synset": "female_child.n.01", "name": "female_child"}, - {"id": 15782, "synset": "fence.n.02", "name": "fence"}, - {"id": 15783, "synset": "fiance.n.01", "name": "fiance"}, - {"id": 15784, "synset": "fielder.n.02", "name": "fielder"}, - {"id": 15785, "synset": "field_judge.n.01", "name": "field_judge"}, - {"id": 15786, "synset": "fighter_pilot.n.01", "name": "fighter_pilot"}, - {"id": 15787, "synset": "filer.n.01", "name": "filer"}, - {"id": 15788, "synset": "film_director.n.01", "name": "film_director"}, - {"id": 15789, "synset": "finder.n.01", "name": "finder"}, - {"id": 15790, "synset": "fire_chief.n.01", "name": "fire_chief"}, - {"id": 15791, "synset": "fire-eater.n.03", "name": "fire-eater"}, - {"id": 15792, "synset": "fire-eater.n.02", "name": "fire-eater"}, - {"id": 15793, "synset": "fireman.n.04", "name": "fireman"}, - {"id": 15794, "synset": "fire_marshall.n.01", "name": "fire_marshall"}, - {"id": 15795, "synset": "fire_walker.n.01", "name": "fire_walker"}, - {"id": 15796, "synset": "first_baseman.n.01", "name": "first_baseman"}, - {"id": 15797, "synset": "firstborn.n.01", "name": "firstborn"}, - {"id": 15798, "synset": "first_lady.n.02", "name": "first_lady"}, - {"id": 15799, "synset": "first_lieutenant.n.01", "name": "first_lieutenant"}, - {"id": 15800, "synset": "first_offender.n.01", "name": "first_offender"}, - {"id": 15801, "synset": "first_sergeant.n.01", "name": "first_sergeant"}, - {"id": 15802, "synset": "fishmonger.n.01", "name": "fishmonger"}, - {"id": 15803, "synset": "flagellant.n.02", "name": "flagellant"}, - {"id": 15804, "synset": "flag_officer.n.01", "name": "flag_officer"}, - {"id": 15805, "synset": "flak_catcher.n.01", "name": "flak_catcher"}, - {"id": 15806, "synset": "flanker_back.n.01", "name": "flanker_back"}, - {"id": 15807, "synset": "flapper.n.01", "name": "flapper"}, - {"id": 15808, "synset": "flatmate.n.01", "name": "flatmate"}, - {"id": 15809, "synset": "flatterer.n.01", "name": "flatterer"}, - {"id": 15810, "synset": "flibbertigibbet.n.01", "name": "flibbertigibbet"}, - {"id": 15811, "synset": "flight_surgeon.n.01", "name": "flight_surgeon"}, - {"id": 15812, "synset": "floorwalker.n.01", "name": "floorwalker"}, - {"id": 15813, "synset": "flop.n.02", "name": "flop"}, - {"id": 15814, "synset": "florentine.n.01", "name": "Florentine"}, - {"id": 15815, "synset": "flower_girl.n.02", "name": "flower_girl"}, - {"id": 15816, "synset": "flower_girl.n.01", "name": "flower_girl"}, - {"id": 15817, "synset": "flutist.n.01", "name": "flutist"}, - {"id": 15818, "synset": "fly-by-night.n.01", "name": "fly-by-night"}, - {"id": 15819, "synset": "flyweight.n.02", "name": "flyweight"}, - {"id": 15820, "synset": "flyweight.n.01", "name": "flyweight"}, - {"id": 15821, "synset": "foe.n.02", "name": "foe"}, - {"id": 15822, "synset": "folk_dancer.n.01", "name": "folk_dancer"}, - {"id": 15823, "synset": "folk_poet.n.01", "name": "folk_poet"}, - {"id": 15824, "synset": "follower.n.01", "name": "follower"}, - {"id": 15825, "synset": "football_hero.n.01", "name": "football_hero"}, - {"id": 15826, "synset": "football_player.n.01", "name": "football_player"}, - {"id": 15827, "synset": "footman.n.01", "name": "footman"}, - {"id": 15828, "synset": "forefather.n.01", "name": "forefather"}, - {"id": 15829, "synset": "foremother.n.01", "name": "foremother"}, - {"id": 15830, "synset": "foreign_agent.n.01", "name": "foreign_agent"}, - {"id": 15831, "synset": "foreigner.n.02", "name": "foreigner"}, - {"id": 15832, "synset": "boss.n.03", "name": "boss"}, - {"id": 15833, "synset": "foreman.n.02", "name": "foreman"}, - {"id": 15834, "synset": "forester.n.02", "name": "forester"}, - {"id": 15835, "synset": "forewoman.n.02", "name": "forewoman"}, - {"id": 15836, "synset": "forger.n.02", "name": "forger"}, - {"id": 15837, "synset": "forward.n.01", "name": "forward"}, - {"id": 15838, "synset": "foster-brother.n.01", "name": "foster-brother"}, - {"id": 15839, "synset": "foster-father.n.01", "name": "foster-father"}, - {"id": 15840, "synset": "foster-mother.n.01", "name": "foster-mother"}, - {"id": 15841, "synset": "foster-sister.n.01", "name": "foster-sister"}, - {"id": 15842, "synset": "foster-son.n.01", "name": "foster-son"}, - {"id": 15843, "synset": "founder.n.02", "name": "founder"}, - {"id": 15844, "synset": "foundress.n.01", "name": "foundress"}, - {"id": 15845, "synset": "four-minute_man.n.01", "name": "four-minute_man"}, - {"id": 15846, "synset": "framer.n.02", "name": "framer"}, - {"id": 15847, "synset": "francophobe.n.01", "name": "Francophobe"}, - {"id": 15848, "synset": "freak.n.01", "name": "freak"}, - {"id": 15849, "synset": "free_agent.n.02", "name": "free_agent"}, - {"id": 15850, "synset": "free_agent.n.01", "name": "free_agent"}, - {"id": 15851, "synset": "freedom_rider.n.01", "name": "freedom_rider"}, - {"id": 15852, "synset": "free-liver.n.01", "name": "free-liver"}, - {"id": 15853, "synset": "freeloader.n.01", "name": "freeloader"}, - {"id": 15854, "synset": "free_trader.n.01", "name": "free_trader"}, - {"id": 15855, "synset": "freudian.n.01", "name": "Freudian"}, - {"id": 15856, "synset": "friar.n.01", "name": "friar"}, - {"id": 15857, "synset": "monk.n.01", "name": "monk"}, - {"id": 15858, "synset": "frontierswoman.n.01", "name": "frontierswoman"}, - {"id": 15859, "synset": "front_man.n.01", "name": "front_man"}, - {"id": 15860, "synset": "frotteur.n.01", "name": "frotteur"}, - {"id": 15861, "synset": "fucker.n.02", "name": "fucker"}, - {"id": 15862, "synset": "fucker.n.01", "name": "fucker"}, - {"id": 15863, "synset": "fuddy-duddy.n.01", "name": "fuddy-duddy"}, - {"id": 15864, "synset": "fullback.n.01", "name": "fullback"}, - {"id": 15865, "synset": "funambulist.n.01", "name": "funambulist"}, - {"id": 15866, "synset": "fundamentalist.n.01", "name": "fundamentalist"}, - {"id": 15867, "synset": "fundraiser.n.01", "name": "fundraiser"}, - {"id": 15868, "synset": "futurist.n.01", "name": "futurist"}, - {"id": 15869, "synset": "gadgeteer.n.01", "name": "gadgeteer"}, - {"id": 15870, "synset": "gagman.n.02", "name": "gagman"}, - {"id": 15871, "synset": "gagman.n.01", "name": "gagman"}, - {"id": 15872, "synset": "gainer.n.01", "name": "gainer"}, - {"id": 15873, "synset": "gal.n.03", "name": "gal"}, - {"id": 15874, "synset": "galoot.n.01", "name": "galoot"}, - {"id": 15875, "synset": "gambist.n.01", "name": "gambist"}, - {"id": 15876, "synset": "gambler.n.01", "name": "gambler"}, - {"id": 15877, "synset": "gamine.n.02", "name": "gamine"}, - {"id": 15878, "synset": "garbage_man.n.01", "name": "garbage_man"}, - {"id": 15879, "synset": "gardener.n.02", "name": "gardener"}, - {"id": 15880, "synset": "garment_cutter.n.01", "name": "garment_cutter"}, - {"id": 15881, "synset": "garroter.n.01", "name": "garroter"}, - {"id": 15882, "synset": "gasman.n.01", "name": "gasman"}, - {"id": 15883, "synset": "gastroenterologist.n.01", "name": "gastroenterologist"}, - {"id": 15884, "synset": "gatherer.n.01", "name": "gatherer"}, - {"id": 15885, "synset": "gawker.n.01", "name": "gawker"}, - {"id": 15886, "synset": "gendarme.n.01", "name": "gendarme"}, - {"id": 15887, "synset": "general.n.01", "name": "general"}, - {"id": 15888, "synset": "generator.n.03", "name": "generator"}, - {"id": 15889, "synset": "geneticist.n.01", "name": "geneticist"}, - {"id": 15890, "synset": "genitor.n.01", "name": "genitor"}, - {"id": 15891, "synset": "gent.n.01", "name": "gent"}, - {"id": 15892, "synset": "geologist.n.01", "name": "geologist"}, - {"id": 15893, "synset": "geophysicist.n.01", "name": "geophysicist"}, - {"id": 15894, "synset": "ghostwriter.n.01", "name": "ghostwriter"}, - {"id": 15895, "synset": "gibson_girl.n.01", "name": "Gibson_girl"}, - {"id": 15896, "synset": "girl.n.01", "name": "girl"}, - {"id": 15897, "synset": "girlfriend.n.02", "name": "girlfriend"}, - {"id": 15898, "synset": "girlfriend.n.01", "name": "girlfriend"}, - {"id": 15899, "synset": "girl_wonder.n.01", "name": "girl_wonder"}, - {"id": 15900, "synset": "girondist.n.01", "name": "Girondist"}, - {"id": 15901, "synset": "gitano.n.01", "name": "gitano"}, - {"id": 15902, "synset": "gladiator.n.01", "name": "gladiator"}, - {"id": 15903, "synset": "glassblower.n.01", "name": "glassblower"}, - {"id": 15904, "synset": "gleaner.n.02", "name": "gleaner"}, - {"id": 15905, "synset": "goat_herder.n.01", "name": "goat_herder"}, - {"id": 15906, "synset": "godchild.n.01", "name": "godchild"}, - {"id": 15907, "synset": "godfather.n.01", "name": "godfather"}, - {"id": 15908, "synset": "godparent.n.01", "name": "godparent"}, - {"id": 15909, "synset": "godson.n.01", "name": "godson"}, - {"id": 15910, "synset": "gofer.n.01", "name": "gofer"}, - {"id": 15911, "synset": "goffer.n.01", "name": "goffer"}, - {"id": 15912, "synset": "goldsmith.n.01", "name": "goldsmith"}, - {"id": 15913, "synset": "golfer.n.01", "name": "golfer"}, - {"id": 15914, "synset": "gondolier.n.01", "name": "gondolier"}, - {"id": 15915, "synset": "good_guy.n.01", "name": "good_guy"}, - {"id": 15916, "synset": "good_old_boy.n.01", "name": "good_old_boy"}, - {"id": 15917, "synset": "good_samaritan.n.01", "name": "good_Samaritan"}, - {"id": 15918, "synset": "gossip_columnist.n.01", "name": "gossip_columnist"}, - {"id": 15919, "synset": "gouger.n.01", "name": "gouger"}, - {"id": 15920, "synset": "governor_general.n.01", "name": "governor_general"}, - {"id": 15921, "synset": "grabber.n.01", "name": "grabber"}, - {"id": 15922, "synset": "grader.n.01", "name": "grader"}, - {"id": 15923, "synset": "graduate_nurse.n.01", "name": "graduate_nurse"}, - {"id": 15924, "synset": "grammarian.n.01", "name": "grammarian"}, - {"id": 15925, "synset": "granddaughter.n.01", "name": "granddaughter"}, - {"id": 15926, "synset": "grande_dame.n.01", "name": "grande_dame"}, - {"id": 15927, "synset": "grandfather.n.01", "name": "grandfather"}, - {"id": 15928, "synset": "grand_inquisitor.n.01", "name": "Grand_Inquisitor"}, - {"id": 15929, "synset": "grandma.n.01", "name": "grandma"}, - {"id": 15930, "synset": "grandmaster.n.01", "name": "grandmaster"}, - {"id": 15931, "synset": "grandparent.n.01", "name": "grandparent"}, - {"id": 15932, "synset": "grantee.n.01", "name": "grantee"}, - {"id": 15933, "synset": "granter.n.01", "name": "granter"}, - {"id": 15934, "synset": "grass_widower.n.01", "name": "grass_widower"}, - {"id": 15935, "synset": "great-aunt.n.01", "name": "great-aunt"}, - {"id": 15936, "synset": "great_grandchild.n.01", "name": "great_grandchild"}, - {"id": 15937, "synset": "great_granddaughter.n.01", "name": "great_granddaughter"}, - {"id": 15938, "synset": "great_grandmother.n.01", "name": "great_grandmother"}, - {"id": 15939, "synset": "great_grandparent.n.01", "name": "great_grandparent"}, - {"id": 15940, "synset": "great_grandson.n.01", "name": "great_grandson"}, - {"id": 15941, "synset": "great-nephew.n.01", "name": "great-nephew"}, - {"id": 15942, "synset": "great-niece.n.01", "name": "great-niece"}, - {"id": 15943, "synset": "green_beret.n.01", "name": "Green_Beret"}, - {"id": 15944, "synset": "grenadier.n.01", "name": "grenadier"}, - {"id": 15945, "synset": "greeter.n.01", "name": "greeter"}, - {"id": 15946, "synset": "gringo.n.01", "name": "gringo"}, - {"id": 15947, "synset": "grinner.n.01", "name": "grinner"}, - {"id": 15948, "synset": "grocer.n.01", "name": "grocer"}, - {"id": 15949, "synset": "groom.n.03", "name": "groom"}, - {"id": 15950, "synset": "groom.n.01", "name": "groom"}, - {"id": 15951, "synset": "grouch.n.01", "name": "grouch"}, - {"id": 15952, "synset": "group_captain.n.01", "name": "group_captain"}, - {"id": 15953, "synset": "grunter.n.01", "name": "grunter"}, - {"id": 15954, "synset": "prison_guard.n.01", "name": "prison_guard"}, - {"id": 15955, "synset": "guard.n.01", "name": "guard"}, - {"id": 15956, "synset": "guesser.n.01", "name": "guesser"}, - {"id": 15957, "synset": "guest.n.01", "name": "guest"}, - {"id": 15958, "synset": "guest.n.03", "name": "guest"}, - {"id": 15959, "synset": "guest_of_honor.n.01", "name": "guest_of_honor"}, - {"id": 15960, "synset": "guest_worker.n.01", "name": "guest_worker"}, - {"id": 15961, "synset": "guide.n.02", "name": "guide"}, - {"id": 15962, "synset": "guitarist.n.01", "name": "guitarist"}, - {"id": 15963, "synset": "gunnery_sergeant.n.01", "name": "gunnery_sergeant"}, - {"id": 15964, "synset": "guru.n.01", "name": "guru"}, - {"id": 15965, "synset": "guru.n.03", "name": "guru"}, - {"id": 15966, "synset": "guvnor.n.01", "name": "guvnor"}, - {"id": 15967, "synset": "guy.n.01", "name": "guy"}, - {"id": 15968, "synset": "gymnast.n.01", "name": "gymnast"}, - {"id": 15969, "synset": "gym_rat.n.01", "name": "gym_rat"}, - {"id": 15970, "synset": "gynecologist.n.01", "name": "gynecologist"}, - {"id": 15971, "synset": "gypsy.n.02", "name": "Gypsy"}, - {"id": 15972, "synset": "hack.n.01", "name": "hack"}, - {"id": 15973, "synset": "hacker.n.02", "name": "hacker"}, - {"id": 15974, "synset": "haggler.n.01", "name": "haggler"}, - {"id": 15975, "synset": "hairdresser.n.01", "name": "hairdresser"}, - {"id": 15976, "synset": "hakim.n.02", "name": "hakim"}, - {"id": 15977, "synset": "hakka.n.01", "name": "Hakka"}, - {"id": 15978, "synset": "halberdier.n.01", "name": "halberdier"}, - {"id": 15979, "synset": "halfback.n.01", "name": "halfback"}, - {"id": 15980, "synset": "half_blood.n.01", "name": "half_blood"}, - {"id": 15981, "synset": "hand.n.10", "name": "hand"}, - {"id": 15982, "synset": "animal_trainer.n.01", "name": "animal_trainer"}, - {"id": 15983, "synset": "handyman.n.01", "name": "handyman"}, - {"id": 15984, "synset": "hang_glider.n.01", "name": "hang_glider"}, - {"id": 15985, "synset": "hardliner.n.01", "name": "hardliner"}, - {"id": 15986, "synset": "harlequin.n.01", "name": "harlequin"}, - {"id": 15987, "synset": "harmonizer.n.02", "name": "harmonizer"}, - {"id": 15988, "synset": "hash_head.n.01", "name": "hash_head"}, - {"id": 15989, "synset": "hatchet_man.n.01", "name": "hatchet_man"}, - {"id": 15990, "synset": "hater.n.01", "name": "hater"}, - {"id": 15991, "synset": "hatmaker.n.01", "name": "hatmaker"}, - {"id": 15992, "synset": "headman.n.02", "name": "headman"}, - {"id": 15993, "synset": "headmaster.n.01", "name": "headmaster"}, - {"id": 15994, "synset": "head_nurse.n.01", "name": "head_nurse"}, - {"id": 15995, "synset": "hearer.n.01", "name": "hearer"}, - {"id": 15996, "synset": "heartbreaker.n.01", "name": "heartbreaker"}, - {"id": 15997, "synset": "heathen.n.01", "name": "heathen"}, - {"id": 15998, "synset": "heavyweight.n.02", "name": "heavyweight"}, - {"id": 15999, "synset": "heavy.n.01", "name": "heavy"}, - {"id": 16000, "synset": "heckler.n.01", "name": "heckler"}, - {"id": 16001, "synset": "hedger.n.02", "name": "hedger"}, - {"id": 16002, "synset": "hedger.n.01", "name": "hedger"}, - {"id": 16003, "synset": "hedonist.n.01", "name": "hedonist"}, - {"id": 16004, "synset": "heir.n.01", "name": "heir"}, - {"id": 16005, "synset": "heir_apparent.n.01", "name": "heir_apparent"}, - {"id": 16006, "synset": "heiress.n.01", "name": "heiress"}, - {"id": 16007, "synset": "heir_presumptive.n.01", "name": "heir_presumptive"}, - {"id": 16008, "synset": "hellion.n.01", "name": "hellion"}, - {"id": 16009, "synset": "helmsman.n.01", "name": "helmsman"}, - {"id": 16010, "synset": "hire.n.01", "name": "hire"}, - {"id": 16011, "synset": "hematologist.n.01", "name": "hematologist"}, - {"id": 16012, "synset": "hemiplegic.n.01", "name": "hemiplegic"}, - {"id": 16013, "synset": "herald.n.01", "name": "herald"}, - {"id": 16014, "synset": "herbalist.n.01", "name": "herbalist"}, - {"id": 16015, "synset": "herder.n.02", "name": "herder"}, - {"id": 16016, "synset": "hermaphrodite.n.01", "name": "hermaphrodite"}, - {"id": 16017, "synset": "heroine.n.02", "name": "heroine"}, - {"id": 16018, "synset": "heroin_addict.n.01", "name": "heroin_addict"}, - {"id": 16019, "synset": "hero_worshiper.n.01", "name": "hero_worshiper"}, - {"id": 16020, "synset": "herr.n.01", "name": "Herr"}, - {"id": 16021, "synset": "highbinder.n.01", "name": "highbinder"}, - {"id": 16022, "synset": "highbrow.n.01", "name": "highbrow"}, - {"id": 16023, "synset": "high_commissioner.n.01", "name": "high_commissioner"}, - {"id": 16024, "synset": "highflier.n.01", "name": "highflier"}, - {"id": 16025, "synset": "highlander.n.02", "name": "Highlander"}, - {"id": 16026, "synset": "high-muck-a-muck.n.01", "name": "high-muck-a-muck"}, - {"id": 16027, "synset": "high_priest.n.01", "name": "high_priest"}, - {"id": 16028, "synset": "highjacker.n.01", "name": "highjacker"}, - {"id": 16029, "synset": "hireling.n.01", "name": "hireling"}, - {"id": 16030, "synset": "historian.n.01", "name": "historian"}, - {"id": 16031, "synset": "hitchhiker.n.01", "name": "hitchhiker"}, - {"id": 16032, "synset": "hitter.n.02", "name": "hitter"}, - {"id": 16033, "synset": "hobbyist.n.01", "name": "hobbyist"}, - {"id": 16034, "synset": "holdout.n.01", "name": "holdout"}, - {"id": 16035, "synset": "holdover.n.01", "name": "holdover"}, - {"id": 16036, "synset": "holdup_man.n.01", "name": "holdup_man"}, - {"id": 16037, "synset": "homeboy.n.02", "name": "homeboy"}, - {"id": 16038, "synset": "homeboy.n.01", "name": "homeboy"}, - {"id": 16039, "synset": "home_buyer.n.01", "name": "home_buyer"}, - {"id": 16040, "synset": "homegirl.n.01", "name": "homegirl"}, - {"id": 16041, "synset": "homeless.n.01", "name": "homeless"}, - {"id": 16042, "synset": "homeopath.n.01", "name": "homeopath"}, - {"id": 16043, "synset": "honest_woman.n.01", "name": "honest_woman"}, - {"id": 16044, "synset": "honor_guard.n.01", "name": "honor_guard"}, - {"id": 16045, "synset": "hooker.n.05", "name": "hooker"}, - {"id": 16046, "synset": "hoper.n.01", "name": "hoper"}, - {"id": 16047, "synset": "hornist.n.01", "name": "hornist"}, - {"id": 16048, "synset": "horseman.n.01", "name": "horseman"}, - {"id": 16049, "synset": "horse_trader.n.01", "name": "horse_trader"}, - {"id": 16050, "synset": "horsewoman.n.01", "name": "horsewoman"}, - {"id": 16051, "synset": "horse_wrangler.n.01", "name": "horse_wrangler"}, - {"id": 16052, "synset": "horticulturist.n.01", "name": "horticulturist"}, - {"id": 16053, "synset": "hospital_chaplain.n.01", "name": "hospital_chaplain"}, - {"id": 16054, "synset": "host.n.08", "name": "host"}, - {"id": 16055, "synset": "host.n.01", "name": "host"}, - {"id": 16056, "synset": "hostess.n.01", "name": "hostess"}, - {"id": 16057, "synset": "hotelier.n.01", "name": "hotelier"}, - {"id": 16058, "synset": "housekeeper.n.01", "name": "housekeeper"}, - {"id": 16059, "synset": "housemaster.n.01", "name": "housemaster"}, - {"id": 16060, "synset": "housemate.n.01", "name": "housemate"}, - {"id": 16061, "synset": "house_physician.n.01", "name": "house_physician"}, - {"id": 16062, "synset": "house_sitter.n.01", "name": "house_sitter"}, - {"id": 16063, "synset": "housing_commissioner.n.01", "name": "housing_commissioner"}, - {"id": 16064, "synset": "huckster.n.01", "name": "huckster"}, - {"id": 16065, "synset": "hugger.n.01", "name": "hugger"}, - {"id": 16066, "synset": "humanist.n.02", "name": "humanist"}, - {"id": 16067, "synset": "humanitarian.n.01", "name": "humanitarian"}, - {"id": 16068, "synset": "hunk.n.01", "name": "hunk"}, - {"id": 16069, "synset": "huntress.n.01", "name": "huntress"}, - {"id": 16070, "synset": "ex-husband.n.01", "name": "ex-husband"}, - {"id": 16071, "synset": "hydrologist.n.01", "name": "hydrologist"}, - {"id": 16072, "synset": "hyperope.n.01", "name": "hyperope"}, - {"id": 16073, "synset": "hypertensive.n.01", "name": "hypertensive"}, - {"id": 16074, "synset": "hypnotist.n.01", "name": "hypnotist"}, - {"id": 16075, "synset": "hypocrite.n.01", "name": "hypocrite"}, - {"id": 16076, "synset": "iceman.n.01", "name": "iceman"}, - {"id": 16077, "synset": "iconoclast.n.02", "name": "iconoclast"}, - {"id": 16078, "synset": "ideologist.n.01", "name": "ideologist"}, - {"id": 16079, "synset": "idol.n.02", "name": "idol"}, - {"id": 16080, "synset": "idolizer.n.01", "name": "idolizer"}, - {"id": 16081, "synset": "imam.n.01", "name": "imam"}, - {"id": 16082, "synset": "imperialist.n.01", "name": "imperialist"}, - {"id": 16083, "synset": "important_person.n.01", "name": "important_person"}, - {"id": 16084, "synset": "inamorato.n.01", "name": "inamorato"}, - {"id": 16085, "synset": "incumbent.n.01", "name": "incumbent"}, - {"id": 16086, "synset": "incurable.n.01", "name": "incurable"}, - {"id": 16087, "synset": "inductee.n.01", "name": "inductee"}, - {"id": 16088, "synset": "industrialist.n.01", "name": "industrialist"}, - {"id": 16089, "synset": "infanticide.n.01", "name": "infanticide"}, - {"id": 16090, "synset": "inferior.n.01", "name": "inferior"}, - {"id": 16091, "synset": "infernal.n.01", "name": "infernal"}, - {"id": 16092, "synset": "infielder.n.01", "name": "infielder"}, - {"id": 16093, "synset": "infiltrator.n.02", "name": "infiltrator"}, - {"id": 16094, "synset": "informer.n.01", "name": "informer"}, - {"id": 16095, "synset": "ingenue.n.02", "name": "ingenue"}, - {"id": 16096, "synset": "ingenue.n.01", "name": "ingenue"}, - {"id": 16097, "synset": "polymath.n.01", "name": "polymath"}, - {"id": 16098, "synset": "in-law.n.01", "name": "in-law"}, - {"id": 16099, "synset": "inquiry_agent.n.01", "name": "inquiry_agent"}, - {"id": 16100, "synset": "inspector.n.01", "name": "inspector"}, - {"id": 16101, "synset": "inspector_general.n.01", "name": "inspector_general"}, - {"id": 16102, "synset": "instigator.n.02", "name": "instigator"}, - {"id": 16103, "synset": "insurance_broker.n.01", "name": "insurance_broker"}, - {"id": 16104, "synset": "insurgent.n.01", "name": "insurgent"}, - {"id": 16105, "synset": "intelligence_analyst.n.01", "name": "intelligence_analyst"}, - {"id": 16106, "synset": "interior_designer.n.01", "name": "interior_designer"}, - {"id": 16107, "synset": "interlocutor.n.02", "name": "interlocutor"}, - {"id": 16108, "synset": "interlocutor.n.01", "name": "interlocutor"}, - {"id": 16109, "synset": "international_grandmaster.n.01", "name": "International_Grandmaster"}, - {"id": 16110, "synset": "internationalist.n.02", "name": "internationalist"}, - {"id": 16111, "synset": "internist.n.01", "name": "internist"}, - {"id": 16112, "synset": "interpreter.n.01", "name": "interpreter"}, - {"id": 16113, "synset": "interpreter.n.02", "name": "interpreter"}, - {"id": 16114, "synset": "intervenor.n.01", "name": "intervenor"}, - {"id": 16115, "synset": "introvert.n.01", "name": "introvert"}, - {"id": 16116, "synset": "invader.n.01", "name": "invader"}, - {"id": 16117, "synset": "invalidator.n.01", "name": "invalidator"}, - {"id": 16118, "synset": "investigator.n.02", "name": "investigator"}, - {"id": 16119, "synset": "investor.n.01", "name": "investor"}, - {"id": 16120, "synset": "invigilator.n.01", "name": "invigilator"}, - {"id": 16121, "synset": "irreligionist.n.01", "name": "irreligionist"}, - {"id": 16122, "synset": "ivy_leaguer.n.01", "name": "Ivy_Leaguer"}, - {"id": 16123, "synset": "jack_of_all_trades.n.01", "name": "Jack_of_all_trades"}, - {"id": 16124, "synset": "jacksonian.n.01", "name": "Jacksonian"}, - {"id": 16125, "synset": "jane_doe.n.01", "name": "Jane_Doe"}, - {"id": 16126, "synset": "janissary.n.01", "name": "janissary"}, - {"id": 16127, "synset": "jat.n.01", "name": "Jat"}, - {"id": 16128, "synset": "javanese.n.01", "name": "Javanese"}, - {"id": 16129, "synset": "jekyll_and_hyde.n.01", "name": "Jekyll_and_Hyde"}, - {"id": 16130, "synset": "jester.n.01", "name": "jester"}, - {"id": 16131, "synset": "jesuit.n.01", "name": "Jesuit"}, - {"id": 16132, "synset": "jezebel.n.02", "name": "jezebel"}, - {"id": 16133, "synset": "jilt.n.01", "name": "jilt"}, - {"id": 16134, "synset": "jobber.n.01", "name": "jobber"}, - {"id": 16135, "synset": "job_candidate.n.01", "name": "job_candidate"}, - {"id": 16136, "synset": "job's_comforter.n.01", "name": "Job's_comforter"}, - {"id": 16137, "synset": "jockey.n.01", "name": "jockey"}, - {"id": 16138, "synset": "john_doe.n.02", "name": "John_Doe"}, - {"id": 16139, "synset": "journalist.n.01", "name": "journalist"}, - {"id": 16140, "synset": "judge.n.01", "name": "judge"}, - {"id": 16141, "synset": "judge_advocate.n.01", "name": "judge_advocate"}, - {"id": 16142, "synset": "juggler.n.01", "name": "juggler"}, - {"id": 16143, "synset": "jungian.n.01", "name": "Jungian"}, - {"id": 16144, "synset": "junior.n.03", "name": "junior"}, - {"id": 16145, "synset": "junior.n.02", "name": "junior"}, - {"id": 16146, "synset": "junior.n.04", "name": "Junior"}, - {"id": 16147, "synset": "junior_lightweight.n.01", "name": "junior_lightweight"}, - {"id": 16148, "synset": "junior_middleweight.n.01", "name": "junior_middleweight"}, - {"id": 16149, "synset": "jurist.n.01", "name": "jurist"}, - {"id": 16150, "synset": "juror.n.01", "name": "juror"}, - {"id": 16151, "synset": "justice_of_the_peace.n.01", "name": "justice_of_the_peace"}, - {"id": 16152, "synset": "justiciar.n.01", "name": "justiciar"}, - {"id": 16153, "synset": "kachina.n.01", "name": "kachina"}, - {"id": 16154, "synset": "keyboardist.n.01", "name": "keyboardist"}, - {"id": 16155, "synset": "khedive.n.01", "name": "Khedive"}, - {"id": 16156, "synset": "kingmaker.n.02", "name": "kingmaker"}, - {"id": 16157, "synset": "king.n.02", "name": "king"}, - {"id": 16158, "synset": "king's_counsel.n.01", "name": "King's_Counsel"}, - {"id": 16159, "synset": "counsel_to_the_crown.n.01", "name": "Counsel_to_the_Crown"}, - {"id": 16160, "synset": "kin.n.01", "name": "kin"}, - {"id": 16161, "synset": "enate.n.01", "name": "enate"}, - {"id": 16162, "synset": "kink.n.03", "name": "kink"}, - {"id": 16163, "synset": "kinswoman.n.01", "name": "kinswoman"}, - {"id": 16164, "synset": "kisser.n.01", "name": "kisser"}, - {"id": 16165, "synset": "kitchen_help.n.01", "name": "kitchen_help"}, - {"id": 16166, "synset": "kitchen_police.n.01", "name": "kitchen_police"}, - {"id": 16167, "synset": "klansman.n.01", "name": "Klansman"}, - {"id": 16168, "synset": "kleptomaniac.n.01", "name": "kleptomaniac"}, - {"id": 16169, "synset": "kneeler.n.01", "name": "kneeler"}, - {"id": 16170, "synset": "knight.n.01", "name": "knight"}, - {"id": 16171, "synset": "knocker.n.01", "name": "knocker"}, - {"id": 16172, "synset": "knower.n.01", "name": "knower"}, - {"id": 16173, "synset": "know-it-all.n.01", "name": "know-it-all"}, - {"id": 16174, "synset": "kolkhoznik.n.01", "name": "kolkhoznik"}, - {"id": 16175, "synset": "kshatriya.n.01", "name": "Kshatriya"}, - {"id": 16176, "synset": "labor_coach.n.01", "name": "labor_coach"}, - {"id": 16177, "synset": "laborer.n.01", "name": "laborer"}, - {"id": 16178, "synset": "labourite.n.01", "name": "Labourite"}, - {"id": 16179, "synset": "lady.n.01", "name": "lady"}, - {"id": 16180, "synset": "lady-in-waiting.n.01", "name": "lady-in-waiting"}, - {"id": 16181, "synset": "lady's_maid.n.01", "name": "lady's_maid"}, - {"id": 16182, "synset": "lama.n.01", "name": "lama"}, - {"id": 16183, "synset": "lamb.n.04", "name": "lamb"}, - {"id": 16184, "synset": "lame_duck.n.01", "name": "lame_duck"}, - {"id": 16185, "synset": "lamplighter.n.01", "name": "lamplighter"}, - {"id": 16186, "synset": "land_agent.n.02", "name": "land_agent"}, - {"id": 16187, "synset": "landgrave.n.01", "name": "landgrave"}, - {"id": 16188, "synset": "landlubber.n.02", "name": "landlubber"}, - {"id": 16189, "synset": "landlubber.n.01", "name": "landlubber"}, - {"id": 16190, "synset": "landowner.n.01", "name": "landowner"}, - {"id": 16191, "synset": "landscape_architect.n.01", "name": "landscape_architect"}, - {"id": 16192, "synset": "langlaufer.n.01", "name": "langlaufer"}, - {"id": 16193, "synset": "languisher.n.01", "name": "languisher"}, - {"id": 16194, "synset": "lapidary.n.01", "name": "lapidary"}, - {"id": 16195, "synset": "lass.n.01", "name": "lass"}, - {"id": 16196, "synset": "latin.n.03", "name": "Latin"}, - {"id": 16197, "synset": "latin.n.02", "name": "Latin"}, - {"id": 16198, "synset": "latitudinarian.n.01", "name": "latitudinarian"}, - {"id": 16199, "synset": "jehovah's_witness.n.01", "name": "Jehovah's_Witness"}, - {"id": 16200, "synset": "law_agent.n.01", "name": "law_agent"}, - {"id": 16201, "synset": "lawgiver.n.01", "name": "lawgiver"}, - {"id": 16202, "synset": "lawman.n.01", "name": "lawman"}, - {"id": 16203, "synset": "law_student.n.01", "name": "law_student"}, - {"id": 16204, "synset": "lawyer.n.01", "name": "lawyer"}, - {"id": 16205, "synset": "lay_reader.n.01", "name": "lay_reader"}, - {"id": 16206, "synset": "lazybones.n.01", "name": "lazybones"}, - {"id": 16207, "synset": "leaker.n.01", "name": "leaker"}, - {"id": 16208, "synset": "leaseholder.n.01", "name": "leaseholder"}, - {"id": 16209, "synset": "lector.n.02", "name": "lector"}, - {"id": 16210, "synset": "lector.n.01", "name": "lector"}, - {"id": 16211, "synset": "lecturer.n.02", "name": "lecturer"}, - {"id": 16212, "synset": "left-hander.n.02", "name": "left-hander"}, - {"id": 16213, "synset": "legal_representative.n.01", "name": "legal_representative"}, - {"id": 16214, "synset": "legate.n.01", "name": "legate"}, - {"id": 16215, "synset": "legatee.n.01", "name": "legatee"}, - {"id": 16216, "synset": "legionnaire.n.02", "name": "legionnaire"}, - {"id": 16217, "synset": "letterman.n.01", "name": "letterman"}, - {"id": 16218, "synset": "liberator.n.01", "name": "liberator"}, - {"id": 16219, "synset": "licenser.n.01", "name": "licenser"}, - {"id": 16220, "synset": "licentiate.n.01", "name": "licentiate"}, - {"id": 16221, "synset": "lieutenant.n.01", "name": "lieutenant"}, - {"id": 16222, "synset": "lieutenant_colonel.n.01", "name": "lieutenant_colonel"}, - {"id": 16223, "synset": "lieutenant_commander.n.01", "name": "lieutenant_commander"}, - {"id": 16224, "synset": "lieutenant_junior_grade.n.01", "name": "lieutenant_junior_grade"}, - {"id": 16225, "synset": "life.n.08", "name": "life"}, - {"id": 16226, "synset": "lifeguard.n.01", "name": "lifeguard"}, - {"id": 16227, "synset": "life_tenant.n.01", "name": "life_tenant"}, - {"id": 16228, "synset": "light_flyweight.n.01", "name": "light_flyweight"}, - {"id": 16229, "synset": "light_heavyweight.n.03", "name": "light_heavyweight"}, - {"id": 16230, "synset": "light_heavyweight.n.01", "name": "light_heavyweight"}, - {"id": 16231, "synset": "light-o'-love.n.01", "name": "light-o'-love"}, - {"id": 16232, "synset": "lightweight.n.01", "name": "lightweight"}, - {"id": 16233, "synset": "lightweight.n.04", "name": "lightweight"}, - {"id": 16234, "synset": "lightweight.n.03", "name": "lightweight"}, - {"id": 16235, "synset": "lilliputian.n.01", "name": "lilliputian"}, - {"id": 16236, "synset": "limnologist.n.01", "name": "limnologist"}, - {"id": 16237, "synset": "lineman.n.01", "name": "lineman"}, - {"id": 16238, "synset": "line_officer.n.01", "name": "line_officer"}, - {"id": 16239, "synset": "lion-hunter.n.01", "name": "lion-hunter"}, - {"id": 16240, "synset": "lisper.n.01", "name": "lisper"}, - {"id": 16241, "synset": "lister.n.02", "name": "lister"}, - {"id": 16242, "synset": "literary_critic.n.01", "name": "literary_critic"}, - {"id": 16243, "synset": "literate.n.01", "name": "literate"}, - {"id": 16244, "synset": "litigant.n.01", "name": "litigant"}, - {"id": 16245, "synset": "litterer.n.01", "name": "litterer"}, - {"id": 16246, "synset": "little_brother.n.01", "name": "little_brother"}, - {"id": 16247, "synset": "little_sister.n.01", "name": "little_sister"}, - {"id": 16248, "synset": "lobbyist.n.01", "name": "lobbyist"}, - {"id": 16249, "synset": "locksmith.n.01", "name": "locksmith"}, - {"id": 16250, "synset": "locum_tenens.n.01", "name": "locum_tenens"}, - {"id": 16251, "synset": "lord.n.03", "name": "Lord"}, - {"id": 16252, "synset": "loser.n.03", "name": "loser"}, - {"id": 16253, "synset": "loser.n.01", "name": "loser"}, - {"id": 16254, "synset": "failure.n.04", "name": "failure"}, - {"id": 16255, "synset": "lothario.n.01", "name": "Lothario"}, - {"id": 16256, "synset": "loudmouth.n.01", "name": "loudmouth"}, - {"id": 16257, "synset": "lowerclassman.n.01", "name": "lowerclassman"}, - {"id": 16258, "synset": "lowlander.n.01", "name": "Lowlander"}, - {"id": 16259, "synset": "loyalist.n.01", "name": "loyalist"}, - {"id": 16260, "synset": "luddite.n.01", "name": "Luddite"}, - {"id": 16261, "synset": "lumberman.n.01", "name": "lumberman"}, - {"id": 16262, "synset": "lumper.n.02", "name": "lumper"}, - {"id": 16263, "synset": "bedlamite.n.01", "name": "bedlamite"}, - {"id": 16264, "synset": "pyromaniac.n.01", "name": "pyromaniac"}, - {"id": 16265, "synset": "lutist.n.01", "name": "lutist"}, - {"id": 16266, "synset": "lutheran.n.01", "name": "Lutheran"}, - {"id": 16267, "synset": "lyricist.n.01", "name": "lyricist"}, - {"id": 16268, "synset": "macebearer.n.01", "name": "macebearer"}, - {"id": 16269, "synset": "machinist.n.01", "name": "machinist"}, - {"id": 16270, "synset": "madame.n.01", "name": "madame"}, - {"id": 16271, "synset": "maenad.n.01", "name": "maenad"}, - {"id": 16272, "synset": "maestro.n.01", "name": "maestro"}, - {"id": 16273, "synset": "magdalen.n.01", "name": "magdalen"}, - {"id": 16274, "synset": "magician.n.01", "name": "magician"}, - {"id": 16275, "synset": "magus.n.01", "name": "magus"}, - {"id": 16276, "synset": "maharani.n.01", "name": "maharani"}, - {"id": 16277, "synset": "mahatma.n.01", "name": "mahatma"}, - {"id": 16278, "synset": "maid.n.02", "name": "maid"}, - {"id": 16279, "synset": "maid.n.01", "name": "maid"}, - {"id": 16280, "synset": "major.n.01", "name": "major"}, - {"id": 16281, "synset": "major.n.03", "name": "major"}, - {"id": 16282, "synset": "major-domo.n.01", "name": "major-domo"}, - {"id": 16283, "synset": "maker.n.01", "name": "maker"}, - {"id": 16284, "synset": "malahini.n.01", "name": "malahini"}, - {"id": 16285, "synset": "malcontent.n.01", "name": "malcontent"}, - {"id": 16286, "synset": "malik.n.01", "name": "malik"}, - {"id": 16287, "synset": "malingerer.n.01", "name": "malingerer"}, - {"id": 16288, "synset": "malthusian.n.01", "name": "Malthusian"}, - {"id": 16289, "synset": "adonis.n.01", "name": "adonis"}, - {"id": 16290, "synset": "man.n.03", "name": "man"}, - {"id": 16291, "synset": "man.n.05", "name": "man"}, - {"id": 16292, "synset": "manageress.n.01", "name": "manageress"}, - {"id": 16293, "synset": "mandarin.n.03", "name": "mandarin"}, - {"id": 16294, "synset": "maneuverer.n.01", "name": "maneuverer"}, - {"id": 16295, "synset": "maniac.n.02", "name": "maniac"}, - {"id": 16296, "synset": "manichaean.n.01", "name": "Manichaean"}, - {"id": 16297, "synset": "manicurist.n.01", "name": "manicurist"}, - {"id": 16298, "synset": "manipulator.n.02", "name": "manipulator"}, - {"id": 16299, "synset": "man-at-arms.n.01", "name": "man-at-arms"}, - {"id": 16300, "synset": "man_of_action.n.01", "name": "man_of_action"}, - {"id": 16301, "synset": "man_of_letters.n.01", "name": "man_of_letters"}, - {"id": 16302, "synset": "manufacturer.n.02", "name": "manufacturer"}, - {"id": 16303, "synset": "marcher.n.02", "name": "marcher"}, - {"id": 16304, "synset": "marchioness.n.02", "name": "marchioness"}, - {"id": 16305, "synset": "margrave.n.02", "name": "margrave"}, - {"id": 16306, "synset": "margrave.n.01", "name": "margrave"}, - {"id": 16307, "synset": "marine.n.01", "name": "Marine"}, - {"id": 16308, "synset": "marquess.n.02", "name": "marquess"}, - {"id": 16309, "synset": "marquis.n.02", "name": "marquis"}, - {"id": 16310, "synset": "marshal.n.02", "name": "marshal"}, - {"id": 16311, "synset": "martinet.n.01", "name": "martinet"}, - {"id": 16312, "synset": "masochist.n.01", "name": "masochist"}, - {"id": 16313, "synset": "mason.n.04", "name": "mason"}, - {"id": 16314, "synset": "masquerader.n.01", "name": "masquerader"}, - {"id": 16315, "synset": "masseur.n.01", "name": "masseur"}, - {"id": 16316, "synset": "masseuse.n.01", "name": "masseuse"}, - {"id": 16317, "synset": "master.n.04", "name": "master"}, - {"id": 16318, "synset": "master.n.07", "name": "master"}, - {"id": 16319, "synset": "master-at-arms.n.01", "name": "master-at-arms"}, - {"id": 16320, "synset": "master_of_ceremonies.n.01", "name": "master_of_ceremonies"}, - {"id": 16321, "synset": "masturbator.n.01", "name": "masturbator"}, - {"id": 16322, "synset": "matchmaker.n.01", "name": "matchmaker"}, - {"id": 16323, "synset": "mate.n.01", "name": "mate"}, - {"id": 16324, "synset": "mate.n.08", "name": "mate"}, - {"id": 16325, "synset": "mate.n.03", "name": "mate"}, - {"id": 16326, "synset": "mater.n.01", "name": "mater"}, - {"id": 16327, "synset": "material.n.05", "name": "material"}, - {"id": 16328, "synset": "materialist.n.02", "name": "materialist"}, - {"id": 16329, "synset": "matriarch.n.01", "name": "matriarch"}, - {"id": 16330, "synset": "matriarch.n.02", "name": "matriarch"}, - {"id": 16331, "synset": "matriculate.n.01", "name": "matriculate"}, - {"id": 16332, "synset": "matron.n.01", "name": "matron"}, - {"id": 16333, "synset": "mayor.n.01", "name": "mayor"}, - {"id": 16334, "synset": "mayoress.n.01", "name": "mayoress"}, - {"id": 16335, "synset": "mechanical_engineer.n.01", "name": "mechanical_engineer"}, - {"id": 16336, "synset": "medalist.n.02", "name": "medalist"}, - {"id": 16337, "synset": "medical_officer.n.01", "name": "medical_officer"}, - {"id": 16338, "synset": "medical_practitioner.n.01", "name": "medical_practitioner"}, - {"id": 16339, "synset": "medical_scientist.n.01", "name": "medical_scientist"}, - {"id": 16340, "synset": "medium.n.09", "name": "medium"}, - {"id": 16341, "synset": "megalomaniac.n.01", "name": "megalomaniac"}, - {"id": 16342, "synset": "melancholic.n.01", "name": "melancholic"}, - {"id": 16343, "synset": "melkite.n.01", "name": "Melkite"}, - {"id": 16344, "synset": "melter.n.01", "name": "melter"}, - {"id": 16345, "synset": "nonmember.n.01", "name": "nonmember"}, - {"id": 16346, "synset": "board_member.n.01", "name": "board_member"}, - {"id": 16347, "synset": "clansman.n.01", "name": "clansman"}, - {"id": 16348, "synset": "memorizer.n.01", "name": "memorizer"}, - {"id": 16349, "synset": "mendelian.n.01", "name": "Mendelian"}, - {"id": 16350, "synset": "mender.n.01", "name": "mender"}, - {"id": 16351, "synset": "mesoamerican.n.01", "name": "Mesoamerican"}, - {"id": 16352, "synset": "messmate.n.01", "name": "messmate"}, - {"id": 16353, "synset": "mestiza.n.01", "name": "mestiza"}, - {"id": 16354, "synset": "meteorologist.n.01", "name": "meteorologist"}, - {"id": 16355, "synset": "meter_maid.n.01", "name": "meter_maid"}, - {"id": 16356, "synset": "methodist.n.01", "name": "Methodist"}, - {"id": 16357, "synset": "metis.n.01", "name": "Metis"}, - {"id": 16358, "synset": "metropolitan.n.01", "name": "metropolitan"}, - {"id": 16359, "synset": "mezzo-soprano.n.01", "name": "mezzo-soprano"}, - {"id": 16360, "synset": "microeconomist.n.01", "name": "microeconomist"}, - {"id": 16361, "synset": "middle-aged_man.n.01", "name": "middle-aged_man"}, - {"id": 16362, "synset": "middlebrow.n.01", "name": "middlebrow"}, - {"id": 16363, "synset": "middleweight.n.01", "name": "middleweight"}, - {"id": 16364, "synset": "midwife.n.01", "name": "midwife"}, - {"id": 16365, "synset": "mikado.n.01", "name": "mikado"}, - {"id": 16366, "synset": "milanese.n.01", "name": "Milanese"}, - {"id": 16367, "synset": "miler.n.02", "name": "miler"}, - {"id": 16368, "synset": "miles_gloriosus.n.01", "name": "miles_gloriosus"}, - {"id": 16369, "synset": "military_attache.n.01", "name": "military_attache"}, - {"id": 16370, "synset": "military_chaplain.n.01", "name": "military_chaplain"}, - {"id": 16371, "synset": "military_leader.n.01", "name": "military_leader"}, - {"id": 16372, "synset": "military_officer.n.01", "name": "military_officer"}, - {"id": 16373, "synset": "military_policeman.n.01", "name": "military_policeman"}, - {"id": 16374, "synset": "mill_agent.n.01", "name": "mill_agent"}, - {"id": 16375, "synset": "mill-hand.n.01", "name": "mill-hand"}, - {"id": 16376, "synset": "millionairess.n.01", "name": "millionairess"}, - {"id": 16377, "synset": "millwright.n.01", "name": "millwright"}, - {"id": 16378, "synset": "minder.n.01", "name": "minder"}, - {"id": 16379, "synset": "mining_engineer.n.01", "name": "mining_engineer"}, - {"id": 16380, "synset": "minister.n.02", "name": "minister"}, - {"id": 16381, "synset": "ministrant.n.01", "name": "ministrant"}, - {"id": 16382, "synset": "minor_leaguer.n.01", "name": "minor_leaguer"}, - {"id": 16383, "synset": "minuteman.n.01", "name": "Minuteman"}, - {"id": 16384, "synset": "misanthrope.n.01", "name": "misanthrope"}, - {"id": 16385, "synset": "misfit.n.01", "name": "misfit"}, - {"id": 16386, "synset": "mistress.n.03", "name": "mistress"}, - {"id": 16387, "synset": "mistress.n.01", "name": "mistress"}, - {"id": 16388, "synset": "mixed-blood.n.01", "name": "mixed-blood"}, - {"id": 16389, "synset": "model.n.03", "name": "model"}, - {"id": 16390, "synset": "class_act.n.01", "name": "class_act"}, - {"id": 16391, "synset": "modeler.n.01", "name": "modeler"}, - {"id": 16392, "synset": "modifier.n.02", "name": "modifier"}, - {"id": 16393, "synset": "molecular_biologist.n.01", "name": "molecular_biologist"}, - {"id": 16394, "synset": "monegasque.n.01", "name": "Monegasque"}, - {"id": 16395, "synset": "monetarist.n.01", "name": "monetarist"}, - {"id": 16396, "synset": "moneygrubber.n.01", "name": "moneygrubber"}, - {"id": 16397, "synset": "moneymaker.n.01", "name": "moneymaker"}, - {"id": 16398, "synset": "mongoloid.n.01", "name": "Mongoloid"}, - {"id": 16399, "synset": "monolingual.n.01", "name": "monolingual"}, - {"id": 16400, "synset": "monologist.n.01", "name": "monologist"}, - {"id": 16401, "synset": "moonlighter.n.01", "name": "moonlighter"}, - {"id": 16402, "synset": "moralist.n.01", "name": "moralist"}, - {"id": 16403, "synset": "morosoph.n.01", "name": "morosoph"}, - {"id": 16404, "synset": "morris_dancer.n.01", "name": "morris_dancer"}, - {"id": 16405, "synset": "mortal_enemy.n.01", "name": "mortal_enemy"}, - {"id": 16406, "synset": "mortgagee.n.01", "name": "mortgagee"}, - {"id": 16407, "synset": "mortician.n.01", "name": "mortician"}, - {"id": 16408, "synset": "moss-trooper.n.01", "name": "moss-trooper"}, - {"id": 16409, "synset": "mother.n.01", "name": "mother"}, - {"id": 16410, "synset": "mother.n.04", "name": "mother"}, - {"id": 16411, "synset": "mother.n.03", "name": "mother"}, - {"id": 16412, "synset": "mother_figure.n.01", "name": "mother_figure"}, - {"id": 16413, "synset": "mother_hen.n.01", "name": "mother_hen"}, - {"id": 16414, "synset": "mother-in-law.n.01", "name": "mother-in-law"}, - {"id": 16415, "synset": "mother's_boy.n.01", "name": "mother's_boy"}, - {"id": 16416, "synset": "mother's_daughter.n.01", "name": "mother's_daughter"}, - {"id": 16417, "synset": "motorcycle_cop.n.01", "name": "motorcycle_cop"}, - {"id": 16418, "synset": "motorcyclist.n.01", "name": "motorcyclist"}, - {"id": 16419, "synset": "mound_builder.n.01", "name": "Mound_Builder"}, - {"id": 16420, "synset": "mountebank.n.01", "name": "mountebank"}, - {"id": 16421, "synset": "mourner.n.01", "name": "mourner"}, - {"id": 16422, "synset": "mouthpiece.n.03", "name": "mouthpiece"}, - {"id": 16423, "synset": "mover.n.03", "name": "mover"}, - {"id": 16424, "synset": "moviegoer.n.01", "name": "moviegoer"}, - {"id": 16425, "synset": "muffin_man.n.01", "name": "muffin_man"}, - {"id": 16426, "synset": "mugwump.n.02", "name": "mugwump"}, - {"id": 16427, "synset": "mullah.n.01", "name": "Mullah"}, - {"id": 16428, "synset": "muncher.n.01", "name": "muncher"}, - {"id": 16429, "synset": "murderess.n.01", "name": "murderess"}, - {"id": 16430, "synset": "murder_suspect.n.01", "name": "murder_suspect"}, - {"id": 16431, "synset": "musher.n.01", "name": "musher"}, - {"id": 16432, "synset": "musician.n.01", "name": "musician"}, - {"id": 16433, "synset": "musicologist.n.01", "name": "musicologist"}, - {"id": 16434, "synset": "music_teacher.n.01", "name": "music_teacher"}, - {"id": 16435, "synset": "musketeer.n.01", "name": "musketeer"}, - {"id": 16436, "synset": "muslimah.n.01", "name": "Muslimah"}, - {"id": 16437, "synset": "mutilator.n.01", "name": "mutilator"}, - {"id": 16438, "synset": "mutineer.n.01", "name": "mutineer"}, - {"id": 16439, "synset": "mute.n.01", "name": "mute"}, - {"id": 16440, "synset": "mutterer.n.01", "name": "mutterer"}, - {"id": 16441, "synset": "muzzler.n.01", "name": "muzzler"}, - {"id": 16442, "synset": "mycenaen.n.01", "name": "Mycenaen"}, - {"id": 16443, "synset": "mycologist.n.01", "name": "mycologist"}, - {"id": 16444, "synset": "myope.n.01", "name": "myope"}, - {"id": 16445, "synset": "myrmidon.n.01", "name": "myrmidon"}, - {"id": 16446, "synset": "mystic.n.01", "name": "mystic"}, - {"id": 16447, "synset": "mythologist.n.01", "name": "mythologist"}, - {"id": 16448, "synset": "naif.n.01", "name": "naif"}, - {"id": 16449, "synset": "nailer.n.01", "name": "nailer"}, - {"id": 16450, "synset": "namby-pamby.n.01", "name": "namby-pamby"}, - {"id": 16451, "synset": "name_dropper.n.01", "name": "name_dropper"}, - {"id": 16452, "synset": "namer.n.01", "name": "namer"}, - {"id": 16453, "synset": "nan.n.01", "name": "nan"}, - {"id": 16454, "synset": "nanny.n.01", "name": "nanny"}, - {"id": 16455, "synset": "narc.n.01", "name": "narc"}, - {"id": 16456, "synset": "narcissist.n.01", "name": "narcissist"}, - {"id": 16457, "synset": "nark.n.01", "name": "nark"}, - {"id": 16458, "synset": "nationalist.n.02", "name": "nationalist"}, - {"id": 16459, "synset": "nautch_girl.n.01", "name": "nautch_girl"}, - {"id": 16460, "synset": "naval_commander.n.01", "name": "naval_commander"}, - {"id": 16461, "synset": "navy_seal.n.01", "name": "Navy_SEAL"}, - {"id": 16462, "synset": "obstructionist.n.01", "name": "obstructionist"}, - {"id": 16463, "synset": "nazarene.n.02", "name": "Nazarene"}, - {"id": 16464, "synset": "nazarene.n.01", "name": "Nazarene"}, - {"id": 16465, "synset": "nazi.n.01", "name": "Nazi"}, - {"id": 16466, "synset": "nebbish.n.01", "name": "nebbish"}, - {"id": 16467, "synset": "necker.n.01", "name": "necker"}, - {"id": 16468, "synset": "neonate.n.01", "name": "neonate"}, - {"id": 16469, "synset": "nephew.n.01", "name": "nephew"}, - {"id": 16470, "synset": "neurobiologist.n.01", "name": "neurobiologist"}, - {"id": 16471, "synset": "neurologist.n.01", "name": "neurologist"}, - {"id": 16472, "synset": "neurosurgeon.n.01", "name": "neurosurgeon"}, - {"id": 16473, "synset": "neutral.n.01", "name": "neutral"}, - {"id": 16474, "synset": "neutralist.n.01", "name": "neutralist"}, - {"id": 16475, "synset": "newcomer.n.01", "name": "newcomer"}, - {"id": 16476, "synset": "newcomer.n.02", "name": "newcomer"}, - {"id": 16477, "synset": "new_dealer.n.01", "name": "New_Dealer"}, - {"id": 16478, "synset": "newspaper_editor.n.01", "name": "newspaper_editor"}, - {"id": 16479, "synset": "newsreader.n.01", "name": "newsreader"}, - {"id": 16480, "synset": "newtonian.n.01", "name": "Newtonian"}, - {"id": 16481, "synset": "niece.n.01", "name": "niece"}, - {"id": 16482, "synset": "niggard.n.01", "name": "niggard"}, - {"id": 16483, "synset": "night_porter.n.01", "name": "night_porter"}, - {"id": 16484, "synset": "night_rider.n.01", "name": "night_rider"}, - {"id": 16485, "synset": "nimby.n.01", "name": "NIMBY"}, - {"id": 16486, "synset": "niqaabi.n.01", "name": "niqaabi"}, - {"id": 16487, "synset": "nitpicker.n.01", "name": "nitpicker"}, - {"id": 16488, "synset": "nobelist.n.01", "name": "Nobelist"}, - {"id": 16489, "synset": "noc.n.01", "name": "NOC"}, - {"id": 16490, "synset": "noncandidate.n.01", "name": "noncandidate"}, - {"id": 16491, "synset": "noncommissioned_officer.n.01", "name": "noncommissioned_officer"}, - {"id": 16492, "synset": "nondescript.n.01", "name": "nondescript"}, - {"id": 16493, "synset": "nondriver.n.01", "name": "nondriver"}, - {"id": 16494, "synset": "nonparticipant.n.01", "name": "nonparticipant"}, - {"id": 16495, "synset": "nonperson.n.01", "name": "nonperson"}, - {"id": 16496, "synset": "nonresident.n.01", "name": "nonresident"}, - {"id": 16497, "synset": "nonsmoker.n.01", "name": "nonsmoker"}, - {"id": 16498, "synset": "northern_baptist.n.01", "name": "Northern_Baptist"}, - {"id": 16499, "synset": "noticer.n.01", "name": "noticer"}, - {"id": 16500, "synset": "novelist.n.01", "name": "novelist"}, - {"id": 16501, "synset": "novitiate.n.02", "name": "novitiate"}, - {"id": 16502, "synset": "nuclear_chemist.n.01", "name": "nuclear_chemist"}, - {"id": 16503, "synset": "nudger.n.01", "name": "nudger"}, - {"id": 16504, "synset": "nullipara.n.01", "name": "nullipara"}, - {"id": 16505, "synset": "number_theorist.n.01", "name": "number_theorist"}, - {"id": 16506, "synset": "nurse.n.01", "name": "nurse"}, - {"id": 16507, "synset": "nursling.n.01", "name": "nursling"}, - {"id": 16508, "synset": "nymph.n.03", "name": "nymph"}, - {"id": 16509, "synset": "nymphet.n.01", "name": "nymphet"}, - {"id": 16510, "synset": "nympholept.n.01", "name": "nympholept"}, - {"id": 16511, "synset": "nymphomaniac.n.01", "name": "nymphomaniac"}, - {"id": 16512, "synset": "oarswoman.n.01", "name": "oarswoman"}, - {"id": 16513, "synset": "oboist.n.01", "name": "oboist"}, - {"id": 16514, "synset": "obscurantist.n.01", "name": "obscurantist"}, - {"id": 16515, "synset": "observer.n.02", "name": "observer"}, - {"id": 16516, "synset": "obstetrician.n.01", "name": "obstetrician"}, - {"id": 16517, "synset": "occupier.n.02", "name": "occupier"}, - {"id": 16518, "synset": "occultist.n.01", "name": "occultist"}, - {"id": 16519, "synset": "wine_lover.n.01", "name": "wine_lover"}, - {"id": 16520, "synset": "offerer.n.01", "name": "offerer"}, - {"id": 16521, "synset": "office-bearer.n.01", "name": "office-bearer"}, - {"id": 16522, "synset": "office_boy.n.01", "name": "office_boy"}, - {"id": 16523, "synset": "officeholder.n.01", "name": "officeholder"}, - {"id": 16524, "synset": "officiant.n.01", "name": "officiant"}, - {"id": 16525, "synset": "federal.n.02", "name": "Federal"}, - {"id": 16526, "synset": "oilman.n.02", "name": "oilman"}, - {"id": 16527, "synset": "oil_tycoon.n.01", "name": "oil_tycoon"}, - {"id": 16528, "synset": "old-age_pensioner.n.01", "name": "old-age_pensioner"}, - {"id": 16529, "synset": "old_boy.n.02", "name": "old_boy"}, - {"id": 16530, "synset": "old_lady.n.01", "name": "old_lady"}, - {"id": 16531, "synset": "old_man.n.03", "name": "old_man"}, - {"id": 16532, "synset": "oldster.n.01", "name": "oldster"}, - {"id": 16533, "synset": "old-timer.n.02", "name": "old-timer"}, - {"id": 16534, "synset": "old_woman.n.01", "name": "old_woman"}, - {"id": 16535, "synset": "oligarch.n.01", "name": "oligarch"}, - {"id": 16536, "synset": "olympian.n.01", "name": "Olympian"}, - {"id": 16537, "synset": "omnivore.n.01", "name": "omnivore"}, - {"id": 16538, "synset": "oncologist.n.01", "name": "oncologist"}, - {"id": 16539, "synset": "onlooker.n.01", "name": "onlooker"}, - {"id": 16540, "synset": "onomancer.n.01", "name": "onomancer"}, - {"id": 16541, "synset": "operator.n.03", "name": "operator"}, - {"id": 16542, "synset": "opportunist.n.01", "name": "opportunist"}, - {"id": 16543, "synset": "optimist.n.01", "name": "optimist"}, - {"id": 16544, "synset": "orangeman.n.01", "name": "Orangeman"}, - {"id": 16545, "synset": "orator.n.01", "name": "orator"}, - {"id": 16546, "synset": "orderly.n.02", "name": "orderly"}, - {"id": 16547, "synset": "orderly.n.01", "name": "orderly"}, - {"id": 16548, "synset": "orderly_sergeant.n.01", "name": "orderly_sergeant"}, - {"id": 16549, "synset": "ordinand.n.01", "name": "ordinand"}, - {"id": 16550, "synset": "ordinary.n.03", "name": "ordinary"}, - {"id": 16551, "synset": "organ-grinder.n.01", "name": "organ-grinder"}, - {"id": 16552, "synset": "organist.n.01", "name": "organist"}, - {"id": 16553, "synset": "organization_man.n.01", "name": "organization_man"}, - {"id": 16554, "synset": "organizer.n.01", "name": "organizer"}, - {"id": 16555, "synset": "organizer.n.02", "name": "organizer"}, - {"id": 16556, "synset": "originator.n.01", "name": "originator"}, - {"id": 16557, "synset": "ornithologist.n.01", "name": "ornithologist"}, - {"id": 16558, "synset": "orphan.n.01", "name": "orphan"}, - {"id": 16559, "synset": "orphan.n.02", "name": "orphan"}, - {"id": 16560, "synset": "osteopath.n.01", "name": "osteopath"}, - {"id": 16561, "synset": "out-and-outer.n.01", "name": "out-and-outer"}, - {"id": 16562, "synset": "outdoorswoman.n.01", "name": "outdoorswoman"}, - {"id": 16563, "synset": "outfielder.n.02", "name": "outfielder"}, - {"id": 16564, "synset": "outfielder.n.01", "name": "outfielder"}, - {"id": 16565, "synset": "right_fielder.n.01", "name": "right_fielder"}, - {"id": 16566, "synset": "right-handed_pitcher.n.01", "name": "right-handed_pitcher"}, - {"id": 16567, "synset": "outlier.n.01", "name": "outlier"}, - {"id": 16568, "synset": "owner-occupier.n.01", "name": "owner-occupier"}, - {"id": 16569, "synset": "oyabun.n.01", "name": "oyabun"}, - {"id": 16570, "synset": "packrat.n.01", "name": "packrat"}, - {"id": 16571, "synset": "padrone.n.02", "name": "padrone"}, - {"id": 16572, "synset": "padrone.n.01", "name": "padrone"}, - {"id": 16573, "synset": "page.n.04", "name": "page"}, - {"id": 16574, "synset": "painter.n.02", "name": "painter"}, - {"id": 16575, "synset": "paleo-american.n.01", "name": "Paleo-American"}, - {"id": 16576, "synset": "paleontologist.n.01", "name": "paleontologist"}, - {"id": 16577, "synset": "pallbearer.n.01", "name": "pallbearer"}, - {"id": 16578, "synset": "palmist.n.01", "name": "palmist"}, - {"id": 16579, "synset": "pamperer.n.01", "name": "pamperer"}, - {"id": 16580, "synset": "panchen_lama.n.01", "name": "Panchen_Lama"}, - {"id": 16581, "synset": "panelist.n.01", "name": "panelist"}, - {"id": 16582, "synset": "panhandler.n.01", "name": "panhandler"}, - {"id": 16583, "synset": "paparazzo.n.01", "name": "paparazzo"}, - {"id": 16584, "synset": "paperboy.n.01", "name": "paperboy"}, - {"id": 16585, "synset": "paperhanger.n.02", "name": "paperhanger"}, - {"id": 16586, "synset": "paperhanger.n.01", "name": "paperhanger"}, - {"id": 16587, "synset": "papoose.n.01", "name": "papoose"}, - {"id": 16588, "synset": "pardoner.n.02", "name": "pardoner"}, - {"id": 16589, "synset": "paretic.n.01", "name": "paretic"}, - {"id": 16590, "synset": "parishioner.n.01", "name": "parishioner"}, - {"id": 16591, "synset": "park_commissioner.n.01", "name": "park_commissioner"}, - {"id": 16592, "synset": "parliamentarian.n.01", "name": "Parliamentarian"}, - {"id": 16593, "synset": "parliamentary_agent.n.01", "name": "parliamentary_agent"}, - {"id": 16594, "synset": "parodist.n.01", "name": "parodist"}, - {"id": 16595, "synset": "parricide.n.01", "name": "parricide"}, - {"id": 16596, "synset": "parrot.n.02", "name": "parrot"}, - {"id": 16597, "synset": "partaker.n.01", "name": "partaker"}, - {"id": 16598, "synset": "part-timer.n.01", "name": "part-timer"}, - {"id": 16599, "synset": "party.n.05", "name": "party"}, - {"id": 16600, "synset": "party_man.n.01", "name": "party_man"}, - {"id": 16601, "synset": "passenger.n.01", "name": "passenger"}, - {"id": 16602, "synset": "passer.n.03", "name": "passer"}, - {"id": 16603, "synset": "paster.n.01", "name": "paster"}, - {"id": 16604, "synset": "pater.n.01", "name": "pater"}, - {"id": 16605, "synset": "patient.n.01", "name": "patient"}, - {"id": 16606, "synset": "patriarch.n.04", "name": "patriarch"}, - {"id": 16607, "synset": "patriarch.n.03", "name": "patriarch"}, - {"id": 16608, "synset": "patriarch.n.02", "name": "patriarch"}, - {"id": 16609, "synset": "patriot.n.01", "name": "patriot"}, - {"id": 16610, "synset": "patron.n.03", "name": "patron"}, - {"id": 16611, "synset": "patternmaker.n.01", "name": "patternmaker"}, - {"id": 16612, "synset": "pawnbroker.n.01", "name": "pawnbroker"}, - {"id": 16613, "synset": "payer.n.01", "name": "payer"}, - {"id": 16614, "synset": "peacekeeper.n.01", "name": "peacekeeper"}, - {"id": 16615, "synset": "peasant.n.02", "name": "peasant"}, - {"id": 16616, "synset": "pedant.n.01", "name": "pedant"}, - {"id": 16617, "synset": "peddler.n.01", "name": "peddler"}, - {"id": 16618, "synset": "pederast.n.01", "name": "pederast"}, - {"id": 16619, "synset": "penologist.n.01", "name": "penologist"}, - {"id": 16620, "synset": "pentathlete.n.01", "name": "pentathlete"}, - {"id": 16621, "synset": "pentecostal.n.01", "name": "Pentecostal"}, - {"id": 16622, "synset": "percussionist.n.01", "name": "percussionist"}, - {"id": 16623, "synset": "periodontist.n.01", "name": "periodontist"}, - {"id": 16624, "synset": "peshmerga.n.01", "name": "peshmerga"}, - {"id": 16625, "synset": "personality.n.02", "name": "personality"}, - {"id": 16626, "synset": "personal_representative.n.01", "name": "personal_representative"}, - {"id": 16627, "synset": "personage.n.01", "name": "personage"}, - {"id": 16628, "synset": "persona_grata.n.01", "name": "persona_grata"}, - {"id": 16629, "synset": "persona_non_grata.n.01", "name": "persona_non_grata"}, - {"id": 16630, "synset": "personification.n.01", "name": "personification"}, - {"id": 16631, "synset": "perspirer.n.01", "name": "perspirer"}, - {"id": 16632, "synset": "pervert.n.01", "name": "pervert"}, - {"id": 16633, "synset": "pessimist.n.01", "name": "pessimist"}, - {"id": 16634, "synset": "pest.n.03", "name": "pest"}, - {"id": 16635, "synset": "peter_pan.n.01", "name": "Peter_Pan"}, - {"id": 16636, "synset": "petitioner.n.01", "name": "petitioner"}, - {"id": 16637, "synset": "petit_juror.n.01", "name": "petit_juror"}, - {"id": 16638, "synset": "pet_sitter.n.01", "name": "pet_sitter"}, - {"id": 16639, "synset": "petter.n.01", "name": "petter"}, - {"id": 16640, "synset": "pharaoh.n.01", "name": "Pharaoh"}, - {"id": 16641, "synset": "pharmacist.n.01", "name": "pharmacist"}, - {"id": 16642, "synset": "philanthropist.n.01", "name": "philanthropist"}, - {"id": 16643, "synset": "philatelist.n.01", "name": "philatelist"}, - {"id": 16644, "synset": "philosopher.n.02", "name": "philosopher"}, - {"id": 16645, "synset": "phonetician.n.01", "name": "phonetician"}, - {"id": 16646, "synset": "phonologist.n.01", "name": "phonologist"}, - {"id": 16647, "synset": "photojournalist.n.01", "name": "photojournalist"}, - {"id": 16648, "synset": "photometrist.n.01", "name": "photometrist"}, - {"id": 16649, "synset": "physical_therapist.n.01", "name": "physical_therapist"}, - {"id": 16650, "synset": "physicist.n.01", "name": "physicist"}, - {"id": 16651, "synset": "piano_maker.n.01", "name": "piano_maker"}, - {"id": 16652, "synset": "picker.n.01", "name": "picker"}, - {"id": 16653, "synset": "picnicker.n.01", "name": "picnicker"}, - {"id": 16654, "synset": "pilgrim.n.01", "name": "pilgrim"}, - {"id": 16655, "synset": "pill.n.03", "name": "pill"}, - {"id": 16656, "synset": "pillar.n.03", "name": "pillar"}, - {"id": 16657, "synset": "pill_head.n.01", "name": "pill_head"}, - {"id": 16658, "synset": "pilot.n.02", "name": "pilot"}, - {"id": 16659, "synset": "piltdown_man.n.01", "name": "Piltdown_man"}, - {"id": 16660, "synset": "pimp.n.01", "name": "pimp"}, - {"id": 16661, "synset": "pipe_smoker.n.01", "name": "pipe_smoker"}, - {"id": 16662, "synset": "pip-squeak.n.01", "name": "pip-squeak"}, - {"id": 16663, "synset": "pisser.n.01", "name": "pisser"}, - {"id": 16664, "synset": "pitcher.n.01", "name": "pitcher"}, - {"id": 16665, "synset": "pitchman.n.01", "name": "pitchman"}, - {"id": 16666, "synset": "placeman.n.01", "name": "placeman"}, - {"id": 16667, "synset": "placer_miner.n.01", "name": "placer_miner"}, - {"id": 16668, "synset": "plagiarist.n.01", "name": "plagiarist"}, - {"id": 16669, "synset": "plainsman.n.01", "name": "plainsman"}, - {"id": 16670, "synset": "planner.n.01", "name": "planner"}, - {"id": 16671, "synset": "planter.n.01", "name": "planter"}, - {"id": 16672, "synset": "plasterer.n.01", "name": "plasterer"}, - {"id": 16673, "synset": "platinum_blond.n.01", "name": "platinum_blond"}, - {"id": 16674, "synset": "platitudinarian.n.01", "name": "platitudinarian"}, - {"id": 16675, "synset": "playboy.n.01", "name": "playboy"}, - {"id": 16676, "synset": "player.n.01", "name": "player"}, - {"id": 16677, "synset": "playmate.n.01", "name": "playmate"}, - {"id": 16678, "synset": "pleaser.n.01", "name": "pleaser"}, - {"id": 16679, "synset": "pledger.n.01", "name": "pledger"}, - {"id": 16680, "synset": "plenipotentiary.n.01", "name": "plenipotentiary"}, - {"id": 16681, "synset": "plier.n.01", "name": "plier"}, - {"id": 16682, "synset": "plodder.n.03", "name": "plodder"}, - {"id": 16683, "synset": "plodder.n.02", "name": "plodder"}, - {"id": 16684, "synset": "plotter.n.02", "name": "plotter"}, - {"id": 16685, "synset": "plumber.n.01", "name": "plumber"}, - {"id": 16686, "synset": "pluralist.n.02", "name": "pluralist"}, - {"id": 16687, "synset": "pluralist.n.01", "name": "pluralist"}, - {"id": 16688, "synset": "poet.n.01", "name": "poet"}, - {"id": 16689, "synset": "pointsman.n.01", "name": "pointsman"}, - {"id": 16690, "synset": "point_woman.n.01", "name": "point_woman"}, - {"id": 16691, "synset": "policyholder.n.01", "name": "policyholder"}, - {"id": 16692, "synset": "political_prisoner.n.01", "name": "political_prisoner"}, - {"id": 16693, "synset": "political_scientist.n.01", "name": "political_scientist"}, - {"id": 16694, "synset": "politician.n.02", "name": "politician"}, - {"id": 16695, "synset": "politician.n.03", "name": "politician"}, - {"id": 16696, "synset": "pollster.n.01", "name": "pollster"}, - {"id": 16697, "synset": "polluter.n.01", "name": "polluter"}, - {"id": 16698, "synset": "pool_player.n.01", "name": "pool_player"}, - {"id": 16699, "synset": "portraitist.n.01", "name": "portraitist"}, - {"id": 16700, "synset": "poseuse.n.01", "name": "poseuse"}, - {"id": 16701, "synset": "positivist.n.01", "name": "positivist"}, - {"id": 16702, "synset": "postdoc.n.02", "name": "postdoc"}, - {"id": 16703, "synset": "poster_girl.n.01", "name": "poster_girl"}, - {"id": 16704, "synset": "postulator.n.02", "name": "postulator"}, - {"id": 16705, "synset": "private_citizen.n.01", "name": "private_citizen"}, - {"id": 16706, "synset": "problem_solver.n.01", "name": "problem_solver"}, - {"id": 16707, "synset": "pro-lifer.n.01", "name": "pro-lifer"}, - {"id": 16708, "synset": "prosthetist.n.01", "name": "prosthetist"}, - {"id": 16709, "synset": "postulant.n.01", "name": "postulant"}, - {"id": 16710, "synset": "potboy.n.01", "name": "potboy"}, - {"id": 16711, "synset": "poultryman.n.01", "name": "poultryman"}, - {"id": 16712, "synset": "power_user.n.01", "name": "power_user"}, - {"id": 16713, "synset": "power_worker.n.01", "name": "power_worker"}, - {"id": 16714, "synset": "practitioner.n.01", "name": "practitioner"}, - {"id": 16715, "synset": "prayer.n.05", "name": "prayer"}, - {"id": 16716, "synset": "preceptor.n.01", "name": "preceptor"}, - {"id": 16717, "synset": "predecessor.n.01", "name": "predecessor"}, - {"id": 16718, "synset": "preemptor.n.02", "name": "preemptor"}, - {"id": 16719, "synset": "preemptor.n.01", "name": "preemptor"}, - {"id": 16720, "synset": "premature_baby.n.01", "name": "premature_baby"}, - {"id": 16721, "synset": "presbyter.n.01", "name": "presbyter"}, - {"id": 16722, "synset": "presenter.n.02", "name": "presenter"}, - {"id": 16723, "synset": "presentist.n.01", "name": "presentist"}, - {"id": 16724, "synset": "preserver.n.03", "name": "preserver"}, - {"id": 16725, "synset": "president.n.03", "name": "president"}, - { - "id": 16726, - "synset": "president_of_the_united_states.n.01", - "name": "President_of_the_United_States", - }, - {"id": 16727, "synset": "president.n.05", "name": "president"}, - {"id": 16728, "synset": "press_agent.n.01", "name": "press_agent"}, - {"id": 16729, "synset": "press_photographer.n.01", "name": "press_photographer"}, - {"id": 16730, "synset": "priest.n.01", "name": "priest"}, - {"id": 16731, "synset": "prima_ballerina.n.01", "name": "prima_ballerina"}, - {"id": 16732, "synset": "prima_donna.n.02", "name": "prima_donna"}, - {"id": 16733, "synset": "prima_donna.n.01", "name": "prima_donna"}, - {"id": 16734, "synset": "primigravida.n.01", "name": "primigravida"}, - {"id": 16735, "synset": "primordial_dwarf.n.01", "name": "primordial_dwarf"}, - {"id": 16736, "synset": "prince_charming.n.01", "name": "prince_charming"}, - {"id": 16737, "synset": "prince_consort.n.01", "name": "prince_consort"}, - {"id": 16738, "synset": "princeling.n.01", "name": "princeling"}, - {"id": 16739, "synset": "prince_of_wales.n.01", "name": "Prince_of_Wales"}, - {"id": 16740, "synset": "princess.n.01", "name": "princess"}, - {"id": 16741, "synset": "princess_royal.n.01", "name": "princess_royal"}, - {"id": 16742, "synset": "principal.n.06", "name": "principal"}, - {"id": 16743, "synset": "principal.n.02", "name": "principal"}, - {"id": 16744, "synset": "print_seller.n.01", "name": "print_seller"}, - {"id": 16745, "synset": "prior.n.01", "name": "prior"}, - {"id": 16746, "synset": "private.n.01", "name": "private"}, - {"id": 16747, "synset": "probationer.n.01", "name": "probationer"}, - {"id": 16748, "synset": "processor.n.02", "name": "processor"}, - {"id": 16749, "synset": "process-server.n.01", "name": "process-server"}, - {"id": 16750, "synset": "proconsul.n.02", "name": "proconsul"}, - {"id": 16751, "synset": "proconsul.n.01", "name": "proconsul"}, - {"id": 16752, "synset": "proctologist.n.01", "name": "proctologist"}, - {"id": 16753, "synset": "proctor.n.01", "name": "proctor"}, - {"id": 16754, "synset": "procurator.n.02", "name": "procurator"}, - {"id": 16755, "synset": "procurer.n.02", "name": "procurer"}, - {"id": 16756, "synset": "profit_taker.n.01", "name": "profit_taker"}, - {"id": 16757, "synset": "programmer.n.01", "name": "programmer"}, - {"id": 16758, "synset": "promiser.n.01", "name": "promiser"}, - {"id": 16759, "synset": "promoter.n.01", "name": "promoter"}, - {"id": 16760, "synset": "promulgator.n.01", "name": "promulgator"}, - {"id": 16761, "synset": "propagandist.n.01", "name": "propagandist"}, - {"id": 16762, "synset": "propagator.n.02", "name": "propagator"}, - {"id": 16763, "synset": "property_man.n.01", "name": "property_man"}, - {"id": 16764, "synset": "prophetess.n.01", "name": "prophetess"}, - {"id": 16765, "synset": "prophet.n.02", "name": "prophet"}, - {"id": 16766, "synset": "prosecutor.n.01", "name": "prosecutor"}, - {"id": 16767, "synset": "prospector.n.01", "name": "prospector"}, - {"id": 16768, "synset": "protectionist.n.01", "name": "protectionist"}, - {"id": 16769, "synset": "protegee.n.01", "name": "protegee"}, - {"id": 16770, "synset": "protozoologist.n.01", "name": "protozoologist"}, - {"id": 16771, "synset": "provost_marshal.n.01", "name": "provost_marshal"}, - {"id": 16772, "synset": "pruner.n.01", "name": "pruner"}, - {"id": 16773, "synset": "psalmist.n.01", "name": "psalmist"}, - {"id": 16774, "synset": "psephologist.n.01", "name": "psephologist"}, - {"id": 16775, "synset": "psychiatrist.n.01", "name": "psychiatrist"}, - {"id": 16776, "synset": "psychic.n.01", "name": "psychic"}, - {"id": 16777, "synset": "psycholinguist.n.01", "name": "psycholinguist"}, - {"id": 16778, "synset": "psychophysicist.n.01", "name": "psychophysicist"}, - {"id": 16779, "synset": "publican.n.01", "name": "publican"}, - {"id": 16780, "synset": "pudge.n.01", "name": "pudge"}, - {"id": 16781, "synset": "puerpera.n.01", "name": "puerpera"}, - {"id": 16782, "synset": "punching_bag.n.01", "name": "punching_bag"}, - {"id": 16783, "synset": "punter.n.02", "name": "punter"}, - {"id": 16784, "synset": "punter.n.01", "name": "punter"}, - {"id": 16785, "synset": "puppeteer.n.01", "name": "puppeteer"}, - {"id": 16786, "synset": "puppy.n.02", "name": "puppy"}, - {"id": 16787, "synset": "purchasing_agent.n.01", "name": "purchasing_agent"}, - {"id": 16788, "synset": "puritan.n.02", "name": "puritan"}, - {"id": 16789, "synset": "puritan.n.01", "name": "Puritan"}, - {"id": 16790, "synset": "pursuer.n.02", "name": "pursuer"}, - {"id": 16791, "synset": "pusher.n.03", "name": "pusher"}, - {"id": 16792, "synset": "pusher.n.02", "name": "pusher"}, - {"id": 16793, "synset": "pusher.n.01", "name": "pusher"}, - {"id": 16794, "synset": "putz.n.01", "name": "putz"}, - {"id": 16795, "synset": "pygmy.n.02", "name": "Pygmy"}, - {"id": 16796, "synset": "qadi.n.01", "name": "qadi"}, - {"id": 16797, "synset": "quadriplegic.n.01", "name": "quadriplegic"}, - {"id": 16798, "synset": "quadruplet.n.02", "name": "quadruplet"}, - {"id": 16799, "synset": "quaker.n.02", "name": "quaker"}, - {"id": 16800, "synset": "quarter.n.11", "name": "quarter"}, - {"id": 16801, "synset": "quarterback.n.01", "name": "quarterback"}, - {"id": 16802, "synset": "quartermaster.n.01", "name": "quartermaster"}, - {"id": 16803, "synset": "quartermaster_general.n.01", "name": "quartermaster_general"}, - {"id": 16804, "synset": "quebecois.n.01", "name": "Quebecois"}, - {"id": 16805, "synset": "queen.n.02", "name": "queen"}, - {"id": 16806, "synset": "queen_of_england.n.01", "name": "Queen_of_England"}, - {"id": 16807, "synset": "queen.n.03", "name": "queen"}, - {"id": 16808, "synset": "queen.n.04", "name": "queen"}, - {"id": 16809, "synset": "queen_consort.n.01", "name": "queen_consort"}, - {"id": 16810, "synset": "queen_mother.n.01", "name": "queen_mother"}, - {"id": 16811, "synset": "queen's_counsel.n.01", "name": "Queen's_Counsel"}, - {"id": 16812, "synset": "question_master.n.01", "name": "question_master"}, - {"id": 16813, "synset": "quick_study.n.01", "name": "quick_study"}, - {"id": 16814, "synset": "quietist.n.01", "name": "quietist"}, - {"id": 16815, "synset": "quitter.n.01", "name": "quitter"}, - {"id": 16816, "synset": "rabbi.n.01", "name": "rabbi"}, - {"id": 16817, "synset": "racist.n.01", "name": "racist"}, - {"id": 16818, "synset": "radiobiologist.n.01", "name": "radiobiologist"}, - {"id": 16819, "synset": "radiologic_technologist.n.01", "name": "radiologic_technologist"}, - {"id": 16820, "synset": "radiologist.n.01", "name": "radiologist"}, - {"id": 16821, "synset": "rainmaker.n.02", "name": "rainmaker"}, - {"id": 16822, "synset": "raiser.n.01", "name": "raiser"}, - {"id": 16823, "synset": "raja.n.01", "name": "raja"}, - {"id": 16824, "synset": "rake.n.01", "name": "rake"}, - {"id": 16825, "synset": "ramrod.n.02", "name": "ramrod"}, - {"id": 16826, "synset": "ranch_hand.n.01", "name": "ranch_hand"}, - {"id": 16827, "synset": "ranker.n.01", "name": "ranker"}, - {"id": 16828, "synset": "ranter.n.01", "name": "ranter"}, - {"id": 16829, "synset": "rape_suspect.n.01", "name": "rape_suspect"}, - {"id": 16830, "synset": "rapper.n.01", "name": "rapper"}, - {"id": 16831, "synset": "rapporteur.n.01", "name": "rapporteur"}, - {"id": 16832, "synset": "rare_bird.n.01", "name": "rare_bird"}, - {"id": 16833, "synset": "ratepayer.n.01", "name": "ratepayer"}, - {"id": 16834, "synset": "raw_recruit.n.01", "name": "raw_recruit"}, - {"id": 16835, "synset": "reader.n.01", "name": "reader"}, - {"id": 16836, "synset": "reading_teacher.n.01", "name": "reading_teacher"}, - {"id": 16837, "synset": "realist.n.01", "name": "realist"}, - {"id": 16838, "synset": "real_estate_broker.n.01", "name": "real_estate_broker"}, - {"id": 16839, "synset": "rear_admiral.n.01", "name": "rear_admiral"}, - {"id": 16840, "synset": "receiver.n.05", "name": "receiver"}, - {"id": 16841, "synset": "reciter.n.01", "name": "reciter"}, - {"id": 16842, "synset": "recruit.n.02", "name": "recruit"}, - {"id": 16843, "synset": "recruit.n.01", "name": "recruit"}, - {"id": 16844, "synset": "recruiter.n.01", "name": "recruiter"}, - {"id": 16845, "synset": "recruiting-sergeant.n.01", "name": "recruiting-sergeant"}, - {"id": 16846, "synset": "redcap.n.01", "name": "redcap"}, - {"id": 16847, "synset": "redhead.n.01", "name": "redhead"}, - {"id": 16848, "synset": "redneck.n.01", "name": "redneck"}, - {"id": 16849, "synset": "reeler.n.02", "name": "reeler"}, - {"id": 16850, "synset": "reenactor.n.01", "name": "reenactor"}, - {"id": 16851, "synset": "referral.n.01", "name": "referral"}, - {"id": 16852, "synset": "referee.n.01", "name": "referee"}, - {"id": 16853, "synset": "refiner.n.01", "name": "refiner"}, - {"id": 16854, "synset": "reform_jew.n.01", "name": "Reform_Jew"}, - {"id": 16855, "synset": "registered_nurse.n.01", "name": "registered_nurse"}, - {"id": 16856, "synset": "registrar.n.01", "name": "registrar"}, - {"id": 16857, "synset": "regius_professor.n.01", "name": "Regius_professor"}, - {"id": 16858, "synset": "reliever.n.02", "name": "reliever"}, - {"id": 16859, "synset": "anchorite.n.01", "name": "anchorite"}, - {"id": 16860, "synset": "religious_leader.n.01", "name": "religious_leader"}, - {"id": 16861, "synset": "remover.n.02", "name": "remover"}, - {"id": 16862, "synset": "renaissance_man.n.01", "name": "Renaissance_man"}, - {"id": 16863, "synset": "renegade.n.01", "name": "renegade"}, - {"id": 16864, "synset": "rentier.n.01", "name": "rentier"}, - {"id": 16865, "synset": "repairman.n.01", "name": "repairman"}, - {"id": 16866, "synset": "reporter.n.01", "name": "reporter"}, - {"id": 16867, "synset": "newswoman.n.01", "name": "newswoman"}, - {"id": 16868, "synset": "representative.n.01", "name": "representative"}, - {"id": 16869, "synset": "reprobate.n.01", "name": "reprobate"}, - {"id": 16870, "synset": "rescuer.n.02", "name": "rescuer"}, - {"id": 16871, "synset": "reservist.n.01", "name": "reservist"}, - {"id": 16872, "synset": "resident_commissioner.n.01", "name": "resident_commissioner"}, - {"id": 16873, "synset": "respecter.n.01", "name": "respecter"}, - {"id": 16874, "synset": "restaurateur.n.01", "name": "restaurateur"}, - {"id": 16875, "synset": "restrainer.n.02", "name": "restrainer"}, - {"id": 16876, "synset": "retailer.n.01", "name": "retailer"}, - {"id": 16877, "synset": "retiree.n.01", "name": "retiree"}, - {"id": 16878, "synset": "returning_officer.n.01", "name": "returning_officer"}, - {"id": 16879, "synset": "revenant.n.01", "name": "revenant"}, - {"id": 16880, "synset": "revisionist.n.01", "name": "revisionist"}, - {"id": 16881, "synset": "revolutionist.n.01", "name": "revolutionist"}, - {"id": 16882, "synset": "rheumatologist.n.01", "name": "rheumatologist"}, - {"id": 16883, "synset": "rhodesian_man.n.01", "name": "Rhodesian_man"}, - {"id": 16884, "synset": "rhymer.n.01", "name": "rhymer"}, - {"id": 16885, "synset": "rich_person.n.01", "name": "rich_person"}, - {"id": 16886, "synset": "rider.n.03", "name": "rider"}, - {"id": 16887, "synset": "riding_master.n.01", "name": "riding_master"}, - {"id": 16888, "synset": "rifleman.n.02", "name": "rifleman"}, - {"id": 16889, "synset": "right-hander.n.02", "name": "right-hander"}, - {"id": 16890, "synset": "right-hand_man.n.01", "name": "right-hand_man"}, - {"id": 16891, "synset": "ringer.n.03", "name": "ringer"}, - {"id": 16892, "synset": "ringleader.n.01", "name": "ringleader"}, - {"id": 16893, "synset": "roadman.n.02", "name": "roadman"}, - {"id": 16894, "synset": "roarer.n.01", "name": "roarer"}, - {"id": 16895, "synset": "rocket_engineer.n.01", "name": "rocket_engineer"}, - {"id": 16896, "synset": "rocket_scientist.n.01", "name": "rocket_scientist"}, - {"id": 16897, "synset": "rock_star.n.01", "name": "rock_star"}, - {"id": 16898, "synset": "romanov.n.01", "name": "Romanov"}, - {"id": 16899, "synset": "romanticist.n.02", "name": "romanticist"}, - {"id": 16900, "synset": "ropemaker.n.01", "name": "ropemaker"}, - {"id": 16901, "synset": "roper.n.02", "name": "roper"}, - {"id": 16902, "synset": "roper.n.01", "name": "roper"}, - {"id": 16903, "synset": "ropewalker.n.01", "name": "ropewalker"}, - {"id": 16904, "synset": "rosebud.n.02", "name": "rosebud"}, - {"id": 16905, "synset": "rosicrucian.n.02", "name": "Rosicrucian"}, - {"id": 16906, "synset": "mountie.n.01", "name": "Mountie"}, - {"id": 16907, "synset": "rough_rider.n.01", "name": "Rough_Rider"}, - {"id": 16908, "synset": "roundhead.n.01", "name": "roundhead"}, - {"id": 16909, "synset": "civil_authority.n.01", "name": "civil_authority"}, - {"id": 16910, "synset": "runner.n.03", "name": "runner"}, - {"id": 16911, "synset": "runner.n.02", "name": "runner"}, - {"id": 16912, "synset": "runner.n.06", "name": "runner"}, - {"id": 16913, "synset": "running_back.n.01", "name": "running_back"}, - {"id": 16914, "synset": "rusher.n.02", "name": "rusher"}, - {"id": 16915, "synset": "rustic.n.01", "name": "rustic"}, - {"id": 16916, "synset": "saboteur.n.01", "name": "saboteur"}, - {"id": 16917, "synset": "sadist.n.01", "name": "sadist"}, - {"id": 16918, "synset": "sailing_master.n.01", "name": "sailing_master"}, - {"id": 16919, "synset": "sailor.n.01", "name": "sailor"}, - {"id": 16920, "synset": "salesgirl.n.01", "name": "salesgirl"}, - {"id": 16921, "synset": "salesman.n.01", "name": "salesman"}, - {"id": 16922, "synset": "salesperson.n.01", "name": "salesperson"}, - {"id": 16923, "synset": "salvager.n.01", "name": "salvager"}, - {"id": 16924, "synset": "sandwichman.n.01", "name": "sandwichman"}, - {"id": 16925, "synset": "sangoma.n.01", "name": "sangoma"}, - {"id": 16926, "synset": "sannup.n.01", "name": "sannup"}, - {"id": 16927, "synset": "sapper.n.02", "name": "sapper"}, - {"id": 16928, "synset": "sassenach.n.01", "name": "Sassenach"}, - {"id": 16929, "synset": "satrap.n.01", "name": "satrap"}, - {"id": 16930, "synset": "saunterer.n.01", "name": "saunterer"}, - {"id": 16931, "synset": "savoyard.n.01", "name": "Savoyard"}, - {"id": 16932, "synset": "sawyer.n.01", "name": "sawyer"}, - {"id": 16933, "synset": "scalper.n.01", "name": "scalper"}, - {"id": 16934, "synset": "scandalmonger.n.01", "name": "scandalmonger"}, - {"id": 16935, "synset": "scapegrace.n.01", "name": "scapegrace"}, - {"id": 16936, "synset": "scene_painter.n.02", "name": "scene_painter"}, - {"id": 16937, "synset": "schemer.n.01", "name": "schemer"}, - {"id": 16938, "synset": "schizophrenic.n.01", "name": "schizophrenic"}, - {"id": 16939, "synset": "schlemiel.n.01", "name": "schlemiel"}, - {"id": 16940, "synset": "schlockmeister.n.01", "name": "schlockmeister"}, - {"id": 16941, "synset": "scholar.n.01", "name": "scholar"}, - {"id": 16942, "synset": "scholiast.n.01", "name": "scholiast"}, - {"id": 16943, "synset": "schoolchild.n.01", "name": "schoolchild"}, - {"id": 16944, "synset": "schoolfriend.n.01", "name": "schoolfriend"}, - {"id": 16945, "synset": "schoolman.n.01", "name": "Schoolman"}, - {"id": 16946, "synset": "schoolmaster.n.02", "name": "schoolmaster"}, - {"id": 16947, "synset": "schoolmate.n.01", "name": "schoolmate"}, - {"id": 16948, "synset": "scientist.n.01", "name": "scientist"}, - {"id": 16949, "synset": "scion.n.01", "name": "scion"}, - {"id": 16950, "synset": "scoffer.n.02", "name": "scoffer"}, - {"id": 16951, "synset": "scofflaw.n.01", "name": "scofflaw"}, - {"id": 16952, "synset": "scorekeeper.n.01", "name": "scorekeeper"}, - {"id": 16953, "synset": "scorer.n.02", "name": "scorer"}, - {"id": 16954, "synset": "scourer.n.02", "name": "scourer"}, - {"id": 16955, "synset": "scout.n.03", "name": "scout"}, - {"id": 16956, "synset": "scoutmaster.n.01", "name": "scoutmaster"}, - {"id": 16957, "synset": "scrambler.n.01", "name": "scrambler"}, - {"id": 16958, "synset": "scratcher.n.02", "name": "scratcher"}, - {"id": 16959, "synset": "screen_actor.n.01", "name": "screen_actor"}, - {"id": 16960, "synset": "scrutineer.n.01", "name": "scrutineer"}, - {"id": 16961, "synset": "scuba_diver.n.01", "name": "scuba_diver"}, - {"id": 16962, "synset": "sculptor.n.01", "name": "sculptor"}, - {"id": 16963, "synset": "sea_scout.n.01", "name": "Sea_Scout"}, - {"id": 16964, "synset": "seasonal_worker.n.01", "name": "seasonal_worker"}, - {"id": 16965, "synset": "seasoner.n.01", "name": "seasoner"}, - {"id": 16966, "synset": "second_baseman.n.01", "name": "second_baseman"}, - {"id": 16967, "synset": "second_cousin.n.01", "name": "second_cousin"}, - {"id": 16968, "synset": "seconder.n.01", "name": "seconder"}, - {"id": 16969, "synset": "second_fiddle.n.01", "name": "second_fiddle"}, - {"id": 16970, "synset": "second-in-command.n.01", "name": "second-in-command"}, - {"id": 16971, "synset": "second_lieutenant.n.01", "name": "second_lieutenant"}, - {"id": 16972, "synset": "second-rater.n.01", "name": "second-rater"}, - {"id": 16973, "synset": "secretary.n.01", "name": "secretary"}, - {"id": 16974, "synset": "secretary_of_agriculture.n.01", "name": "Secretary_of_Agriculture"}, - { - "id": 16975, - "synset": "secretary_of_health_and_human_services.n.01", - "name": "Secretary_of_Health_and_Human_Services", - }, - {"id": 16976, "synset": "secretary_of_state.n.01", "name": "Secretary_of_State"}, - {"id": 16977, "synset": "secretary_of_the_interior.n.02", "name": "Secretary_of_the_Interior"}, - {"id": 16978, "synset": "sectarian.n.01", "name": "sectarian"}, - {"id": 16979, "synset": "section_hand.n.01", "name": "section_hand"}, - {"id": 16980, "synset": "secularist.n.01", "name": "secularist"}, - {"id": 16981, "synset": "security_consultant.n.01", "name": "security_consultant"}, - {"id": 16982, "synset": "seeded_player.n.01", "name": "seeded_player"}, - {"id": 16983, "synset": "seeder.n.01", "name": "seeder"}, - {"id": 16984, "synset": "seeker.n.01", "name": "seeker"}, - {"id": 16985, "synset": "segregate.n.01", "name": "segregate"}, - {"id": 16986, "synset": "segregator.n.01", "name": "segregator"}, - {"id": 16987, "synset": "selectman.n.01", "name": "selectman"}, - {"id": 16988, "synset": "selectwoman.n.01", "name": "selectwoman"}, - {"id": 16989, "synset": "selfish_person.n.01", "name": "selfish_person"}, - {"id": 16990, "synset": "self-starter.n.01", "name": "self-starter"}, - {"id": 16991, "synset": "seller.n.01", "name": "seller"}, - {"id": 16992, "synset": "selling_agent.n.01", "name": "selling_agent"}, - {"id": 16993, "synset": "semanticist.n.01", "name": "semanticist"}, - {"id": 16994, "synset": "semifinalist.n.01", "name": "semifinalist"}, - {"id": 16995, "synset": "seminarian.n.01", "name": "seminarian"}, - {"id": 16996, "synset": "senator.n.01", "name": "senator"}, - {"id": 16997, "synset": "sendee.n.01", "name": "sendee"}, - {"id": 16998, "synset": "senior.n.01", "name": "senior"}, - {"id": 16999, "synset": "senior_vice_president.n.01", "name": "senior_vice_president"}, - {"id": 17000, "synset": "separatist.n.01", "name": "separatist"}, - {"id": 17001, "synset": "septuagenarian.n.01", "name": "septuagenarian"}, - {"id": 17002, "synset": "serf.n.01", "name": "serf"}, - {"id": 17003, "synset": "spree_killer.n.01", "name": "spree_killer"}, - {"id": 17004, "synset": "serjeant-at-law.n.01", "name": "serjeant-at-law"}, - {"id": 17005, "synset": "server.n.02", "name": "server"}, - {"id": 17006, "synset": "serviceman.n.01", "name": "serviceman"}, - {"id": 17007, "synset": "settler.n.01", "name": "settler"}, - {"id": 17008, "synset": "settler.n.03", "name": "settler"}, - {"id": 17009, "synset": "sex_symbol.n.01", "name": "sex_symbol"}, - {"id": 17010, "synset": "sexton.n.02", "name": "sexton"}, - {"id": 17011, "synset": "shaheed.n.01", "name": "shaheed"}, - {"id": 17012, "synset": "shakespearian.n.01", "name": "Shakespearian"}, - {"id": 17013, "synset": "shanghaier.n.01", "name": "shanghaier"}, - {"id": 17014, "synset": "sharecropper.n.01", "name": "sharecropper"}, - {"id": 17015, "synset": "shaver.n.01", "name": "shaver"}, - {"id": 17016, "synset": "shavian.n.01", "name": "Shavian"}, - {"id": 17017, "synset": "sheep.n.02", "name": "sheep"}, - {"id": 17018, "synset": "sheik.n.01", "name": "sheik"}, - {"id": 17019, "synset": "shelver.n.01", "name": "shelver"}, - {"id": 17020, "synset": "shepherd.n.01", "name": "shepherd"}, - {"id": 17021, "synset": "ship-breaker.n.01", "name": "ship-breaker"}, - {"id": 17022, "synset": "shipmate.n.01", "name": "shipmate"}, - {"id": 17023, "synset": "shipowner.n.01", "name": "shipowner"}, - {"id": 17024, "synset": "shipping_agent.n.01", "name": "shipping_agent"}, - {"id": 17025, "synset": "shirtmaker.n.01", "name": "shirtmaker"}, - {"id": 17026, "synset": "shogun.n.01", "name": "shogun"}, - {"id": 17027, "synset": "shopaholic.n.01", "name": "shopaholic"}, - {"id": 17028, "synset": "shop_girl.n.01", "name": "shop_girl"}, - {"id": 17029, "synset": "shop_steward.n.01", "name": "shop_steward"}, - {"id": 17030, "synset": "shot_putter.n.01", "name": "shot_putter"}, - {"id": 17031, "synset": "shrew.n.01", "name": "shrew"}, - {"id": 17032, "synset": "shuffler.n.01", "name": "shuffler"}, - {"id": 17033, "synset": "shyster.n.01", "name": "shyster"}, - {"id": 17034, "synset": "sibling.n.01", "name": "sibling"}, - {"id": 17035, "synset": "sick_person.n.01", "name": "sick_person"}, - {"id": 17036, "synset": "sightreader.n.01", "name": "sightreader"}, - {"id": 17037, "synset": "signaler.n.01", "name": "signaler"}, - {"id": 17038, "synset": "signer.n.01", "name": "signer"}, - {"id": 17039, "synset": "signor.n.01", "name": "signor"}, - {"id": 17040, "synset": "signora.n.01", "name": "signora"}, - {"id": 17041, "synset": "signore.n.01", "name": "signore"}, - {"id": 17042, "synset": "signorina.n.01", "name": "signorina"}, - {"id": 17043, "synset": "silent_partner.n.01", "name": "silent_partner"}, - {"id": 17044, "synset": "addle-head.n.01", "name": "addle-head"}, - {"id": 17045, "synset": "simperer.n.01", "name": "simperer"}, - {"id": 17046, "synset": "singer.n.01", "name": "singer"}, - {"id": 17047, "synset": "sinologist.n.01", "name": "Sinologist"}, - {"id": 17048, "synset": "sipper.n.01", "name": "sipper"}, - {"id": 17049, "synset": "sirrah.n.01", "name": "sirrah"}, - {"id": 17050, "synset": "sister.n.02", "name": "Sister"}, - {"id": 17051, "synset": "sister.n.01", "name": "sister"}, - {"id": 17052, "synset": "waverer.n.01", "name": "waverer"}, - {"id": 17053, "synset": "sitar_player.n.01", "name": "sitar_player"}, - {"id": 17054, "synset": "sixth-former.n.01", "name": "sixth-former"}, - {"id": 17055, "synset": "skateboarder.n.01", "name": "skateboarder"}, - {"id": 17056, "synset": "skeptic.n.01", "name": "skeptic"}, - {"id": 17057, "synset": "sketcher.n.01", "name": "sketcher"}, - {"id": 17058, "synset": "skidder.n.02", "name": "skidder"}, - {"id": 17059, "synset": "skier.n.01", "name": "skier"}, - {"id": 17060, "synset": "skinny-dipper.n.01", "name": "skinny-dipper"}, - {"id": 17061, "synset": "skin-diver.n.01", "name": "skin-diver"}, - {"id": 17062, "synset": "skinhead.n.01", "name": "skinhead"}, - {"id": 17063, "synset": "slasher.n.01", "name": "slasher"}, - {"id": 17064, "synset": "slattern.n.02", "name": "slattern"}, - {"id": 17065, "synset": "sleeper.n.01", "name": "sleeper"}, - {"id": 17066, "synset": "sleeper.n.02", "name": "sleeper"}, - {"id": 17067, "synset": "sleeping_beauty.n.02", "name": "sleeping_beauty"}, - {"id": 17068, "synset": "sleuth.n.01", "name": "sleuth"}, - {"id": 17069, "synset": "slob.n.01", "name": "slob"}, - {"id": 17070, "synset": "sloganeer.n.01", "name": "sloganeer"}, - {"id": 17071, "synset": "slopseller.n.01", "name": "slopseller"}, - {"id": 17072, "synset": "smasher.n.02", "name": "smasher"}, - {"id": 17073, "synset": "smirker.n.01", "name": "smirker"}, - {"id": 17074, "synset": "smith.n.10", "name": "smith"}, - {"id": 17075, "synset": "smoothie.n.01", "name": "smoothie"}, - {"id": 17076, "synset": "smuggler.n.01", "name": "smuggler"}, - {"id": 17077, "synset": "sneezer.n.01", "name": "sneezer"}, - {"id": 17078, "synset": "snob.n.01", "name": "snob"}, - {"id": 17079, "synset": "snoop.n.01", "name": "snoop"}, - {"id": 17080, "synset": "snorer.n.01", "name": "snorer"}, - {"id": 17081, "synset": "sob_sister.n.01", "name": "sob_sister"}, - {"id": 17082, "synset": "soccer_player.n.01", "name": "soccer_player"}, - {"id": 17083, "synset": "social_anthropologist.n.01", "name": "social_anthropologist"}, - {"id": 17084, "synset": "social_climber.n.01", "name": "social_climber"}, - {"id": 17085, "synset": "socialist.n.01", "name": "socialist"}, - {"id": 17086, "synset": "socializer.n.01", "name": "socializer"}, - {"id": 17087, "synset": "social_scientist.n.01", "name": "social_scientist"}, - {"id": 17088, "synset": "social_secretary.n.01", "name": "social_secretary"}, - {"id": 17089, "synset": "socinian.n.01", "name": "Socinian"}, - {"id": 17090, "synset": "sociolinguist.n.01", "name": "sociolinguist"}, - {"id": 17091, "synset": "sociologist.n.01", "name": "sociologist"}, - {"id": 17092, "synset": "soda_jerk.n.01", "name": "soda_jerk"}, - {"id": 17093, "synset": "sodalist.n.01", "name": "sodalist"}, - {"id": 17094, "synset": "sodomite.n.01", "name": "sodomite"}, - {"id": 17095, "synset": "soldier.n.01", "name": "soldier"}, - {"id": 17096, "synset": "son.n.01", "name": "son"}, - {"id": 17097, "synset": "songster.n.02", "name": "songster"}, - {"id": 17098, "synset": "songstress.n.01", "name": "songstress"}, - {"id": 17099, "synset": "songwriter.n.01", "name": "songwriter"}, - {"id": 17100, "synset": "sorcerer.n.01", "name": "sorcerer"}, - {"id": 17101, "synset": "sorehead.n.01", "name": "sorehead"}, - {"id": 17102, "synset": "soul_mate.n.01", "name": "soul_mate"}, - {"id": 17103, "synset": "southern_baptist.n.01", "name": "Southern_Baptist"}, - {"id": 17104, "synset": "sovereign.n.01", "name": "sovereign"}, - {"id": 17105, "synset": "spacewalker.n.01", "name": "spacewalker"}, - {"id": 17106, "synset": "spanish_american.n.01", "name": "Spanish_American"}, - {"id": 17107, "synset": "sparring_partner.n.01", "name": "sparring_partner"}, - {"id": 17108, "synset": "spastic.n.01", "name": "spastic"}, - {"id": 17109, "synset": "speaker.n.01", "name": "speaker"}, - {"id": 17110, "synset": "native_speaker.n.01", "name": "native_speaker"}, - {"id": 17111, "synset": "speaker.n.03", "name": "Speaker"}, - {"id": 17112, "synset": "speechwriter.n.01", "name": "speechwriter"}, - {"id": 17113, "synset": "specialist.n.02", "name": "specialist"}, - {"id": 17114, "synset": "specifier.n.01", "name": "specifier"}, - {"id": 17115, "synset": "spectator.n.01", "name": "spectator"}, - {"id": 17116, "synset": "speech_therapist.n.01", "name": "speech_therapist"}, - {"id": 17117, "synset": "speedskater.n.01", "name": "speedskater"}, - {"id": 17118, "synset": "spellbinder.n.01", "name": "spellbinder"}, - {"id": 17119, "synset": "sphinx.n.01", "name": "sphinx"}, - {"id": 17120, "synset": "spinster.n.01", "name": "spinster"}, - {"id": 17121, "synset": "split_end.n.01", "name": "split_end"}, - {"id": 17122, "synset": "sport.n.05", "name": "sport"}, - {"id": 17123, "synset": "sport.n.03", "name": "sport"}, - {"id": 17124, "synset": "sporting_man.n.02", "name": "sporting_man"}, - {"id": 17125, "synset": "sports_announcer.n.01", "name": "sports_announcer"}, - {"id": 17126, "synset": "sports_editor.n.01", "name": "sports_editor"}, - {"id": 17127, "synset": "sprog.n.02", "name": "sprog"}, - {"id": 17128, "synset": "square_dancer.n.01", "name": "square_dancer"}, - {"id": 17129, "synset": "square_shooter.n.01", "name": "square_shooter"}, - {"id": 17130, "synset": "squatter.n.02", "name": "squatter"}, - {"id": 17131, "synset": "squire.n.02", "name": "squire"}, - {"id": 17132, "synset": "squire.n.01", "name": "squire"}, - {"id": 17133, "synset": "staff_member.n.01", "name": "staff_member"}, - {"id": 17134, "synset": "staff_sergeant.n.01", "name": "staff_sergeant"}, - {"id": 17135, "synset": "stage_director.n.01", "name": "stage_director"}, - {"id": 17136, "synset": "stainer.n.01", "name": "stainer"}, - {"id": 17137, "synset": "stakeholder.n.01", "name": "stakeholder"}, - {"id": 17138, "synset": "stalker.n.02", "name": "stalker"}, - {"id": 17139, "synset": "stalking-horse.n.01", "name": "stalking-horse"}, - {"id": 17140, "synset": "stammerer.n.01", "name": "stammerer"}, - {"id": 17141, "synset": "stamper.n.02", "name": "stamper"}, - {"id": 17142, "synset": "standee.n.01", "name": "standee"}, - {"id": 17143, "synset": "stand-in.n.01", "name": "stand-in"}, - {"id": 17144, "synset": "star.n.04", "name": "star"}, - {"id": 17145, "synset": "starlet.n.01", "name": "starlet"}, - {"id": 17146, "synset": "starter.n.03", "name": "starter"}, - {"id": 17147, "synset": "statesman.n.01", "name": "statesman"}, - {"id": 17148, "synset": "state_treasurer.n.01", "name": "state_treasurer"}, - {"id": 17149, "synset": "stationer.n.01", "name": "stationer"}, - {"id": 17150, "synset": "stenographer.n.01", "name": "stenographer"}, - {"id": 17151, "synset": "stentor.n.01", "name": "stentor"}, - {"id": 17152, "synset": "stepbrother.n.01", "name": "stepbrother"}, - {"id": 17153, "synset": "stepmother.n.01", "name": "stepmother"}, - {"id": 17154, "synset": "stepparent.n.01", "name": "stepparent"}, - {"id": 17155, "synset": "stevedore.n.01", "name": "stevedore"}, - {"id": 17156, "synset": "steward.n.01", "name": "steward"}, - {"id": 17157, "synset": "steward.n.03", "name": "steward"}, - {"id": 17158, "synset": "steward.n.02", "name": "steward"}, - {"id": 17159, "synset": "stickler.n.01", "name": "stickler"}, - {"id": 17160, "synset": "stiff.n.01", "name": "stiff"}, - {"id": 17161, "synset": "stifler.n.01", "name": "stifler"}, - {"id": 17162, "synset": "stipendiary.n.01", "name": "stipendiary"}, - {"id": 17163, "synset": "stitcher.n.01", "name": "stitcher"}, - {"id": 17164, "synset": "stockjobber.n.01", "name": "stockjobber"}, - {"id": 17165, "synset": "stock_trader.n.01", "name": "stock_trader"}, - {"id": 17166, "synset": "stockist.n.01", "name": "stockist"}, - {"id": 17167, "synset": "stoker.n.02", "name": "stoker"}, - {"id": 17168, "synset": "stooper.n.02", "name": "stooper"}, - {"id": 17169, "synset": "store_detective.n.01", "name": "store_detective"}, - {"id": 17170, "synset": "strafer.n.01", "name": "strafer"}, - {"id": 17171, "synset": "straight_man.n.01", "name": "straight_man"}, - {"id": 17172, "synset": "stranger.n.01", "name": "stranger"}, - {"id": 17173, "synset": "stranger.n.02", "name": "stranger"}, - {"id": 17174, "synset": "strategist.n.01", "name": "strategist"}, - {"id": 17175, "synset": "straw_boss.n.01", "name": "straw_boss"}, - {"id": 17176, "synset": "streetwalker.n.01", "name": "streetwalker"}, - {"id": 17177, "synset": "stretcher-bearer.n.01", "name": "stretcher-bearer"}, - {"id": 17178, "synset": "struggler.n.01", "name": "struggler"}, - {"id": 17179, "synset": "stud.n.01", "name": "stud"}, - {"id": 17180, "synset": "student.n.01", "name": "student"}, - {"id": 17181, "synset": "stumblebum.n.01", "name": "stumblebum"}, - {"id": 17182, "synset": "stylist.n.01", "name": "stylist"}, - {"id": 17183, "synset": "subaltern.n.01", "name": "subaltern"}, - {"id": 17184, "synset": "subcontractor.n.01", "name": "subcontractor"}, - {"id": 17185, "synset": "subduer.n.01", "name": "subduer"}, - {"id": 17186, "synset": "subject.n.06", "name": "subject"}, - {"id": 17187, "synset": "subordinate.n.01", "name": "subordinate"}, - {"id": 17188, "synset": "substitute.n.02", "name": "substitute"}, - {"id": 17189, "synset": "successor.n.03", "name": "successor"}, - {"id": 17190, "synset": "successor.n.01", "name": "successor"}, - {"id": 17191, "synset": "succorer.n.01", "name": "succorer"}, - {"id": 17192, "synset": "sufi.n.01", "name": "Sufi"}, - {"id": 17193, "synset": "suffragan.n.01", "name": "suffragan"}, - {"id": 17194, "synset": "suffragette.n.01", "name": "suffragette"}, - {"id": 17195, "synset": "sugar_daddy.n.01", "name": "sugar_daddy"}, - {"id": 17196, "synset": "suicide_bomber.n.01", "name": "suicide_bomber"}, - {"id": 17197, "synset": "suitor.n.01", "name": "suitor"}, - {"id": 17198, "synset": "sumo_wrestler.n.01", "name": "sumo_wrestler"}, - {"id": 17199, "synset": "sunbather.n.01", "name": "sunbather"}, - {"id": 17200, "synset": "sundowner.n.01", "name": "sundowner"}, - {"id": 17201, "synset": "super_heavyweight.n.01", "name": "super_heavyweight"}, - {"id": 17202, "synset": "superior.n.01", "name": "superior"}, - {"id": 17203, "synset": "supermom.n.01", "name": "supermom"}, - {"id": 17204, "synset": "supernumerary.n.02", "name": "supernumerary"}, - {"id": 17205, "synset": "supremo.n.01", "name": "supremo"}, - {"id": 17206, "synset": "surgeon.n.01", "name": "surgeon"}, - {"id": 17207, "synset": "surgeon_general.n.02", "name": "Surgeon_General"}, - {"id": 17208, "synset": "surgeon_general.n.01", "name": "Surgeon_General"}, - {"id": 17209, "synset": "surpriser.n.01", "name": "surpriser"}, - {"id": 17210, "synset": "surveyor.n.01", "name": "surveyor"}, - {"id": 17211, "synset": "surveyor.n.02", "name": "surveyor"}, - {"id": 17212, "synset": "survivor.n.01", "name": "survivor"}, - {"id": 17213, "synset": "sutler.n.01", "name": "sutler"}, - {"id": 17214, "synset": "sweeper.n.01", "name": "sweeper"}, - {"id": 17215, "synset": "sweetheart.n.01", "name": "sweetheart"}, - {"id": 17216, "synset": "swinger.n.02", "name": "swinger"}, - {"id": 17217, "synset": "switcher.n.01", "name": "switcher"}, - {"id": 17218, "synset": "swot.n.01", "name": "swot"}, - {"id": 17219, "synset": "sycophant.n.01", "name": "sycophant"}, - {"id": 17220, "synset": "sylph.n.01", "name": "sylph"}, - {"id": 17221, "synset": "sympathizer.n.02", "name": "sympathizer"}, - {"id": 17222, "synset": "symphonist.n.01", "name": "symphonist"}, - {"id": 17223, "synset": "syncopator.n.01", "name": "syncopator"}, - {"id": 17224, "synset": "syndic.n.01", "name": "syndic"}, - {"id": 17225, "synset": "tactician.n.01", "name": "tactician"}, - {"id": 17226, "synset": "tagger.n.02", "name": "tagger"}, - {"id": 17227, "synset": "tailback.n.01", "name": "tailback"}, - {"id": 17228, "synset": "tallyman.n.02", "name": "tallyman"}, - {"id": 17229, "synset": "tallyman.n.01", "name": "tallyman"}, - {"id": 17230, "synset": "tanker.n.02", "name": "tanker"}, - {"id": 17231, "synset": "tapper.n.04", "name": "tapper"}, - {"id": 17232, "synset": "tartuffe.n.01", "name": "Tartuffe"}, - {"id": 17233, "synset": "tarzan.n.01", "name": "Tarzan"}, - {"id": 17234, "synset": "taster.n.01", "name": "taster"}, - {"id": 17235, "synset": "tax_assessor.n.01", "name": "tax_assessor"}, - {"id": 17236, "synset": "taxer.n.01", "name": "taxer"}, - {"id": 17237, "synset": "taxi_dancer.n.01", "name": "taxi_dancer"}, - {"id": 17238, "synset": "taxonomist.n.01", "name": "taxonomist"}, - {"id": 17239, "synset": "teacher.n.01", "name": "teacher"}, - {"id": 17240, "synset": "teaching_fellow.n.01", "name": "teaching_fellow"}, - {"id": 17241, "synset": "tearaway.n.01", "name": "tearaway"}, - {"id": 17242, "synset": "technical_sergeant.n.01", "name": "technical_sergeant"}, - {"id": 17243, "synset": "technician.n.02", "name": "technician"}, - {"id": 17244, "synset": "ted.n.01", "name": "Ted"}, - {"id": 17245, "synset": "teetotaler.n.01", "name": "teetotaler"}, - {"id": 17246, "synset": "television_reporter.n.01", "name": "television_reporter"}, - {"id": 17247, "synset": "temporizer.n.01", "name": "temporizer"}, - {"id": 17248, "synset": "tempter.n.01", "name": "tempter"}, - {"id": 17249, "synset": "term_infant.n.01", "name": "term_infant"}, - {"id": 17250, "synset": "toiler.n.01", "name": "toiler"}, - {"id": 17251, "synset": "tenant.n.01", "name": "tenant"}, - {"id": 17252, "synset": "tenant.n.02", "name": "tenant"}, - {"id": 17253, "synset": "tenderfoot.n.01", "name": "tenderfoot"}, - {"id": 17254, "synset": "tennis_player.n.01", "name": "tennis_player"}, - {"id": 17255, "synset": "tennis_pro.n.01", "name": "tennis_pro"}, - {"id": 17256, "synset": "tenor_saxophonist.n.01", "name": "tenor_saxophonist"}, - {"id": 17257, "synset": "termer.n.01", "name": "termer"}, - {"id": 17258, "synset": "terror.n.02", "name": "terror"}, - {"id": 17259, "synset": "tertigravida.n.01", "name": "tertigravida"}, - {"id": 17260, "synset": "testator.n.01", "name": "testator"}, - {"id": 17261, "synset": "testatrix.n.01", "name": "testatrix"}, - {"id": 17262, "synset": "testee.n.01", "name": "testee"}, - {"id": 17263, "synset": "test-tube_baby.n.01", "name": "test-tube_baby"}, - {"id": 17264, "synset": "texas_ranger.n.01", "name": "Texas_Ranger"}, - {"id": 17265, "synset": "thane.n.02", "name": "thane"}, - {"id": 17266, "synset": "theatrical_producer.n.01", "name": "theatrical_producer"}, - {"id": 17267, "synset": "theologian.n.01", "name": "theologian"}, - {"id": 17268, "synset": "theorist.n.01", "name": "theorist"}, - {"id": 17269, "synset": "theosophist.n.01", "name": "theosophist"}, - {"id": 17270, "synset": "therapist.n.01", "name": "therapist"}, - {"id": 17271, "synset": "thessalonian.n.01", "name": "Thessalonian"}, - {"id": 17272, "synset": "thinker.n.01", "name": "thinker"}, - {"id": 17273, "synset": "thinker.n.02", "name": "thinker"}, - {"id": 17274, "synset": "thrower.n.02", "name": "thrower"}, - {"id": 17275, "synset": "thurifer.n.01", "name": "thurifer"}, - {"id": 17276, "synset": "ticket_collector.n.01", "name": "ticket_collector"}, - {"id": 17277, "synset": "tight_end.n.01", "name": "tight_end"}, - {"id": 17278, "synset": "tiler.n.01", "name": "tiler"}, - {"id": 17279, "synset": "timekeeper.n.01", "name": "timekeeper"}, - {"id": 17280, "synset": "timorese.n.01", "name": "Timorese"}, - {"id": 17281, "synset": "tinkerer.n.01", "name": "tinkerer"}, - {"id": 17282, "synset": "tinsmith.n.01", "name": "tinsmith"}, - {"id": 17283, "synset": "tinter.n.01", "name": "tinter"}, - {"id": 17284, "synset": "tippler.n.01", "name": "tippler"}, - {"id": 17285, "synset": "tipster.n.01", "name": "tipster"}, - {"id": 17286, "synset": "t-man.n.01", "name": "T-man"}, - {"id": 17287, "synset": "toastmaster.n.01", "name": "toastmaster"}, - {"id": 17288, "synset": "toast_mistress.n.01", "name": "toast_mistress"}, - {"id": 17289, "synset": "tobogganist.n.01", "name": "tobogganist"}, - {"id": 17290, "synset": "tomboy.n.01", "name": "tomboy"}, - {"id": 17291, "synset": "toolmaker.n.01", "name": "toolmaker"}, - {"id": 17292, "synset": "torchbearer.n.01", "name": "torchbearer"}, - {"id": 17293, "synset": "tory.n.01", "name": "Tory"}, - {"id": 17294, "synset": "tory.n.02", "name": "Tory"}, - {"id": 17295, "synset": "tosser.n.02", "name": "tosser"}, - {"id": 17296, "synset": "tosser.n.01", "name": "tosser"}, - {"id": 17297, "synset": "totalitarian.n.01", "name": "totalitarian"}, - {"id": 17298, "synset": "tourist.n.01", "name": "tourist"}, - {"id": 17299, "synset": "tout.n.02", "name": "tout"}, - {"id": 17300, "synset": "tout.n.01", "name": "tout"}, - {"id": 17301, "synset": "tovarich.n.01", "name": "tovarich"}, - {"id": 17302, "synset": "towhead.n.01", "name": "towhead"}, - {"id": 17303, "synset": "town_clerk.n.01", "name": "town_clerk"}, - {"id": 17304, "synset": "town_crier.n.01", "name": "town_crier"}, - {"id": 17305, "synset": "townsman.n.02", "name": "townsman"}, - {"id": 17306, "synset": "toxicologist.n.01", "name": "toxicologist"}, - {"id": 17307, "synset": "track_star.n.01", "name": "track_star"}, - {"id": 17308, "synset": "trader.n.01", "name": "trader"}, - {"id": 17309, "synset": "trade_unionist.n.01", "name": "trade_unionist"}, - {"id": 17310, "synset": "traditionalist.n.01", "name": "traditionalist"}, - {"id": 17311, "synset": "traffic_cop.n.01", "name": "traffic_cop"}, - {"id": 17312, "synset": "tragedian.n.02", "name": "tragedian"}, - {"id": 17313, "synset": "tragedian.n.01", "name": "tragedian"}, - {"id": 17314, "synset": "tragedienne.n.01", "name": "tragedienne"}, - {"id": 17315, "synset": "trail_boss.n.01", "name": "trail_boss"}, - {"id": 17316, "synset": "trainer.n.01", "name": "trainer"}, - {"id": 17317, "synset": "traitor.n.01", "name": "traitor"}, - {"id": 17318, "synset": "traitress.n.01", "name": "traitress"}, - {"id": 17319, "synset": "transactor.n.01", "name": "transactor"}, - {"id": 17320, "synset": "transcriber.n.03", "name": "transcriber"}, - {"id": 17321, "synset": "transfer.n.02", "name": "transfer"}, - {"id": 17322, "synset": "transferee.n.01", "name": "transferee"}, - {"id": 17323, "synset": "translator.n.01", "name": "translator"}, - {"id": 17324, "synset": "transvestite.n.01", "name": "transvestite"}, - {"id": 17325, "synset": "traveling_salesman.n.01", "name": "traveling_salesman"}, - {"id": 17326, "synset": "traverser.n.01", "name": "traverser"}, - {"id": 17327, "synset": "trawler.n.01", "name": "trawler"}, - {"id": 17328, "synset": "treasury.n.04", "name": "Treasury"}, - {"id": 17329, "synset": "trencher.n.01", "name": "trencher"}, - {"id": 17330, "synset": "trend-setter.n.01", "name": "trend-setter"}, - {"id": 17331, "synset": "tribesman.n.01", "name": "tribesman"}, - {"id": 17332, "synset": "trier.n.02", "name": "trier"}, - {"id": 17333, "synset": "trifler.n.01", "name": "trifler"}, - {"id": 17334, "synset": "trooper.n.02", "name": "trooper"}, - {"id": 17335, "synset": "trooper.n.03", "name": "trooper"}, - {"id": 17336, "synset": "trotskyite.n.01", "name": "Trotskyite"}, - {"id": 17337, "synset": "truant.n.01", "name": "truant"}, - {"id": 17338, "synset": "trumpeter.n.01", "name": "trumpeter"}, - {"id": 17339, "synset": "trusty.n.01", "name": "trusty"}, - {"id": 17340, "synset": "tudor.n.03", "name": "Tudor"}, - {"id": 17341, "synset": "tumbler.n.01", "name": "tumbler"}, - {"id": 17342, "synset": "tutee.n.01", "name": "tutee"}, - {"id": 17343, "synset": "twin.n.01", "name": "twin"}, - {"id": 17344, "synset": "two-timer.n.01", "name": "two-timer"}, - {"id": 17345, "synset": "tyke.n.01", "name": "Tyke"}, - {"id": 17346, "synset": "tympanist.n.01", "name": "tympanist"}, - {"id": 17347, "synset": "typist.n.01", "name": "typist"}, - {"id": 17348, "synset": "tyrant.n.01", "name": "tyrant"}, - {"id": 17349, "synset": "umpire.n.01", "name": "umpire"}, - {"id": 17350, "synset": "understudy.n.01", "name": "understudy"}, - {"id": 17351, "synset": "undesirable.n.01", "name": "undesirable"}, - {"id": 17352, "synset": "unicyclist.n.01", "name": "unicyclist"}, - {"id": 17353, "synset": "unilateralist.n.01", "name": "unilateralist"}, - {"id": 17354, "synset": "unitarian.n.01", "name": "Unitarian"}, - {"id": 17355, "synset": "arminian.n.01", "name": "Arminian"}, - {"id": 17356, "synset": "universal_donor.n.01", "name": "universal_donor"}, - {"id": 17357, "synset": "unix_guru.n.01", "name": "UNIX_guru"}, - {"id": 17358, "synset": "unknown_soldier.n.01", "name": "Unknown_Soldier"}, - {"id": 17359, "synset": "upsetter.n.01", "name": "upsetter"}, - {"id": 17360, "synset": "upstager.n.01", "name": "upstager"}, - {"id": 17361, "synset": "upstart.n.02", "name": "upstart"}, - {"id": 17362, "synset": "upstart.n.01", "name": "upstart"}, - {"id": 17363, "synset": "urchin.n.01", "name": "urchin"}, - {"id": 17364, "synset": "urologist.n.01", "name": "urologist"}, - {"id": 17365, "synset": "usherette.n.01", "name": "usherette"}, - {"id": 17366, "synset": "usher.n.02", "name": "usher"}, - {"id": 17367, "synset": "usurper.n.01", "name": "usurper"}, - {"id": 17368, "synset": "utility_man.n.01", "name": "utility_man"}, - {"id": 17369, "synset": "utilizer.n.01", "name": "utilizer"}, - {"id": 17370, "synset": "utopian.n.01", "name": "Utopian"}, - {"id": 17371, "synset": "uxoricide.n.01", "name": "uxoricide"}, - {"id": 17372, "synset": "vacationer.n.01", "name": "vacationer"}, - {"id": 17373, "synset": "valedictorian.n.01", "name": "valedictorian"}, - {"id": 17374, "synset": "valley_girl.n.01", "name": "valley_girl"}, - {"id": 17375, "synset": "vaulter.n.01", "name": "vaulter"}, - {"id": 17376, "synset": "vegetarian.n.01", "name": "vegetarian"}, - {"id": 17377, "synset": "vegan.n.01", "name": "vegan"}, - {"id": 17378, "synset": "venerator.n.01", "name": "venerator"}, - {"id": 17379, "synset": "venture_capitalist.n.01", "name": "venture_capitalist"}, - {"id": 17380, "synset": "venturer.n.01", "name": "venturer"}, - {"id": 17381, "synset": "vermin.n.01", "name": "vermin"}, - {"id": 17382, "synset": "very_important_person.n.01", "name": "very_important_person"}, - {"id": 17383, "synset": "vibist.n.01", "name": "vibist"}, - {"id": 17384, "synset": "vicar.n.01", "name": "vicar"}, - {"id": 17385, "synset": "vicar.n.03", "name": "vicar"}, - {"id": 17386, "synset": "vicar-general.n.01", "name": "vicar-general"}, - {"id": 17387, "synset": "vice_chancellor.n.01", "name": "vice_chancellor"}, - {"id": 17388, "synset": "vicegerent.n.01", "name": "vicegerent"}, - {"id": 17389, "synset": "vice_president.n.01", "name": "vice_president"}, - {"id": 17390, "synset": "vice-regent.n.01", "name": "vice-regent"}, - {"id": 17391, "synset": "victim.n.02", "name": "victim"}, - {"id": 17392, "synset": "victorian.n.01", "name": "Victorian"}, - {"id": 17393, "synset": "victualer.n.01", "name": "victualer"}, - {"id": 17394, "synset": "vigilante.n.01", "name": "vigilante"}, - {"id": 17395, "synset": "villager.n.01", "name": "villager"}, - {"id": 17396, "synset": "vintager.n.01", "name": "vintager"}, - {"id": 17397, "synset": "vintner.n.01", "name": "vintner"}, - {"id": 17398, "synset": "violator.n.02", "name": "violator"}, - {"id": 17399, "synset": "violator.n.01", "name": "violator"}, - {"id": 17400, "synset": "violist.n.01", "name": "violist"}, - {"id": 17401, "synset": "virago.n.01", "name": "virago"}, - {"id": 17402, "synset": "virologist.n.01", "name": "virologist"}, - {"id": 17403, "synset": "visayan.n.01", "name": "Visayan"}, - {"id": 17404, "synset": "viscountess.n.01", "name": "viscountess"}, - {"id": 17405, "synset": "viscount.n.01", "name": "viscount"}, - {"id": 17406, "synset": "visigoth.n.01", "name": "Visigoth"}, - {"id": 17407, "synset": "visionary.n.01", "name": "visionary"}, - {"id": 17408, "synset": "visiting_fireman.n.01", "name": "visiting_fireman"}, - {"id": 17409, "synset": "visiting_professor.n.01", "name": "visiting_professor"}, - {"id": 17410, "synset": "visualizer.n.01", "name": "visualizer"}, - {"id": 17411, "synset": "vixen.n.01", "name": "vixen"}, - {"id": 17412, "synset": "vizier.n.01", "name": "vizier"}, - {"id": 17413, "synset": "voicer.n.01", "name": "voicer"}, - {"id": 17414, "synset": "volunteer.n.02", "name": "volunteer"}, - {"id": 17415, "synset": "volunteer.n.01", "name": "volunteer"}, - {"id": 17416, "synset": "votary.n.02", "name": "votary"}, - {"id": 17417, "synset": "votary.n.01", "name": "votary"}, - {"id": 17418, "synset": "vouchee.n.01", "name": "vouchee"}, - {"id": 17419, "synset": "vower.n.01", "name": "vower"}, - {"id": 17420, "synset": "voyager.n.01", "name": "voyager"}, - {"id": 17421, "synset": "voyeur.n.01", "name": "voyeur"}, - {"id": 17422, "synset": "vulcanizer.n.01", "name": "vulcanizer"}, - {"id": 17423, "synset": "waffler.n.01", "name": "waffler"}, - {"id": 17424, "synset": "wagnerian.n.01", "name": "Wagnerian"}, - {"id": 17425, "synset": "waif.n.01", "name": "waif"}, - {"id": 17426, "synset": "wailer.n.01", "name": "wailer"}, - {"id": 17427, "synset": "waiter.n.01", "name": "waiter"}, - {"id": 17428, "synset": "waitress.n.01", "name": "waitress"}, - {"id": 17429, "synset": "walking_delegate.n.01", "name": "walking_delegate"}, - {"id": 17430, "synset": "walk-on.n.01", "name": "walk-on"}, - {"id": 17431, "synset": "wallah.n.01", "name": "wallah"}, - {"id": 17432, "synset": "wally.n.01", "name": "wally"}, - {"id": 17433, "synset": "waltzer.n.01", "name": "waltzer"}, - {"id": 17434, "synset": "wanderer.n.01", "name": "wanderer"}, - {"id": 17435, "synset": "wandering_jew.n.01", "name": "Wandering_Jew"}, - {"id": 17436, "synset": "wanton.n.01", "name": "wanton"}, - {"id": 17437, "synset": "warrantee.n.02", "name": "warrantee"}, - {"id": 17438, "synset": "warrantee.n.01", "name": "warrantee"}, - {"id": 17439, "synset": "washer.n.01", "name": "washer"}, - {"id": 17440, "synset": "washerman.n.01", "name": "washerman"}, - {"id": 17441, "synset": "washwoman.n.01", "name": "washwoman"}, - {"id": 17442, "synset": "wassailer.n.01", "name": "wassailer"}, - {"id": 17443, "synset": "wastrel.n.01", "name": "wastrel"}, - {"id": 17444, "synset": "wave.n.09", "name": "Wave"}, - {"id": 17445, "synset": "weatherman.n.01", "name": "weatherman"}, - {"id": 17446, "synset": "weekend_warrior.n.02", "name": "weekend_warrior"}, - {"id": 17447, "synset": "weeder.n.01", "name": "weeder"}, - {"id": 17448, "synset": "welder.n.01", "name": "welder"}, - {"id": 17449, "synset": "welfare_case.n.01", "name": "welfare_case"}, - {"id": 17450, "synset": "westerner.n.01", "name": "westerner"}, - {"id": 17451, "synset": "west-sider.n.01", "name": "West-sider"}, - {"id": 17452, "synset": "wetter.n.02", "name": "wetter"}, - {"id": 17453, "synset": "whaler.n.01", "name": "whaler"}, - {"id": 17454, "synset": "whig.n.02", "name": "Whig"}, - {"id": 17455, "synset": "whiner.n.01", "name": "whiner"}, - {"id": 17456, "synset": "whipper-in.n.01", "name": "whipper-in"}, - {"id": 17457, "synset": "whisperer.n.01", "name": "whisperer"}, - {"id": 17458, "synset": "whiteface.n.02", "name": "whiteface"}, - {"id": 17459, "synset": "carmelite.n.01", "name": "Carmelite"}, - {"id": 17460, "synset": "augustinian.n.01", "name": "Augustinian"}, - {"id": 17461, "synset": "white_hope.n.01", "name": "white_hope"}, - {"id": 17462, "synset": "white_supremacist.n.01", "name": "white_supremacist"}, - {"id": 17463, "synset": "whoremaster.n.02", "name": "whoremaster"}, - {"id": 17464, "synset": "whoremaster.n.01", "name": "whoremaster"}, - {"id": 17465, "synset": "widow.n.01", "name": "widow"}, - {"id": 17466, "synset": "wife.n.01", "name": "wife"}, - {"id": 17467, "synset": "wiggler.n.01", "name": "wiggler"}, - {"id": 17468, "synset": "wimp.n.01", "name": "wimp"}, - {"id": 17469, "synset": "wing_commander.n.01", "name": "wing_commander"}, - {"id": 17470, "synset": "winger.n.01", "name": "winger"}, - {"id": 17471, "synset": "winner.n.02", "name": "winner"}, - {"id": 17472, "synset": "winner.n.01", "name": "winner"}, - {"id": 17473, "synset": "window_dresser.n.01", "name": "window_dresser"}, - {"id": 17474, "synset": "winker.n.01", "name": "winker"}, - {"id": 17475, "synset": "wiper.n.01", "name": "wiper"}, - {"id": 17476, "synset": "wireman.n.01", "name": "wireman"}, - {"id": 17477, "synset": "wise_guy.n.01", "name": "wise_guy"}, - {"id": 17478, "synset": "witch_doctor.n.01", "name": "witch_doctor"}, - {"id": 17479, "synset": "withdrawer.n.05", "name": "withdrawer"}, - {"id": 17480, "synset": "withdrawer.n.01", "name": "withdrawer"}, - {"id": 17481, "synset": "woman.n.01", "name": "woman"}, - {"id": 17482, "synset": "woman.n.02", "name": "woman"}, - {"id": 17483, "synset": "wonder_boy.n.01", "name": "wonder_boy"}, - {"id": 17484, "synset": "wonderer.n.01", "name": "wonderer"}, - {"id": 17485, "synset": "working_girl.n.01", "name": "working_girl"}, - {"id": 17486, "synset": "workman.n.01", "name": "workman"}, - {"id": 17487, "synset": "workmate.n.01", "name": "workmate"}, - {"id": 17488, "synset": "worldling.n.01", "name": "worldling"}, - {"id": 17489, "synset": "worshiper.n.01", "name": "worshiper"}, - {"id": 17490, "synset": "worthy.n.01", "name": "worthy"}, - {"id": 17491, "synset": "wrecker.n.01", "name": "wrecker"}, - {"id": 17492, "synset": "wright.n.07", "name": "wright"}, - {"id": 17493, "synset": "write-in_candidate.n.01", "name": "write-in_candidate"}, - {"id": 17494, "synset": "writer.n.01", "name": "writer"}, - {"id": 17495, "synset": "wykehamist.n.01", "name": "Wykehamist"}, - {"id": 17496, "synset": "yakuza.n.01", "name": "yakuza"}, - {"id": 17497, "synset": "yard_bird.n.01", "name": "yard_bird"}, - {"id": 17498, "synset": "yardie.n.01", "name": "yardie"}, - {"id": 17499, "synset": "yardman.n.01", "name": "yardman"}, - {"id": 17500, "synset": "yardmaster.n.01", "name": "yardmaster"}, - {"id": 17501, "synset": "yenta.n.02", "name": "yenta"}, - {"id": 17502, "synset": "yogi.n.02", "name": "yogi"}, - {"id": 17503, "synset": "young_buck.n.01", "name": "young_buck"}, - {"id": 17504, "synset": "young_turk.n.02", "name": "young_Turk"}, - {"id": 17505, "synset": "young_turk.n.01", "name": "Young_Turk"}, - {"id": 17506, "synset": "zionist.n.01", "name": "Zionist"}, - {"id": 17507, "synset": "zoo_keeper.n.01", "name": "zoo_keeper"}, - {"id": 17508, "synset": "genet.n.01", "name": "Genet"}, - {"id": 17509, "synset": "kennan.n.01", "name": "Kennan"}, - {"id": 17510, "synset": "munro.n.01", "name": "Munro"}, - {"id": 17511, "synset": "popper.n.01", "name": "Popper"}, - {"id": 17512, "synset": "stoker.n.01", "name": "Stoker"}, - {"id": 17513, "synset": "townes.n.01", "name": "Townes"}, - {"id": 17514, "synset": "dust_storm.n.01", "name": "dust_storm"}, - {"id": 17515, "synset": "parhelion.n.01", "name": "parhelion"}, - {"id": 17516, "synset": "snow.n.01", "name": "snow"}, - {"id": 17517, "synset": "facula.n.01", "name": "facula"}, - {"id": 17518, "synset": "wave.n.08", "name": "wave"}, - {"id": 17519, "synset": "microflora.n.01", "name": "microflora"}, - {"id": 17520, "synset": "wilding.n.01", "name": "wilding"}, - {"id": 17521, "synset": "semi-climber.n.01", "name": "semi-climber"}, - {"id": 17522, "synset": "volva.n.01", "name": "volva"}, - {"id": 17523, "synset": "basidiocarp.n.01", "name": "basidiocarp"}, - {"id": 17524, "synset": "domatium.n.01", "name": "domatium"}, - {"id": 17525, "synset": "apomict.n.01", "name": "apomict"}, - {"id": 17526, "synset": "aquatic.n.01", "name": "aquatic"}, - {"id": 17527, "synset": "bryophyte.n.01", "name": "bryophyte"}, - {"id": 17528, "synset": "acrocarp.n.01", "name": "acrocarp"}, - {"id": 17529, "synset": "sphagnum.n.01", "name": "sphagnum"}, - {"id": 17530, "synset": "liverwort.n.01", "name": "liverwort"}, - {"id": 17531, "synset": "hepatica.n.02", "name": "hepatica"}, - {"id": 17532, "synset": "pecopteris.n.01", "name": "pecopteris"}, - {"id": 17533, "synset": "pteridophyte.n.01", "name": "pteridophyte"}, - {"id": 17534, "synset": "fern.n.01", "name": "fern"}, - {"id": 17535, "synset": "fern_ally.n.01", "name": "fern_ally"}, - {"id": 17536, "synset": "spore.n.01", "name": "spore"}, - {"id": 17537, "synset": "carpospore.n.01", "name": "carpospore"}, - {"id": 17538, "synset": "chlamydospore.n.01", "name": "chlamydospore"}, - {"id": 17539, "synset": "conidium.n.01", "name": "conidium"}, - {"id": 17540, "synset": "oospore.n.01", "name": "oospore"}, - {"id": 17541, "synset": "tetraspore.n.01", "name": "tetraspore"}, - {"id": 17542, "synset": "zoospore.n.01", "name": "zoospore"}, - {"id": 17543, "synset": "cryptogam.n.01", "name": "cryptogam"}, - {"id": 17544, "synset": "spermatophyte.n.01", "name": "spermatophyte"}, - {"id": 17545, "synset": "seedling.n.01", "name": "seedling"}, - {"id": 17546, "synset": "annual.n.01", "name": "annual"}, - {"id": 17547, "synset": "biennial.n.01", "name": "biennial"}, - {"id": 17548, "synset": "perennial.n.01", "name": "perennial"}, - {"id": 17549, "synset": "hygrophyte.n.01", "name": "hygrophyte"}, - {"id": 17550, "synset": "gymnosperm.n.01", "name": "gymnosperm"}, - {"id": 17551, "synset": "gnetum.n.01", "name": "gnetum"}, - {"id": 17552, "synset": "catha_edulis.n.01", "name": "Catha_edulis"}, - {"id": 17553, "synset": "ephedra.n.01", "name": "ephedra"}, - {"id": 17554, "synset": "mahuang.n.01", "name": "mahuang"}, - {"id": 17555, "synset": "welwitschia.n.01", "name": "welwitschia"}, - {"id": 17556, "synset": "cycad.n.01", "name": "cycad"}, - {"id": 17557, "synset": "sago_palm.n.02", "name": "sago_palm"}, - {"id": 17558, "synset": "false_sago.n.01", "name": "false_sago"}, - {"id": 17559, "synset": "zamia.n.01", "name": "zamia"}, - {"id": 17560, "synset": "coontie.n.01", "name": "coontie"}, - {"id": 17561, "synset": "ceratozamia.n.01", "name": "ceratozamia"}, - {"id": 17562, "synset": "dioon.n.01", "name": "dioon"}, - {"id": 17563, "synset": "encephalartos.n.01", "name": "encephalartos"}, - {"id": 17564, "synset": "kaffir_bread.n.01", "name": "kaffir_bread"}, - {"id": 17565, "synset": "macrozamia.n.01", "name": "macrozamia"}, - {"id": 17566, "synset": "burrawong.n.01", "name": "burrawong"}, - {"id": 17567, "synset": "pine.n.01", "name": "pine"}, - {"id": 17568, "synset": "pinon.n.01", "name": "pinon"}, - {"id": 17569, "synset": "nut_pine.n.01", "name": "nut_pine"}, - {"id": 17570, "synset": "pinon_pine.n.01", "name": "pinon_pine"}, - {"id": 17571, "synset": "rocky_mountain_pinon.n.01", "name": "Rocky_mountain_pinon"}, - {"id": 17572, "synset": "single-leaf.n.01", "name": "single-leaf"}, - {"id": 17573, "synset": "bishop_pine.n.01", "name": "bishop_pine"}, - { - "id": 17574, - "synset": "california_single-leaf_pinyon.n.01", - "name": "California_single-leaf_pinyon", - }, - {"id": 17575, "synset": "parry's_pinyon.n.01", "name": "Parry's_pinyon"}, - {"id": 17576, "synset": "spruce_pine.n.04", "name": "spruce_pine"}, - {"id": 17577, "synset": "black_pine.n.05", "name": "black_pine"}, - {"id": 17578, "synset": "pitch_pine.n.02", "name": "pitch_pine"}, - {"id": 17579, "synset": "pond_pine.n.01", "name": "pond_pine"}, - {"id": 17580, "synset": "stone_pine.n.01", "name": "stone_pine"}, - {"id": 17581, "synset": "swiss_pine.n.01", "name": "Swiss_pine"}, - {"id": 17582, "synset": "cembra_nut.n.01", "name": "cembra_nut"}, - {"id": 17583, "synset": "swiss_mountain_pine.n.01", "name": "Swiss_mountain_pine"}, - {"id": 17584, "synset": "ancient_pine.n.01", "name": "ancient_pine"}, - {"id": 17585, "synset": "white_pine.n.01", "name": "white_pine"}, - {"id": 17586, "synset": "american_white_pine.n.01", "name": "American_white_pine"}, - {"id": 17587, "synset": "western_white_pine.n.01", "name": "western_white_pine"}, - {"id": 17588, "synset": "southwestern_white_pine.n.01", "name": "southwestern_white_pine"}, - {"id": 17589, "synset": "limber_pine.n.01", "name": "limber_pine"}, - {"id": 17590, "synset": "whitebark_pine.n.01", "name": "whitebark_pine"}, - {"id": 17591, "synset": "yellow_pine.n.01", "name": "yellow_pine"}, - {"id": 17592, "synset": "ponderosa.n.01", "name": "ponderosa"}, - {"id": 17593, "synset": "jeffrey_pine.n.01", "name": "Jeffrey_pine"}, - {"id": 17594, "synset": "shore_pine.n.01", "name": "shore_pine"}, - {"id": 17595, "synset": "sierra_lodgepole_pine.n.01", "name": "Sierra_lodgepole_pine"}, - {"id": 17596, "synset": "loblolly_pine.n.01", "name": "loblolly_pine"}, - {"id": 17597, "synset": "jack_pine.n.01", "name": "jack_pine"}, - {"id": 17598, "synset": "swamp_pine.n.01", "name": "swamp_pine"}, - {"id": 17599, "synset": "longleaf_pine.n.01", "name": "longleaf_pine"}, - {"id": 17600, "synset": "shortleaf_pine.n.01", "name": "shortleaf_pine"}, - {"id": 17601, "synset": "red_pine.n.02", "name": "red_pine"}, - {"id": 17602, "synset": "scotch_pine.n.01", "name": "Scotch_pine"}, - {"id": 17603, "synset": "scrub_pine.n.01", "name": "scrub_pine"}, - {"id": 17604, "synset": "monterey_pine.n.01", "name": "Monterey_pine"}, - {"id": 17605, "synset": "bristlecone_pine.n.01", "name": "bristlecone_pine"}, - {"id": 17606, "synset": "table-mountain_pine.n.01", "name": "table-mountain_pine"}, - {"id": 17607, "synset": "knobcone_pine.n.01", "name": "knobcone_pine"}, - {"id": 17608, "synset": "japanese_red_pine.n.01", "name": "Japanese_red_pine"}, - {"id": 17609, "synset": "japanese_black_pine.n.01", "name": "Japanese_black_pine"}, - {"id": 17610, "synset": "torrey_pine.n.01", "name": "Torrey_pine"}, - {"id": 17611, "synset": "larch.n.02", "name": "larch"}, - {"id": 17612, "synset": "american_larch.n.01", "name": "American_larch"}, - {"id": 17613, "synset": "western_larch.n.01", "name": "western_larch"}, - {"id": 17614, "synset": "subalpine_larch.n.01", "name": "subalpine_larch"}, - {"id": 17615, "synset": "european_larch.n.01", "name": "European_larch"}, - {"id": 17616, "synset": "siberian_larch.n.01", "name": "Siberian_larch"}, - {"id": 17617, "synset": "golden_larch.n.01", "name": "golden_larch"}, - {"id": 17618, "synset": "fir.n.02", "name": "fir"}, - {"id": 17619, "synset": "silver_fir.n.01", "name": "silver_fir"}, - {"id": 17620, "synset": "amabilis_fir.n.01", "name": "amabilis_fir"}, - {"id": 17621, "synset": "european_silver_fir.n.01", "name": "European_silver_fir"}, - {"id": 17622, "synset": "white_fir.n.01", "name": "white_fir"}, - {"id": 17623, "synset": "balsam_fir.n.01", "name": "balsam_fir"}, - {"id": 17624, "synset": "fraser_fir.n.01", "name": "Fraser_fir"}, - {"id": 17625, "synset": "lowland_fir.n.01", "name": "lowland_fir"}, - {"id": 17626, "synset": "alpine_fir.n.01", "name": "Alpine_fir"}, - {"id": 17627, "synset": "santa_lucia_fir.n.01", "name": "Santa_Lucia_fir"}, - {"id": 17628, "synset": "cedar.n.03", "name": "cedar"}, - {"id": 17629, "synset": "cedar_of_lebanon.n.01", "name": "cedar_of_Lebanon"}, - {"id": 17630, "synset": "deodar.n.01", "name": "deodar"}, - {"id": 17631, "synset": "atlas_cedar.n.01", "name": "Atlas_cedar"}, - {"id": 17632, "synset": "spruce.n.02", "name": "spruce"}, - {"id": 17633, "synset": "norway_spruce.n.01", "name": "Norway_spruce"}, - {"id": 17634, "synset": "weeping_spruce.n.01", "name": "weeping_spruce"}, - {"id": 17635, "synset": "engelmann_spruce.n.01", "name": "Engelmann_spruce"}, - {"id": 17636, "synset": "white_spruce.n.01", "name": "white_spruce"}, - {"id": 17637, "synset": "black_spruce.n.01", "name": "black_spruce"}, - {"id": 17638, "synset": "siberian_spruce.n.01", "name": "Siberian_spruce"}, - {"id": 17639, "synset": "sitka_spruce.n.01", "name": "Sitka_spruce"}, - {"id": 17640, "synset": "oriental_spruce.n.01", "name": "oriental_spruce"}, - {"id": 17641, "synset": "colorado_spruce.n.01", "name": "Colorado_spruce"}, - {"id": 17642, "synset": "red_spruce.n.01", "name": "red_spruce"}, - {"id": 17643, "synset": "hemlock.n.04", "name": "hemlock"}, - {"id": 17644, "synset": "eastern_hemlock.n.01", "name": "eastern_hemlock"}, - {"id": 17645, "synset": "carolina_hemlock.n.01", "name": "Carolina_hemlock"}, - {"id": 17646, "synset": "mountain_hemlock.n.01", "name": "mountain_hemlock"}, - {"id": 17647, "synset": "western_hemlock.n.01", "name": "western_hemlock"}, - {"id": 17648, "synset": "douglas_fir.n.02", "name": "douglas_fir"}, - {"id": 17649, "synset": "green_douglas_fir.n.01", "name": "green_douglas_fir"}, - {"id": 17650, "synset": "big-cone_spruce.n.01", "name": "big-cone_spruce"}, - {"id": 17651, "synset": "cathaya.n.01", "name": "Cathaya"}, - {"id": 17652, "synset": "cedar.n.01", "name": "cedar"}, - {"id": 17653, "synset": "cypress.n.02", "name": "cypress"}, - {"id": 17654, "synset": "gowen_cypress.n.01", "name": "gowen_cypress"}, - {"id": 17655, "synset": "pygmy_cypress.n.01", "name": "pygmy_cypress"}, - {"id": 17656, "synset": "santa_cruz_cypress.n.01", "name": "Santa_Cruz_cypress"}, - {"id": 17657, "synset": "arizona_cypress.n.01", "name": "Arizona_cypress"}, - {"id": 17658, "synset": "guadalupe_cypress.n.01", "name": "Guadalupe_cypress"}, - {"id": 17659, "synset": "monterey_cypress.n.01", "name": "Monterey_cypress"}, - {"id": 17660, "synset": "mexican_cypress.n.01", "name": "Mexican_cypress"}, - {"id": 17661, "synset": "italian_cypress.n.01", "name": "Italian_cypress"}, - {"id": 17662, "synset": "king_william_pine.n.01", "name": "King_William_pine"}, - {"id": 17663, "synset": "chilean_cedar.n.01", "name": "Chilean_cedar"}, - {"id": 17664, "synset": "incense_cedar.n.02", "name": "incense_cedar"}, - {"id": 17665, "synset": "southern_white_cedar.n.01", "name": "southern_white_cedar"}, - {"id": 17666, "synset": "oregon_cedar.n.01", "name": "Oregon_cedar"}, - {"id": 17667, "synset": "yellow_cypress.n.01", "name": "yellow_cypress"}, - {"id": 17668, "synset": "japanese_cedar.n.01", "name": "Japanese_cedar"}, - {"id": 17669, "synset": "juniper_berry.n.01", "name": "juniper_berry"}, - {"id": 17670, "synset": "incense_cedar.n.01", "name": "incense_cedar"}, - {"id": 17671, "synset": "kawaka.n.01", "name": "kawaka"}, - {"id": 17672, "synset": "pahautea.n.01", "name": "pahautea"}, - {"id": 17673, "synset": "metasequoia.n.01", "name": "metasequoia"}, - {"id": 17674, "synset": "arborvitae.n.01", "name": "arborvitae"}, - {"id": 17675, "synset": "western_red_cedar.n.01", "name": "western_red_cedar"}, - {"id": 17676, "synset": "american_arborvitae.n.01", "name": "American_arborvitae"}, - {"id": 17677, "synset": "oriental_arborvitae.n.01", "name": "Oriental_arborvitae"}, - {"id": 17678, "synset": "hiba_arborvitae.n.01", "name": "hiba_arborvitae"}, - {"id": 17679, "synset": "keteleeria.n.01", "name": "keteleeria"}, - {"id": 17680, "synset": "wollemi_pine.n.01", "name": "Wollemi_pine"}, - {"id": 17681, "synset": "araucaria.n.01", "name": "araucaria"}, - {"id": 17682, "synset": "monkey_puzzle.n.01", "name": "monkey_puzzle"}, - {"id": 17683, "synset": "norfolk_island_pine.n.01", "name": "norfolk_island_pine"}, - {"id": 17684, "synset": "new_caledonian_pine.n.01", "name": "new_caledonian_pine"}, - {"id": 17685, "synset": "bunya_bunya.n.01", "name": "bunya_bunya"}, - {"id": 17686, "synset": "hoop_pine.n.01", "name": "hoop_pine"}, - {"id": 17687, "synset": "kauri_pine.n.01", "name": "kauri_pine"}, - {"id": 17688, "synset": "kauri.n.02", "name": "kauri"}, - {"id": 17689, "synset": "amboina_pine.n.01", "name": "amboina_pine"}, - {"id": 17690, "synset": "dundathu_pine.n.01", "name": "dundathu_pine"}, - {"id": 17691, "synset": "red_kauri.n.01", "name": "red_kauri"}, - {"id": 17692, "synset": "plum-yew.n.01", "name": "plum-yew"}, - {"id": 17693, "synset": "california_nutmeg.n.01", "name": "California_nutmeg"}, - {"id": 17694, "synset": "stinking_cedar.n.01", "name": "stinking_cedar"}, - {"id": 17695, "synset": "celery_pine.n.01", "name": "celery_pine"}, - {"id": 17696, "synset": "celery_top_pine.n.01", "name": "celery_top_pine"}, - {"id": 17697, "synset": "tanekaha.n.01", "name": "tanekaha"}, - {"id": 17698, "synset": "alpine_celery_pine.n.01", "name": "Alpine_celery_pine"}, - {"id": 17699, "synset": "yellowwood.n.02", "name": "yellowwood"}, - {"id": 17700, "synset": "gymnospermous_yellowwood.n.01", "name": "gymnospermous_yellowwood"}, - {"id": 17701, "synset": "podocarp.n.01", "name": "podocarp"}, - {"id": 17702, "synset": "yacca.n.01", "name": "yacca"}, - {"id": 17703, "synset": "brown_pine.n.01", "name": "brown_pine"}, - {"id": 17704, "synset": "cape_yellowwood.n.01", "name": "cape_yellowwood"}, - {"id": 17705, "synset": "south-african_yellowwood.n.01", "name": "South-African_yellowwood"}, - {"id": 17706, "synset": "alpine_totara.n.01", "name": "alpine_totara"}, - {"id": 17707, "synset": "totara.n.01", "name": "totara"}, - {"id": 17708, "synset": "common_yellowwood.n.01", "name": "common_yellowwood"}, - {"id": 17709, "synset": "kahikatea.n.01", "name": "kahikatea"}, - {"id": 17710, "synset": "rimu.n.01", "name": "rimu"}, - {"id": 17711, "synset": "tarwood.n.02", "name": "tarwood"}, - {"id": 17712, "synset": "common_sickle_pine.n.01", "name": "common_sickle_pine"}, - {"id": 17713, "synset": "yellow-leaf_sickle_pine.n.01", "name": "yellow-leaf_sickle_pine"}, - {"id": 17714, "synset": "tarwood.n.01", "name": "tarwood"}, - {"id": 17715, "synset": "westland_pine.n.01", "name": "westland_pine"}, - {"id": 17716, "synset": "huon_pine.n.01", "name": "huon_pine"}, - {"id": 17717, "synset": "chilean_rimu.n.01", "name": "Chilean_rimu"}, - {"id": 17718, "synset": "mountain_rimu.n.01", "name": "mountain_rimu"}, - {"id": 17719, "synset": "nagi.n.01", "name": "nagi"}, - {"id": 17720, "synset": "miro.n.01", "name": "miro"}, - {"id": 17721, "synset": "matai.n.01", "name": "matai"}, - {"id": 17722, "synset": "plum-fruited_yew.n.01", "name": "plum-fruited_yew"}, - {"id": 17723, "synset": "prince_albert_yew.n.01", "name": "Prince_Albert_yew"}, - {"id": 17724, "synset": "sundacarpus_amara.n.01", "name": "Sundacarpus_amara"}, - {"id": 17725, "synset": "japanese_umbrella_pine.n.01", "name": "Japanese_umbrella_pine"}, - {"id": 17726, "synset": "yew.n.02", "name": "yew"}, - {"id": 17727, "synset": "old_world_yew.n.01", "name": "Old_World_yew"}, - {"id": 17728, "synset": "pacific_yew.n.01", "name": "Pacific_yew"}, - {"id": 17729, "synset": "japanese_yew.n.01", "name": "Japanese_yew"}, - {"id": 17730, "synset": "florida_yew.n.01", "name": "Florida_yew"}, - {"id": 17731, "synset": "new_caledonian_yew.n.01", "name": "New_Caledonian_yew"}, - {"id": 17732, "synset": "white-berry_yew.n.01", "name": "white-berry_yew"}, - {"id": 17733, "synset": "ginkgo.n.01", "name": "ginkgo"}, - {"id": 17734, "synset": "angiosperm.n.01", "name": "angiosperm"}, - {"id": 17735, "synset": "dicot.n.01", "name": "dicot"}, - {"id": 17736, "synset": "monocot.n.01", "name": "monocot"}, - {"id": 17737, "synset": "floret.n.01", "name": "floret"}, - {"id": 17738, "synset": "flower.n.01", "name": "flower"}, - {"id": 17739, "synset": "bloomer.n.01", "name": "bloomer"}, - {"id": 17740, "synset": "wildflower.n.01", "name": "wildflower"}, - {"id": 17741, "synset": "apetalous_flower.n.01", "name": "apetalous_flower"}, - {"id": 17742, "synset": "inflorescence.n.02", "name": "inflorescence"}, - {"id": 17743, "synset": "rosebud.n.01", "name": "rosebud"}, - {"id": 17744, "synset": "gynostegium.n.01", "name": "gynostegium"}, - {"id": 17745, "synset": "pollinium.n.01", "name": "pollinium"}, - {"id": 17746, "synset": "pistil.n.01", "name": "pistil"}, - {"id": 17747, "synset": "gynobase.n.01", "name": "gynobase"}, - {"id": 17748, "synset": "gynophore.n.01", "name": "gynophore"}, - {"id": 17749, "synset": "stylopodium.n.01", "name": "stylopodium"}, - {"id": 17750, "synset": "carpophore.n.01", "name": "carpophore"}, - {"id": 17751, "synset": "cornstalk.n.01", "name": "cornstalk"}, - {"id": 17752, "synset": "petiolule.n.01", "name": "petiolule"}, - {"id": 17753, "synset": "mericarp.n.01", "name": "mericarp"}, - {"id": 17754, "synset": "micropyle.n.01", "name": "micropyle"}, - {"id": 17755, "synset": "germ_tube.n.01", "name": "germ_tube"}, - {"id": 17756, "synset": "pollen_tube.n.01", "name": "pollen_tube"}, - {"id": 17757, "synset": "gemma.n.01", "name": "gemma"}, - {"id": 17758, "synset": "galbulus.n.01", "name": "galbulus"}, - {"id": 17759, "synset": "nectary.n.01", "name": "nectary"}, - {"id": 17760, "synset": "pericarp.n.01", "name": "pericarp"}, - {"id": 17761, "synset": "epicarp.n.01", "name": "epicarp"}, - {"id": 17762, "synset": "mesocarp.n.01", "name": "mesocarp"}, - {"id": 17763, "synset": "pip.n.03", "name": "pip"}, - {"id": 17764, "synset": "silique.n.01", "name": "silique"}, - {"id": 17765, "synset": "cataphyll.n.01", "name": "cataphyll"}, - {"id": 17766, "synset": "perisperm.n.01", "name": "perisperm"}, - {"id": 17767, "synset": "monocarp.n.01", "name": "monocarp"}, - {"id": 17768, "synset": "sporophyte.n.01", "name": "sporophyte"}, - {"id": 17769, "synset": "gametophyte.n.01", "name": "gametophyte"}, - {"id": 17770, "synset": "megasporangium.n.01", "name": "megasporangium"}, - {"id": 17771, "synset": "microspore.n.01", "name": "microspore"}, - {"id": 17772, "synset": "microsporangium.n.01", "name": "microsporangium"}, - {"id": 17773, "synset": "microsporophyll.n.01", "name": "microsporophyll"}, - {"id": 17774, "synset": "archespore.n.01", "name": "archespore"}, - {"id": 17775, "synset": "bonduc_nut.n.01", "name": "bonduc_nut"}, - {"id": 17776, "synset": "job's_tears.n.01", "name": "Job's_tears"}, - {"id": 17777, "synset": "oilseed.n.01", "name": "oilseed"}, - {"id": 17778, "synset": "castor_bean.n.01", "name": "castor_bean"}, - {"id": 17779, "synset": "cottonseed.n.01", "name": "cottonseed"}, - {"id": 17780, "synset": "candlenut.n.02", "name": "candlenut"}, - {"id": 17781, "synset": "peach_pit.n.01", "name": "peach_pit"}, - {"id": 17782, "synset": "hypanthium.n.01", "name": "hypanthium"}, - {"id": 17783, "synset": "petal.n.01", "name": "petal"}, - {"id": 17784, "synset": "corolla.n.01", "name": "corolla"}, - {"id": 17785, "synset": "lip.n.02", "name": "lip"}, - {"id": 17786, "synset": "perianth.n.01", "name": "perianth"}, - {"id": 17787, "synset": "thistledown.n.01", "name": "thistledown"}, - {"id": 17788, "synset": "custard_apple.n.01", "name": "custard_apple"}, - {"id": 17789, "synset": "cherimoya.n.01", "name": "cherimoya"}, - {"id": 17790, "synset": "ilama.n.01", "name": "ilama"}, - {"id": 17791, "synset": "soursop.n.01", "name": "soursop"}, - {"id": 17792, "synset": "bullock's_heart.n.01", "name": "bullock's_heart"}, - {"id": 17793, "synset": "sweetsop.n.01", "name": "sweetsop"}, - {"id": 17794, "synset": "pond_apple.n.01", "name": "pond_apple"}, - {"id": 17795, "synset": "pawpaw.n.02", "name": "pawpaw"}, - {"id": 17796, "synset": "ilang-ilang.n.02", "name": "ilang-ilang"}, - {"id": 17797, "synset": "lancewood.n.02", "name": "lancewood"}, - {"id": 17798, "synset": "guinea_pepper.n.02", "name": "Guinea_pepper"}, - {"id": 17799, "synset": "barberry.n.01", "name": "barberry"}, - {"id": 17800, "synset": "american_barberry.n.01", "name": "American_barberry"}, - {"id": 17801, "synset": "common_barberry.n.01", "name": "common_barberry"}, - {"id": 17802, "synset": "japanese_barberry.n.01", "name": "Japanese_barberry"}, - {"id": 17803, "synset": "oregon_grape.n.02", "name": "Oregon_grape"}, - {"id": 17804, "synset": "oregon_grape.n.01", "name": "Oregon_grape"}, - {"id": 17805, "synset": "mayapple.n.01", "name": "mayapple"}, - {"id": 17806, "synset": "may_apple.n.01", "name": "May_apple"}, - {"id": 17807, "synset": "allspice.n.02", "name": "allspice"}, - {"id": 17808, "synset": "carolina_allspice.n.01", "name": "Carolina_allspice"}, - {"id": 17809, "synset": "spicebush.n.02", "name": "spicebush"}, - {"id": 17810, "synset": "katsura_tree.n.01", "name": "katsura_tree"}, - {"id": 17811, "synset": "laurel.n.01", "name": "laurel"}, - {"id": 17812, "synset": "true_laurel.n.01", "name": "true_laurel"}, - {"id": 17813, "synset": "camphor_tree.n.01", "name": "camphor_tree"}, - {"id": 17814, "synset": "cinnamon.n.02", "name": "cinnamon"}, - {"id": 17815, "synset": "cassia.n.03", "name": "cassia"}, - {"id": 17816, "synset": "cassia_bark.n.01", "name": "cassia_bark"}, - {"id": 17817, "synset": "saigon_cinnamon.n.01", "name": "Saigon_cinnamon"}, - {"id": 17818, "synset": "cinnamon_bark.n.01", "name": "cinnamon_bark"}, - {"id": 17819, "synset": "spicebush.n.01", "name": "spicebush"}, - {"id": 17820, "synset": "avocado.n.02", "name": "avocado"}, - {"id": 17821, "synset": "laurel-tree.n.01", "name": "laurel-tree"}, - {"id": 17822, "synset": "sassafras.n.01", "name": "sassafras"}, - {"id": 17823, "synset": "california_laurel.n.01", "name": "California_laurel"}, - {"id": 17824, "synset": "anise_tree.n.01", "name": "anise_tree"}, - {"id": 17825, "synset": "purple_anise.n.01", "name": "purple_anise"}, - {"id": 17826, "synset": "star_anise.n.02", "name": "star_anise"}, - {"id": 17827, "synset": "star_anise.n.01", "name": "star_anise"}, - {"id": 17828, "synset": "magnolia.n.02", "name": "magnolia"}, - {"id": 17829, "synset": "southern_magnolia.n.01", "name": "southern_magnolia"}, - {"id": 17830, "synset": "umbrella_tree.n.02", "name": "umbrella_tree"}, - {"id": 17831, "synset": "earleaved_umbrella_tree.n.01", "name": "earleaved_umbrella_tree"}, - {"id": 17832, "synset": "cucumber_tree.n.01", "name": "cucumber_tree"}, - {"id": 17833, "synset": "large-leaved_magnolia.n.01", "name": "large-leaved_magnolia"}, - {"id": 17834, "synset": "saucer_magnolia.n.01", "name": "saucer_magnolia"}, - {"id": 17835, "synset": "star_magnolia.n.01", "name": "star_magnolia"}, - {"id": 17836, "synset": "sweet_bay.n.01", "name": "sweet_bay"}, - {"id": 17837, "synset": "manglietia.n.01", "name": "manglietia"}, - {"id": 17838, "synset": "tulip_tree.n.01", "name": "tulip_tree"}, - {"id": 17839, "synset": "moonseed.n.01", "name": "moonseed"}, - {"id": 17840, "synset": "common_moonseed.n.01", "name": "common_moonseed"}, - {"id": 17841, "synset": "carolina_moonseed.n.01", "name": "Carolina_moonseed"}, - {"id": 17842, "synset": "nutmeg.n.01", "name": "nutmeg"}, - {"id": 17843, "synset": "water_nymph.n.02", "name": "water_nymph"}, - {"id": 17844, "synset": "european_white_lily.n.01", "name": "European_white_lily"}, - {"id": 17845, "synset": "southern_spatterdock.n.01", "name": "southern_spatterdock"}, - {"id": 17846, "synset": "lotus.n.01", "name": "lotus"}, - {"id": 17847, "synset": "water_chinquapin.n.01", "name": "water_chinquapin"}, - {"id": 17848, "synset": "water-shield.n.02", "name": "water-shield"}, - {"id": 17849, "synset": "water-shield.n.01", "name": "water-shield"}, - {"id": 17850, "synset": "peony.n.01", "name": "peony"}, - {"id": 17851, "synset": "buttercup.n.01", "name": "buttercup"}, - {"id": 17852, "synset": "meadow_buttercup.n.01", "name": "meadow_buttercup"}, - {"id": 17853, "synset": "water_crowfoot.n.01", "name": "water_crowfoot"}, - {"id": 17854, "synset": "lesser_celandine.n.01", "name": "lesser_celandine"}, - {"id": 17855, "synset": "lesser_spearwort.n.01", "name": "lesser_spearwort"}, - {"id": 17856, "synset": "greater_spearwort.n.01", "name": "greater_spearwort"}, - {"id": 17857, "synset": "western_buttercup.n.01", "name": "western_buttercup"}, - {"id": 17858, "synset": "creeping_buttercup.n.01", "name": "creeping_buttercup"}, - {"id": 17859, "synset": "cursed_crowfoot.n.01", "name": "cursed_crowfoot"}, - {"id": 17860, "synset": "aconite.n.01", "name": "aconite"}, - {"id": 17861, "synset": "monkshood.n.01", "name": "monkshood"}, - {"id": 17862, "synset": "wolfsbane.n.01", "name": "wolfsbane"}, - {"id": 17863, "synset": "baneberry.n.02", "name": "baneberry"}, - {"id": 17864, "synset": "baneberry.n.01", "name": "baneberry"}, - {"id": 17865, "synset": "red_baneberry.n.01", "name": "red_baneberry"}, - {"id": 17866, "synset": "pheasant's-eye.n.01", "name": "pheasant's-eye"}, - {"id": 17867, "synset": "anemone.n.01", "name": "anemone"}, - {"id": 17868, "synset": "alpine_anemone.n.01", "name": "Alpine_anemone"}, - {"id": 17869, "synset": "canada_anemone.n.01", "name": "Canada_anemone"}, - {"id": 17870, "synset": "thimbleweed.n.01", "name": "thimbleweed"}, - {"id": 17871, "synset": "wood_anemone.n.02", "name": "wood_anemone"}, - {"id": 17872, "synset": "wood_anemone.n.01", "name": "wood_anemone"}, - {"id": 17873, "synset": "longheaded_thimbleweed.n.01", "name": "longheaded_thimbleweed"}, - {"id": 17874, "synset": "snowdrop_anemone.n.01", "name": "snowdrop_anemone"}, - {"id": 17875, "synset": "virginia_thimbleweed.n.01", "name": "Virginia_thimbleweed"}, - {"id": 17876, "synset": "rue_anemone.n.01", "name": "rue_anemone"}, - {"id": 17877, "synset": "columbine.n.01", "name": "columbine"}, - {"id": 17878, "synset": "meeting_house.n.01", "name": "meeting_house"}, - {"id": 17879, "synset": "blue_columbine.n.01", "name": "blue_columbine"}, - {"id": 17880, "synset": "granny's_bonnets.n.01", "name": "granny's_bonnets"}, - {"id": 17881, "synset": "marsh_marigold.n.01", "name": "marsh_marigold"}, - {"id": 17882, "synset": "american_bugbane.n.01", "name": "American_bugbane"}, - {"id": 17883, "synset": "black_cohosh.n.01", "name": "black_cohosh"}, - {"id": 17884, "synset": "fetid_bugbane.n.01", "name": "fetid_bugbane"}, - {"id": 17885, "synset": "clematis.n.01", "name": "clematis"}, - {"id": 17886, "synset": "pine_hyacinth.n.01", "name": "pine_hyacinth"}, - {"id": 17887, "synset": "blue_jasmine.n.01", "name": "blue_jasmine"}, - {"id": 17888, "synset": "golden_clematis.n.01", "name": "golden_clematis"}, - {"id": 17889, "synset": "scarlet_clematis.n.01", "name": "scarlet_clematis"}, - {"id": 17890, "synset": "leather_flower.n.02", "name": "leather_flower"}, - {"id": 17891, "synset": "leather_flower.n.01", "name": "leather_flower"}, - {"id": 17892, "synset": "virgin's_bower.n.01", "name": "virgin's_bower"}, - {"id": 17893, "synset": "purple_clematis.n.01", "name": "purple_clematis"}, - {"id": 17894, "synset": "goldthread.n.01", "name": "goldthread"}, - {"id": 17895, "synset": "rocket_larkspur.n.01", "name": "rocket_larkspur"}, - {"id": 17896, "synset": "delphinium.n.01", "name": "delphinium"}, - {"id": 17897, "synset": "larkspur.n.01", "name": "larkspur"}, - {"id": 17898, "synset": "winter_aconite.n.01", "name": "winter_aconite"}, - {"id": 17899, "synset": "lenten_rose.n.01", "name": "lenten_rose"}, - {"id": 17900, "synset": "green_hellebore.n.01", "name": "green_hellebore"}, - {"id": 17901, "synset": "hepatica.n.01", "name": "hepatica"}, - {"id": 17902, "synset": "goldenseal.n.01", "name": "goldenseal"}, - {"id": 17903, "synset": "false_rue_anemone.n.01", "name": "false_rue_anemone"}, - {"id": 17904, "synset": "giant_buttercup.n.01", "name": "giant_buttercup"}, - {"id": 17905, "synset": "nigella.n.01", "name": "nigella"}, - {"id": 17906, "synset": "love-in-a-mist.n.03", "name": "love-in-a-mist"}, - {"id": 17907, "synset": "fennel_flower.n.01", "name": "fennel_flower"}, - {"id": 17908, "synset": "black_caraway.n.01", "name": "black_caraway"}, - {"id": 17909, "synset": "pasqueflower.n.01", "name": "pasqueflower"}, - {"id": 17910, "synset": "meadow_rue.n.01", "name": "meadow_rue"}, - {"id": 17911, "synset": "false_bugbane.n.01", "name": "false_bugbane"}, - {"id": 17912, "synset": "globeflower.n.01", "name": "globeflower"}, - {"id": 17913, "synset": "winter's_bark.n.02", "name": "winter's_bark"}, - {"id": 17914, "synset": "pepper_shrub.n.01", "name": "pepper_shrub"}, - {"id": 17915, "synset": "sweet_gale.n.01", "name": "sweet_gale"}, - {"id": 17916, "synset": "wax_myrtle.n.01", "name": "wax_myrtle"}, - {"id": 17917, "synset": "bay_myrtle.n.01", "name": "bay_myrtle"}, - {"id": 17918, "synset": "bayberry.n.02", "name": "bayberry"}, - {"id": 17919, "synset": "sweet_fern.n.02", "name": "sweet_fern"}, - {"id": 17920, "synset": "corkwood.n.01", "name": "corkwood"}, - {"id": 17921, "synset": "jointed_rush.n.01", "name": "jointed_rush"}, - {"id": 17922, "synset": "toad_rush.n.01", "name": "toad_rush"}, - {"id": 17923, "synset": "slender_rush.n.01", "name": "slender_rush"}, - {"id": 17924, "synset": "zebrawood.n.02", "name": "zebrawood"}, - {"id": 17925, "synset": "connarus_guianensis.n.01", "name": "Connarus_guianensis"}, - {"id": 17926, "synset": "legume.n.01", "name": "legume"}, - {"id": 17927, "synset": "peanut.n.01", "name": "peanut"}, - {"id": 17928, "synset": "granadilla_tree.n.01", "name": "granadilla_tree"}, - {"id": 17929, "synset": "arariba.n.01", "name": "arariba"}, - {"id": 17930, "synset": "tonka_bean.n.01", "name": "tonka_bean"}, - {"id": 17931, "synset": "courbaril.n.01", "name": "courbaril"}, - {"id": 17932, "synset": "melilotus.n.01", "name": "melilotus"}, - {"id": 17933, "synset": "darling_pea.n.01", "name": "darling_pea"}, - {"id": 17934, "synset": "smooth_darling_pea.n.01", "name": "smooth_darling_pea"}, - {"id": 17935, "synset": "clover.n.01", "name": "clover"}, - {"id": 17936, "synset": "alpine_clover.n.01", "name": "alpine_clover"}, - {"id": 17937, "synset": "hop_clover.n.02", "name": "hop_clover"}, - {"id": 17938, "synset": "crimson_clover.n.01", "name": "crimson_clover"}, - {"id": 17939, "synset": "red_clover.n.01", "name": "red_clover"}, - {"id": 17940, "synset": "buffalo_clover.n.02", "name": "buffalo_clover"}, - {"id": 17941, "synset": "white_clover.n.01", "name": "white_clover"}, - {"id": 17942, "synset": "mimosa.n.02", "name": "mimosa"}, - {"id": 17943, "synset": "acacia.n.01", "name": "acacia"}, - {"id": 17944, "synset": "shittah.n.01", "name": "shittah"}, - {"id": 17945, "synset": "wattle.n.03", "name": "wattle"}, - {"id": 17946, "synset": "black_wattle.n.01", "name": "black_wattle"}, - {"id": 17947, "synset": "gidgee.n.01", "name": "gidgee"}, - {"id": 17948, "synset": "catechu.n.02", "name": "catechu"}, - {"id": 17949, "synset": "silver_wattle.n.01", "name": "silver_wattle"}, - {"id": 17950, "synset": "huisache.n.01", "name": "huisache"}, - {"id": 17951, "synset": "lightwood.n.01", "name": "lightwood"}, - {"id": 17952, "synset": "golden_wattle.n.01", "name": "golden_wattle"}, - {"id": 17953, "synset": "fever_tree.n.04", "name": "fever_tree"}, - {"id": 17954, "synset": "coralwood.n.01", "name": "coralwood"}, - {"id": 17955, "synset": "albizzia.n.01", "name": "albizzia"}, - {"id": 17956, "synset": "silk_tree.n.01", "name": "silk_tree"}, - {"id": 17957, "synset": "siris.n.01", "name": "siris"}, - {"id": 17958, "synset": "rain_tree.n.01", "name": "rain_tree"}, - {"id": 17959, "synset": "calliandra.n.01", "name": "calliandra"}, - {"id": 17960, "synset": "conacaste.n.01", "name": "conacaste"}, - {"id": 17961, "synset": "inga.n.01", "name": "inga"}, - {"id": 17962, "synset": "ice-cream_bean.n.01", "name": "ice-cream_bean"}, - {"id": 17963, "synset": "guama.n.01", "name": "guama"}, - {"id": 17964, "synset": "lead_tree.n.01", "name": "lead_tree"}, - {"id": 17965, "synset": "wild_tamarind.n.02", "name": "wild_tamarind"}, - {"id": 17966, "synset": "sabicu.n.02", "name": "sabicu"}, - {"id": 17967, "synset": "nitta_tree.n.01", "name": "nitta_tree"}, - {"id": 17968, "synset": "parkia_javanica.n.01", "name": "Parkia_javanica"}, - {"id": 17969, "synset": "manila_tamarind.n.01", "name": "manila_tamarind"}, - {"id": 17970, "synset": "cat's-claw.n.01", "name": "cat's-claw"}, - {"id": 17971, "synset": "honey_mesquite.n.01", "name": "honey_mesquite"}, - {"id": 17972, "synset": "algarroba.n.03", "name": "algarroba"}, - {"id": 17973, "synset": "screw_bean.n.02", "name": "screw_bean"}, - {"id": 17974, "synset": "screw_bean.n.01", "name": "screw_bean"}, - {"id": 17975, "synset": "dogbane.n.01", "name": "dogbane"}, - {"id": 17976, "synset": "indian_hemp.n.03", "name": "Indian_hemp"}, - {"id": 17977, "synset": "bushman's_poison.n.01", "name": "bushman's_poison"}, - {"id": 17978, "synset": "impala_lily.n.01", "name": "impala_lily"}, - {"id": 17979, "synset": "allamanda.n.01", "name": "allamanda"}, - {"id": 17980, "synset": "common_allamanda.n.01", "name": "common_allamanda"}, - {"id": 17981, "synset": "dita.n.01", "name": "dita"}, - {"id": 17982, "synset": "nepal_trumpet_flower.n.01", "name": "Nepal_trumpet_flower"}, - {"id": 17983, "synset": "carissa.n.01", "name": "carissa"}, - {"id": 17984, "synset": "hedge_thorn.n.01", "name": "hedge_thorn"}, - {"id": 17985, "synset": "natal_plum.n.01", "name": "natal_plum"}, - {"id": 17986, "synset": "periwinkle.n.02", "name": "periwinkle"}, - {"id": 17987, "synset": "ivory_tree.n.01", "name": "ivory_tree"}, - {"id": 17988, "synset": "white_dipladenia.n.01", "name": "white_dipladenia"}, - {"id": 17989, "synset": "chilean_jasmine.n.01", "name": "Chilean_jasmine"}, - {"id": 17990, "synset": "oleander.n.01", "name": "oleander"}, - {"id": 17991, "synset": "frangipani.n.01", "name": "frangipani"}, - {"id": 17992, "synset": "west_indian_jasmine.n.01", "name": "West_Indian_jasmine"}, - {"id": 17993, "synset": "rauwolfia.n.02", "name": "rauwolfia"}, - {"id": 17994, "synset": "snakewood.n.01", "name": "snakewood"}, - {"id": 17995, "synset": "strophanthus_kombe.n.01", "name": "Strophanthus_kombe"}, - {"id": 17996, "synset": "yellow_oleander.n.01", "name": "yellow_oleander"}, - {"id": 17997, "synset": "myrtle.n.01", "name": "myrtle"}, - {"id": 17998, "synset": "large_periwinkle.n.01", "name": "large_periwinkle"}, - {"id": 17999, "synset": "arum.n.02", "name": "arum"}, - {"id": 18000, "synset": "cuckoopint.n.01", "name": "cuckoopint"}, - {"id": 18001, "synset": "black_calla.n.01", "name": "black_calla"}, - {"id": 18002, "synset": "calamus.n.02", "name": "calamus"}, - {"id": 18003, "synset": "alocasia.n.01", "name": "alocasia"}, - {"id": 18004, "synset": "giant_taro.n.01", "name": "giant_taro"}, - {"id": 18005, "synset": "amorphophallus.n.01", "name": "amorphophallus"}, - {"id": 18006, "synset": "pungapung.n.01", "name": "pungapung"}, - {"id": 18007, "synset": "devil's_tongue.n.01", "name": "devil's_tongue"}, - {"id": 18008, "synset": "anthurium.n.01", "name": "anthurium"}, - {"id": 18009, "synset": "flamingo_flower.n.01", "name": "flamingo_flower"}, - {"id": 18010, "synset": "jack-in-the-pulpit.n.01", "name": "jack-in-the-pulpit"}, - {"id": 18011, "synset": "friar's-cowl.n.01", "name": "friar's-cowl"}, - {"id": 18012, "synset": "caladium.n.01", "name": "caladium"}, - {"id": 18013, "synset": "caladium_bicolor.n.01", "name": "Caladium_bicolor"}, - {"id": 18014, "synset": "wild_calla.n.01", "name": "wild_calla"}, - {"id": 18015, "synset": "taro.n.02", "name": "taro"}, - {"id": 18016, "synset": "taro.n.01", "name": "taro"}, - {"id": 18017, "synset": "cryptocoryne.n.01", "name": "cryptocoryne"}, - {"id": 18018, "synset": "dracontium.n.01", "name": "dracontium"}, - {"id": 18019, "synset": "golden_pothos.n.01", "name": "golden_pothos"}, - {"id": 18020, "synset": "skunk_cabbage.n.02", "name": "skunk_cabbage"}, - {"id": 18021, "synset": "monstera.n.01", "name": "monstera"}, - {"id": 18022, "synset": "ceriman.n.01", "name": "ceriman"}, - {"id": 18023, "synset": "nephthytis.n.01", "name": "nephthytis"}, - {"id": 18024, "synset": "nephthytis_afzelii.n.01", "name": "Nephthytis_afzelii"}, - {"id": 18025, "synset": "arrow_arum.n.01", "name": "arrow_arum"}, - {"id": 18026, "synset": "green_arrow_arum.n.01", "name": "green_arrow_arum"}, - {"id": 18027, "synset": "philodendron.n.01", "name": "philodendron"}, - {"id": 18028, "synset": "pistia.n.01", "name": "pistia"}, - {"id": 18029, "synset": "pothos.n.01", "name": "pothos"}, - {"id": 18030, "synset": "spathiphyllum.n.01", "name": "spathiphyllum"}, - {"id": 18031, "synset": "skunk_cabbage.n.01", "name": "skunk_cabbage"}, - {"id": 18032, "synset": "yautia.n.01", "name": "yautia"}, - {"id": 18033, "synset": "calla_lily.n.01", "name": "calla_lily"}, - {"id": 18034, "synset": "pink_calla.n.01", "name": "pink_calla"}, - {"id": 18035, "synset": "golden_calla.n.01", "name": "golden_calla"}, - {"id": 18036, "synset": "duckweed.n.01", "name": "duckweed"}, - {"id": 18037, "synset": "common_duckweed.n.01", "name": "common_duckweed"}, - {"id": 18038, "synset": "star-duckweed.n.01", "name": "star-duckweed"}, - {"id": 18039, "synset": "great_duckweed.n.01", "name": "great_duckweed"}, - {"id": 18040, "synset": "watermeal.n.01", "name": "watermeal"}, - {"id": 18041, "synset": "common_wolffia.n.01", "name": "common_wolffia"}, - {"id": 18042, "synset": "aralia.n.01", "name": "aralia"}, - {"id": 18043, "synset": "american_angelica_tree.n.01", "name": "American_angelica_tree"}, - {"id": 18044, "synset": "american_spikenard.n.01", "name": "American_spikenard"}, - {"id": 18045, "synset": "bristly_sarsaparilla.n.01", "name": "bristly_sarsaparilla"}, - {"id": 18046, "synset": "japanese_angelica_tree.n.01", "name": "Japanese_angelica_tree"}, - {"id": 18047, "synset": "chinese_angelica.n.01", "name": "Chinese_angelica"}, - {"id": 18048, "synset": "ivy.n.01", "name": "ivy"}, - {"id": 18049, "synset": "puka.n.02", "name": "puka"}, - {"id": 18050, "synset": "ginseng.n.02", "name": "ginseng"}, - {"id": 18051, "synset": "ginseng.n.01", "name": "ginseng"}, - {"id": 18052, "synset": "umbrella_tree.n.01", "name": "umbrella_tree"}, - {"id": 18053, "synset": "birthwort.n.01", "name": "birthwort"}, - {"id": 18054, "synset": "dutchman's-pipe.n.01", "name": "Dutchman's-pipe"}, - {"id": 18055, "synset": "virginia_snakeroot.n.01", "name": "Virginia_snakeroot"}, - {"id": 18056, "synset": "canada_ginger.n.01", "name": "Canada_ginger"}, - {"id": 18057, "synset": "heartleaf.n.02", "name": "heartleaf"}, - {"id": 18058, "synset": "heartleaf.n.01", "name": "heartleaf"}, - {"id": 18059, "synset": "asarabacca.n.01", "name": "asarabacca"}, - {"id": 18060, "synset": "caryophyllaceous_plant.n.01", "name": "caryophyllaceous_plant"}, - {"id": 18061, "synset": "corn_cockle.n.01", "name": "corn_cockle"}, - {"id": 18062, "synset": "sandwort.n.03", "name": "sandwort"}, - {"id": 18063, "synset": "mountain_sandwort.n.01", "name": "mountain_sandwort"}, - {"id": 18064, "synset": "pine-barren_sandwort.n.01", "name": "pine-barren_sandwort"}, - {"id": 18065, "synset": "seabeach_sandwort.n.01", "name": "seabeach_sandwort"}, - {"id": 18066, "synset": "rock_sandwort.n.01", "name": "rock_sandwort"}, - {"id": 18067, "synset": "thyme-leaved_sandwort.n.01", "name": "thyme-leaved_sandwort"}, - {"id": 18068, "synset": "mouse-ear_chickweed.n.01", "name": "mouse-ear_chickweed"}, - {"id": 18069, "synset": "snow-in-summer.n.02", "name": "snow-in-summer"}, - {"id": 18070, "synset": "alpine_mouse-ear.n.01", "name": "Alpine_mouse-ear"}, - {"id": 18071, "synset": "pink.n.02", "name": "pink"}, - {"id": 18072, "synset": "sweet_william.n.01", "name": "sweet_William"}, - {"id": 18073, "synset": "china_pink.n.01", "name": "china_pink"}, - {"id": 18074, "synset": "japanese_pink.n.01", "name": "Japanese_pink"}, - {"id": 18075, "synset": "maiden_pink.n.01", "name": "maiden_pink"}, - {"id": 18076, "synset": "cheddar_pink.n.01", "name": "cheddar_pink"}, - {"id": 18077, "synset": "button_pink.n.01", "name": "button_pink"}, - {"id": 18078, "synset": "cottage_pink.n.01", "name": "cottage_pink"}, - {"id": 18079, "synset": "fringed_pink.n.02", "name": "fringed_pink"}, - {"id": 18080, "synset": "drypis.n.01", "name": "drypis"}, - {"id": 18081, "synset": "baby's_breath.n.01", "name": "baby's_breath"}, - {"id": 18082, "synset": "coral_necklace.n.01", "name": "coral_necklace"}, - {"id": 18083, "synset": "lychnis.n.01", "name": "lychnis"}, - {"id": 18084, "synset": "ragged_robin.n.01", "name": "ragged_robin"}, - {"id": 18085, "synset": "scarlet_lychnis.n.01", "name": "scarlet_lychnis"}, - {"id": 18086, "synset": "mullein_pink.n.01", "name": "mullein_pink"}, - {"id": 18087, "synset": "sandwort.n.02", "name": "sandwort"}, - {"id": 18088, "synset": "sandwort.n.01", "name": "sandwort"}, - {"id": 18089, "synset": "soapwort.n.01", "name": "soapwort"}, - {"id": 18090, "synset": "knawel.n.01", "name": "knawel"}, - {"id": 18091, "synset": "silene.n.01", "name": "silene"}, - {"id": 18092, "synset": "moss_campion.n.01", "name": "moss_campion"}, - {"id": 18093, "synset": "wild_pink.n.02", "name": "wild_pink"}, - {"id": 18094, "synset": "red_campion.n.01", "name": "red_campion"}, - {"id": 18095, "synset": "white_campion.n.01", "name": "white_campion"}, - {"id": 18096, "synset": "fire_pink.n.01", "name": "fire_pink"}, - {"id": 18097, "synset": "bladder_campion.n.01", "name": "bladder_campion"}, - {"id": 18098, "synset": "corn_spurry.n.01", "name": "corn_spurry"}, - {"id": 18099, "synset": "sand_spurry.n.01", "name": "sand_spurry"}, - {"id": 18100, "synset": "chickweed.n.01", "name": "chickweed"}, - {"id": 18101, "synset": "common_chickweed.n.01", "name": "common_chickweed"}, - {"id": 18102, "synset": "cowherb.n.01", "name": "cowherb"}, - {"id": 18103, "synset": "hottentot_fig.n.01", "name": "Hottentot_fig"}, - {"id": 18104, "synset": "livingstone_daisy.n.01", "name": "livingstone_daisy"}, - {"id": 18105, "synset": "fig_marigold.n.01", "name": "fig_marigold"}, - {"id": 18106, "synset": "ice_plant.n.01", "name": "ice_plant"}, - {"id": 18107, "synset": "new_zealand_spinach.n.01", "name": "New_Zealand_spinach"}, - {"id": 18108, "synset": "amaranth.n.02", "name": "amaranth"}, - {"id": 18109, "synset": "amaranth.n.01", "name": "amaranth"}, - {"id": 18110, "synset": "tumbleweed.n.04", "name": "tumbleweed"}, - {"id": 18111, "synset": "prince's-feather.n.02", "name": "prince's-feather"}, - {"id": 18112, "synset": "pigweed.n.02", "name": "pigweed"}, - {"id": 18113, "synset": "thorny_amaranth.n.01", "name": "thorny_amaranth"}, - {"id": 18114, "synset": "alligator_weed.n.01", "name": "alligator_weed"}, - {"id": 18115, "synset": "cockscomb.n.01", "name": "cockscomb"}, - {"id": 18116, "synset": "cottonweed.n.02", "name": "cottonweed"}, - {"id": 18117, "synset": "globe_amaranth.n.01", "name": "globe_amaranth"}, - {"id": 18118, "synset": "bloodleaf.n.01", "name": "bloodleaf"}, - {"id": 18119, "synset": "saltwort.n.02", "name": "saltwort"}, - {"id": 18120, "synset": "lamb's-quarters.n.01", "name": "lamb's-quarters"}, - {"id": 18121, "synset": "good-king-henry.n.01", "name": "good-king-henry"}, - {"id": 18122, "synset": "jerusalem_oak.n.01", "name": "Jerusalem_oak"}, - {"id": 18123, "synset": "oak-leaved_goosefoot.n.01", "name": "oak-leaved_goosefoot"}, - {"id": 18124, "synset": "sowbane.n.01", "name": "sowbane"}, - {"id": 18125, "synset": "nettle-leaved_goosefoot.n.01", "name": "nettle-leaved_goosefoot"}, - {"id": 18126, "synset": "red_goosefoot.n.01", "name": "red_goosefoot"}, - {"id": 18127, "synset": "stinking_goosefoot.n.01", "name": "stinking_goosefoot"}, - {"id": 18128, "synset": "orach.n.01", "name": "orach"}, - {"id": 18129, "synset": "saltbush.n.01", "name": "saltbush"}, - {"id": 18130, "synset": "garden_orache.n.01", "name": "garden_orache"}, - {"id": 18131, "synset": "desert_holly.n.01", "name": "desert_holly"}, - {"id": 18132, "synset": "quail_bush.n.01", "name": "quail_bush"}, - {"id": 18133, "synset": "beet.n.01", "name": "beet"}, - {"id": 18134, "synset": "beetroot.n.01", "name": "beetroot"}, - {"id": 18135, "synset": "chard.n.01", "name": "chard"}, - {"id": 18136, "synset": "mangel-wurzel.n.01", "name": "mangel-wurzel"}, - {"id": 18137, "synset": "winged_pigweed.n.01", "name": "winged_pigweed"}, - {"id": 18138, "synset": "halogeton.n.01", "name": "halogeton"}, - {"id": 18139, "synset": "glasswort.n.02", "name": "glasswort"}, - {"id": 18140, "synset": "saltwort.n.01", "name": "saltwort"}, - {"id": 18141, "synset": "russian_thistle.n.01", "name": "Russian_thistle"}, - {"id": 18142, "synset": "greasewood.n.01", "name": "greasewood"}, - {"id": 18143, "synset": "scarlet_musk_flower.n.01", "name": "scarlet_musk_flower"}, - {"id": 18144, "synset": "sand_verbena.n.01", "name": "sand_verbena"}, - {"id": 18145, "synset": "sweet_sand_verbena.n.01", "name": "sweet_sand_verbena"}, - {"id": 18146, "synset": "yellow_sand_verbena.n.01", "name": "yellow_sand_verbena"}, - {"id": 18147, "synset": "beach_pancake.n.01", "name": "beach_pancake"}, - {"id": 18148, "synset": "beach_sand_verbena.n.01", "name": "beach_sand_verbena"}, - {"id": 18149, "synset": "desert_sand_verbena.n.01", "name": "desert_sand_verbena"}, - {"id": 18150, "synset": "trailing_four_o'clock.n.01", "name": "trailing_four_o'clock"}, - {"id": 18151, "synset": "bougainvillea.n.01", "name": "bougainvillea"}, - {"id": 18152, "synset": "umbrellawort.n.01", "name": "umbrellawort"}, - {"id": 18153, "synset": "four_o'clock.n.01", "name": "four_o'clock"}, - {"id": 18154, "synset": "common_four-o'clock.n.01", "name": "common_four-o'clock"}, - {"id": 18155, "synset": "california_four_o'clock.n.01", "name": "California_four_o'clock"}, - {"id": 18156, "synset": "sweet_four_o'clock.n.01", "name": "sweet_four_o'clock"}, - {"id": 18157, "synset": "desert_four_o'clock.n.01", "name": "desert_four_o'clock"}, - {"id": 18158, "synset": "mountain_four_o'clock.n.01", "name": "mountain_four_o'clock"}, - {"id": 18159, "synset": "cockspur.n.02", "name": "cockspur"}, - {"id": 18160, "synset": "rattail_cactus.n.01", "name": "rattail_cactus"}, - {"id": 18161, "synset": "saguaro.n.01", "name": "saguaro"}, - {"id": 18162, "synset": "night-blooming_cereus.n.03", "name": "night-blooming_cereus"}, - {"id": 18163, "synset": "echinocactus.n.01", "name": "echinocactus"}, - {"id": 18164, "synset": "hedgehog_cactus.n.01", "name": "hedgehog_cactus"}, - {"id": 18165, "synset": "golden_barrel_cactus.n.01", "name": "golden_barrel_cactus"}, - {"id": 18166, "synset": "hedgehog_cereus.n.01", "name": "hedgehog_cereus"}, - {"id": 18167, "synset": "rainbow_cactus.n.01", "name": "rainbow_cactus"}, - {"id": 18168, "synset": "epiphyllum.n.01", "name": "epiphyllum"}, - {"id": 18169, "synset": "barrel_cactus.n.01", "name": "barrel_cactus"}, - {"id": 18170, "synset": "night-blooming_cereus.n.02", "name": "night-blooming_cereus"}, - {"id": 18171, "synset": "chichipe.n.01", "name": "chichipe"}, - {"id": 18172, "synset": "mescal.n.01", "name": "mescal"}, - {"id": 18173, "synset": "mescal_button.n.01", "name": "mescal_button"}, - {"id": 18174, "synset": "mammillaria.n.01", "name": "mammillaria"}, - {"id": 18175, "synset": "feather_ball.n.01", "name": "feather_ball"}, - {"id": 18176, "synset": "garambulla.n.01", "name": "garambulla"}, - {"id": 18177, "synset": "knowlton's_cactus.n.01", "name": "Knowlton's_cactus"}, - {"id": 18178, "synset": "nopal.n.02", "name": "nopal"}, - {"id": 18179, "synset": "prickly_pear.n.01", "name": "prickly_pear"}, - {"id": 18180, "synset": "cholla.n.01", "name": "cholla"}, - {"id": 18181, "synset": "nopal.n.01", "name": "nopal"}, - {"id": 18182, "synset": "tuna.n.01", "name": "tuna"}, - {"id": 18183, "synset": "barbados_gooseberry.n.01", "name": "Barbados_gooseberry"}, - {"id": 18184, "synset": "mistletoe_cactus.n.01", "name": "mistletoe_cactus"}, - {"id": 18185, "synset": "christmas_cactus.n.01", "name": "Christmas_cactus"}, - {"id": 18186, "synset": "night-blooming_cereus.n.01", "name": "night-blooming_cereus"}, - {"id": 18187, "synset": "crab_cactus.n.01", "name": "crab_cactus"}, - {"id": 18188, "synset": "pokeweed.n.01", "name": "pokeweed"}, - {"id": 18189, "synset": "indian_poke.n.02", "name": "Indian_poke"}, - {"id": 18190, "synset": "poke.n.01", "name": "poke"}, - {"id": 18191, "synset": "ombu.n.01", "name": "ombu"}, - {"id": 18192, "synset": "bloodberry.n.01", "name": "bloodberry"}, - {"id": 18193, "synset": "portulaca.n.01", "name": "portulaca"}, - {"id": 18194, "synset": "rose_moss.n.01", "name": "rose_moss"}, - {"id": 18195, "synset": "common_purslane.n.01", "name": "common_purslane"}, - {"id": 18196, "synset": "rock_purslane.n.01", "name": "rock_purslane"}, - {"id": 18197, "synset": "red_maids.n.01", "name": "red_maids"}, - {"id": 18198, "synset": "carolina_spring_beauty.n.01", "name": "Carolina_spring_beauty"}, - {"id": 18199, "synset": "spring_beauty.n.01", "name": "spring_beauty"}, - {"id": 18200, "synset": "virginia_spring_beauty.n.01", "name": "Virginia_spring_beauty"}, - {"id": 18201, "synset": "siskiyou_lewisia.n.01", "name": "siskiyou_lewisia"}, - {"id": 18202, "synset": "bitterroot.n.01", "name": "bitterroot"}, - {"id": 18203, "synset": "broad-leaved_montia.n.01", "name": "broad-leaved_montia"}, - {"id": 18204, "synset": "blinks.n.01", "name": "blinks"}, - {"id": 18205, "synset": "toad_lily.n.01", "name": "toad_lily"}, - {"id": 18206, "synset": "winter_purslane.n.01", "name": "winter_purslane"}, - {"id": 18207, "synset": "flame_flower.n.02", "name": "flame_flower"}, - {"id": 18208, "synset": "pigmy_talinum.n.01", "name": "pigmy_talinum"}, - {"id": 18209, "synset": "jewels-of-opar.n.01", "name": "jewels-of-opar"}, - {"id": 18210, "synset": "caper.n.01", "name": "caper"}, - {"id": 18211, "synset": "native_pomegranate.n.01", "name": "native_pomegranate"}, - {"id": 18212, "synset": "caper_tree.n.02", "name": "caper_tree"}, - {"id": 18213, "synset": "caper_tree.n.01", "name": "caper_tree"}, - {"id": 18214, "synset": "common_caper.n.01", "name": "common_caper"}, - {"id": 18215, "synset": "spiderflower.n.01", "name": "spiderflower"}, - {"id": 18216, "synset": "rocky_mountain_bee_plant.n.01", "name": "Rocky_Mountain_bee_plant"}, - {"id": 18217, "synset": "clammyweed.n.01", "name": "clammyweed"}, - {"id": 18218, "synset": "crucifer.n.01", "name": "crucifer"}, - {"id": 18219, "synset": "cress.n.01", "name": "cress"}, - {"id": 18220, "synset": "watercress.n.01", "name": "watercress"}, - {"id": 18221, "synset": "stonecress.n.01", "name": "stonecress"}, - {"id": 18222, "synset": "garlic_mustard.n.01", "name": "garlic_mustard"}, - {"id": 18223, "synset": "alyssum.n.01", "name": "alyssum"}, - {"id": 18224, "synset": "rose_of_jericho.n.02", "name": "rose_of_Jericho"}, - {"id": 18225, "synset": "arabidopsis_thaliana.n.01", "name": "Arabidopsis_thaliana"}, - {"id": 18226, "synset": "arabidopsis_lyrata.n.01", "name": "Arabidopsis_lyrata"}, - {"id": 18227, "synset": "rock_cress.n.01", "name": "rock_cress"}, - {"id": 18228, "synset": "sicklepod.n.02", "name": "sicklepod"}, - {"id": 18229, "synset": "tower_mustard.n.01", "name": "tower_mustard"}, - {"id": 18230, "synset": "horseradish.n.01", "name": "horseradish"}, - {"id": 18231, "synset": "winter_cress.n.01", "name": "winter_cress"}, - {"id": 18232, "synset": "yellow_rocket.n.01", "name": "yellow_rocket"}, - {"id": 18233, "synset": "hoary_alison.n.01", "name": "hoary_alison"}, - {"id": 18234, "synset": "buckler_mustard.n.01", "name": "buckler_mustard"}, - {"id": 18235, "synset": "wild_cabbage.n.01", "name": "wild_cabbage"}, - {"id": 18236, "synset": "cabbage.n.03", "name": "cabbage"}, - {"id": 18237, "synset": "head_cabbage.n.01", "name": "head_cabbage"}, - {"id": 18238, "synset": "savoy_cabbage.n.01", "name": "savoy_cabbage"}, - {"id": 18239, "synset": "brussels_sprout.n.01", "name": "brussels_sprout"}, - {"id": 18240, "synset": "cauliflower.n.01", "name": "cauliflower"}, - {"id": 18241, "synset": "collard.n.01", "name": "collard"}, - {"id": 18242, "synset": "kohlrabi.n.01", "name": "kohlrabi"}, - {"id": 18243, "synset": "turnip_plant.n.01", "name": "turnip_plant"}, - {"id": 18244, "synset": "rutabaga.n.02", "name": "rutabaga"}, - {"id": 18245, "synset": "broccoli_raab.n.01", "name": "broccoli_raab"}, - {"id": 18246, "synset": "mustard.n.01", "name": "mustard"}, - {"id": 18247, "synset": "chinese_mustard.n.01", "name": "chinese_mustard"}, - {"id": 18248, "synset": "bok_choy.n.01", "name": "bok_choy"}, - {"id": 18249, "synset": "rape.n.01", "name": "rape"}, - {"id": 18250, "synset": "rapeseed.n.01", "name": "rapeseed"}, - {"id": 18251, "synset": "shepherd's_purse.n.01", "name": "shepherd's_purse"}, - {"id": 18252, "synset": "lady's_smock.n.01", "name": "lady's_smock"}, - {"id": 18253, "synset": "coral-root_bittercress.n.01", "name": "coral-root_bittercress"}, - {"id": 18254, "synset": "crinkleroot.n.01", "name": "crinkleroot"}, - {"id": 18255, "synset": "american_watercress.n.01", "name": "American_watercress"}, - {"id": 18256, "synset": "spring_cress.n.01", "name": "spring_cress"}, - {"id": 18257, "synset": "purple_cress.n.01", "name": "purple_cress"}, - {"id": 18258, "synset": "wallflower.n.02", "name": "wallflower"}, - {"id": 18259, "synset": "prairie_rocket.n.02", "name": "prairie_rocket"}, - {"id": 18260, "synset": "scurvy_grass.n.01", "name": "scurvy_grass"}, - {"id": 18261, "synset": "sea_kale.n.01", "name": "sea_kale"}, - {"id": 18262, "synset": "tansy_mustard.n.01", "name": "tansy_mustard"}, - {"id": 18263, "synset": "draba.n.01", "name": "draba"}, - {"id": 18264, "synset": "wallflower.n.01", "name": "wallflower"}, - {"id": 18265, "synset": "prairie_rocket.n.01", "name": "prairie_rocket"}, - {"id": 18266, "synset": "siberian_wall_flower.n.01", "name": "Siberian_wall_flower"}, - {"id": 18267, "synset": "western_wall_flower.n.01", "name": "western_wall_flower"}, - {"id": 18268, "synset": "wormseed_mustard.n.01", "name": "wormseed_mustard"}, - {"id": 18269, "synset": "heliophila.n.01", "name": "heliophila"}, - {"id": 18270, "synset": "damask_violet.n.01", "name": "damask_violet"}, - {"id": 18271, "synset": "tansy-leaved_rocket.n.01", "name": "tansy-leaved_rocket"}, - {"id": 18272, "synset": "candytuft.n.01", "name": "candytuft"}, - {"id": 18273, "synset": "woad.n.02", "name": "woad"}, - {"id": 18274, "synset": "dyer's_woad.n.01", "name": "dyer's_woad"}, - {"id": 18275, "synset": "bladderpod.n.04", "name": "bladderpod"}, - {"id": 18276, "synset": "sweet_alyssum.n.01", "name": "sweet_alyssum"}, - {"id": 18277, "synset": "malcolm_stock.n.01", "name": "Malcolm_stock"}, - {"id": 18278, "synset": "virginian_stock.n.01", "name": "Virginian_stock"}, - {"id": 18279, "synset": "stock.n.12", "name": "stock"}, - {"id": 18280, "synset": "brompton_stock.n.01", "name": "brompton_stock"}, - {"id": 18281, "synset": "bladderpod.n.03", "name": "bladderpod"}, - {"id": 18282, "synset": "chamois_cress.n.01", "name": "chamois_cress"}, - {"id": 18283, "synset": "radish_plant.n.01", "name": "radish_plant"}, - {"id": 18284, "synset": "jointed_charlock.n.01", "name": "jointed_charlock"}, - {"id": 18285, "synset": "radish.n.04", "name": "radish"}, - {"id": 18286, "synset": "radish.n.02", "name": "radish"}, - {"id": 18287, "synset": "marsh_cress.n.01", "name": "marsh_cress"}, - {"id": 18288, "synset": "great_yellowcress.n.01", "name": "great_yellowcress"}, - {"id": 18289, "synset": "schizopetalon.n.01", "name": "schizopetalon"}, - {"id": 18290, "synset": "field_mustard.n.01", "name": "field_mustard"}, - {"id": 18291, "synset": "hedge_mustard.n.01", "name": "hedge_mustard"}, - {"id": 18292, "synset": "desert_plume.n.01", "name": "desert_plume"}, - {"id": 18293, "synset": "pennycress.n.01", "name": "pennycress"}, - {"id": 18294, "synset": "field_pennycress.n.01", "name": "field_pennycress"}, - {"id": 18295, "synset": "fringepod.n.01", "name": "fringepod"}, - {"id": 18296, "synset": "bladderpod.n.02", "name": "bladderpod"}, - {"id": 18297, "synset": "wasabi.n.01", "name": "wasabi"}, - {"id": 18298, "synset": "poppy.n.01", "name": "poppy"}, - {"id": 18299, "synset": "iceland_poppy.n.02", "name": "Iceland_poppy"}, - {"id": 18300, "synset": "western_poppy.n.01", "name": "western_poppy"}, - {"id": 18301, "synset": "prickly_poppy.n.02", "name": "prickly_poppy"}, - {"id": 18302, "synset": "iceland_poppy.n.01", "name": "Iceland_poppy"}, - {"id": 18303, "synset": "oriental_poppy.n.01", "name": "oriental_poppy"}, - {"id": 18304, "synset": "corn_poppy.n.01", "name": "corn_poppy"}, - {"id": 18305, "synset": "opium_poppy.n.01", "name": "opium_poppy"}, - {"id": 18306, "synset": "prickly_poppy.n.01", "name": "prickly_poppy"}, - {"id": 18307, "synset": "mexican_poppy.n.01", "name": "Mexican_poppy"}, - {"id": 18308, "synset": "bocconia.n.02", "name": "bocconia"}, - {"id": 18309, "synset": "celandine.n.02", "name": "celandine"}, - {"id": 18310, "synset": "corydalis.n.01", "name": "corydalis"}, - {"id": 18311, "synset": "climbing_corydalis.n.01", "name": "climbing_corydalis"}, - {"id": 18312, "synset": "california_poppy.n.01", "name": "California_poppy"}, - {"id": 18313, "synset": "horn_poppy.n.01", "name": "horn_poppy"}, - {"id": 18314, "synset": "golden_cup.n.01", "name": "golden_cup"}, - {"id": 18315, "synset": "plume_poppy.n.01", "name": "plume_poppy"}, - {"id": 18316, "synset": "blue_poppy.n.01", "name": "blue_poppy"}, - {"id": 18317, "synset": "welsh_poppy.n.01", "name": "Welsh_poppy"}, - {"id": 18318, "synset": "creamcups.n.01", "name": "creamcups"}, - {"id": 18319, "synset": "matilija_poppy.n.01", "name": "matilija_poppy"}, - {"id": 18320, "synset": "wind_poppy.n.01", "name": "wind_poppy"}, - {"id": 18321, "synset": "celandine_poppy.n.01", "name": "celandine_poppy"}, - {"id": 18322, "synset": "climbing_fumitory.n.01", "name": "climbing_fumitory"}, - {"id": 18323, "synset": "bleeding_heart.n.01", "name": "bleeding_heart"}, - {"id": 18324, "synset": "dutchman's_breeches.n.01", "name": "Dutchman's_breeches"}, - {"id": 18325, "synset": "squirrel_corn.n.01", "name": "squirrel_corn"}, - {"id": 18326, "synset": "composite.n.02", "name": "composite"}, - {"id": 18327, "synset": "compass_plant.n.02", "name": "compass_plant"}, - {"id": 18328, "synset": "everlasting.n.01", "name": "everlasting"}, - {"id": 18329, "synset": "achillea.n.01", "name": "achillea"}, - {"id": 18330, "synset": "yarrow.n.01", "name": "yarrow"}, - { - "id": 18331, - "synset": "pink-and-white_everlasting.n.01", - "name": "pink-and-white_everlasting", - }, - {"id": 18332, "synset": "white_snakeroot.n.01", "name": "white_snakeroot"}, - {"id": 18333, "synset": "ageratum.n.02", "name": "ageratum"}, - {"id": 18334, "synset": "common_ageratum.n.01", "name": "common_ageratum"}, - {"id": 18335, "synset": "sweet_sultan.n.03", "name": "sweet_sultan"}, - {"id": 18336, "synset": "ragweed.n.02", "name": "ragweed"}, - {"id": 18337, "synset": "common_ragweed.n.01", "name": "common_ragweed"}, - {"id": 18338, "synset": "great_ragweed.n.01", "name": "great_ragweed"}, - {"id": 18339, "synset": "western_ragweed.n.01", "name": "western_ragweed"}, - {"id": 18340, "synset": "ammobium.n.01", "name": "ammobium"}, - {"id": 18341, "synset": "winged_everlasting.n.01", "name": "winged_everlasting"}, - {"id": 18342, "synset": "pellitory.n.02", "name": "pellitory"}, - {"id": 18343, "synset": "pearly_everlasting.n.01", "name": "pearly_everlasting"}, - {"id": 18344, "synset": "andryala.n.01", "name": "andryala"}, - {"id": 18345, "synset": "plantain-leaved_pussytoes.n.01", "name": "plantain-leaved_pussytoes"}, - {"id": 18346, "synset": "field_pussytoes.n.01", "name": "field_pussytoes"}, - {"id": 18347, "synset": "solitary_pussytoes.n.01", "name": "solitary_pussytoes"}, - {"id": 18348, "synset": "mountain_everlasting.n.01", "name": "mountain_everlasting"}, - {"id": 18349, "synset": "mayweed.n.01", "name": "mayweed"}, - {"id": 18350, "synset": "yellow_chamomile.n.01", "name": "yellow_chamomile"}, - {"id": 18351, "synset": "corn_chamomile.n.01", "name": "corn_chamomile"}, - {"id": 18352, "synset": "woolly_daisy.n.01", "name": "woolly_daisy"}, - {"id": 18353, "synset": "burdock.n.01", "name": "burdock"}, - {"id": 18354, "synset": "great_burdock.n.01", "name": "great_burdock"}, - {"id": 18355, "synset": "african_daisy.n.03", "name": "African_daisy"}, - {"id": 18356, "synset": "blue-eyed_african_daisy.n.01", "name": "blue-eyed_African_daisy"}, - {"id": 18357, "synset": "marguerite.n.02", "name": "marguerite"}, - {"id": 18358, "synset": "silversword.n.01", "name": "silversword"}, - {"id": 18359, "synset": "arnica.n.02", "name": "arnica"}, - {"id": 18360, "synset": "heartleaf_arnica.n.01", "name": "heartleaf_arnica"}, - {"id": 18361, "synset": "arnica_montana.n.01", "name": "Arnica_montana"}, - {"id": 18362, "synset": "lamb_succory.n.01", "name": "lamb_succory"}, - {"id": 18363, "synset": "artemisia.n.01", "name": "artemisia"}, - {"id": 18364, "synset": "mugwort.n.01", "name": "mugwort"}, - {"id": 18365, "synset": "sweet_wormwood.n.01", "name": "sweet_wormwood"}, - {"id": 18366, "synset": "field_wormwood.n.01", "name": "field_wormwood"}, - {"id": 18367, "synset": "tarragon.n.01", "name": "tarragon"}, - {"id": 18368, "synset": "sand_sage.n.01", "name": "sand_sage"}, - {"id": 18369, "synset": "wormwood_sage.n.01", "name": "wormwood_sage"}, - {"id": 18370, "synset": "western_mugwort.n.01", "name": "western_mugwort"}, - {"id": 18371, "synset": "roman_wormwood.n.01", "name": "Roman_wormwood"}, - {"id": 18372, "synset": "bud_brush.n.01", "name": "bud_brush"}, - {"id": 18373, "synset": "common_mugwort.n.01", "name": "common_mugwort"}, - {"id": 18374, "synset": "aster.n.01", "name": "aster"}, - {"id": 18375, "synset": "wood_aster.n.01", "name": "wood_aster"}, - {"id": 18376, "synset": "whorled_aster.n.01", "name": "whorled_aster"}, - {"id": 18377, "synset": "heath_aster.n.02", "name": "heath_aster"}, - {"id": 18378, "synset": "heart-leaved_aster.n.01", "name": "heart-leaved_aster"}, - {"id": 18379, "synset": "white_wood_aster.n.01", "name": "white_wood_aster"}, - {"id": 18380, "synset": "bushy_aster.n.01", "name": "bushy_aster"}, - {"id": 18381, "synset": "heath_aster.n.01", "name": "heath_aster"}, - {"id": 18382, "synset": "white_prairie_aster.n.01", "name": "white_prairie_aster"}, - {"id": 18383, "synset": "stiff_aster.n.01", "name": "stiff_aster"}, - {"id": 18384, "synset": "goldilocks.n.01", "name": "goldilocks"}, - {"id": 18385, "synset": "large-leaved_aster.n.01", "name": "large-leaved_aster"}, - {"id": 18386, "synset": "new_england_aster.n.01", "name": "New_England_aster"}, - {"id": 18387, "synset": "michaelmas_daisy.n.01", "name": "Michaelmas_daisy"}, - {"id": 18388, "synset": "upland_white_aster.n.01", "name": "upland_white_aster"}, - {"id": 18389, "synset": "short's_aster.n.01", "name": "Short's_aster"}, - {"id": 18390, "synset": "sea_aster.n.01", "name": "sea_aster"}, - {"id": 18391, "synset": "prairie_aster.n.01", "name": "prairie_aster"}, - {"id": 18392, "synset": "annual_salt-marsh_aster.n.01", "name": "annual_salt-marsh_aster"}, - {"id": 18393, "synset": "aromatic_aster.n.01", "name": "aromatic_aster"}, - {"id": 18394, "synset": "arrow_leaved_aster.n.01", "name": "arrow_leaved_aster"}, - {"id": 18395, "synset": "azure_aster.n.01", "name": "azure_aster"}, - {"id": 18396, "synset": "bog_aster.n.01", "name": "bog_aster"}, - {"id": 18397, "synset": "crooked-stemmed_aster.n.01", "name": "crooked-stemmed_aster"}, - {"id": 18398, "synset": "eastern_silvery_aster.n.01", "name": "Eastern_silvery_aster"}, - {"id": 18399, "synset": "flat-topped_white_aster.n.01", "name": "flat-topped_white_aster"}, - {"id": 18400, "synset": "late_purple_aster.n.01", "name": "late_purple_aster"}, - {"id": 18401, "synset": "panicled_aster.n.01", "name": "panicled_aster"}, - { - "id": 18402, - "synset": "perennial_salt_marsh_aster.n.01", - "name": "perennial_salt_marsh_aster", - }, - {"id": 18403, "synset": "purple-stemmed_aster.n.01", "name": "purple-stemmed_aster"}, - {"id": 18404, "synset": "rough-leaved_aster.n.01", "name": "rough-leaved_aster"}, - {"id": 18405, "synset": "rush_aster.n.01", "name": "rush_aster"}, - {"id": 18406, "synset": "schreiber's_aster.n.01", "name": "Schreiber's_aster"}, - {"id": 18407, "synset": "small_white_aster.n.01", "name": "small_white_aster"}, - {"id": 18408, "synset": "smooth_aster.n.01", "name": "smooth_aster"}, - {"id": 18409, "synset": "southern_aster.n.01", "name": "southern_aster"}, - {"id": 18410, "synset": "starved_aster.n.01", "name": "starved_aster"}, - {"id": 18411, "synset": "tradescant's_aster.n.01", "name": "tradescant's_aster"}, - {"id": 18412, "synset": "wavy-leaved_aster.n.01", "name": "wavy-leaved_aster"}, - {"id": 18413, "synset": "western_silvery_aster.n.01", "name": "Western_silvery_aster"}, - {"id": 18414, "synset": "willow_aster.n.01", "name": "willow_aster"}, - {"id": 18415, "synset": "ayapana.n.01", "name": "ayapana"}, - {"id": 18416, "synset": "mule_fat.n.01", "name": "mule_fat"}, - {"id": 18417, "synset": "balsamroot.n.01", "name": "balsamroot"}, - {"id": 18418, "synset": "daisy.n.01", "name": "daisy"}, - {"id": 18419, "synset": "common_daisy.n.01", "name": "common_daisy"}, - {"id": 18420, "synset": "bur_marigold.n.01", "name": "bur_marigold"}, - {"id": 18421, "synset": "spanish_needles.n.02", "name": "Spanish_needles"}, - {"id": 18422, "synset": "tickseed_sunflower.n.01", "name": "tickseed_sunflower"}, - {"id": 18423, "synset": "european_beggar-ticks.n.01", "name": "European_beggar-ticks"}, - {"id": 18424, "synset": "slender_knapweed.n.01", "name": "slender_knapweed"}, - {"id": 18425, "synset": "false_chamomile.n.01", "name": "false_chamomile"}, - {"id": 18426, "synset": "swan_river_daisy.n.01", "name": "Swan_River_daisy"}, - {"id": 18427, "synset": "woodland_oxeye.n.01", "name": "woodland_oxeye"}, - {"id": 18428, "synset": "indian_plantain.n.01", "name": "Indian_plantain"}, - {"id": 18429, "synset": "calendula.n.01", "name": "calendula"}, - {"id": 18430, "synset": "common_marigold.n.01", "name": "common_marigold"}, - {"id": 18431, "synset": "china_aster.n.01", "name": "China_aster"}, - {"id": 18432, "synset": "thistle.n.01", "name": "thistle"}, - {"id": 18433, "synset": "welted_thistle.n.01", "name": "welted_thistle"}, - {"id": 18434, "synset": "musk_thistle.n.01", "name": "musk_thistle"}, - {"id": 18435, "synset": "carline_thistle.n.01", "name": "carline_thistle"}, - {"id": 18436, "synset": "stemless_carline_thistle.n.01", "name": "stemless_carline_thistle"}, - {"id": 18437, "synset": "common_carline_thistle.n.01", "name": "common_carline_thistle"}, - {"id": 18438, "synset": "safflower.n.01", "name": "safflower"}, - {"id": 18439, "synset": "safflower_seed.n.01", "name": "safflower_seed"}, - {"id": 18440, "synset": "catananche.n.01", "name": "catananche"}, - {"id": 18441, "synset": "blue_succory.n.01", "name": "blue_succory"}, - {"id": 18442, "synset": "centaury.n.02", "name": "centaury"}, - {"id": 18443, "synset": "dusty_miller.n.03", "name": "dusty_miller"}, - {"id": 18444, "synset": "cornflower.n.02", "name": "cornflower"}, - {"id": 18445, "synset": "star-thistle.n.01", "name": "star-thistle"}, - {"id": 18446, "synset": "knapweed.n.01", "name": "knapweed"}, - {"id": 18447, "synset": "sweet_sultan.n.02", "name": "sweet_sultan"}, - {"id": 18448, "synset": "great_knapweed.n.01", "name": "great_knapweed"}, - {"id": 18449, "synset": "barnaby's_thistle.n.01", "name": "Barnaby's_thistle"}, - {"id": 18450, "synset": "chamomile.n.01", "name": "chamomile"}, - {"id": 18451, "synset": "chaenactis.n.01", "name": "chaenactis"}, - {"id": 18452, "synset": "chrysanthemum.n.02", "name": "chrysanthemum"}, - {"id": 18453, "synset": "corn_marigold.n.01", "name": "corn_marigold"}, - {"id": 18454, "synset": "crown_daisy.n.01", "name": "crown_daisy"}, - {"id": 18455, "synset": "chop-suey_greens.n.01", "name": "chop-suey_greens"}, - {"id": 18456, "synset": "golden_aster.n.01", "name": "golden_aster"}, - {"id": 18457, "synset": "maryland_golden_aster.n.01", "name": "Maryland_golden_aster"}, - {"id": 18458, "synset": "goldenbush.n.02", "name": "goldenbush"}, - {"id": 18459, "synset": "rabbit_brush.n.01", "name": "rabbit_brush"}, - {"id": 18460, "synset": "chicory.n.02", "name": "chicory"}, - {"id": 18461, "synset": "endive.n.01", "name": "endive"}, - {"id": 18462, "synset": "chicory.n.01", "name": "chicory"}, - {"id": 18463, "synset": "plume_thistle.n.01", "name": "plume_thistle"}, - {"id": 18464, "synset": "canada_thistle.n.01", "name": "Canada_thistle"}, - {"id": 18465, "synset": "field_thistle.n.01", "name": "field_thistle"}, - {"id": 18466, "synset": "woolly_thistle.n.02", "name": "woolly_thistle"}, - {"id": 18467, "synset": "european_woolly_thistle.n.01", "name": "European_woolly_thistle"}, - {"id": 18468, "synset": "melancholy_thistle.n.01", "name": "melancholy_thistle"}, - {"id": 18469, "synset": "brook_thistle.n.01", "name": "brook_thistle"}, - {"id": 18470, "synset": "bull_thistle.n.01", "name": "bull_thistle"}, - {"id": 18471, "synset": "blessed_thistle.n.02", "name": "blessed_thistle"}, - {"id": 18472, "synset": "mistflower.n.01", "name": "mistflower"}, - {"id": 18473, "synset": "horseweed.n.02", "name": "horseweed"}, - {"id": 18474, "synset": "coreopsis.n.01", "name": "coreopsis"}, - {"id": 18475, "synset": "giant_coreopsis.n.01", "name": "giant_coreopsis"}, - {"id": 18476, "synset": "sea_dahlia.n.01", "name": "sea_dahlia"}, - {"id": 18477, "synset": "calliopsis.n.01", "name": "calliopsis"}, - {"id": 18478, "synset": "cosmos.n.02", "name": "cosmos"}, - {"id": 18479, "synset": "brass_buttons.n.01", "name": "brass_buttons"}, - {"id": 18480, "synset": "billy_buttons.n.01", "name": "billy_buttons"}, - {"id": 18481, "synset": "hawk's-beard.n.01", "name": "hawk's-beard"}, - {"id": 18482, "synset": "artichoke.n.01", "name": "artichoke"}, - {"id": 18483, "synset": "cardoon.n.01", "name": "cardoon"}, - {"id": 18484, "synset": "dahlia.n.01", "name": "dahlia"}, - {"id": 18485, "synset": "german_ivy.n.01", "name": "German_ivy"}, - {"id": 18486, "synset": "florist's_chrysanthemum.n.01", "name": "florist's_chrysanthemum"}, - {"id": 18487, "synset": "cape_marigold.n.01", "name": "cape_marigold"}, - {"id": 18488, "synset": "leopard's-bane.n.01", "name": "leopard's-bane"}, - {"id": 18489, "synset": "coneflower.n.03", "name": "coneflower"}, - {"id": 18490, "synset": "globe_thistle.n.01", "name": "globe_thistle"}, - {"id": 18491, "synset": "elephant's-foot.n.02", "name": "elephant's-foot"}, - {"id": 18492, "synset": "tassel_flower.n.01", "name": "tassel_flower"}, - {"id": 18493, "synset": "brittlebush.n.01", "name": "brittlebush"}, - {"id": 18494, "synset": "sunray.n.02", "name": "sunray"}, - {"id": 18495, "synset": "engelmannia.n.01", "name": "engelmannia"}, - {"id": 18496, "synset": "fireweed.n.02", "name": "fireweed"}, - {"id": 18497, "synset": "fleabane.n.02", "name": "fleabane"}, - {"id": 18498, "synset": "blue_fleabane.n.01", "name": "blue_fleabane"}, - {"id": 18499, "synset": "daisy_fleabane.n.01", "name": "daisy_fleabane"}, - {"id": 18500, "synset": "orange_daisy.n.01", "name": "orange_daisy"}, - {"id": 18501, "synset": "spreading_fleabane.n.01", "name": "spreading_fleabane"}, - {"id": 18502, "synset": "seaside_daisy.n.01", "name": "seaside_daisy"}, - {"id": 18503, "synset": "philadelphia_fleabane.n.01", "name": "Philadelphia_fleabane"}, - {"id": 18504, "synset": "robin's_plantain.n.01", "name": "robin's_plantain"}, - {"id": 18505, "synset": "showy_daisy.n.01", "name": "showy_daisy"}, - {"id": 18506, "synset": "woolly_sunflower.n.01", "name": "woolly_sunflower"}, - {"id": 18507, "synset": "golden_yarrow.n.01", "name": "golden_yarrow"}, - {"id": 18508, "synset": "dog_fennel.n.01", "name": "dog_fennel"}, - {"id": 18509, "synset": "joe-pye_weed.n.02", "name": "Joe-Pye_weed"}, - {"id": 18510, "synset": "boneset.n.02", "name": "boneset"}, - {"id": 18511, "synset": "joe-pye_weed.n.01", "name": "Joe-Pye_weed"}, - {"id": 18512, "synset": "blue_daisy.n.01", "name": "blue_daisy"}, - {"id": 18513, "synset": "kingfisher_daisy.n.01", "name": "kingfisher_daisy"}, - {"id": 18514, "synset": "cotton_rose.n.02", "name": "cotton_rose"}, - {"id": 18515, "synset": "herba_impia.n.01", "name": "herba_impia"}, - {"id": 18516, "synset": "gaillardia.n.01", "name": "gaillardia"}, - {"id": 18517, "synset": "gazania.n.01", "name": "gazania"}, - {"id": 18518, "synset": "treasure_flower.n.01", "name": "treasure_flower"}, - {"id": 18519, "synset": "african_daisy.n.02", "name": "African_daisy"}, - {"id": 18520, "synset": "barberton_daisy.n.01", "name": "Barberton_daisy"}, - {"id": 18521, "synset": "desert_sunflower.n.01", "name": "desert_sunflower"}, - {"id": 18522, "synset": "cudweed.n.01", "name": "cudweed"}, - {"id": 18523, "synset": "chafeweed.n.01", "name": "chafeweed"}, - {"id": 18524, "synset": "gumweed.n.01", "name": "gumweed"}, - {"id": 18525, "synset": "grindelia_robusta.n.01", "name": "Grindelia_robusta"}, - {"id": 18526, "synset": "curlycup_gumweed.n.01", "name": "curlycup_gumweed"}, - {"id": 18527, "synset": "little-head_snakeweed.n.01", "name": "little-head_snakeweed"}, - {"id": 18528, "synset": "rabbitweed.n.01", "name": "rabbitweed"}, - {"id": 18529, "synset": "broomweed.n.01", "name": "broomweed"}, - {"id": 18530, "synset": "velvet_plant.n.02", "name": "velvet_plant"}, - {"id": 18531, "synset": "goldenbush.n.01", "name": "goldenbush"}, - {"id": 18532, "synset": "camphor_daisy.n.01", "name": "camphor_daisy"}, - {"id": 18533, "synset": "yellow_spiny_daisy.n.01", "name": "yellow_spiny_daisy"}, - {"id": 18534, "synset": "hoary_golden_bush.n.01", "name": "hoary_golden_bush"}, - {"id": 18535, "synset": "sneezeweed.n.01", "name": "sneezeweed"}, - {"id": 18536, "synset": "orange_sneezeweed.n.01", "name": "orange_sneezeweed"}, - {"id": 18537, "synset": "rosilla.n.01", "name": "rosilla"}, - {"id": 18538, "synset": "swamp_sunflower.n.01", "name": "swamp_sunflower"}, - {"id": 18539, "synset": "common_sunflower.n.01", "name": "common_sunflower"}, - {"id": 18540, "synset": "giant_sunflower.n.01", "name": "giant_sunflower"}, - {"id": 18541, "synset": "showy_sunflower.n.01", "name": "showy_sunflower"}, - {"id": 18542, "synset": "maximilian's_sunflower.n.01", "name": "Maximilian's_sunflower"}, - {"id": 18543, "synset": "prairie_sunflower.n.01", "name": "prairie_sunflower"}, - {"id": 18544, "synset": "jerusalem_artichoke.n.02", "name": "Jerusalem_artichoke"}, - {"id": 18545, "synset": "jerusalem_artichoke.n.01", "name": "Jerusalem_artichoke"}, - {"id": 18546, "synset": "strawflower.n.03", "name": "strawflower"}, - {"id": 18547, "synset": "heliopsis.n.01", "name": "heliopsis"}, - {"id": 18548, "synset": "strawflower.n.02", "name": "strawflower"}, - {"id": 18549, "synset": "hairy_golden_aster.n.01", "name": "hairy_golden_aster"}, - {"id": 18550, "synset": "hawkweed.n.02", "name": "hawkweed"}, - {"id": 18551, "synset": "rattlesnake_weed.n.01", "name": "rattlesnake_weed"}, - {"id": 18552, "synset": "alpine_coltsfoot.n.01", "name": "alpine_coltsfoot"}, - {"id": 18553, "synset": "alpine_gold.n.01", "name": "alpine_gold"}, - {"id": 18554, "synset": "dwarf_hulsea.n.01", "name": "dwarf_hulsea"}, - {"id": 18555, "synset": "cat's-ear.n.02", "name": "cat's-ear"}, - {"id": 18556, "synset": "inula.n.01", "name": "inula"}, - {"id": 18557, "synset": "marsh_elder.n.01", "name": "marsh_elder"}, - {"id": 18558, "synset": "burweed_marsh_elder.n.01", "name": "burweed_marsh_elder"}, - {"id": 18559, "synset": "krigia.n.01", "name": "krigia"}, - {"id": 18560, "synset": "dwarf_dandelion.n.01", "name": "dwarf_dandelion"}, - {"id": 18561, "synset": "garden_lettuce.n.01", "name": "garden_lettuce"}, - {"id": 18562, "synset": "cos_lettuce.n.01", "name": "cos_lettuce"}, - {"id": 18563, "synset": "leaf_lettuce.n.01", "name": "leaf_lettuce"}, - {"id": 18564, "synset": "celtuce.n.01", "name": "celtuce"}, - {"id": 18565, "synset": "prickly_lettuce.n.01", "name": "prickly_lettuce"}, - {"id": 18566, "synset": "goldfields.n.01", "name": "goldfields"}, - {"id": 18567, "synset": "tidytips.n.01", "name": "tidytips"}, - {"id": 18568, "synset": "hawkbit.n.01", "name": "hawkbit"}, - {"id": 18569, "synset": "fall_dandelion.n.01", "name": "fall_dandelion"}, - {"id": 18570, "synset": "edelweiss.n.01", "name": "edelweiss"}, - {"id": 18571, "synset": "oxeye_daisy.n.02", "name": "oxeye_daisy"}, - {"id": 18572, "synset": "oxeye_daisy.n.01", "name": "oxeye_daisy"}, - {"id": 18573, "synset": "shasta_daisy.n.01", "name": "shasta_daisy"}, - {"id": 18574, "synset": "pyrenees_daisy.n.01", "name": "Pyrenees_daisy"}, - {"id": 18575, "synset": "north_island_edelweiss.n.01", "name": "north_island_edelweiss"}, - {"id": 18576, "synset": "blazing_star.n.02", "name": "blazing_star"}, - {"id": 18577, "synset": "dotted_gayfeather.n.01", "name": "dotted_gayfeather"}, - {"id": 18578, "synset": "dense_blazing_star.n.01", "name": "dense_blazing_star"}, - {"id": 18579, "synset": "texas_star.n.02", "name": "Texas_star"}, - {"id": 18580, "synset": "african_daisy.n.01", "name": "African_daisy"}, - {"id": 18581, "synset": "tahoka_daisy.n.01", "name": "tahoka_daisy"}, - {"id": 18582, "synset": "sticky_aster.n.01", "name": "sticky_aster"}, - {"id": 18583, "synset": "mojave_aster.n.01", "name": "Mojave_aster"}, - {"id": 18584, "synset": "tarweed.n.01", "name": "tarweed"}, - {"id": 18585, "synset": "sweet_false_chamomile.n.01", "name": "sweet_false_chamomile"}, - {"id": 18586, "synset": "pineapple_weed.n.01", "name": "pineapple_weed"}, - {"id": 18587, "synset": "climbing_hempweed.n.01", "name": "climbing_hempweed"}, - {"id": 18588, "synset": "mutisia.n.01", "name": "mutisia"}, - {"id": 18589, "synset": "rattlesnake_root.n.02", "name": "rattlesnake_root"}, - {"id": 18590, "synset": "white_lettuce.n.01", "name": "white_lettuce"}, - {"id": 18591, "synset": "daisybush.n.01", "name": "daisybush"}, - {"id": 18592, "synset": "new_zealand_daisybush.n.01", "name": "New_Zealand_daisybush"}, - {"id": 18593, "synset": "cotton_thistle.n.01", "name": "cotton_thistle"}, - {"id": 18594, "synset": "othonna.n.01", "name": "othonna"}, - {"id": 18595, "synset": "cascade_everlasting.n.01", "name": "cascade_everlasting"}, - {"id": 18596, "synset": "butterweed.n.02", "name": "butterweed"}, - {"id": 18597, "synset": "american_feverfew.n.01", "name": "American_feverfew"}, - {"id": 18598, "synset": "cineraria.n.01", "name": "cineraria"}, - {"id": 18599, "synset": "florest's_cineraria.n.01", "name": "florest's_cineraria"}, - {"id": 18600, "synset": "butterbur.n.01", "name": "butterbur"}, - {"id": 18601, "synset": "winter_heliotrope.n.01", "name": "winter_heliotrope"}, - {"id": 18602, "synset": "sweet_coltsfoot.n.01", "name": "sweet_coltsfoot"}, - {"id": 18603, "synset": "oxtongue.n.01", "name": "oxtongue"}, - {"id": 18604, "synset": "hawkweed.n.01", "name": "hawkweed"}, - {"id": 18605, "synset": "mouse-ear_hawkweed.n.01", "name": "mouse-ear_hawkweed"}, - {"id": 18606, "synset": "stevia.n.02", "name": "stevia"}, - {"id": 18607, "synset": "rattlesnake_root.n.01", "name": "rattlesnake_root"}, - {"id": 18608, "synset": "fleabane.n.01", "name": "fleabane"}, - {"id": 18609, "synset": "sheep_plant.n.01", "name": "sheep_plant"}, - {"id": 18610, "synset": "coneflower.n.02", "name": "coneflower"}, - {"id": 18611, "synset": "mexican_hat.n.01", "name": "Mexican_hat"}, - {"id": 18612, "synset": "long-head_coneflower.n.01", "name": "long-head_coneflower"}, - {"id": 18613, "synset": "prairie_coneflower.n.01", "name": "prairie_coneflower"}, - {"id": 18614, "synset": "swan_river_everlasting.n.01", "name": "Swan_River_everlasting"}, - {"id": 18615, "synset": "coneflower.n.01", "name": "coneflower"}, - {"id": 18616, "synset": "black-eyed_susan.n.03", "name": "black-eyed_Susan"}, - {"id": 18617, "synset": "cutleaved_coneflower.n.01", "name": "cutleaved_coneflower"}, - {"id": 18618, "synset": "golden_glow.n.01", "name": "golden_glow"}, - {"id": 18619, "synset": "lavender_cotton.n.01", "name": "lavender_cotton"}, - {"id": 18620, "synset": "creeping_zinnia.n.01", "name": "creeping_zinnia"}, - {"id": 18621, "synset": "golden_thistle.n.01", "name": "golden_thistle"}, - {"id": 18622, "synset": "spanish_oyster_plant.n.01", "name": "Spanish_oyster_plant"}, - {"id": 18623, "synset": "nodding_groundsel.n.01", "name": "nodding_groundsel"}, - {"id": 18624, "synset": "dusty_miller.n.02", "name": "dusty_miller"}, - {"id": 18625, "synset": "butterweed.n.01", "name": "butterweed"}, - {"id": 18626, "synset": "ragwort.n.01", "name": "ragwort"}, - {"id": 18627, "synset": "arrowleaf_groundsel.n.01", "name": "arrowleaf_groundsel"}, - {"id": 18628, "synset": "black_salsify.n.01", "name": "black_salsify"}, - {"id": 18629, "synset": "white-topped_aster.n.01", "name": "white-topped_aster"}, - { - "id": 18630, - "synset": "narrow-leaved_white-topped_aster.n.01", - "name": "narrow-leaved_white-topped_aster", - }, - {"id": 18631, "synset": "silver_sage.n.01", "name": "silver_sage"}, - {"id": 18632, "synset": "sea_wormwood.n.01", "name": "sea_wormwood"}, - {"id": 18633, "synset": "sawwort.n.01", "name": "sawwort"}, - {"id": 18634, "synset": "rosinweed.n.01", "name": "rosinweed"}, - {"id": 18635, "synset": "milk_thistle.n.02", "name": "milk_thistle"}, - {"id": 18636, "synset": "goldenrod.n.01", "name": "goldenrod"}, - {"id": 18637, "synset": "silverrod.n.01", "name": "silverrod"}, - {"id": 18638, "synset": "meadow_goldenrod.n.01", "name": "meadow_goldenrod"}, - {"id": 18639, "synset": "missouri_goldenrod.n.01", "name": "Missouri_goldenrod"}, - {"id": 18640, "synset": "alpine_goldenrod.n.01", "name": "alpine_goldenrod"}, - {"id": 18641, "synset": "grey_goldenrod.n.01", "name": "grey_goldenrod"}, - {"id": 18642, "synset": "blue_mountain_tea.n.01", "name": "Blue_Mountain_tea"}, - {"id": 18643, "synset": "dyer's_weed.n.01", "name": "dyer's_weed"}, - {"id": 18644, "synset": "seaside_goldenrod.n.01", "name": "seaside_goldenrod"}, - {"id": 18645, "synset": "narrow_goldenrod.n.01", "name": "narrow_goldenrod"}, - {"id": 18646, "synset": "boott's_goldenrod.n.01", "name": "Boott's_goldenrod"}, - {"id": 18647, "synset": "elliott's_goldenrod.n.01", "name": "Elliott's_goldenrod"}, - {"id": 18648, "synset": "ohio_goldenrod.n.01", "name": "Ohio_goldenrod"}, - {"id": 18649, "synset": "rough-stemmed_goldenrod.n.01", "name": "rough-stemmed_goldenrod"}, - {"id": 18650, "synset": "showy_goldenrod.n.01", "name": "showy_goldenrod"}, - {"id": 18651, "synset": "tall_goldenrod.n.01", "name": "tall_goldenrod"}, - {"id": 18652, "synset": "zigzag_goldenrod.n.01", "name": "zigzag_goldenrod"}, - {"id": 18653, "synset": "sow_thistle.n.01", "name": "sow_thistle"}, - {"id": 18654, "synset": "milkweed.n.02", "name": "milkweed"}, - {"id": 18655, "synset": "stevia.n.01", "name": "stevia"}, - {"id": 18656, "synset": "stokes'_aster.n.01", "name": "stokes'_aster"}, - {"id": 18657, "synset": "marigold.n.01", "name": "marigold"}, - {"id": 18658, "synset": "african_marigold.n.01", "name": "African_marigold"}, - {"id": 18659, "synset": "french_marigold.n.01", "name": "French_marigold"}, - {"id": 18660, "synset": "painted_daisy.n.01", "name": "painted_daisy"}, - {"id": 18661, "synset": "pyrethrum.n.02", "name": "pyrethrum"}, - {"id": 18662, "synset": "northern_dune_tansy.n.01", "name": "northern_dune_tansy"}, - {"id": 18663, "synset": "feverfew.n.01", "name": "feverfew"}, - {"id": 18664, "synset": "dusty_miller.n.01", "name": "dusty_miller"}, - {"id": 18665, "synset": "tansy.n.01", "name": "tansy"}, - {"id": 18666, "synset": "dandelion.n.01", "name": "dandelion"}, - {"id": 18667, "synset": "common_dandelion.n.01", "name": "common_dandelion"}, - {"id": 18668, "synset": "dandelion_green.n.01", "name": "dandelion_green"}, - {"id": 18669, "synset": "russian_dandelion.n.01", "name": "Russian_dandelion"}, - {"id": 18670, "synset": "stemless_hymenoxys.n.01", "name": "stemless_hymenoxys"}, - {"id": 18671, "synset": "mexican_sunflower.n.01", "name": "Mexican_sunflower"}, - {"id": 18672, "synset": "easter_daisy.n.01", "name": "Easter_daisy"}, - {"id": 18673, "synset": "yellow_salsify.n.01", "name": "yellow_salsify"}, - {"id": 18674, "synset": "salsify.n.02", "name": "salsify"}, - {"id": 18675, "synset": "meadow_salsify.n.01", "name": "meadow_salsify"}, - {"id": 18676, "synset": "scentless_camomile.n.01", "name": "scentless_camomile"}, - {"id": 18677, "synset": "turfing_daisy.n.01", "name": "turfing_daisy"}, - {"id": 18678, "synset": "coltsfoot.n.02", "name": "coltsfoot"}, - {"id": 18679, "synset": "ursinia.n.01", "name": "ursinia"}, - {"id": 18680, "synset": "crownbeard.n.01", "name": "crownbeard"}, - {"id": 18681, "synset": "wingstem.n.01", "name": "wingstem"}, - {"id": 18682, "synset": "cowpen_daisy.n.01", "name": "cowpen_daisy"}, - {"id": 18683, "synset": "gravelweed.n.01", "name": "gravelweed"}, - {"id": 18684, "synset": "virginia_crownbeard.n.01", "name": "Virginia_crownbeard"}, - {"id": 18685, "synset": "ironweed.n.01", "name": "ironweed"}, - {"id": 18686, "synset": "mule's_ears.n.01", "name": "mule's_ears"}, - {"id": 18687, "synset": "white-rayed_mule's_ears.n.01", "name": "white-rayed_mule's_ears"}, - {"id": 18688, "synset": "cocklebur.n.01", "name": "cocklebur"}, - {"id": 18689, "synset": "xeranthemum.n.01", "name": "xeranthemum"}, - {"id": 18690, "synset": "immortelle.n.01", "name": "immortelle"}, - {"id": 18691, "synset": "zinnia.n.01", "name": "zinnia"}, - {"id": 18692, "synset": "white_zinnia.n.01", "name": "white_zinnia"}, - {"id": 18693, "synset": "little_golden_zinnia.n.01", "name": "little_golden_zinnia"}, - {"id": 18694, "synset": "blazing_star.n.01", "name": "blazing_star"}, - {"id": 18695, "synset": "bartonia.n.01", "name": "bartonia"}, - {"id": 18696, "synset": "achene.n.01", "name": "achene"}, - {"id": 18697, "synset": "samara.n.01", "name": "samara"}, - {"id": 18698, "synset": "campanula.n.01", "name": "campanula"}, - {"id": 18699, "synset": "creeping_bellflower.n.01", "name": "creeping_bellflower"}, - {"id": 18700, "synset": "canterbury_bell.n.02", "name": "Canterbury_bell"}, - {"id": 18701, "synset": "tall_bellflower.n.01", "name": "tall_bellflower"}, - {"id": 18702, "synset": "marsh_bellflower.n.01", "name": "marsh_bellflower"}, - {"id": 18703, "synset": "clustered_bellflower.n.01", "name": "clustered_bellflower"}, - {"id": 18704, "synset": "peach_bells.n.01", "name": "peach_bells"}, - {"id": 18705, "synset": "chimney_plant.n.01", "name": "chimney_plant"}, - {"id": 18706, "synset": "rampion.n.01", "name": "rampion"}, - {"id": 18707, "synset": "tussock_bellflower.n.01", "name": "tussock_bellflower"}, - {"id": 18708, "synset": "orchid.n.01", "name": "orchid"}, - {"id": 18709, "synset": "orchis.n.01", "name": "orchis"}, - {"id": 18710, "synset": "male_orchis.n.01", "name": "male_orchis"}, - {"id": 18711, "synset": "butterfly_orchid.n.05", "name": "butterfly_orchid"}, - {"id": 18712, "synset": "showy_orchis.n.01", "name": "showy_orchis"}, - {"id": 18713, "synset": "aerides.n.01", "name": "aerides"}, - {"id": 18714, "synset": "angrecum.n.01", "name": "angrecum"}, - {"id": 18715, "synset": "jewel_orchid.n.01", "name": "jewel_orchid"}, - {"id": 18716, "synset": "puttyroot.n.01", "name": "puttyroot"}, - {"id": 18717, "synset": "arethusa.n.01", "name": "arethusa"}, - {"id": 18718, "synset": "bog_rose.n.01", "name": "bog_rose"}, - {"id": 18719, "synset": "bletia.n.01", "name": "bletia"}, - {"id": 18720, "synset": "bletilla_striata.n.01", "name": "Bletilla_striata"}, - {"id": 18721, "synset": "brassavola.n.01", "name": "brassavola"}, - {"id": 18722, "synset": "spider_orchid.n.03", "name": "spider_orchid"}, - {"id": 18723, "synset": "spider_orchid.n.02", "name": "spider_orchid"}, - {"id": 18724, "synset": "caladenia.n.01", "name": "caladenia"}, - {"id": 18725, "synset": "calanthe.n.01", "name": "calanthe"}, - {"id": 18726, "synset": "grass_pink.n.01", "name": "grass_pink"}, - {"id": 18727, "synset": "calypso.n.01", "name": "calypso"}, - {"id": 18728, "synset": "cattleya.n.01", "name": "cattleya"}, - {"id": 18729, "synset": "helleborine.n.03", "name": "helleborine"}, - {"id": 18730, "synset": "red_helleborine.n.01", "name": "red_helleborine"}, - {"id": 18731, "synset": "spreading_pogonia.n.01", "name": "spreading_pogonia"}, - {"id": 18732, "synset": "rosebud_orchid.n.01", "name": "rosebud_orchid"}, - {"id": 18733, "synset": "satyr_orchid.n.01", "name": "satyr_orchid"}, - {"id": 18734, "synset": "frog_orchid.n.02", "name": "frog_orchid"}, - {"id": 18735, "synset": "coelogyne.n.01", "name": "coelogyne"}, - {"id": 18736, "synset": "coral_root.n.01", "name": "coral_root"}, - {"id": 18737, "synset": "spotted_coral_root.n.01", "name": "spotted_coral_root"}, - {"id": 18738, "synset": "striped_coral_root.n.01", "name": "striped_coral_root"}, - {"id": 18739, "synset": "early_coral_root.n.01", "name": "early_coral_root"}, - {"id": 18740, "synset": "swan_orchid.n.01", "name": "swan_orchid"}, - {"id": 18741, "synset": "cymbid.n.01", "name": "cymbid"}, - {"id": 18742, "synset": "cypripedia.n.01", "name": "cypripedia"}, - {"id": 18743, "synset": "lady's_slipper.n.01", "name": "lady's_slipper"}, - {"id": 18744, "synset": "moccasin_flower.n.01", "name": "moccasin_flower"}, - {"id": 18745, "synset": "common_lady's-slipper.n.01", "name": "common_lady's-slipper"}, - {"id": 18746, "synset": "ram's-head.n.01", "name": "ram's-head"}, - {"id": 18747, "synset": "yellow_lady's_slipper.n.01", "name": "yellow_lady's_slipper"}, - { - "id": 18748, - "synset": "large_yellow_lady's_slipper.n.01", - "name": "large_yellow_lady's_slipper", - }, - {"id": 18749, "synset": "california_lady's_slipper.n.01", "name": "California_lady's_slipper"}, - {"id": 18750, "synset": "clustered_lady's_slipper.n.01", "name": "clustered_lady's_slipper"}, - {"id": 18751, "synset": "mountain_lady's_slipper.n.01", "name": "mountain_lady's_slipper"}, - {"id": 18752, "synset": "marsh_orchid.n.01", "name": "marsh_orchid"}, - {"id": 18753, "synset": "common_spotted_orchid.n.01", "name": "common_spotted_orchid"}, - {"id": 18754, "synset": "dendrobium.n.01", "name": "dendrobium"}, - {"id": 18755, "synset": "disa.n.01", "name": "disa"}, - {"id": 18756, "synset": "phantom_orchid.n.01", "name": "phantom_orchid"}, - {"id": 18757, "synset": "tulip_orchid.n.01", "name": "tulip_orchid"}, - {"id": 18758, "synset": "butterfly_orchid.n.04", "name": "butterfly_orchid"}, - {"id": 18759, "synset": "butterfly_orchid.n.03", "name": "butterfly_orchid"}, - {"id": 18760, "synset": "epidendron.n.01", "name": "epidendron"}, - {"id": 18761, "synset": "helleborine.n.02", "name": "helleborine"}, - {"id": 18762, "synset": "epipactis_helleborine.n.01", "name": "Epipactis_helleborine"}, - {"id": 18763, "synset": "stream_orchid.n.01", "name": "stream_orchid"}, - {"id": 18764, "synset": "tongueflower.n.01", "name": "tongueflower"}, - {"id": 18765, "synset": "rattlesnake_plantain.n.01", "name": "rattlesnake_plantain"}, - {"id": 18766, "synset": "fragrant_orchid.n.01", "name": "fragrant_orchid"}, - { - "id": 18767, - "synset": "short-spurred_fragrant_orchid.n.01", - "name": "short-spurred_fragrant_orchid", - }, - {"id": 18768, "synset": "fringed_orchis.n.01", "name": "fringed_orchis"}, - {"id": 18769, "synset": "frog_orchid.n.01", "name": "frog_orchid"}, - {"id": 18770, "synset": "rein_orchid.n.01", "name": "rein_orchid"}, - {"id": 18771, "synset": "bog_rein_orchid.n.01", "name": "bog_rein_orchid"}, - {"id": 18772, "synset": "white_fringed_orchis.n.01", "name": "white_fringed_orchis"}, - {"id": 18773, "synset": "elegant_habenaria.n.01", "name": "elegant_Habenaria"}, - {"id": 18774, "synset": "purple-fringed_orchid.n.02", "name": "purple-fringed_orchid"}, - {"id": 18775, "synset": "coastal_rein_orchid.n.01", "name": "coastal_rein_orchid"}, - {"id": 18776, "synset": "hooker's_orchid.n.01", "name": "Hooker's_orchid"}, - {"id": 18777, "synset": "ragged_orchid.n.01", "name": "ragged_orchid"}, - {"id": 18778, "synset": "prairie_orchid.n.01", "name": "prairie_orchid"}, - {"id": 18779, "synset": "snowy_orchid.n.01", "name": "snowy_orchid"}, - {"id": 18780, "synset": "round-leaved_rein_orchid.n.01", "name": "round-leaved_rein_orchid"}, - {"id": 18781, "synset": "purple_fringeless_orchid.n.01", "name": "purple_fringeless_orchid"}, - {"id": 18782, "synset": "purple-fringed_orchid.n.01", "name": "purple-fringed_orchid"}, - {"id": 18783, "synset": "alaska_rein_orchid.n.01", "name": "Alaska_rein_orchid"}, - {"id": 18784, "synset": "crested_coral_root.n.01", "name": "crested_coral_root"}, - {"id": 18785, "synset": "texas_purple_spike.n.01", "name": "Texas_purple_spike"}, - {"id": 18786, "synset": "lizard_orchid.n.01", "name": "lizard_orchid"}, - {"id": 18787, "synset": "laelia.n.01", "name": "laelia"}, - {"id": 18788, "synset": "liparis.n.01", "name": "liparis"}, - {"id": 18789, "synset": "twayblade.n.02", "name": "twayblade"}, - {"id": 18790, "synset": "fen_orchid.n.01", "name": "fen_orchid"}, - {"id": 18791, "synset": "broad-leaved_twayblade.n.01", "name": "broad-leaved_twayblade"}, - {"id": 18792, "synset": "lesser_twayblade.n.01", "name": "lesser_twayblade"}, - {"id": 18793, "synset": "twayblade.n.01", "name": "twayblade"}, - {"id": 18794, "synset": "green_adder's_mouth.n.01", "name": "green_adder's_mouth"}, - {"id": 18795, "synset": "masdevallia.n.01", "name": "masdevallia"}, - {"id": 18796, "synset": "maxillaria.n.01", "name": "maxillaria"}, - {"id": 18797, "synset": "pansy_orchid.n.01", "name": "pansy_orchid"}, - {"id": 18798, "synset": "odontoglossum.n.01", "name": "odontoglossum"}, - {"id": 18799, "synset": "oncidium.n.01", "name": "oncidium"}, - {"id": 18800, "synset": "bee_orchid.n.01", "name": "bee_orchid"}, - {"id": 18801, "synset": "fly_orchid.n.02", "name": "fly_orchid"}, - {"id": 18802, "synset": "spider_orchid.n.01", "name": "spider_orchid"}, - {"id": 18803, "synset": "early_spider_orchid.n.01", "name": "early_spider_orchid"}, - {"id": 18804, "synset": "venus'_slipper.n.01", "name": "Venus'_slipper"}, - {"id": 18805, "synset": "phaius.n.01", "name": "phaius"}, - {"id": 18806, "synset": "moth_orchid.n.01", "name": "moth_orchid"}, - {"id": 18807, "synset": "butterfly_plant.n.01", "name": "butterfly_plant"}, - {"id": 18808, "synset": "rattlesnake_orchid.n.01", "name": "rattlesnake_orchid"}, - {"id": 18809, "synset": "lesser_butterfly_orchid.n.01", "name": "lesser_butterfly_orchid"}, - {"id": 18810, "synset": "greater_butterfly_orchid.n.01", "name": "greater_butterfly_orchid"}, - { - "id": 18811, - "synset": "prairie_white-fringed_orchid.n.01", - "name": "prairie_white-fringed_orchid", - }, - {"id": 18812, "synset": "tangle_orchid.n.01", "name": "tangle_orchid"}, - {"id": 18813, "synset": "indian_crocus.n.01", "name": "Indian_crocus"}, - {"id": 18814, "synset": "pleurothallis.n.01", "name": "pleurothallis"}, - {"id": 18815, "synset": "pogonia.n.01", "name": "pogonia"}, - {"id": 18816, "synset": "butterfly_orchid.n.01", "name": "butterfly_orchid"}, - {"id": 18817, "synset": "psychopsis_krameriana.n.01", "name": "Psychopsis_krameriana"}, - {"id": 18818, "synset": "psychopsis_papilio.n.01", "name": "Psychopsis_papilio"}, - {"id": 18819, "synset": "helmet_orchid.n.01", "name": "helmet_orchid"}, - {"id": 18820, "synset": "foxtail_orchid.n.01", "name": "foxtail_orchid"}, - {"id": 18821, "synset": "orange-blossom_orchid.n.01", "name": "orange-blossom_orchid"}, - {"id": 18822, "synset": "sobralia.n.01", "name": "sobralia"}, - {"id": 18823, "synset": "ladies'_tresses.n.01", "name": "ladies'_tresses"}, - {"id": 18824, "synset": "screw_augur.n.01", "name": "screw_augur"}, - {"id": 18825, "synset": "hooded_ladies'_tresses.n.01", "name": "hooded_ladies'_tresses"}, - {"id": 18826, "synset": "western_ladies'_tresses.n.01", "name": "western_ladies'_tresses"}, - {"id": 18827, "synset": "european_ladies'_tresses.n.01", "name": "European_ladies'_tresses"}, - {"id": 18828, "synset": "stanhopea.n.01", "name": "stanhopea"}, - {"id": 18829, "synset": "stelis.n.01", "name": "stelis"}, - {"id": 18830, "synset": "fly_orchid.n.01", "name": "fly_orchid"}, - {"id": 18831, "synset": "vanda.n.01", "name": "vanda"}, - {"id": 18832, "synset": "blue_orchid.n.01", "name": "blue_orchid"}, - {"id": 18833, "synset": "vanilla.n.01", "name": "vanilla"}, - {"id": 18834, "synset": "vanilla_orchid.n.01", "name": "vanilla_orchid"}, - {"id": 18835, "synset": "yam.n.02", "name": "yam"}, - {"id": 18836, "synset": "yam.n.01", "name": "yam"}, - {"id": 18837, "synset": "white_yam.n.01", "name": "white_yam"}, - {"id": 18838, "synset": "cinnamon_vine.n.01", "name": "cinnamon_vine"}, - {"id": 18839, "synset": "elephant's-foot.n.01", "name": "elephant's-foot"}, - {"id": 18840, "synset": "wild_yam.n.01", "name": "wild_yam"}, - {"id": 18841, "synset": "cush-cush.n.01", "name": "cush-cush"}, - {"id": 18842, "synset": "black_bryony.n.01", "name": "black_bryony"}, - {"id": 18843, "synset": "primrose.n.01", "name": "primrose"}, - {"id": 18844, "synset": "english_primrose.n.01", "name": "English_primrose"}, - {"id": 18845, "synset": "cowslip.n.01", "name": "cowslip"}, - {"id": 18846, "synset": "oxlip.n.01", "name": "oxlip"}, - {"id": 18847, "synset": "chinese_primrose.n.01", "name": "Chinese_primrose"}, - {"id": 18848, "synset": "polyanthus.n.01", "name": "polyanthus"}, - {"id": 18849, "synset": "pimpernel.n.02", "name": "pimpernel"}, - {"id": 18850, "synset": "scarlet_pimpernel.n.01", "name": "scarlet_pimpernel"}, - {"id": 18851, "synset": "bog_pimpernel.n.01", "name": "bog_pimpernel"}, - {"id": 18852, "synset": "chaffweed.n.01", "name": "chaffweed"}, - {"id": 18853, "synset": "cyclamen.n.01", "name": "cyclamen"}, - {"id": 18854, "synset": "sowbread.n.01", "name": "sowbread"}, - {"id": 18855, "synset": "sea_milkwort.n.01", "name": "sea_milkwort"}, - {"id": 18856, "synset": "featherfoil.n.01", "name": "featherfoil"}, - {"id": 18857, "synset": "water_gillyflower.n.01", "name": "water_gillyflower"}, - {"id": 18858, "synset": "water_violet.n.01", "name": "water_violet"}, - {"id": 18859, "synset": "loosestrife.n.02", "name": "loosestrife"}, - {"id": 18860, "synset": "gooseneck_loosestrife.n.01", "name": "gooseneck_loosestrife"}, - {"id": 18861, "synset": "yellow_pimpernel.n.01", "name": "yellow_pimpernel"}, - {"id": 18862, "synset": "fringed_loosestrife.n.01", "name": "fringed_loosestrife"}, - {"id": 18863, "synset": "moneywort.n.01", "name": "moneywort"}, - {"id": 18864, "synset": "swamp_candles.n.01", "name": "swamp_candles"}, - {"id": 18865, "synset": "whorled_loosestrife.n.01", "name": "whorled_loosestrife"}, - {"id": 18866, "synset": "water_pimpernel.n.01", "name": "water_pimpernel"}, - {"id": 18867, "synset": "brookweed.n.02", "name": "brookweed"}, - {"id": 18868, "synset": "brookweed.n.01", "name": "brookweed"}, - {"id": 18869, "synset": "coralberry.n.02", "name": "coralberry"}, - {"id": 18870, "synset": "marlberry.n.01", "name": "marlberry"}, - {"id": 18871, "synset": "plumbago.n.02", "name": "plumbago"}, - {"id": 18872, "synset": "leadwort.n.01", "name": "leadwort"}, - {"id": 18873, "synset": "thrift.n.01", "name": "thrift"}, - {"id": 18874, "synset": "sea_lavender.n.01", "name": "sea_lavender"}, - {"id": 18875, "synset": "barbasco.n.01", "name": "barbasco"}, - {"id": 18876, "synset": "gramineous_plant.n.01", "name": "gramineous_plant"}, - {"id": 18877, "synset": "grass.n.01", "name": "grass"}, - {"id": 18878, "synset": "midgrass.n.01", "name": "midgrass"}, - {"id": 18879, "synset": "shortgrass.n.01", "name": "shortgrass"}, - {"id": 18880, "synset": "sword_grass.n.01", "name": "sword_grass"}, - {"id": 18881, "synset": "tallgrass.n.01", "name": "tallgrass"}, - {"id": 18882, "synset": "herbage.n.01", "name": "herbage"}, - {"id": 18883, "synset": "goat_grass.n.01", "name": "goat_grass"}, - {"id": 18884, "synset": "wheatgrass.n.01", "name": "wheatgrass"}, - {"id": 18885, "synset": "crested_wheatgrass.n.01", "name": "crested_wheatgrass"}, - {"id": 18886, "synset": "bearded_wheatgrass.n.01", "name": "bearded_wheatgrass"}, - {"id": 18887, "synset": "western_wheatgrass.n.01", "name": "western_wheatgrass"}, - {"id": 18888, "synset": "intermediate_wheatgrass.n.01", "name": "intermediate_wheatgrass"}, - {"id": 18889, "synset": "slender_wheatgrass.n.01", "name": "slender_wheatgrass"}, - {"id": 18890, "synset": "velvet_bent.n.01", "name": "velvet_bent"}, - {"id": 18891, "synset": "cloud_grass.n.01", "name": "cloud_grass"}, - {"id": 18892, "synset": "meadow_foxtail.n.01", "name": "meadow_foxtail"}, - {"id": 18893, "synset": "foxtail.n.01", "name": "foxtail"}, - {"id": 18894, "synset": "broom_grass.n.01", "name": "broom_grass"}, - {"id": 18895, "synset": "broom_sedge.n.01", "name": "broom_sedge"}, - {"id": 18896, "synset": "tall_oat_grass.n.01", "name": "tall_oat_grass"}, - {"id": 18897, "synset": "toetoe.n.02", "name": "toetoe"}, - {"id": 18898, "synset": "oat.n.01", "name": "oat"}, - {"id": 18899, "synset": "cereal_oat.n.01", "name": "cereal_oat"}, - {"id": 18900, "synset": "wild_oat.n.01", "name": "wild_oat"}, - {"id": 18901, "synset": "slender_wild_oat.n.01", "name": "slender_wild_oat"}, - {"id": 18902, "synset": "wild_red_oat.n.01", "name": "wild_red_oat"}, - {"id": 18903, "synset": "brome.n.01", "name": "brome"}, - {"id": 18904, "synset": "chess.n.01", "name": "chess"}, - {"id": 18905, "synset": "field_brome.n.01", "name": "field_brome"}, - {"id": 18906, "synset": "grama.n.01", "name": "grama"}, - {"id": 18907, "synset": "black_grama.n.01", "name": "black_grama"}, - {"id": 18908, "synset": "buffalo_grass.n.02", "name": "buffalo_grass"}, - {"id": 18909, "synset": "reed_grass.n.01", "name": "reed_grass"}, - {"id": 18910, "synset": "feather_reed_grass.n.01", "name": "feather_reed_grass"}, - {"id": 18911, "synset": "australian_reed_grass.n.01", "name": "Australian_reed_grass"}, - {"id": 18912, "synset": "burgrass.n.01", "name": "burgrass"}, - {"id": 18913, "synset": "buffel_grass.n.01", "name": "buffel_grass"}, - {"id": 18914, "synset": "rhodes_grass.n.01", "name": "Rhodes_grass"}, - {"id": 18915, "synset": "pampas_grass.n.01", "name": "pampas_grass"}, - {"id": 18916, "synset": "giant_star_grass.n.01", "name": "giant_star_grass"}, - {"id": 18917, "synset": "orchard_grass.n.01", "name": "orchard_grass"}, - {"id": 18918, "synset": "egyptian_grass.n.01", "name": "Egyptian_grass"}, - {"id": 18919, "synset": "crabgrass.n.01", "name": "crabgrass"}, - {"id": 18920, "synset": "smooth_crabgrass.n.01", "name": "smooth_crabgrass"}, - {"id": 18921, "synset": "large_crabgrass.n.01", "name": "large_crabgrass"}, - {"id": 18922, "synset": "barnyard_grass.n.01", "name": "barnyard_grass"}, - {"id": 18923, "synset": "japanese_millet.n.01", "name": "Japanese_millet"}, - {"id": 18924, "synset": "yardgrass.n.01", "name": "yardgrass"}, - {"id": 18925, "synset": "finger_millet.n.01", "name": "finger_millet"}, - {"id": 18926, "synset": "lyme_grass.n.01", "name": "lyme_grass"}, - {"id": 18927, "synset": "wild_rye.n.01", "name": "wild_rye"}, - {"id": 18928, "synset": "giant_ryegrass.n.01", "name": "giant_ryegrass"}, - {"id": 18929, "synset": "sea_lyme_grass.n.01", "name": "sea_lyme_grass"}, - {"id": 18930, "synset": "canada_wild_rye.n.01", "name": "Canada_wild_rye"}, - {"id": 18931, "synset": "teff.n.01", "name": "teff"}, - {"id": 18932, "synset": "weeping_love_grass.n.01", "name": "weeping_love_grass"}, - {"id": 18933, "synset": "plume_grass.n.01", "name": "plume_grass"}, - {"id": 18934, "synset": "ravenna_grass.n.01", "name": "Ravenna_grass"}, - {"id": 18935, "synset": "fescue.n.01", "name": "fescue"}, - {"id": 18936, "synset": "reed_meadow_grass.n.01", "name": "reed_meadow_grass"}, - {"id": 18937, "synset": "velvet_grass.n.01", "name": "velvet_grass"}, - {"id": 18938, "synset": "creeping_soft_grass.n.01", "name": "creeping_soft_grass"}, - {"id": 18939, "synset": "barleycorn.n.01", "name": "barleycorn"}, - {"id": 18940, "synset": "barley_grass.n.01", "name": "barley_grass"}, - {"id": 18941, "synset": "little_barley.n.01", "name": "little_barley"}, - {"id": 18942, "synset": "rye_grass.n.01", "name": "rye_grass"}, - {"id": 18943, "synset": "perennial_ryegrass.n.01", "name": "perennial_ryegrass"}, - {"id": 18944, "synset": "italian_ryegrass.n.01", "name": "Italian_ryegrass"}, - {"id": 18945, "synset": "darnel.n.01", "name": "darnel"}, - {"id": 18946, "synset": "nimblewill.n.01", "name": "nimblewill"}, - {"id": 18947, "synset": "cultivated_rice.n.01", "name": "cultivated_rice"}, - {"id": 18948, "synset": "ricegrass.n.01", "name": "ricegrass"}, - {"id": 18949, "synset": "smilo.n.01", "name": "smilo"}, - {"id": 18950, "synset": "switch_grass.n.01", "name": "switch_grass"}, - {"id": 18951, "synset": "broomcorn_millet.n.01", "name": "broomcorn_millet"}, - {"id": 18952, "synset": "goose_grass.n.03", "name": "goose_grass"}, - {"id": 18953, "synset": "dallisgrass.n.01", "name": "dallisgrass"}, - {"id": 18954, "synset": "bahia_grass.n.01", "name": "Bahia_grass"}, - {"id": 18955, "synset": "knotgrass.n.01", "name": "knotgrass"}, - {"id": 18956, "synset": "fountain_grass.n.01", "name": "fountain_grass"}, - {"id": 18957, "synset": "reed_canary_grass.n.01", "name": "reed_canary_grass"}, - {"id": 18958, "synset": "canary_grass.n.01", "name": "canary_grass"}, - {"id": 18959, "synset": "timothy.n.01", "name": "timothy"}, - {"id": 18960, "synset": "bluegrass.n.01", "name": "bluegrass"}, - {"id": 18961, "synset": "meadowgrass.n.01", "name": "meadowgrass"}, - {"id": 18962, "synset": "wood_meadowgrass.n.01", "name": "wood_meadowgrass"}, - {"id": 18963, "synset": "noble_cane.n.01", "name": "noble_cane"}, - {"id": 18964, "synset": "munj.n.01", "name": "munj"}, - {"id": 18965, "synset": "broom_beard_grass.n.01", "name": "broom_beard_grass"}, - {"id": 18966, "synset": "bluestem.n.01", "name": "bluestem"}, - {"id": 18967, "synset": "rye.n.02", "name": "rye"}, - {"id": 18968, "synset": "bristlegrass.n.01", "name": "bristlegrass"}, - {"id": 18969, "synset": "giant_foxtail.n.01", "name": "giant_foxtail"}, - {"id": 18970, "synset": "yellow_bristlegrass.n.01", "name": "yellow_bristlegrass"}, - {"id": 18971, "synset": "green_bristlegrass.n.01", "name": "green_bristlegrass"}, - {"id": 18972, "synset": "siberian_millet.n.01", "name": "Siberian_millet"}, - {"id": 18973, "synset": "german_millet.n.01", "name": "German_millet"}, - {"id": 18974, "synset": "millet.n.01", "name": "millet"}, - {"id": 18975, "synset": "rattan.n.02", "name": "rattan"}, - {"id": 18976, "synset": "malacca.n.01", "name": "malacca"}, - {"id": 18977, "synset": "reed.n.01", "name": "reed"}, - {"id": 18978, "synset": "sorghum.n.01", "name": "sorghum"}, - {"id": 18979, "synset": "grain_sorghum.n.01", "name": "grain_sorghum"}, - {"id": 18980, "synset": "durra.n.01", "name": "durra"}, - {"id": 18981, "synset": "feterita.n.01", "name": "feterita"}, - {"id": 18982, "synset": "hegari.n.01", "name": "hegari"}, - {"id": 18983, "synset": "kaoliang.n.01", "name": "kaoliang"}, - {"id": 18984, "synset": "milo.n.01", "name": "milo"}, - {"id": 18985, "synset": "shallu.n.01", "name": "shallu"}, - {"id": 18986, "synset": "broomcorn.n.01", "name": "broomcorn"}, - {"id": 18987, "synset": "cordgrass.n.01", "name": "cordgrass"}, - {"id": 18988, "synset": "salt_reed_grass.n.01", "name": "salt_reed_grass"}, - {"id": 18989, "synset": "prairie_cordgrass.n.01", "name": "prairie_cordgrass"}, - {"id": 18990, "synset": "smut_grass.n.01", "name": "smut_grass"}, - {"id": 18991, "synset": "sand_dropseed.n.01", "name": "sand_dropseed"}, - {"id": 18992, "synset": "rush_grass.n.01", "name": "rush_grass"}, - {"id": 18993, "synset": "st._augustine_grass.n.01", "name": "St._Augustine_grass"}, - {"id": 18994, "synset": "grain.n.08", "name": "grain"}, - {"id": 18995, "synset": "cereal.n.01", "name": "cereal"}, - {"id": 18996, "synset": "wheat.n.01", "name": "wheat"}, - {"id": 18997, "synset": "wheat_berry.n.01", "name": "wheat_berry"}, - {"id": 18998, "synset": "durum.n.01", "name": "durum"}, - {"id": 18999, "synset": "spelt.n.01", "name": "spelt"}, - {"id": 19000, "synset": "emmer.n.01", "name": "emmer"}, - {"id": 19001, "synset": "wild_wheat.n.01", "name": "wild_wheat"}, - {"id": 19002, "synset": "corn.n.01", "name": "corn"}, - {"id": 19003, "synset": "mealie.n.01", "name": "mealie"}, - {"id": 19004, "synset": "corn.n.02", "name": "corn"}, - {"id": 19005, "synset": "dent_corn.n.01", "name": "dent_corn"}, - {"id": 19006, "synset": "flint_corn.n.01", "name": "flint_corn"}, - {"id": 19007, "synset": "popcorn.n.01", "name": "popcorn"}, - {"id": 19008, "synset": "zoysia.n.01", "name": "zoysia"}, - {"id": 19009, "synset": "manila_grass.n.01", "name": "Manila_grass"}, - {"id": 19010, "synset": "korean_lawn_grass.n.01", "name": "Korean_lawn_grass"}, - {"id": 19011, "synset": "common_bamboo.n.01", "name": "common_bamboo"}, - {"id": 19012, "synset": "giant_bamboo.n.01", "name": "giant_bamboo"}, - {"id": 19013, "synset": "umbrella_plant.n.03", "name": "umbrella_plant"}, - {"id": 19014, "synset": "chufa.n.01", "name": "chufa"}, - {"id": 19015, "synset": "galingale.n.01", "name": "galingale"}, - {"id": 19016, "synset": "nutgrass.n.01", "name": "nutgrass"}, - {"id": 19017, "synset": "sand_sedge.n.01", "name": "sand_sedge"}, - {"id": 19018, "synset": "cypress_sedge.n.01", "name": "cypress_sedge"}, - {"id": 19019, "synset": "cotton_grass.n.01", "name": "cotton_grass"}, - {"id": 19020, "synset": "common_cotton_grass.n.01", "name": "common_cotton_grass"}, - {"id": 19021, "synset": "hardstem_bulrush.n.01", "name": "hardstem_bulrush"}, - {"id": 19022, "synset": "wool_grass.n.01", "name": "wool_grass"}, - {"id": 19023, "synset": "spike_rush.n.01", "name": "spike_rush"}, - {"id": 19024, "synset": "water_chestnut.n.02", "name": "water_chestnut"}, - {"id": 19025, "synset": "needle_spike_rush.n.01", "name": "needle_spike_rush"}, - {"id": 19026, "synset": "creeping_spike_rush.n.01", "name": "creeping_spike_rush"}, - {"id": 19027, "synset": "pandanus.n.02", "name": "pandanus"}, - {"id": 19028, "synset": "textile_screw_pine.n.01", "name": "textile_screw_pine"}, - {"id": 19029, "synset": "cattail.n.01", "name": "cattail"}, - {"id": 19030, "synset": "cat's-tail.n.01", "name": "cat's-tail"}, - {"id": 19031, "synset": "bur_reed.n.01", "name": "bur_reed"}, - {"id": 19032, "synset": "grain.n.07", "name": "grain"}, - {"id": 19033, "synset": "kernel.n.02", "name": "kernel"}, - {"id": 19034, "synset": "rye.n.01", "name": "rye"}, - {"id": 19035, "synset": "gourd.n.03", "name": "gourd"}, - {"id": 19036, "synset": "pumpkin.n.01", "name": "pumpkin"}, - {"id": 19037, "synset": "squash.n.01", "name": "squash"}, - {"id": 19038, "synset": "summer_squash.n.01", "name": "summer_squash"}, - {"id": 19039, "synset": "yellow_squash.n.01", "name": "yellow_squash"}, - {"id": 19040, "synset": "marrow.n.02", "name": "marrow"}, - {"id": 19041, "synset": "zucchini.n.01", "name": "zucchini"}, - {"id": 19042, "synset": "cocozelle.n.01", "name": "cocozelle"}, - {"id": 19043, "synset": "cymling.n.01", "name": "cymling"}, - {"id": 19044, "synset": "spaghetti_squash.n.01", "name": "spaghetti_squash"}, - {"id": 19045, "synset": "winter_squash.n.01", "name": "winter_squash"}, - {"id": 19046, "synset": "acorn_squash.n.01", "name": "acorn_squash"}, - {"id": 19047, "synset": "hubbard_squash.n.01", "name": "hubbard_squash"}, - {"id": 19048, "synset": "turban_squash.n.01", "name": "turban_squash"}, - {"id": 19049, "synset": "buttercup_squash.n.01", "name": "buttercup_squash"}, - {"id": 19050, "synset": "butternut_squash.n.01", "name": "butternut_squash"}, - {"id": 19051, "synset": "winter_crookneck.n.01", "name": "winter_crookneck"}, - {"id": 19052, "synset": "cushaw.n.01", "name": "cushaw"}, - {"id": 19053, "synset": "prairie_gourd.n.02", "name": "prairie_gourd"}, - {"id": 19054, "synset": "prairie_gourd.n.01", "name": "prairie_gourd"}, - {"id": 19055, "synset": "bryony.n.01", "name": "bryony"}, - {"id": 19056, "synset": "white_bryony.n.01", "name": "white_bryony"}, - {"id": 19057, "synset": "sweet_melon.n.01", "name": "sweet_melon"}, - {"id": 19058, "synset": "cantaloupe.n.01", "name": "cantaloupe"}, - {"id": 19059, "synset": "winter_melon.n.01", "name": "winter_melon"}, - {"id": 19060, "synset": "net_melon.n.01", "name": "net_melon"}, - {"id": 19061, "synset": "cucumber.n.01", "name": "cucumber"}, - {"id": 19062, "synset": "squirting_cucumber.n.01", "name": "squirting_cucumber"}, - {"id": 19063, "synset": "bottle_gourd.n.01", "name": "bottle_gourd"}, - {"id": 19064, "synset": "luffa.n.02", "name": "luffa"}, - {"id": 19065, "synset": "loofah.n.02", "name": "loofah"}, - {"id": 19066, "synset": "angled_loofah.n.01", "name": "angled_loofah"}, - {"id": 19067, "synset": "loofa.n.01", "name": "loofa"}, - {"id": 19068, "synset": "balsam_apple.n.01", "name": "balsam_apple"}, - {"id": 19069, "synset": "balsam_pear.n.01", "name": "balsam_pear"}, - {"id": 19070, "synset": "lobelia.n.01", "name": "lobelia"}, - {"id": 19071, "synset": "water_lobelia.n.01", "name": "water_lobelia"}, - {"id": 19072, "synset": "mallow.n.01", "name": "mallow"}, - {"id": 19073, "synset": "musk_mallow.n.02", "name": "musk_mallow"}, - {"id": 19074, "synset": "common_mallow.n.01", "name": "common_mallow"}, - {"id": 19075, "synset": "okra.n.02", "name": "okra"}, - {"id": 19076, "synset": "okra.n.01", "name": "okra"}, - {"id": 19077, "synset": "abelmosk.n.01", "name": "abelmosk"}, - {"id": 19078, "synset": "flowering_maple.n.01", "name": "flowering_maple"}, - {"id": 19079, "synset": "velvetleaf.n.02", "name": "velvetleaf"}, - {"id": 19080, "synset": "hollyhock.n.02", "name": "hollyhock"}, - {"id": 19081, "synset": "rose_mallow.n.02", "name": "rose_mallow"}, - {"id": 19082, "synset": "althea.n.01", "name": "althea"}, - {"id": 19083, "synset": "marsh_mallow.n.01", "name": "marsh_mallow"}, - {"id": 19084, "synset": "poppy_mallow.n.01", "name": "poppy_mallow"}, - {"id": 19085, "synset": "fringed_poppy_mallow.n.01", "name": "fringed_poppy_mallow"}, - {"id": 19086, "synset": "purple_poppy_mallow.n.01", "name": "purple_poppy_mallow"}, - {"id": 19087, "synset": "clustered_poppy_mallow.n.01", "name": "clustered_poppy_mallow"}, - {"id": 19088, "synset": "sea_island_cotton.n.01", "name": "sea_island_cotton"}, - {"id": 19089, "synset": "levant_cotton.n.01", "name": "Levant_cotton"}, - {"id": 19090, "synset": "upland_cotton.n.01", "name": "upland_cotton"}, - {"id": 19091, "synset": "peruvian_cotton.n.01", "name": "Peruvian_cotton"}, - {"id": 19092, "synset": "wild_cotton.n.01", "name": "wild_cotton"}, - {"id": 19093, "synset": "kenaf.n.02", "name": "kenaf"}, - {"id": 19094, "synset": "sorrel_tree.n.02", "name": "sorrel_tree"}, - {"id": 19095, "synset": "rose_mallow.n.01", "name": "rose_mallow"}, - {"id": 19096, "synset": "cotton_rose.n.01", "name": "cotton_rose"}, - {"id": 19097, "synset": "roselle.n.01", "name": "roselle"}, - {"id": 19098, "synset": "mahoe.n.01", "name": "mahoe"}, - {"id": 19099, "synset": "flower-of-an-hour.n.01", "name": "flower-of-an-hour"}, - {"id": 19100, "synset": "lacebark.n.01", "name": "lacebark"}, - {"id": 19101, "synset": "wild_hollyhock.n.02", "name": "wild_hollyhock"}, - {"id": 19102, "synset": "mountain_hollyhock.n.01", "name": "mountain_hollyhock"}, - {"id": 19103, "synset": "seashore_mallow.n.01", "name": "seashore_mallow"}, - {"id": 19104, "synset": "salt_marsh_mallow.n.01", "name": "salt_marsh_mallow"}, - {"id": 19105, "synset": "chaparral_mallow.n.01", "name": "chaparral_mallow"}, - {"id": 19106, "synset": "malope.n.01", "name": "malope"}, - {"id": 19107, "synset": "false_mallow.n.02", "name": "false_mallow"}, - {"id": 19108, "synset": "waxmallow.n.01", "name": "waxmallow"}, - {"id": 19109, "synset": "glade_mallow.n.01", "name": "glade_mallow"}, - {"id": 19110, "synset": "pavonia.n.01", "name": "pavonia"}, - {"id": 19111, "synset": "ribbon_tree.n.01", "name": "ribbon_tree"}, - {"id": 19112, "synset": "bush_hibiscus.n.01", "name": "bush_hibiscus"}, - {"id": 19113, "synset": "virginia_mallow.n.01", "name": "Virginia_mallow"}, - {"id": 19114, "synset": "queensland_hemp.n.01", "name": "Queensland_hemp"}, - {"id": 19115, "synset": "indian_mallow.n.01", "name": "Indian_mallow"}, - {"id": 19116, "synset": "checkerbloom.n.01", "name": "checkerbloom"}, - {"id": 19117, "synset": "globe_mallow.n.01", "name": "globe_mallow"}, - {"id": 19118, "synset": "prairie_mallow.n.01", "name": "prairie_mallow"}, - {"id": 19119, "synset": "tulipwood_tree.n.01", "name": "tulipwood_tree"}, - {"id": 19120, "synset": "portia_tree.n.01", "name": "portia_tree"}, - {"id": 19121, "synset": "red_silk-cotton_tree.n.01", "name": "red_silk-cotton_tree"}, - {"id": 19122, "synset": "cream-of-tartar_tree.n.01", "name": "cream-of-tartar_tree"}, - {"id": 19123, "synset": "baobab.n.01", "name": "baobab"}, - {"id": 19124, "synset": "kapok.n.02", "name": "kapok"}, - {"id": 19125, "synset": "durian.n.01", "name": "durian"}, - {"id": 19126, "synset": "montezuma.n.01", "name": "Montezuma"}, - {"id": 19127, "synset": "shaving-brush_tree.n.01", "name": "shaving-brush_tree"}, - {"id": 19128, "synset": "quandong.n.03", "name": "quandong"}, - {"id": 19129, "synset": "quandong.n.02", "name": "quandong"}, - {"id": 19130, "synset": "makomako.n.01", "name": "makomako"}, - {"id": 19131, "synset": "jamaican_cherry.n.01", "name": "Jamaican_cherry"}, - {"id": 19132, "synset": "breakax.n.01", "name": "breakax"}, - {"id": 19133, "synset": "sterculia.n.01", "name": "sterculia"}, - {"id": 19134, "synset": "panama_tree.n.01", "name": "Panama_tree"}, - {"id": 19135, "synset": "kalumpang.n.01", "name": "kalumpang"}, - {"id": 19136, "synset": "bottle-tree.n.01", "name": "bottle-tree"}, - {"id": 19137, "synset": "flame_tree.n.04", "name": "flame_tree"}, - {"id": 19138, "synset": "flame_tree.n.03", "name": "flame_tree"}, - {"id": 19139, "synset": "kurrajong.n.01", "name": "kurrajong"}, - {"id": 19140, "synset": "queensland_bottletree.n.01", "name": "Queensland_bottletree"}, - {"id": 19141, "synset": "kola.n.01", "name": "kola"}, - {"id": 19142, "synset": "kola_nut.n.01", "name": "kola_nut"}, - {"id": 19143, "synset": "chinese_parasol_tree.n.01", "name": "Chinese_parasol_tree"}, - {"id": 19144, "synset": "flannelbush.n.01", "name": "flannelbush"}, - {"id": 19145, "synset": "screw_tree.n.01", "name": "screw_tree"}, - {"id": 19146, "synset": "nut-leaved_screw_tree.n.01", "name": "nut-leaved_screw_tree"}, - {"id": 19147, "synset": "red_beech.n.02", "name": "red_beech"}, - {"id": 19148, "synset": "looking_glass_tree.n.01", "name": "looking_glass_tree"}, - {"id": 19149, "synset": "looking-glass_plant.n.01", "name": "looking-glass_plant"}, - {"id": 19150, "synset": "honey_bell.n.01", "name": "honey_bell"}, - {"id": 19151, "synset": "mayeng.n.01", "name": "mayeng"}, - {"id": 19152, "synset": "silver_tree.n.02", "name": "silver_tree"}, - {"id": 19153, "synset": "cacao.n.01", "name": "cacao"}, - {"id": 19154, "synset": "obeche.n.02", "name": "obeche"}, - {"id": 19155, "synset": "linden.n.02", "name": "linden"}, - {"id": 19156, "synset": "american_basswood.n.01", "name": "American_basswood"}, - {"id": 19157, "synset": "small-leaved_linden.n.01", "name": "small-leaved_linden"}, - {"id": 19158, "synset": "white_basswood.n.01", "name": "white_basswood"}, - {"id": 19159, "synset": "japanese_linden.n.01", "name": "Japanese_linden"}, - {"id": 19160, "synset": "silver_lime.n.01", "name": "silver_lime"}, - {"id": 19161, "synset": "corchorus.n.01", "name": "corchorus"}, - {"id": 19162, "synset": "african_hemp.n.02", "name": "African_hemp"}, - {"id": 19163, "synset": "herb.n.01", "name": "herb"}, - {"id": 19164, "synset": "protea.n.01", "name": "protea"}, - {"id": 19165, "synset": "honeypot.n.01", "name": "honeypot"}, - {"id": 19166, "synset": "honeyflower.n.02", "name": "honeyflower"}, - {"id": 19167, "synset": "banksia.n.01", "name": "banksia"}, - {"id": 19168, "synset": "honeysuckle.n.02", "name": "honeysuckle"}, - {"id": 19169, "synset": "smoke_bush.n.02", "name": "smoke_bush"}, - {"id": 19170, "synset": "chilean_firebush.n.01", "name": "Chilean_firebush"}, - {"id": 19171, "synset": "chilean_nut.n.01", "name": "Chilean_nut"}, - {"id": 19172, "synset": "grevillea.n.01", "name": "grevillea"}, - {"id": 19173, "synset": "red-flowered_silky_oak.n.01", "name": "red-flowered_silky_oak"}, - {"id": 19174, "synset": "silky_oak.n.01", "name": "silky_oak"}, - {"id": 19175, "synset": "beefwood.n.05", "name": "beefwood"}, - {"id": 19176, "synset": "cushion_flower.n.01", "name": "cushion_flower"}, - {"id": 19177, "synset": "rewa-rewa.n.01", "name": "rewa-rewa"}, - {"id": 19178, "synset": "honeyflower.n.01", "name": "honeyflower"}, - {"id": 19179, "synset": "silver_tree.n.01", "name": "silver_tree"}, - {"id": 19180, "synset": "lomatia.n.01", "name": "lomatia"}, - {"id": 19181, "synset": "macadamia.n.01", "name": "macadamia"}, - {"id": 19182, "synset": "macadamia_integrifolia.n.01", "name": "Macadamia_integrifolia"}, - {"id": 19183, "synset": "macadamia_nut.n.01", "name": "macadamia_nut"}, - {"id": 19184, "synset": "queensland_nut.n.01", "name": "Queensland_nut"}, - {"id": 19185, "synset": "prickly_ash.n.02", "name": "prickly_ash"}, - {"id": 19186, "synset": "geebung.n.01", "name": "geebung"}, - {"id": 19187, "synset": "wheel_tree.n.01", "name": "wheel_tree"}, - {"id": 19188, "synset": "scrub_beefwood.n.01", "name": "scrub_beefwood"}, - {"id": 19189, "synset": "waratah.n.02", "name": "waratah"}, - {"id": 19190, "synset": "waratah.n.01", "name": "waratah"}, - {"id": 19191, "synset": "casuarina.n.01", "name": "casuarina"}, - {"id": 19192, "synset": "she-oak.n.01", "name": "she-oak"}, - {"id": 19193, "synset": "beefwood.n.03", "name": "beefwood"}, - {"id": 19194, "synset": "australian_pine.n.01", "name": "Australian_pine"}, - {"id": 19195, "synset": "heath.n.01", "name": "heath"}, - {"id": 19196, "synset": "tree_heath.n.02", "name": "tree_heath"}, - {"id": 19197, "synset": "briarroot.n.01", "name": "briarroot"}, - {"id": 19198, "synset": "winter_heath.n.01", "name": "winter_heath"}, - {"id": 19199, "synset": "bell_heather.n.02", "name": "bell_heather"}, - {"id": 19200, "synset": "cornish_heath.n.01", "name": "Cornish_heath"}, - {"id": 19201, "synset": "spanish_heath.n.01", "name": "Spanish_heath"}, - {"id": 19202, "synset": "prince-of-wales'-heath.n.01", "name": "Prince-of-Wales'-heath"}, - {"id": 19203, "synset": "bog_rosemary.n.01", "name": "bog_rosemary"}, - {"id": 19204, "synset": "marsh_andromeda.n.01", "name": "marsh_andromeda"}, - {"id": 19205, "synset": "madrona.n.01", "name": "madrona"}, - {"id": 19206, "synset": "strawberry_tree.n.01", "name": "strawberry_tree"}, - {"id": 19207, "synset": "bearberry.n.03", "name": "bearberry"}, - {"id": 19208, "synset": "alpine_bearberry.n.01", "name": "alpine_bearberry"}, - {"id": 19209, "synset": "heartleaf_manzanita.n.01", "name": "heartleaf_manzanita"}, - {"id": 19210, "synset": "parry_manzanita.n.01", "name": "Parry_manzanita"}, - {"id": 19211, "synset": "spike_heath.n.01", "name": "spike_heath"}, - {"id": 19212, "synset": "bryanthus.n.01", "name": "bryanthus"}, - {"id": 19213, "synset": "leatherleaf.n.02", "name": "leatherleaf"}, - {"id": 19214, "synset": "connemara_heath.n.01", "name": "Connemara_heath"}, - {"id": 19215, "synset": "trailing_arbutus.n.01", "name": "trailing_arbutus"}, - {"id": 19216, "synset": "creeping_snowberry.n.01", "name": "creeping_snowberry"}, - {"id": 19217, "synset": "salal.n.01", "name": "salal"}, - {"id": 19218, "synset": "huckleberry.n.02", "name": "huckleberry"}, - {"id": 19219, "synset": "black_huckleberry.n.01", "name": "black_huckleberry"}, - {"id": 19220, "synset": "dangleberry.n.01", "name": "dangleberry"}, - {"id": 19221, "synset": "box_huckleberry.n.01", "name": "box_huckleberry"}, - {"id": 19222, "synset": "kalmia.n.01", "name": "kalmia"}, - {"id": 19223, "synset": "mountain_laurel.n.01", "name": "mountain_laurel"}, - {"id": 19224, "synset": "swamp_laurel.n.01", "name": "swamp_laurel"}, - {"id": 19225, "synset": "trapper's_tea.n.01", "name": "trapper's_tea"}, - {"id": 19226, "synset": "wild_rosemary.n.01", "name": "wild_rosemary"}, - {"id": 19227, "synset": "sand_myrtle.n.01", "name": "sand_myrtle"}, - {"id": 19228, "synset": "leucothoe.n.01", "name": "leucothoe"}, - {"id": 19229, "synset": "dog_laurel.n.01", "name": "dog_laurel"}, - {"id": 19230, "synset": "sweet_bells.n.01", "name": "sweet_bells"}, - {"id": 19231, "synset": "alpine_azalea.n.01", "name": "alpine_azalea"}, - {"id": 19232, "synset": "staggerbush.n.01", "name": "staggerbush"}, - {"id": 19233, "synset": "maleberry.n.01", "name": "maleberry"}, - {"id": 19234, "synset": "fetterbush.n.02", "name": "fetterbush"}, - {"id": 19235, "synset": "false_azalea.n.01", "name": "false_azalea"}, - {"id": 19236, "synset": "minniebush.n.01", "name": "minniebush"}, - {"id": 19237, "synset": "sorrel_tree.n.01", "name": "sorrel_tree"}, - {"id": 19238, "synset": "mountain_heath.n.01", "name": "mountain_heath"}, - {"id": 19239, "synset": "purple_heather.n.01", "name": "purple_heather"}, - {"id": 19240, "synset": "fetterbush.n.01", "name": "fetterbush"}, - {"id": 19241, "synset": "rhododendron.n.01", "name": "rhododendron"}, - {"id": 19242, "synset": "coast_rhododendron.n.01", "name": "coast_rhododendron"}, - {"id": 19243, "synset": "rosebay.n.01", "name": "rosebay"}, - {"id": 19244, "synset": "swamp_azalea.n.01", "name": "swamp_azalea"}, - {"id": 19245, "synset": "azalea.n.01", "name": "azalea"}, - {"id": 19246, "synset": "cranberry.n.01", "name": "cranberry"}, - {"id": 19247, "synset": "american_cranberry.n.01", "name": "American_cranberry"}, - {"id": 19248, "synset": "european_cranberry.n.01", "name": "European_cranberry"}, - {"id": 19249, "synset": "blueberry.n.01", "name": "blueberry"}, - {"id": 19250, "synset": "farkleberry.n.01", "name": "farkleberry"}, - {"id": 19251, "synset": "low-bush_blueberry.n.01", "name": "low-bush_blueberry"}, - {"id": 19252, "synset": "rabbiteye_blueberry.n.01", "name": "rabbiteye_blueberry"}, - {"id": 19253, "synset": "dwarf_bilberry.n.01", "name": "dwarf_bilberry"}, - {"id": 19254, "synset": "evergreen_blueberry.n.01", "name": "evergreen_blueberry"}, - {"id": 19255, "synset": "evergreen_huckleberry.n.01", "name": "evergreen_huckleberry"}, - {"id": 19256, "synset": "bilberry.n.02", "name": "bilberry"}, - {"id": 19257, "synset": "bilberry.n.01", "name": "bilberry"}, - {"id": 19258, "synset": "bog_bilberry.n.01", "name": "bog_bilberry"}, - {"id": 19259, "synset": "dryland_blueberry.n.01", "name": "dryland_blueberry"}, - {"id": 19260, "synset": "grouseberry.n.01", "name": "grouseberry"}, - {"id": 19261, "synset": "deerberry.n.01", "name": "deerberry"}, - {"id": 19262, "synset": "cowberry.n.01", "name": "cowberry"}, - {"id": 19263, "synset": "diapensia.n.01", "name": "diapensia"}, - {"id": 19264, "synset": "galax.n.01", "name": "galax"}, - {"id": 19265, "synset": "pyxie.n.01", "name": "pyxie"}, - {"id": 19266, "synset": "shortia.n.01", "name": "shortia"}, - {"id": 19267, "synset": "oconee_bells.n.01", "name": "oconee_bells"}, - {"id": 19268, "synset": "australian_heath.n.01", "name": "Australian_heath"}, - {"id": 19269, "synset": "epacris.n.01", "name": "epacris"}, - {"id": 19270, "synset": "common_heath.n.02", "name": "common_heath"}, - {"id": 19271, "synset": "common_heath.n.01", "name": "common_heath"}, - {"id": 19272, "synset": "port_jackson_heath.n.01", "name": "Port_Jackson_heath"}, - {"id": 19273, "synset": "native_cranberry.n.01", "name": "native_cranberry"}, - {"id": 19274, "synset": "pink_fivecorner.n.01", "name": "pink_fivecorner"}, - {"id": 19275, "synset": "wintergreen.n.01", "name": "wintergreen"}, - {"id": 19276, "synset": "false_wintergreen.n.01", "name": "false_wintergreen"}, - {"id": 19277, "synset": "lesser_wintergreen.n.01", "name": "lesser_wintergreen"}, - {"id": 19278, "synset": "wild_lily_of_the_valley.n.02", "name": "wild_lily_of_the_valley"}, - {"id": 19279, "synset": "wild_lily_of_the_valley.n.01", "name": "wild_lily_of_the_valley"}, - {"id": 19280, "synset": "pipsissewa.n.01", "name": "pipsissewa"}, - {"id": 19281, "synset": "love-in-winter.n.01", "name": "love-in-winter"}, - {"id": 19282, "synset": "one-flowered_wintergreen.n.01", "name": "one-flowered_wintergreen"}, - {"id": 19283, "synset": "indian_pipe.n.01", "name": "Indian_pipe"}, - {"id": 19284, "synset": "pinesap.n.01", "name": "pinesap"}, - {"id": 19285, "synset": "beech.n.01", "name": "beech"}, - {"id": 19286, "synset": "common_beech.n.01", "name": "common_beech"}, - {"id": 19287, "synset": "copper_beech.n.01", "name": "copper_beech"}, - {"id": 19288, "synset": "american_beech.n.01", "name": "American_beech"}, - {"id": 19289, "synset": "weeping_beech.n.01", "name": "weeping_beech"}, - {"id": 19290, "synset": "japanese_beech.n.01", "name": "Japanese_beech"}, - {"id": 19291, "synset": "chestnut.n.02", "name": "chestnut"}, - {"id": 19292, "synset": "american_chestnut.n.01", "name": "American_chestnut"}, - {"id": 19293, "synset": "european_chestnut.n.01", "name": "European_chestnut"}, - {"id": 19294, "synset": "chinese_chestnut.n.01", "name": "Chinese_chestnut"}, - {"id": 19295, "synset": "japanese_chestnut.n.01", "name": "Japanese_chestnut"}, - {"id": 19296, "synset": "allegheny_chinkapin.n.01", "name": "Allegheny_chinkapin"}, - {"id": 19297, "synset": "ozark_chinkapin.n.01", "name": "Ozark_chinkapin"}, - {"id": 19298, "synset": "oak_chestnut.n.01", "name": "oak_chestnut"}, - {"id": 19299, "synset": "giant_chinkapin.n.01", "name": "giant_chinkapin"}, - {"id": 19300, "synset": "dwarf_golden_chinkapin.n.01", "name": "dwarf_golden_chinkapin"}, - {"id": 19301, "synset": "tanbark_oak.n.01", "name": "tanbark_oak"}, - {"id": 19302, "synset": "japanese_oak.n.02", "name": "Japanese_oak"}, - {"id": 19303, "synset": "southern_beech.n.01", "name": "southern_beech"}, - {"id": 19304, "synset": "myrtle_beech.n.01", "name": "myrtle_beech"}, - {"id": 19305, "synset": "coigue.n.01", "name": "Coigue"}, - {"id": 19306, "synset": "new_zealand_beech.n.01", "name": "New_Zealand_beech"}, - {"id": 19307, "synset": "silver_beech.n.01", "name": "silver_beech"}, - {"id": 19308, "synset": "roble_beech.n.01", "name": "roble_beech"}, - {"id": 19309, "synset": "rauli_beech.n.01", "name": "rauli_beech"}, - {"id": 19310, "synset": "black_beech.n.01", "name": "black_beech"}, - {"id": 19311, "synset": "hard_beech.n.01", "name": "hard_beech"}, - {"id": 19312, "synset": "acorn.n.01", "name": "acorn"}, - {"id": 19313, "synset": "cupule.n.01", "name": "cupule"}, - {"id": 19314, "synset": "oak.n.02", "name": "oak"}, - {"id": 19315, "synset": "live_oak.n.01", "name": "live_oak"}, - {"id": 19316, "synset": "coast_live_oak.n.01", "name": "coast_live_oak"}, - {"id": 19317, "synset": "white_oak.n.01", "name": "white_oak"}, - {"id": 19318, "synset": "american_white_oak.n.01", "name": "American_white_oak"}, - {"id": 19319, "synset": "arizona_white_oak.n.01", "name": "Arizona_white_oak"}, - {"id": 19320, "synset": "swamp_white_oak.n.01", "name": "swamp_white_oak"}, - {"id": 19321, "synset": "european_turkey_oak.n.01", "name": "European_turkey_oak"}, - {"id": 19322, "synset": "canyon_oak.n.01", "name": "canyon_oak"}, - {"id": 19323, "synset": "scarlet_oak.n.01", "name": "scarlet_oak"}, - {"id": 19324, "synset": "jack_oak.n.02", "name": "jack_oak"}, - {"id": 19325, "synset": "red_oak.n.01", "name": "red_oak"}, - {"id": 19326, "synset": "southern_red_oak.n.01", "name": "southern_red_oak"}, - {"id": 19327, "synset": "oregon_white_oak.n.01", "name": "Oregon_white_oak"}, - {"id": 19328, "synset": "holm_oak.n.02", "name": "holm_oak"}, - {"id": 19329, "synset": "bear_oak.n.01", "name": "bear_oak"}, - {"id": 19330, "synset": "shingle_oak.n.01", "name": "shingle_oak"}, - {"id": 19331, "synset": "bluejack_oak.n.01", "name": "bluejack_oak"}, - {"id": 19332, "synset": "california_black_oak.n.01", "name": "California_black_oak"}, - {"id": 19333, "synset": "american_turkey_oak.n.01", "name": "American_turkey_oak"}, - {"id": 19334, "synset": "laurel_oak.n.01", "name": "laurel_oak"}, - {"id": 19335, "synset": "california_white_oak.n.01", "name": "California_white_oak"}, - {"id": 19336, "synset": "overcup_oak.n.01", "name": "overcup_oak"}, - {"id": 19337, "synset": "bur_oak.n.01", "name": "bur_oak"}, - {"id": 19338, "synset": "scrub_oak.n.01", "name": "scrub_oak"}, - {"id": 19339, "synset": "blackjack_oak.n.01", "name": "blackjack_oak"}, - {"id": 19340, "synset": "swamp_chestnut_oak.n.01", "name": "swamp_chestnut_oak"}, - {"id": 19341, "synset": "japanese_oak.n.01", "name": "Japanese_oak"}, - {"id": 19342, "synset": "chestnut_oak.n.01", "name": "chestnut_oak"}, - {"id": 19343, "synset": "chinquapin_oak.n.01", "name": "chinquapin_oak"}, - {"id": 19344, "synset": "myrtle_oak.n.01", "name": "myrtle_oak"}, - {"id": 19345, "synset": "water_oak.n.01", "name": "water_oak"}, - {"id": 19346, "synset": "nuttall_oak.n.01", "name": "Nuttall_oak"}, - {"id": 19347, "synset": "durmast.n.01", "name": "durmast"}, - {"id": 19348, "synset": "basket_oak.n.01", "name": "basket_oak"}, - {"id": 19349, "synset": "pin_oak.n.01", "name": "pin_oak"}, - {"id": 19350, "synset": "willow_oak.n.01", "name": "willow_oak"}, - {"id": 19351, "synset": "dwarf_chinkapin_oak.n.01", "name": "dwarf_chinkapin_oak"}, - {"id": 19352, "synset": "common_oak.n.01", "name": "common_oak"}, - {"id": 19353, "synset": "northern_red_oak.n.01", "name": "northern_red_oak"}, - {"id": 19354, "synset": "shumard_oak.n.01", "name": "Shumard_oak"}, - {"id": 19355, "synset": "post_oak.n.01", "name": "post_oak"}, - {"id": 19356, "synset": "cork_oak.n.01", "name": "cork_oak"}, - {"id": 19357, "synset": "spanish_oak.n.01", "name": "Spanish_oak"}, - {"id": 19358, "synset": "huckleberry_oak.n.01", "name": "huckleberry_oak"}, - {"id": 19359, "synset": "chinese_cork_oak.n.01", "name": "Chinese_cork_oak"}, - {"id": 19360, "synset": "black_oak.n.01", "name": "black_oak"}, - {"id": 19361, "synset": "southern_live_oak.n.01", "name": "southern_live_oak"}, - {"id": 19362, "synset": "interior_live_oak.n.01", "name": "interior_live_oak"}, - {"id": 19363, "synset": "mast.n.02", "name": "mast"}, - {"id": 19364, "synset": "birch.n.02", "name": "birch"}, - {"id": 19365, "synset": "yellow_birch.n.01", "name": "yellow_birch"}, - {"id": 19366, "synset": "american_white_birch.n.01", "name": "American_white_birch"}, - {"id": 19367, "synset": "grey_birch.n.01", "name": "grey_birch"}, - {"id": 19368, "synset": "silver_birch.n.01", "name": "silver_birch"}, - {"id": 19369, "synset": "downy_birch.n.01", "name": "downy_birch"}, - {"id": 19370, "synset": "black_birch.n.02", "name": "black_birch"}, - {"id": 19371, "synset": "sweet_birch.n.01", "name": "sweet_birch"}, - {"id": 19372, "synset": "yukon_white_birch.n.01", "name": "Yukon_white_birch"}, - {"id": 19373, "synset": "swamp_birch.n.01", "name": "swamp_birch"}, - {"id": 19374, "synset": "newfoundland_dwarf_birch.n.01", "name": "Newfoundland_dwarf_birch"}, - {"id": 19375, "synset": "alder.n.02", "name": "alder"}, - {"id": 19376, "synset": "common_alder.n.01", "name": "common_alder"}, - {"id": 19377, "synset": "grey_alder.n.01", "name": "grey_alder"}, - {"id": 19378, "synset": "seaside_alder.n.01", "name": "seaside_alder"}, - {"id": 19379, "synset": "white_alder.n.01", "name": "white_alder"}, - {"id": 19380, "synset": "red_alder.n.01", "name": "red_alder"}, - {"id": 19381, "synset": "speckled_alder.n.01", "name": "speckled_alder"}, - {"id": 19382, "synset": "smooth_alder.n.01", "name": "smooth_alder"}, - {"id": 19383, "synset": "green_alder.n.02", "name": "green_alder"}, - {"id": 19384, "synset": "green_alder.n.01", "name": "green_alder"}, - {"id": 19385, "synset": "hornbeam.n.01", "name": "hornbeam"}, - {"id": 19386, "synset": "european_hornbeam.n.01", "name": "European_hornbeam"}, - {"id": 19387, "synset": "american_hornbeam.n.01", "name": "American_hornbeam"}, - {"id": 19388, "synset": "hop_hornbeam.n.01", "name": "hop_hornbeam"}, - {"id": 19389, "synset": "old_world_hop_hornbeam.n.01", "name": "Old_World_hop_hornbeam"}, - {"id": 19390, "synset": "eastern_hop_hornbeam.n.01", "name": "Eastern_hop_hornbeam"}, - {"id": 19391, "synset": "hazelnut.n.01", "name": "hazelnut"}, - {"id": 19392, "synset": "american_hazel.n.01", "name": "American_hazel"}, - {"id": 19393, "synset": "cobnut.n.01", "name": "cobnut"}, - {"id": 19394, "synset": "beaked_hazelnut.n.01", "name": "beaked_hazelnut"}, - {"id": 19395, "synset": "centaury.n.01", "name": "centaury"}, - {"id": 19396, "synset": "rosita.n.01", "name": "rosita"}, - {"id": 19397, "synset": "lesser_centaury.n.01", "name": "lesser_centaury"}, - {"id": 19398, "synset": "seaside_centaury.n.01", "name": "seaside_centaury"}, - {"id": 19399, "synset": "slender_centaury.n.01", "name": "slender_centaury"}, - {"id": 19400, "synset": "prairie_gentian.n.01", "name": "prairie_gentian"}, - {"id": 19401, "synset": "persian_violet.n.01", "name": "Persian_violet"}, - {"id": 19402, "synset": "columbo.n.01", "name": "columbo"}, - {"id": 19403, "synset": "gentian.n.01", "name": "gentian"}, - {"id": 19404, "synset": "gentianella.n.02", "name": "gentianella"}, - {"id": 19405, "synset": "closed_gentian.n.02", "name": "closed_gentian"}, - {"id": 19406, "synset": "explorer's_gentian.n.01", "name": "explorer's_gentian"}, - {"id": 19407, "synset": "closed_gentian.n.01", "name": "closed_gentian"}, - {"id": 19408, "synset": "great_yellow_gentian.n.01", "name": "great_yellow_gentian"}, - {"id": 19409, "synset": "marsh_gentian.n.01", "name": "marsh_gentian"}, - {"id": 19410, "synset": "soapwort_gentian.n.01", "name": "soapwort_gentian"}, - {"id": 19411, "synset": "striped_gentian.n.01", "name": "striped_gentian"}, - {"id": 19412, "synset": "agueweed.n.01", "name": "agueweed"}, - {"id": 19413, "synset": "felwort.n.01", "name": "felwort"}, - {"id": 19414, "synset": "fringed_gentian.n.01", "name": "fringed_gentian"}, - {"id": 19415, "synset": "gentianopsis_crinita.n.01", "name": "Gentianopsis_crinita"}, - {"id": 19416, "synset": "gentianopsis_detonsa.n.01", "name": "Gentianopsis_detonsa"}, - {"id": 19417, "synset": "gentianopsid_procera.n.01", "name": "Gentianopsid_procera"}, - {"id": 19418, "synset": "gentianopsis_thermalis.n.01", "name": "Gentianopsis_thermalis"}, - {"id": 19419, "synset": "tufted_gentian.n.01", "name": "tufted_gentian"}, - {"id": 19420, "synset": "spurred_gentian.n.01", "name": "spurred_gentian"}, - {"id": 19421, "synset": "sabbatia.n.01", "name": "sabbatia"}, - {"id": 19422, "synset": "toothbrush_tree.n.01", "name": "toothbrush_tree"}, - {"id": 19423, "synset": "olive_tree.n.01", "name": "olive_tree"}, - {"id": 19424, "synset": "olive.n.02", "name": "olive"}, - {"id": 19425, "synset": "olive.n.01", "name": "olive"}, - {"id": 19426, "synset": "black_maire.n.01", "name": "black_maire"}, - {"id": 19427, "synset": "white_maire.n.01", "name": "white_maire"}, - {"id": 19428, "synset": "fringe_tree.n.01", "name": "fringe_tree"}, - {"id": 19429, "synset": "fringe_bush.n.01", "name": "fringe_bush"}, - {"id": 19430, "synset": "forestiera.n.01", "name": "forestiera"}, - {"id": 19431, "synset": "forsythia.n.01", "name": "forsythia"}, - {"id": 19432, "synset": "ash.n.02", "name": "ash"}, - {"id": 19433, "synset": "white_ash.n.02", "name": "white_ash"}, - {"id": 19434, "synset": "swamp_ash.n.01", "name": "swamp_ash"}, - {"id": 19435, "synset": "flowering_ash.n.03", "name": "flowering_ash"}, - {"id": 19436, "synset": "european_ash.n.01", "name": "European_ash"}, - {"id": 19437, "synset": "oregon_ash.n.01", "name": "Oregon_ash"}, - {"id": 19438, "synset": "black_ash.n.01", "name": "black_ash"}, - {"id": 19439, "synset": "manna_ash.n.01", "name": "manna_ash"}, - {"id": 19440, "synset": "red_ash.n.01", "name": "red_ash"}, - {"id": 19441, "synset": "green_ash.n.01", "name": "green_ash"}, - {"id": 19442, "synset": "blue_ash.n.01", "name": "blue_ash"}, - {"id": 19443, "synset": "mountain_ash.n.03", "name": "mountain_ash"}, - {"id": 19444, "synset": "pumpkin_ash.n.01", "name": "pumpkin_ash"}, - {"id": 19445, "synset": "arizona_ash.n.01", "name": "Arizona_ash"}, - {"id": 19446, "synset": "jasmine.n.01", "name": "jasmine"}, - {"id": 19447, "synset": "primrose_jasmine.n.01", "name": "primrose_jasmine"}, - {"id": 19448, "synset": "winter_jasmine.n.01", "name": "winter_jasmine"}, - {"id": 19449, "synset": "common_jasmine.n.01", "name": "common_jasmine"}, - {"id": 19450, "synset": "privet.n.01", "name": "privet"}, - {"id": 19451, "synset": "amur_privet.n.01", "name": "Amur_privet"}, - {"id": 19452, "synset": "japanese_privet.n.01", "name": "Japanese_privet"}, - {"id": 19453, "synset": "ligustrum_obtusifolium.n.01", "name": "Ligustrum_obtusifolium"}, - {"id": 19454, "synset": "common_privet.n.01", "name": "common_privet"}, - {"id": 19455, "synset": "devilwood.n.01", "name": "devilwood"}, - {"id": 19456, "synset": "mock_privet.n.01", "name": "mock_privet"}, - {"id": 19457, "synset": "lilac.n.01", "name": "lilac"}, - {"id": 19458, "synset": "himalayan_lilac.n.01", "name": "Himalayan_lilac"}, - {"id": 19459, "synset": "persian_lilac.n.02", "name": "Persian_lilac"}, - {"id": 19460, "synset": "japanese_tree_lilac.n.01", "name": "Japanese_tree_lilac"}, - {"id": 19461, "synset": "japanese_lilac.n.01", "name": "Japanese_lilac"}, - {"id": 19462, "synset": "common_lilac.n.01", "name": "common_lilac"}, - {"id": 19463, "synset": "bloodwort.n.01", "name": "bloodwort"}, - {"id": 19464, "synset": "kangaroo_paw.n.01", "name": "kangaroo_paw"}, - {"id": 19465, "synset": "virginian_witch_hazel.n.01", "name": "Virginian_witch_hazel"}, - {"id": 19466, "synset": "vernal_witch_hazel.n.01", "name": "vernal_witch_hazel"}, - {"id": 19467, "synset": "winter_hazel.n.01", "name": "winter_hazel"}, - {"id": 19468, "synset": "fothergilla.n.01", "name": "fothergilla"}, - {"id": 19469, "synset": "liquidambar.n.02", "name": "liquidambar"}, - {"id": 19470, "synset": "sweet_gum.n.03", "name": "sweet_gum"}, - {"id": 19471, "synset": "iron_tree.n.01", "name": "iron_tree"}, - {"id": 19472, "synset": "walnut.n.03", "name": "walnut"}, - {"id": 19473, "synset": "california_black_walnut.n.01", "name": "California_black_walnut"}, - {"id": 19474, "synset": "butternut.n.01", "name": "butternut"}, - {"id": 19475, "synset": "black_walnut.n.01", "name": "black_walnut"}, - {"id": 19476, "synset": "english_walnut.n.01", "name": "English_walnut"}, - {"id": 19477, "synset": "hickory.n.02", "name": "hickory"}, - {"id": 19478, "synset": "water_hickory.n.01", "name": "water_hickory"}, - {"id": 19479, "synset": "pignut.n.01", "name": "pignut"}, - {"id": 19480, "synset": "bitternut.n.01", "name": "bitternut"}, - {"id": 19481, "synset": "pecan.n.02", "name": "pecan"}, - {"id": 19482, "synset": "big_shellbark.n.01", "name": "big_shellbark"}, - {"id": 19483, "synset": "nutmeg_hickory.n.01", "name": "nutmeg_hickory"}, - {"id": 19484, "synset": "shagbark.n.01", "name": "shagbark"}, - {"id": 19485, "synset": "mockernut.n.01", "name": "mockernut"}, - {"id": 19486, "synset": "wing_nut.n.01", "name": "wing_nut"}, - {"id": 19487, "synset": "caucasian_walnut.n.01", "name": "Caucasian_walnut"}, - {"id": 19488, "synset": "dhawa.n.01", "name": "dhawa"}, - {"id": 19489, "synset": "combretum.n.01", "name": "combretum"}, - {"id": 19490, "synset": "hiccup_nut.n.01", "name": "hiccup_nut"}, - {"id": 19491, "synset": "bush_willow.n.02", "name": "bush_willow"}, - {"id": 19492, "synset": "bush_willow.n.01", "name": "bush_willow"}, - {"id": 19493, "synset": "button_tree.n.01", "name": "button_tree"}, - {"id": 19494, "synset": "white_mangrove.n.02", "name": "white_mangrove"}, - {"id": 19495, "synset": "oleaster.n.01", "name": "oleaster"}, - {"id": 19496, "synset": "water_milfoil.n.01", "name": "water_milfoil"}, - {"id": 19497, "synset": "anchovy_pear.n.01", "name": "anchovy_pear"}, - {"id": 19498, "synset": "brazil_nut.n.01", "name": "brazil_nut"}, - {"id": 19499, "synset": "loosestrife.n.01", "name": "loosestrife"}, - {"id": 19500, "synset": "purple_loosestrife.n.01", "name": "purple_loosestrife"}, - {"id": 19501, "synset": "grass_poly.n.01", "name": "grass_poly"}, - {"id": 19502, "synset": "crape_myrtle.n.01", "name": "crape_myrtle"}, - {"id": 19503, "synset": "queen's_crape_myrtle.n.01", "name": "Queen's_crape_myrtle"}, - {"id": 19504, "synset": "myrtaceous_tree.n.01", "name": "myrtaceous_tree"}, - {"id": 19505, "synset": "myrtle.n.02", "name": "myrtle"}, - {"id": 19506, "synset": "common_myrtle.n.01", "name": "common_myrtle"}, - {"id": 19507, "synset": "bayberry.n.01", "name": "bayberry"}, - {"id": 19508, "synset": "allspice.n.01", "name": "allspice"}, - {"id": 19509, "synset": "allspice_tree.n.01", "name": "allspice_tree"}, - {"id": 19510, "synset": "sour_cherry.n.02", "name": "sour_cherry"}, - {"id": 19511, "synset": "nakedwood.n.02", "name": "nakedwood"}, - {"id": 19512, "synset": "surinam_cherry.n.02", "name": "Surinam_cherry"}, - {"id": 19513, "synset": "rose_apple.n.01", "name": "rose_apple"}, - {"id": 19514, "synset": "feijoa.n.01", "name": "feijoa"}, - {"id": 19515, "synset": "jaboticaba.n.01", "name": "jaboticaba"}, - {"id": 19516, "synset": "guava.n.02", "name": "guava"}, - {"id": 19517, "synset": "guava.n.01", "name": "guava"}, - {"id": 19518, "synset": "cattley_guava.n.01", "name": "cattley_guava"}, - {"id": 19519, "synset": "brazilian_guava.n.01", "name": "Brazilian_guava"}, - {"id": 19520, "synset": "gum_tree.n.01", "name": "gum_tree"}, - {"id": 19521, "synset": "eucalyptus.n.02", "name": "eucalyptus"}, - {"id": 19522, "synset": "flooded_gum.n.01", "name": "flooded_gum"}, - {"id": 19523, "synset": "mallee.n.01", "name": "mallee"}, - {"id": 19524, "synset": "stringybark.n.01", "name": "stringybark"}, - {"id": 19525, "synset": "smoothbark.n.01", "name": "smoothbark"}, - {"id": 19526, "synset": "red_gum.n.03", "name": "red_gum"}, - {"id": 19527, "synset": "red_gum.n.02", "name": "red_gum"}, - {"id": 19528, "synset": "river_red_gum.n.01", "name": "river_red_gum"}, - {"id": 19529, "synset": "mountain_swamp_gum.n.01", "name": "mountain_swamp_gum"}, - {"id": 19530, "synset": "snow_gum.n.01", "name": "snow_gum"}, - {"id": 19531, "synset": "alpine_ash.n.01", "name": "alpine_ash"}, - {"id": 19532, "synset": "white_mallee.n.01", "name": "white_mallee"}, - {"id": 19533, "synset": "white_stringybark.n.01", "name": "white_stringybark"}, - {"id": 19534, "synset": "white_mountain_ash.n.01", "name": "white_mountain_ash"}, - {"id": 19535, "synset": "blue_gum.n.01", "name": "blue_gum"}, - {"id": 19536, "synset": "rose_gum.n.01", "name": "rose_gum"}, - {"id": 19537, "synset": "cider_gum.n.01", "name": "cider_gum"}, - {"id": 19538, "synset": "swamp_gum.n.01", "name": "swamp_gum"}, - {"id": 19539, "synset": "spotted_gum.n.01", "name": "spotted_gum"}, - {"id": 19540, "synset": "lemon-scented_gum.n.01", "name": "lemon-scented_gum"}, - {"id": 19541, "synset": "black_mallee.n.01", "name": "black_mallee"}, - {"id": 19542, "synset": "forest_red_gum.n.01", "name": "forest_red_gum"}, - {"id": 19543, "synset": "mountain_ash.n.02", "name": "mountain_ash"}, - {"id": 19544, "synset": "manna_gum.n.01", "name": "manna_gum"}, - {"id": 19545, "synset": "clove.n.02", "name": "clove"}, - {"id": 19546, "synset": "clove.n.01", "name": "clove"}, - {"id": 19547, "synset": "tupelo.n.02", "name": "tupelo"}, - {"id": 19548, "synset": "water_gum.n.01", "name": "water_gum"}, - {"id": 19549, "synset": "sour_gum.n.01", "name": "sour_gum"}, - {"id": 19550, "synset": "enchanter's_nightshade.n.01", "name": "enchanter's_nightshade"}, - {"id": 19551, "synset": "circaea_lutetiana.n.01", "name": "Circaea_lutetiana"}, - {"id": 19552, "synset": "willowherb.n.01", "name": "willowherb"}, - {"id": 19553, "synset": "fireweed.n.01", "name": "fireweed"}, - {"id": 19554, "synset": "california_fuchsia.n.01", "name": "California_fuchsia"}, - {"id": 19555, "synset": "fuchsia.n.01", "name": "fuchsia"}, - {"id": 19556, "synset": "lady's-eardrop.n.01", "name": "lady's-eardrop"}, - {"id": 19557, "synset": "evening_primrose.n.01", "name": "evening_primrose"}, - {"id": 19558, "synset": "common_evening_primrose.n.01", "name": "common_evening_primrose"}, - {"id": 19559, "synset": "sundrops.n.01", "name": "sundrops"}, - {"id": 19560, "synset": "missouri_primrose.n.01", "name": "Missouri_primrose"}, - {"id": 19561, "synset": "pomegranate.n.01", "name": "pomegranate"}, - {"id": 19562, "synset": "mangrove.n.01", "name": "mangrove"}, - {"id": 19563, "synset": "daphne.n.01", "name": "daphne"}, - {"id": 19564, "synset": "garland_flower.n.01", "name": "garland_flower"}, - {"id": 19565, "synset": "spurge_laurel.n.01", "name": "spurge_laurel"}, - {"id": 19566, "synset": "mezereon.n.01", "name": "mezereon"}, - {"id": 19567, "synset": "indian_rhododendron.n.01", "name": "Indian_rhododendron"}, - {"id": 19568, "synset": "medinilla_magnifica.n.01", "name": "Medinilla_magnifica"}, - {"id": 19569, "synset": "deer_grass.n.01", "name": "deer_grass"}, - {"id": 19570, "synset": "canna.n.01", "name": "canna"}, - {"id": 19571, "synset": "achira.n.01", "name": "achira"}, - {"id": 19572, "synset": "arrowroot.n.02", "name": "arrowroot"}, - {"id": 19573, "synset": "banana.n.01", "name": "banana"}, - {"id": 19574, "synset": "dwarf_banana.n.01", "name": "dwarf_banana"}, - {"id": 19575, "synset": "japanese_banana.n.01", "name": "Japanese_banana"}, - {"id": 19576, "synset": "plantain.n.02", "name": "plantain"}, - {"id": 19577, "synset": "edible_banana.n.01", "name": "edible_banana"}, - {"id": 19578, "synset": "abaca.n.02", "name": "abaca"}, - {"id": 19579, "synset": "abyssinian_banana.n.01", "name": "Abyssinian_banana"}, - {"id": 19580, "synset": "ginger.n.01", "name": "ginger"}, - {"id": 19581, "synset": "common_ginger.n.01", "name": "common_ginger"}, - {"id": 19582, "synset": "turmeric.n.01", "name": "turmeric"}, - {"id": 19583, "synset": "galangal.n.01", "name": "galangal"}, - {"id": 19584, "synset": "shellflower.n.02", "name": "shellflower"}, - {"id": 19585, "synset": "grains_of_paradise.n.01", "name": "grains_of_paradise"}, - {"id": 19586, "synset": "cardamom.n.01", "name": "cardamom"}, - {"id": 19587, "synset": "begonia.n.01", "name": "begonia"}, - {"id": 19588, "synset": "fibrous-rooted_begonia.n.01", "name": "fibrous-rooted_begonia"}, - {"id": 19589, "synset": "tuberous_begonia.n.01", "name": "tuberous_begonia"}, - {"id": 19590, "synset": "rhizomatous_begonia.n.01", "name": "rhizomatous_begonia"}, - {"id": 19591, "synset": "christmas_begonia.n.01", "name": "Christmas_begonia"}, - {"id": 19592, "synset": "angel-wing_begonia.n.01", "name": "angel-wing_begonia"}, - {"id": 19593, "synset": "beefsteak_begonia.n.01", "name": "beefsteak_begonia"}, - {"id": 19594, "synset": "star_begonia.n.01", "name": "star_begonia"}, - {"id": 19595, "synset": "rex_begonia.n.01", "name": "rex_begonia"}, - {"id": 19596, "synset": "wax_begonia.n.01", "name": "wax_begonia"}, - {"id": 19597, "synset": "socotra_begonia.n.01", "name": "Socotra_begonia"}, - {"id": 19598, "synset": "hybrid_tuberous_begonia.n.01", "name": "hybrid_tuberous_begonia"}, - {"id": 19599, "synset": "dillenia.n.01", "name": "dillenia"}, - {"id": 19600, "synset": "guinea_gold_vine.n.01", "name": "guinea_gold_vine"}, - {"id": 19601, "synset": "poon.n.02", "name": "poon"}, - {"id": 19602, "synset": "calaba.n.01", "name": "calaba"}, - {"id": 19603, "synset": "maria.n.02", "name": "Maria"}, - {"id": 19604, "synset": "laurelwood.n.01", "name": "laurelwood"}, - {"id": 19605, "synset": "alexandrian_laurel.n.01", "name": "Alexandrian_laurel"}, - {"id": 19606, "synset": "clusia.n.01", "name": "clusia"}, - {"id": 19607, "synset": "wild_fig.n.02", "name": "wild_fig"}, - {"id": 19608, "synset": "waxflower.n.02", "name": "waxflower"}, - {"id": 19609, "synset": "pitch_apple.n.01", "name": "pitch_apple"}, - {"id": 19610, "synset": "mangosteen.n.01", "name": "mangosteen"}, - {"id": 19611, "synset": "gamboge_tree.n.01", "name": "gamboge_tree"}, - {"id": 19612, "synset": "st_john's_wort.n.01", "name": "St_John's_wort"}, - {"id": 19613, "synset": "common_st_john's_wort.n.01", "name": "common_St_John's_wort"}, - {"id": 19614, "synset": "great_st_john's_wort.n.01", "name": "great_St_John's_wort"}, - {"id": 19615, "synset": "creeping_st_john's_wort.n.01", "name": "creeping_St_John's_wort"}, - {"id": 19616, "synset": "low_st_andrew's_cross.n.01", "name": "low_St_Andrew's_cross"}, - {"id": 19617, "synset": "klammath_weed.n.01", "name": "klammath_weed"}, - {"id": 19618, "synset": "shrubby_st_john's_wort.n.01", "name": "shrubby_St_John's_wort"}, - {"id": 19619, "synset": "st_peter's_wort.n.01", "name": "St_Peter's_wort"}, - {"id": 19620, "synset": "marsh_st-john's_wort.n.01", "name": "marsh_St-John's_wort"}, - {"id": 19621, "synset": "mammee_apple.n.01", "name": "mammee_apple"}, - {"id": 19622, "synset": "rose_chestnut.n.01", "name": "rose_chestnut"}, - {"id": 19623, "synset": "bower_actinidia.n.01", "name": "bower_actinidia"}, - {"id": 19624, "synset": "chinese_gooseberry.n.01", "name": "Chinese_gooseberry"}, - {"id": 19625, "synset": "silvervine.n.01", "name": "silvervine"}, - {"id": 19626, "synset": "wild_cinnamon.n.01", "name": "wild_cinnamon"}, - {"id": 19627, "synset": "papaya.n.01", "name": "papaya"}, - {"id": 19628, "synset": "souari.n.01", "name": "souari"}, - {"id": 19629, "synset": "rockrose.n.02", "name": "rockrose"}, - {"id": 19630, "synset": "white-leaved_rockrose.n.01", "name": "white-leaved_rockrose"}, - {"id": 19631, "synset": "common_gum_cistus.n.01", "name": "common_gum_cistus"}, - {"id": 19632, "synset": "frostweed.n.01", "name": "frostweed"}, - {"id": 19633, "synset": "dipterocarp.n.01", "name": "dipterocarp"}, - {"id": 19634, "synset": "red_lauan.n.02", "name": "red_lauan"}, - {"id": 19635, "synset": "governor's_plum.n.01", "name": "governor's_plum"}, - {"id": 19636, "synset": "kei_apple.n.01", "name": "kei_apple"}, - {"id": 19637, "synset": "ketembilla.n.01", "name": "ketembilla"}, - {"id": 19638, "synset": "chaulmoogra.n.01", "name": "chaulmoogra"}, - {"id": 19639, "synset": "wild_peach.n.01", "name": "wild_peach"}, - {"id": 19640, "synset": "candlewood.n.01", "name": "candlewood"}, - {"id": 19641, "synset": "boojum_tree.n.01", "name": "boojum_tree"}, - {"id": 19642, "synset": "bird's-eye_bush.n.01", "name": "bird's-eye_bush"}, - {"id": 19643, "synset": "granadilla.n.03", "name": "granadilla"}, - {"id": 19644, "synset": "granadilla.n.02", "name": "granadilla"}, - {"id": 19645, "synset": "granadilla.n.01", "name": "granadilla"}, - {"id": 19646, "synset": "maypop.n.01", "name": "maypop"}, - {"id": 19647, "synset": "jamaica_honeysuckle.n.01", "name": "Jamaica_honeysuckle"}, - {"id": 19648, "synset": "banana_passion_fruit.n.01", "name": "banana_passion_fruit"}, - {"id": 19649, "synset": "sweet_calabash.n.01", "name": "sweet_calabash"}, - {"id": 19650, "synset": "love-in-a-mist.n.01", "name": "love-in-a-mist"}, - {"id": 19651, "synset": "reseda.n.01", "name": "reseda"}, - {"id": 19652, "synset": "mignonette.n.01", "name": "mignonette"}, - {"id": 19653, "synset": "dyer's_rocket.n.01", "name": "dyer's_rocket"}, - {"id": 19654, "synset": "false_tamarisk.n.01", "name": "false_tamarisk"}, - {"id": 19655, "synset": "halophyte.n.01", "name": "halophyte"}, - {"id": 19656, "synset": "viola.n.01", "name": "viola"}, - {"id": 19657, "synset": "violet.n.01", "name": "violet"}, - {"id": 19658, "synset": "field_pansy.n.01", "name": "field_pansy"}, - {"id": 19659, "synset": "american_dog_violet.n.01", "name": "American_dog_violet"}, - {"id": 19660, "synset": "dog_violet.n.01", "name": "dog_violet"}, - {"id": 19661, "synset": "horned_violet.n.01", "name": "horned_violet"}, - {"id": 19662, "synset": "two-eyed_violet.n.01", "name": "two-eyed_violet"}, - {"id": 19663, "synset": "bird's-foot_violet.n.01", "name": "bird's-foot_violet"}, - {"id": 19664, "synset": "downy_yellow_violet.n.01", "name": "downy_yellow_violet"}, - {"id": 19665, "synset": "long-spurred_violet.n.01", "name": "long-spurred_violet"}, - {"id": 19666, "synset": "pale_violet.n.01", "name": "pale_violet"}, - {"id": 19667, "synset": "hedge_violet.n.01", "name": "hedge_violet"}, - {"id": 19668, "synset": "nettle.n.01", "name": "nettle"}, - {"id": 19669, "synset": "stinging_nettle.n.01", "name": "stinging_nettle"}, - {"id": 19670, "synset": "roman_nettle.n.01", "name": "Roman_nettle"}, - {"id": 19671, "synset": "ramie.n.01", "name": "ramie"}, - {"id": 19672, "synset": "wood_nettle.n.01", "name": "wood_nettle"}, - {"id": 19673, "synset": "australian_nettle.n.01", "name": "Australian_nettle"}, - {"id": 19674, "synset": "pellitory-of-the-wall.n.01", "name": "pellitory-of-the-wall"}, - {"id": 19675, "synset": "richweed.n.02", "name": "richweed"}, - {"id": 19676, "synset": "artillery_plant.n.01", "name": "artillery_plant"}, - {"id": 19677, "synset": "friendship_plant.n.01", "name": "friendship_plant"}, - { - "id": 19678, - "synset": "queensland_grass-cloth_plant.n.01", - "name": "Queensland_grass-cloth_plant", - }, - {"id": 19679, "synset": "pipturus_albidus.n.01", "name": "Pipturus_albidus"}, - {"id": 19680, "synset": "cannabis.n.01", "name": "cannabis"}, - {"id": 19681, "synset": "indian_hemp.n.01", "name": "Indian_hemp"}, - {"id": 19682, "synset": "mulberry.n.01", "name": "mulberry"}, - {"id": 19683, "synset": "white_mulberry.n.01", "name": "white_mulberry"}, - {"id": 19684, "synset": "black_mulberry.n.01", "name": "black_mulberry"}, - {"id": 19685, "synset": "red_mulberry.n.01", "name": "red_mulberry"}, - {"id": 19686, "synset": "osage_orange.n.01", "name": "osage_orange"}, - {"id": 19687, "synset": "breadfruit.n.01", "name": "breadfruit"}, - {"id": 19688, "synset": "jackfruit.n.01", "name": "jackfruit"}, - {"id": 19689, "synset": "marang.n.01", "name": "marang"}, - {"id": 19690, "synset": "fig_tree.n.01", "name": "fig_tree"}, - {"id": 19691, "synset": "fig.n.02", "name": "fig"}, - {"id": 19692, "synset": "caprifig.n.01", "name": "caprifig"}, - {"id": 19693, "synset": "golden_fig.n.01", "name": "golden_fig"}, - {"id": 19694, "synset": "banyan.n.01", "name": "banyan"}, - {"id": 19695, "synset": "pipal.n.01", "name": "pipal"}, - {"id": 19696, "synset": "india-rubber_tree.n.01", "name": "India-rubber_tree"}, - {"id": 19697, "synset": "mistletoe_fig.n.01", "name": "mistletoe_fig"}, - {"id": 19698, "synset": "port_jackson_fig.n.01", "name": "Port_Jackson_fig"}, - {"id": 19699, "synset": "sycamore.n.04", "name": "sycamore"}, - {"id": 19700, "synset": "paper_mulberry.n.01", "name": "paper_mulberry"}, - {"id": 19701, "synset": "trumpetwood.n.01", "name": "trumpetwood"}, - {"id": 19702, "synset": "elm.n.01", "name": "elm"}, - {"id": 19703, "synset": "winged_elm.n.01", "name": "winged_elm"}, - {"id": 19704, "synset": "american_elm.n.01", "name": "American_elm"}, - {"id": 19705, "synset": "smooth-leaved_elm.n.01", "name": "smooth-leaved_elm"}, - {"id": 19706, "synset": "cedar_elm.n.01", "name": "cedar_elm"}, - {"id": 19707, "synset": "witch_elm.n.01", "name": "witch_elm"}, - {"id": 19708, "synset": "dutch_elm.n.01", "name": "Dutch_elm"}, - {"id": 19709, "synset": "huntingdon_elm.n.01", "name": "Huntingdon_elm"}, - {"id": 19710, "synset": "water_elm.n.01", "name": "water_elm"}, - {"id": 19711, "synset": "chinese_elm.n.02", "name": "Chinese_elm"}, - {"id": 19712, "synset": "english_elm.n.01", "name": "English_elm"}, - {"id": 19713, "synset": "siberian_elm.n.01", "name": "Siberian_elm"}, - {"id": 19714, "synset": "slippery_elm.n.01", "name": "slippery_elm"}, - {"id": 19715, "synset": "jersey_elm.n.01", "name": "Jersey_elm"}, - {"id": 19716, "synset": "september_elm.n.01", "name": "September_elm"}, - {"id": 19717, "synset": "rock_elm.n.01", "name": "rock_elm"}, - {"id": 19718, "synset": "hackberry.n.01", "name": "hackberry"}, - {"id": 19719, "synset": "european_hackberry.n.01", "name": "European_hackberry"}, - {"id": 19720, "synset": "american_hackberry.n.01", "name": "American_hackberry"}, - {"id": 19721, "synset": "sugarberry.n.01", "name": "sugarberry"}, - {"id": 19722, "synset": "iridaceous_plant.n.01", "name": "iridaceous_plant"}, - {"id": 19723, "synset": "bearded_iris.n.01", "name": "bearded_iris"}, - {"id": 19724, "synset": "beardless_iris.n.01", "name": "beardless_iris"}, - {"id": 19725, "synset": "orrisroot.n.01", "name": "orrisroot"}, - {"id": 19726, "synset": "dwarf_iris.n.02", "name": "dwarf_iris"}, - {"id": 19727, "synset": "dutch_iris.n.02", "name": "Dutch_iris"}, - {"id": 19728, "synset": "florentine_iris.n.01", "name": "Florentine_iris"}, - {"id": 19729, "synset": "stinking_iris.n.01", "name": "stinking_iris"}, - {"id": 19730, "synset": "german_iris.n.02", "name": "German_iris"}, - {"id": 19731, "synset": "japanese_iris.n.01", "name": "Japanese_iris"}, - {"id": 19732, "synset": "german_iris.n.01", "name": "German_iris"}, - {"id": 19733, "synset": "dalmatian_iris.n.01", "name": "Dalmatian_iris"}, - {"id": 19734, "synset": "persian_iris.n.01", "name": "Persian_iris"}, - {"id": 19735, "synset": "dutch_iris.n.01", "name": "Dutch_iris"}, - {"id": 19736, "synset": "dwarf_iris.n.01", "name": "dwarf_iris"}, - {"id": 19737, "synset": "spanish_iris.n.01", "name": "Spanish_iris"}, - {"id": 19738, "synset": "blackberry-lily.n.01", "name": "blackberry-lily"}, - {"id": 19739, "synset": "crocus.n.01", "name": "crocus"}, - {"id": 19740, "synset": "saffron.n.01", "name": "saffron"}, - {"id": 19741, "synset": "corn_lily.n.01", "name": "corn_lily"}, - {"id": 19742, "synset": "blue-eyed_grass.n.01", "name": "blue-eyed_grass"}, - {"id": 19743, "synset": "wandflower.n.01", "name": "wandflower"}, - {"id": 19744, "synset": "amaryllis.n.01", "name": "amaryllis"}, - {"id": 19745, "synset": "salsilla.n.02", "name": "salsilla"}, - {"id": 19746, "synset": "salsilla.n.01", "name": "salsilla"}, - {"id": 19747, "synset": "blood_lily.n.01", "name": "blood_lily"}, - {"id": 19748, "synset": "cape_tulip.n.01", "name": "Cape_tulip"}, - {"id": 19749, "synset": "hippeastrum.n.01", "name": "hippeastrum"}, - {"id": 19750, "synset": "narcissus.n.01", "name": "narcissus"}, - {"id": 19751, "synset": "daffodil.n.01", "name": "daffodil"}, - {"id": 19752, "synset": "jonquil.n.01", "name": "jonquil"}, - {"id": 19753, "synset": "jonquil.n.02", "name": "jonquil"}, - {"id": 19754, "synset": "jacobean_lily.n.01", "name": "Jacobean_lily"}, - {"id": 19755, "synset": "liliaceous_plant.n.01", "name": "liliaceous_plant"}, - {"id": 19756, "synset": "mountain_lily.n.01", "name": "mountain_lily"}, - {"id": 19757, "synset": "canada_lily.n.01", "name": "Canada_lily"}, - {"id": 19758, "synset": "tiger_lily.n.02", "name": "tiger_lily"}, - {"id": 19759, "synset": "columbia_tiger_lily.n.01", "name": "Columbia_tiger_lily"}, - {"id": 19760, "synset": "tiger_lily.n.01", "name": "tiger_lily"}, - {"id": 19761, "synset": "easter_lily.n.01", "name": "Easter_lily"}, - {"id": 19762, "synset": "coast_lily.n.01", "name": "coast_lily"}, - {"id": 19763, "synset": "turk's-cap.n.02", "name": "Turk's-cap"}, - {"id": 19764, "synset": "michigan_lily.n.01", "name": "Michigan_lily"}, - {"id": 19765, "synset": "leopard_lily.n.01", "name": "leopard_lily"}, - {"id": 19766, "synset": "turk's-cap.n.01", "name": "Turk's-cap"}, - {"id": 19767, "synset": "african_lily.n.01", "name": "African_lily"}, - {"id": 19768, "synset": "colicroot.n.01", "name": "colicroot"}, - {"id": 19769, "synset": "ague_root.n.01", "name": "ague_root"}, - {"id": 19770, "synset": "yellow_colicroot.n.01", "name": "yellow_colicroot"}, - {"id": 19771, "synset": "alliaceous_plant.n.01", "name": "alliaceous_plant"}, - {"id": 19772, "synset": "hooker's_onion.n.01", "name": "Hooker's_onion"}, - {"id": 19773, "synset": "wild_leek.n.02", "name": "wild_leek"}, - {"id": 19774, "synset": "canada_garlic.n.01", "name": "Canada_garlic"}, - {"id": 19775, "synset": "keeled_garlic.n.01", "name": "keeled_garlic"}, - {"id": 19776, "synset": "shallot.n.02", "name": "shallot"}, - {"id": 19777, "synset": "nodding_onion.n.01", "name": "nodding_onion"}, - {"id": 19778, "synset": "welsh_onion.n.01", "name": "Welsh_onion"}, - {"id": 19779, "synset": "red-skinned_onion.n.01", "name": "red-skinned_onion"}, - {"id": 19780, "synset": "daffodil_garlic.n.01", "name": "daffodil_garlic"}, - {"id": 19781, "synset": "few-flowered_leek.n.01", "name": "few-flowered_leek"}, - {"id": 19782, "synset": "garlic.n.01", "name": "garlic"}, - {"id": 19783, "synset": "sand_leek.n.01", "name": "sand_leek"}, - {"id": 19784, "synset": "chives.n.01", "name": "chives"}, - {"id": 19785, "synset": "crow_garlic.n.01", "name": "crow_garlic"}, - {"id": 19786, "synset": "wild_garlic.n.01", "name": "wild_garlic"}, - {"id": 19787, "synset": "garlic_chive.n.01", "name": "garlic_chive"}, - {"id": 19788, "synset": "round-headed_leek.n.01", "name": "round-headed_leek"}, - {"id": 19789, "synset": "three-cornered_leek.n.01", "name": "three-cornered_leek"}, - {"id": 19790, "synset": "cape_aloe.n.01", "name": "cape_aloe"}, - {"id": 19791, "synset": "kniphofia.n.01", "name": "kniphofia"}, - {"id": 19792, "synset": "poker_plant.n.01", "name": "poker_plant"}, - {"id": 19793, "synset": "red-hot_poker.n.01", "name": "red-hot_poker"}, - {"id": 19794, "synset": "fly_poison.n.01", "name": "fly_poison"}, - {"id": 19795, "synset": "amber_lily.n.01", "name": "amber_lily"}, - {"id": 19796, "synset": "asparagus.n.01", "name": "asparagus"}, - {"id": 19797, "synset": "asparagus_fern.n.01", "name": "asparagus_fern"}, - {"id": 19798, "synset": "smilax.n.02", "name": "smilax"}, - {"id": 19799, "synset": "asphodel.n.01", "name": "asphodel"}, - {"id": 19800, "synset": "jacob's_rod.n.01", "name": "Jacob's_rod"}, - {"id": 19801, "synset": "aspidistra.n.01", "name": "aspidistra"}, - {"id": 19802, "synset": "coral_drops.n.01", "name": "coral_drops"}, - {"id": 19803, "synset": "christmas_bells.n.01", "name": "Christmas_bells"}, - {"id": 19804, "synset": "climbing_onion.n.01", "name": "climbing_onion"}, - {"id": 19805, "synset": "mariposa.n.01", "name": "mariposa"}, - {"id": 19806, "synset": "globe_lily.n.01", "name": "globe_lily"}, - {"id": 19807, "synset": "cat's-ear.n.01", "name": "cat's-ear"}, - {"id": 19808, "synset": "white_globe_lily.n.01", "name": "white_globe_lily"}, - {"id": 19809, "synset": "yellow_globe_lily.n.01", "name": "yellow_globe_lily"}, - {"id": 19810, "synset": "rose_globe_lily.n.01", "name": "rose_globe_lily"}, - {"id": 19811, "synset": "star_tulip.n.01", "name": "star_tulip"}, - {"id": 19812, "synset": "desert_mariposa_tulip.n.01", "name": "desert_mariposa_tulip"}, - {"id": 19813, "synset": "yellow_mariposa_tulip.n.01", "name": "yellow_mariposa_tulip"}, - {"id": 19814, "synset": "sagebrush_mariposa_tulip.n.01", "name": "sagebrush_mariposa_tulip"}, - {"id": 19815, "synset": "sego_lily.n.01", "name": "sego_lily"}, - {"id": 19816, "synset": "camas.n.01", "name": "camas"}, - {"id": 19817, "synset": "common_camas.n.01", "name": "common_camas"}, - {"id": 19818, "synset": "leichtlin's_camas.n.01", "name": "Leichtlin's_camas"}, - {"id": 19819, "synset": "wild_hyacinth.n.02", "name": "wild_hyacinth"}, - {"id": 19820, "synset": "dogtooth_violet.n.01", "name": "dogtooth_violet"}, - {"id": 19821, "synset": "white_dogtooth_violet.n.01", "name": "white_dogtooth_violet"}, - {"id": 19822, "synset": "yellow_adder's_tongue.n.01", "name": "yellow_adder's_tongue"}, - {"id": 19823, "synset": "european_dogtooth.n.01", "name": "European_dogtooth"}, - {"id": 19824, "synset": "fawn_lily.n.01", "name": "fawn_lily"}, - {"id": 19825, "synset": "glacier_lily.n.01", "name": "glacier_lily"}, - {"id": 19826, "synset": "avalanche_lily.n.01", "name": "avalanche_lily"}, - {"id": 19827, "synset": "fritillary.n.01", "name": "fritillary"}, - {"id": 19828, "synset": "mission_bells.n.02", "name": "mission_bells"}, - {"id": 19829, "synset": "mission_bells.n.01", "name": "mission_bells"}, - {"id": 19830, "synset": "stink_bell.n.01", "name": "stink_bell"}, - {"id": 19831, "synset": "crown_imperial.n.01", "name": "crown_imperial"}, - {"id": 19832, "synset": "white_fritillary.n.01", "name": "white_fritillary"}, - {"id": 19833, "synset": "snake's_head_fritillary.n.01", "name": "snake's_head_fritillary"}, - {"id": 19834, "synset": "adobe_lily.n.01", "name": "adobe_lily"}, - {"id": 19835, "synset": "scarlet_fritillary.n.01", "name": "scarlet_fritillary"}, - {"id": 19836, "synset": "tulip.n.01", "name": "tulip"}, - {"id": 19837, "synset": "dwarf_tulip.n.01", "name": "dwarf_tulip"}, - {"id": 19838, "synset": "lady_tulip.n.01", "name": "lady_tulip"}, - {"id": 19839, "synset": "tulipa_gesneriana.n.01", "name": "Tulipa_gesneriana"}, - {"id": 19840, "synset": "cottage_tulip.n.01", "name": "cottage_tulip"}, - {"id": 19841, "synset": "darwin_tulip.n.01", "name": "Darwin_tulip"}, - {"id": 19842, "synset": "gloriosa.n.01", "name": "gloriosa"}, - {"id": 19843, "synset": "lemon_lily.n.01", "name": "lemon_lily"}, - {"id": 19844, "synset": "common_hyacinth.n.01", "name": "common_hyacinth"}, - {"id": 19845, "synset": "roman_hyacinth.n.01", "name": "Roman_hyacinth"}, - {"id": 19846, "synset": "summer_hyacinth.n.01", "name": "summer_hyacinth"}, - {"id": 19847, "synset": "star-of-bethlehem.n.01", "name": "star-of-Bethlehem"}, - {"id": 19848, "synset": "bath_asparagus.n.01", "name": "bath_asparagus"}, - {"id": 19849, "synset": "grape_hyacinth.n.01", "name": "grape_hyacinth"}, - {"id": 19850, "synset": "common_grape_hyacinth.n.01", "name": "common_grape_hyacinth"}, - {"id": 19851, "synset": "tassel_hyacinth.n.01", "name": "tassel_hyacinth"}, - {"id": 19852, "synset": "scilla.n.01", "name": "scilla"}, - {"id": 19853, "synset": "spring_squill.n.01", "name": "spring_squill"}, - {"id": 19854, "synset": "false_asphodel.n.01", "name": "false_asphodel"}, - {"id": 19855, "synset": "scotch_asphodel.n.01", "name": "Scotch_asphodel"}, - {"id": 19856, "synset": "sea_squill.n.01", "name": "sea_squill"}, - {"id": 19857, "synset": "squill.n.01", "name": "squill"}, - {"id": 19858, "synset": "butcher's_broom.n.01", "name": "butcher's_broom"}, - {"id": 19859, "synset": "bog_asphodel.n.01", "name": "bog_asphodel"}, - {"id": 19860, "synset": "european_bog_asphodel.n.01", "name": "European_bog_asphodel"}, - {"id": 19861, "synset": "american_bog_asphodel.n.01", "name": "American_bog_asphodel"}, - {"id": 19862, "synset": "hellebore.n.01", "name": "hellebore"}, - {"id": 19863, "synset": "white_hellebore.n.01", "name": "white_hellebore"}, - {"id": 19864, "synset": "squaw_grass.n.01", "name": "squaw_grass"}, - {"id": 19865, "synset": "death_camas.n.01", "name": "death_camas"}, - {"id": 19866, "synset": "alkali_grass.n.01", "name": "alkali_grass"}, - {"id": 19867, "synset": "white_camas.n.01", "name": "white_camas"}, - {"id": 19868, "synset": "poison_camas.n.01", "name": "poison_camas"}, - {"id": 19869, "synset": "grassy_death_camas.n.01", "name": "grassy_death_camas"}, - {"id": 19870, "synset": "prairie_wake-robin.n.01", "name": "prairie_wake-robin"}, - {"id": 19871, "synset": "dwarf-white_trillium.n.01", "name": "dwarf-white_trillium"}, - {"id": 19872, "synset": "herb_paris.n.01", "name": "herb_Paris"}, - {"id": 19873, "synset": "sarsaparilla.n.01", "name": "sarsaparilla"}, - {"id": 19874, "synset": "bullbrier.n.01", "name": "bullbrier"}, - {"id": 19875, "synset": "rough_bindweed.n.01", "name": "rough_bindweed"}, - {"id": 19876, "synset": "clintonia.n.01", "name": "clintonia"}, - {"id": 19877, "synset": "false_lily_of_the_valley.n.02", "name": "false_lily_of_the_valley"}, - {"id": 19878, "synset": "false_lily_of_the_valley.n.01", "name": "false_lily_of_the_valley"}, - {"id": 19879, "synset": "solomon's-seal.n.01", "name": "Solomon's-seal"}, - {"id": 19880, "synset": "great_solomon's-seal.n.01", "name": "great_Solomon's-seal"}, - {"id": 19881, "synset": "bellwort.n.01", "name": "bellwort"}, - {"id": 19882, "synset": "strawflower.n.01", "name": "strawflower"}, - {"id": 19883, "synset": "pia.n.01", "name": "pia"}, - {"id": 19884, "synset": "agave.n.01", "name": "agave"}, - {"id": 19885, "synset": "american_agave.n.01", "name": "American_agave"}, - {"id": 19886, "synset": "sisal.n.02", "name": "sisal"}, - {"id": 19887, "synset": "maguey.n.02", "name": "maguey"}, - {"id": 19888, "synset": "maguey.n.01", "name": "maguey"}, - {"id": 19889, "synset": "agave_tequilana.n.01", "name": "Agave_tequilana"}, - {"id": 19890, "synset": "cabbage_tree.n.03", "name": "cabbage_tree"}, - {"id": 19891, "synset": "dracaena.n.01", "name": "dracaena"}, - {"id": 19892, "synset": "tuberose.n.01", "name": "tuberose"}, - {"id": 19893, "synset": "sansevieria.n.01", "name": "sansevieria"}, - {"id": 19894, "synset": "african_bowstring_hemp.n.01", "name": "African_bowstring_hemp"}, - {"id": 19895, "synset": "ceylon_bowstring_hemp.n.01", "name": "Ceylon_bowstring_hemp"}, - {"id": 19896, "synset": "mother-in-law's_tongue.n.01", "name": "mother-in-law's_tongue"}, - {"id": 19897, "synset": "spanish_bayonet.n.02", "name": "Spanish_bayonet"}, - {"id": 19898, "synset": "spanish_bayonet.n.01", "name": "Spanish_bayonet"}, - {"id": 19899, "synset": "joshua_tree.n.01", "name": "Joshua_tree"}, - {"id": 19900, "synset": "soapweed.n.01", "name": "soapweed"}, - {"id": 19901, "synset": "adam's_needle.n.01", "name": "Adam's_needle"}, - {"id": 19902, "synset": "bear_grass.n.02", "name": "bear_grass"}, - {"id": 19903, "synset": "spanish_dagger.n.01", "name": "Spanish_dagger"}, - {"id": 19904, "synset": "our_lord's_candle.n.01", "name": "Our_Lord's_candle"}, - {"id": 19905, "synset": "water_shamrock.n.01", "name": "water_shamrock"}, - {"id": 19906, "synset": "butterfly_bush.n.01", "name": "butterfly_bush"}, - {"id": 19907, "synset": "yellow_jasmine.n.01", "name": "yellow_jasmine"}, - {"id": 19908, "synset": "flax.n.02", "name": "flax"}, - {"id": 19909, "synset": "calabar_bean.n.01", "name": "calabar_bean"}, - {"id": 19910, "synset": "bonduc.n.02", "name": "bonduc"}, - {"id": 19911, "synset": "divi-divi.n.02", "name": "divi-divi"}, - {"id": 19912, "synset": "mysore_thorn.n.01", "name": "Mysore_thorn"}, - {"id": 19913, "synset": "brazilian_ironwood.n.01", "name": "brazilian_ironwood"}, - {"id": 19914, "synset": "bird_of_paradise.n.01", "name": "bird_of_paradise"}, - {"id": 19915, "synset": "shingle_tree.n.01", "name": "shingle_tree"}, - {"id": 19916, "synset": "mountain_ebony.n.01", "name": "mountain_ebony"}, - {"id": 19917, "synset": "msasa.n.01", "name": "msasa"}, - {"id": 19918, "synset": "cassia.n.01", "name": "cassia"}, - {"id": 19919, "synset": "golden_shower_tree.n.01", "name": "golden_shower_tree"}, - {"id": 19920, "synset": "pink_shower.n.01", "name": "pink_shower"}, - {"id": 19921, "synset": "rainbow_shower.n.01", "name": "rainbow_shower"}, - {"id": 19922, "synset": "horse_cassia.n.01", "name": "horse_cassia"}, - {"id": 19923, "synset": "carob.n.02", "name": "carob"}, - {"id": 19924, "synset": "carob.n.01", "name": "carob"}, - {"id": 19925, "synset": "paloverde.n.01", "name": "paloverde"}, - {"id": 19926, "synset": "royal_poinciana.n.01", "name": "royal_poinciana"}, - {"id": 19927, "synset": "locust_tree.n.01", "name": "locust_tree"}, - {"id": 19928, "synset": "water_locust.n.01", "name": "water_locust"}, - {"id": 19929, "synset": "honey_locust.n.01", "name": "honey_locust"}, - {"id": 19930, "synset": "kentucky_coffee_tree.n.01", "name": "Kentucky_coffee_tree"}, - {"id": 19931, "synset": "logwood.n.02", "name": "logwood"}, - {"id": 19932, "synset": "jerusalem_thorn.n.03", "name": "Jerusalem_thorn"}, - {"id": 19933, "synset": "palo_verde.n.01", "name": "palo_verde"}, - {"id": 19934, "synset": "dalmatian_laburnum.n.01", "name": "Dalmatian_laburnum"}, - {"id": 19935, "synset": "senna.n.01", "name": "senna"}, - {"id": 19936, "synset": "avaram.n.01", "name": "avaram"}, - {"id": 19937, "synset": "alexandria_senna.n.01", "name": "Alexandria_senna"}, - {"id": 19938, "synset": "wild_senna.n.01", "name": "wild_senna"}, - {"id": 19939, "synset": "sicklepod.n.01", "name": "sicklepod"}, - {"id": 19940, "synset": "coffee_senna.n.01", "name": "coffee_senna"}, - {"id": 19941, "synset": "tamarind.n.01", "name": "tamarind"}, - {"id": 19942, "synset": "false_indigo.n.03", "name": "false_indigo"}, - {"id": 19943, "synset": "false_indigo.n.02", "name": "false_indigo"}, - {"id": 19944, "synset": "hog_peanut.n.01", "name": "hog_peanut"}, - {"id": 19945, "synset": "angelim.n.01", "name": "angelim"}, - {"id": 19946, "synset": "cabbage_bark.n.01", "name": "cabbage_bark"}, - {"id": 19947, "synset": "kidney_vetch.n.01", "name": "kidney_vetch"}, - {"id": 19948, "synset": "groundnut.n.01", "name": "groundnut"}, - {"id": 19949, "synset": "rooibos.n.01", "name": "rooibos"}, - {"id": 19950, "synset": "milk_vetch.n.01", "name": "milk_vetch"}, - {"id": 19951, "synset": "alpine_milk_vetch.n.01", "name": "alpine_milk_vetch"}, - {"id": 19952, "synset": "purple_milk_vetch.n.01", "name": "purple_milk_vetch"}, - {"id": 19953, "synset": "camwood.n.01", "name": "camwood"}, - {"id": 19954, "synset": "wild_indigo.n.01", "name": "wild_indigo"}, - {"id": 19955, "synset": "blue_false_indigo.n.01", "name": "blue_false_indigo"}, - {"id": 19956, "synset": "white_false_indigo.n.01", "name": "white_false_indigo"}, - {"id": 19957, "synset": "indigo_broom.n.01", "name": "indigo_broom"}, - {"id": 19958, "synset": "dhak.n.01", "name": "dhak"}, - {"id": 19959, "synset": "pigeon_pea.n.01", "name": "pigeon_pea"}, - {"id": 19960, "synset": "sword_bean.n.01", "name": "sword_bean"}, - {"id": 19961, "synset": "pea_tree.n.01", "name": "pea_tree"}, - {"id": 19962, "synset": "siberian_pea_tree.n.01", "name": "Siberian_pea_tree"}, - {"id": 19963, "synset": "chinese_pea_tree.n.01", "name": "Chinese_pea_tree"}, - {"id": 19964, "synset": "moreton_bay_chestnut.n.01", "name": "Moreton_Bay_chestnut"}, - {"id": 19965, "synset": "butterfly_pea.n.03", "name": "butterfly_pea"}, - {"id": 19966, "synset": "judas_tree.n.01", "name": "Judas_tree"}, - {"id": 19967, "synset": "redbud.n.01", "name": "redbud"}, - {"id": 19968, "synset": "western_redbud.n.01", "name": "western_redbud"}, - {"id": 19969, "synset": "tagasaste.n.01", "name": "tagasaste"}, - {"id": 19970, "synset": "weeping_tree_broom.n.01", "name": "weeping_tree_broom"}, - {"id": 19971, "synset": "flame_pea.n.01", "name": "flame_pea"}, - {"id": 19972, "synset": "chickpea.n.02", "name": "chickpea"}, - {"id": 19973, "synset": "kentucky_yellowwood.n.01", "name": "Kentucky_yellowwood"}, - {"id": 19974, "synset": "glory_pea.n.01", "name": "glory_pea"}, - {"id": 19975, "synset": "desert_pea.n.01", "name": "desert_pea"}, - {"id": 19976, "synset": "parrot's_beak.n.01", "name": "parrot's_beak"}, - {"id": 19977, "synset": "butterfly_pea.n.02", "name": "butterfly_pea"}, - {"id": 19978, "synset": "blue_pea.n.01", "name": "blue_pea"}, - {"id": 19979, "synset": "telegraph_plant.n.01", "name": "telegraph_plant"}, - {"id": 19980, "synset": "bladder_senna.n.01", "name": "bladder_senna"}, - {"id": 19981, "synset": "axseed.n.01", "name": "axseed"}, - {"id": 19982, "synset": "crotalaria.n.01", "name": "crotalaria"}, - {"id": 19983, "synset": "guar.n.01", "name": "guar"}, - {"id": 19984, "synset": "white_broom.n.01", "name": "white_broom"}, - {"id": 19985, "synset": "common_broom.n.01", "name": "common_broom"}, - {"id": 19986, "synset": "rosewood.n.02", "name": "rosewood"}, - {"id": 19987, "synset": "indian_blackwood.n.01", "name": "Indian_blackwood"}, - {"id": 19988, "synset": "sissoo.n.01", "name": "sissoo"}, - {"id": 19989, "synset": "kingwood.n.02", "name": "kingwood"}, - {"id": 19990, "synset": "brazilian_rosewood.n.01", "name": "Brazilian_rosewood"}, - {"id": 19991, "synset": "cocobolo.n.01", "name": "cocobolo"}, - {"id": 19992, "synset": "blackwood.n.02", "name": "blackwood"}, - {"id": 19993, "synset": "bitter_pea.n.01", "name": "bitter_pea"}, - {"id": 19994, "synset": "derris.n.01", "name": "derris"}, - {"id": 19995, "synset": "derris_root.n.01", "name": "derris_root"}, - {"id": 19996, "synset": "prairie_mimosa.n.01", "name": "prairie_mimosa"}, - {"id": 19997, "synset": "tick_trefoil.n.01", "name": "tick_trefoil"}, - {"id": 19998, "synset": "beggarweed.n.01", "name": "beggarweed"}, - {"id": 19999, "synset": "australian_pea.n.01", "name": "Australian_pea"}, - {"id": 20000, "synset": "coral_tree.n.01", "name": "coral_tree"}, - {"id": 20001, "synset": "kaffir_boom.n.02", "name": "kaffir_boom"}, - {"id": 20002, "synset": "coral_bean_tree.n.01", "name": "coral_bean_tree"}, - {"id": 20003, "synset": "ceibo.n.01", "name": "ceibo"}, - {"id": 20004, "synset": "kaffir_boom.n.01", "name": "kaffir_boom"}, - {"id": 20005, "synset": "indian_coral_tree.n.01", "name": "Indian_coral_tree"}, - {"id": 20006, "synset": "cork_tree.n.02", "name": "cork_tree"}, - {"id": 20007, "synset": "goat's_rue.n.02", "name": "goat's_rue"}, - {"id": 20008, "synset": "poison_bush.n.01", "name": "poison_bush"}, - {"id": 20009, "synset": "spanish_broom.n.02", "name": "Spanish_broom"}, - {"id": 20010, "synset": "woodwaxen.n.01", "name": "woodwaxen"}, - {"id": 20011, "synset": "chanar.n.01", "name": "chanar"}, - {"id": 20012, "synset": "gliricidia.n.01", "name": "gliricidia"}, - {"id": 20013, "synset": "soy.n.01", "name": "soy"}, - {"id": 20014, "synset": "licorice.n.01", "name": "licorice"}, - {"id": 20015, "synset": "wild_licorice.n.02", "name": "wild_licorice"}, - {"id": 20016, "synset": "licorice_root.n.01", "name": "licorice_root"}, - { - "id": 20017, - "synset": "western_australia_coral_pea.n.01", - "name": "Western_Australia_coral_pea", - }, - {"id": 20018, "synset": "sweet_vetch.n.01", "name": "sweet_vetch"}, - {"id": 20019, "synset": "french_honeysuckle.n.02", "name": "French_honeysuckle"}, - {"id": 20020, "synset": "anil.n.02", "name": "anil"}, - {"id": 20021, "synset": "scarlet_runner.n.02", "name": "scarlet_runner"}, - {"id": 20022, "synset": "hyacinth_bean.n.01", "name": "hyacinth_bean"}, - {"id": 20023, "synset": "scotch_laburnum.n.01", "name": "Scotch_laburnum"}, - {"id": 20024, "synset": "vetchling.n.01", "name": "vetchling"}, - {"id": 20025, "synset": "wild_pea.n.01", "name": "wild_pea"}, - {"id": 20026, "synset": "everlasting_pea.n.01", "name": "everlasting_pea"}, - {"id": 20027, "synset": "beach_pea.n.01", "name": "beach_pea"}, - {"id": 20028, "synset": "grass_vetch.n.01", "name": "grass_vetch"}, - {"id": 20029, "synset": "marsh_pea.n.01", "name": "marsh_pea"}, - {"id": 20030, "synset": "common_vetchling.n.01", "name": "common_vetchling"}, - {"id": 20031, "synset": "grass_pea.n.01", "name": "grass_pea"}, - {"id": 20032, "synset": "tangier_pea.n.01", "name": "Tangier_pea"}, - {"id": 20033, "synset": "heath_pea.n.01", "name": "heath_pea"}, - {"id": 20034, "synset": "bicolor_lespediza.n.01", "name": "bicolor_lespediza"}, - {"id": 20035, "synset": "japanese_clover.n.01", "name": "japanese_clover"}, - {"id": 20036, "synset": "korean_lespedeza.n.01", "name": "Korean_lespedeza"}, - {"id": 20037, "synset": "sericea_lespedeza.n.01", "name": "sericea_lespedeza"}, - {"id": 20038, "synset": "lentil.n.03", "name": "lentil"}, - {"id": 20039, "synset": "lentil.n.02", "name": "lentil"}, - { - "id": 20040, - "synset": "prairie_bird's-foot_trefoil.n.01", - "name": "prairie_bird's-foot_trefoil", - }, - {"id": 20041, "synset": "bird's_foot_trefoil.n.02", "name": "bird's_foot_trefoil"}, - {"id": 20042, "synset": "winged_pea.n.02", "name": "winged_pea"}, - {"id": 20043, "synset": "lupine.n.01", "name": "lupine"}, - {"id": 20044, "synset": "white_lupine.n.01", "name": "white_lupine"}, - {"id": 20045, "synset": "tree_lupine.n.01", "name": "tree_lupine"}, - {"id": 20046, "synset": "wild_lupine.n.01", "name": "wild_lupine"}, - {"id": 20047, "synset": "bluebonnet.n.01", "name": "bluebonnet"}, - {"id": 20048, "synset": "texas_bluebonnet.n.01", "name": "Texas_bluebonnet"}, - {"id": 20049, "synset": "medic.n.01", "name": "medic"}, - {"id": 20050, "synset": "moon_trefoil.n.01", "name": "moon_trefoil"}, - {"id": 20051, "synset": "sickle_alfalfa.n.01", "name": "sickle_alfalfa"}, - {"id": 20052, "synset": "calvary_clover.n.01", "name": "Calvary_clover"}, - {"id": 20053, "synset": "black_medick.n.01", "name": "black_medick"}, - {"id": 20054, "synset": "alfalfa.n.01", "name": "alfalfa"}, - {"id": 20055, "synset": "millettia.n.01", "name": "millettia"}, - {"id": 20056, "synset": "mucuna.n.01", "name": "mucuna"}, - {"id": 20057, "synset": "cowage.n.02", "name": "cowage"}, - {"id": 20058, "synset": "tolu_tree.n.01", "name": "tolu_tree"}, - {"id": 20059, "synset": "peruvian_balsam.n.01", "name": "Peruvian_balsam"}, - {"id": 20060, "synset": "sainfoin.n.01", "name": "sainfoin"}, - {"id": 20061, "synset": "restharrow.n.02", "name": "restharrow"}, - {"id": 20062, "synset": "bead_tree.n.01", "name": "bead_tree"}, - {"id": 20063, "synset": "jumby_bead.n.01", "name": "jumby_bead"}, - {"id": 20064, "synset": "locoweed.n.01", "name": "locoweed"}, - {"id": 20065, "synset": "purple_locoweed.n.01", "name": "purple_locoweed"}, - {"id": 20066, "synset": "tumbleweed.n.01", "name": "tumbleweed"}, - {"id": 20067, "synset": "yam_bean.n.02", "name": "yam_bean"}, - {"id": 20068, "synset": "shamrock_pea.n.01", "name": "shamrock_pea"}, - {"id": 20069, "synset": "pole_bean.n.01", "name": "pole_bean"}, - {"id": 20070, "synset": "kidney_bean.n.01", "name": "kidney_bean"}, - {"id": 20071, "synset": "haricot.n.01", "name": "haricot"}, - {"id": 20072, "synset": "wax_bean.n.01", "name": "wax_bean"}, - {"id": 20073, "synset": "scarlet_runner.n.01", "name": "scarlet_runner"}, - {"id": 20074, "synset": "lima_bean.n.02", "name": "lima_bean"}, - {"id": 20075, "synset": "sieva_bean.n.01", "name": "sieva_bean"}, - {"id": 20076, "synset": "tepary_bean.n.01", "name": "tepary_bean"}, - {"id": 20077, "synset": "chaparral_pea.n.01", "name": "chaparral_pea"}, - {"id": 20078, "synset": "jamaica_dogwood.n.01", "name": "Jamaica_dogwood"}, - {"id": 20079, "synset": "pea.n.02", "name": "pea"}, - {"id": 20080, "synset": "garden_pea.n.01", "name": "garden_pea"}, - {"id": 20081, "synset": "edible-pod_pea.n.01", "name": "edible-pod_pea"}, - {"id": 20082, "synset": "sugar_snap_pea.n.01", "name": "sugar_snap_pea"}, - {"id": 20083, "synset": "field_pea.n.02", "name": "field_pea"}, - {"id": 20084, "synset": "field_pea.n.01", "name": "field_pea"}, - {"id": 20085, "synset": "common_flat_pea.n.01", "name": "common_flat_pea"}, - {"id": 20086, "synset": "quira.n.02", "name": "quira"}, - {"id": 20087, "synset": "roble.n.01", "name": "roble"}, - {"id": 20088, "synset": "panama_redwood_tree.n.01", "name": "Panama_redwood_tree"}, - {"id": 20089, "synset": "indian_beech.n.01", "name": "Indian_beech"}, - {"id": 20090, "synset": "winged_bean.n.01", "name": "winged_bean"}, - {"id": 20091, "synset": "breadroot.n.01", "name": "breadroot"}, - {"id": 20092, "synset": "bloodwood_tree.n.01", "name": "bloodwood_tree"}, - {"id": 20093, "synset": "kino.n.02", "name": "kino"}, - {"id": 20094, "synset": "red_sandalwood.n.02", "name": "red_sandalwood"}, - {"id": 20095, "synset": "kudzu.n.01", "name": "kudzu"}, - {"id": 20096, "synset": "bristly_locust.n.01", "name": "bristly_locust"}, - {"id": 20097, "synset": "black_locust.n.02", "name": "black_locust"}, - {"id": 20098, "synset": "clammy_locust.n.01", "name": "clammy_locust"}, - {"id": 20099, "synset": "carib_wood.n.01", "name": "carib_wood"}, - {"id": 20100, "synset": "colorado_river_hemp.n.01", "name": "Colorado_River_hemp"}, - {"id": 20101, "synset": "scarlet_wisteria_tree.n.01", "name": "scarlet_wisteria_tree"}, - {"id": 20102, "synset": "japanese_pagoda_tree.n.01", "name": "Japanese_pagoda_tree"}, - {"id": 20103, "synset": "mescal_bean.n.01", "name": "mescal_bean"}, - {"id": 20104, "synset": "kowhai.n.01", "name": "kowhai"}, - {"id": 20105, "synset": "jade_vine.n.01", "name": "jade_vine"}, - {"id": 20106, "synset": "hoary_pea.n.01", "name": "hoary_pea"}, - {"id": 20107, "synset": "bastard_indigo.n.01", "name": "bastard_indigo"}, - {"id": 20108, "synset": "catgut.n.01", "name": "catgut"}, - {"id": 20109, "synset": "bush_pea.n.01", "name": "bush_pea"}, - {"id": 20110, "synset": "false_lupine.n.01", "name": "false_lupine"}, - {"id": 20111, "synset": "carolina_lupine.n.01", "name": "Carolina_lupine"}, - {"id": 20112, "synset": "tipu.n.01", "name": "tipu"}, - {"id": 20113, "synset": "bird's_foot_trefoil.n.01", "name": "bird's_foot_trefoil"}, - {"id": 20114, "synset": "fenugreek.n.01", "name": "fenugreek"}, - {"id": 20115, "synset": "gorse.n.01", "name": "gorse"}, - {"id": 20116, "synset": "vetch.n.01", "name": "vetch"}, - {"id": 20117, "synset": "tufted_vetch.n.01", "name": "tufted_vetch"}, - {"id": 20118, "synset": "broad_bean.n.01", "name": "broad_bean"}, - {"id": 20119, "synset": "bitter_betch.n.01", "name": "bitter_betch"}, - {"id": 20120, "synset": "bush_vetch.n.01", "name": "bush_vetch"}, - {"id": 20121, "synset": "moth_bean.n.01", "name": "moth_bean"}, - {"id": 20122, "synset": "snailflower.n.01", "name": "snailflower"}, - {"id": 20123, "synset": "mung.n.01", "name": "mung"}, - {"id": 20124, "synset": "cowpea.n.02", "name": "cowpea"}, - {"id": 20125, "synset": "cowpea.n.01", "name": "cowpea"}, - {"id": 20126, "synset": "asparagus_bean.n.01", "name": "asparagus_bean"}, - {"id": 20127, "synset": "swamp_oak.n.01", "name": "swamp_oak"}, - {"id": 20128, "synset": "keurboom.n.02", "name": "keurboom"}, - {"id": 20129, "synset": "keurboom.n.01", "name": "keurboom"}, - {"id": 20130, "synset": "japanese_wistaria.n.01", "name": "Japanese_wistaria"}, - {"id": 20131, "synset": "chinese_wistaria.n.01", "name": "Chinese_wistaria"}, - {"id": 20132, "synset": "american_wistaria.n.01", "name": "American_wistaria"}, - {"id": 20133, "synset": "silky_wisteria.n.01", "name": "silky_wisteria"}, - {"id": 20134, "synset": "palm.n.03", "name": "palm"}, - {"id": 20135, "synset": "sago_palm.n.01", "name": "sago_palm"}, - {"id": 20136, "synset": "feather_palm.n.01", "name": "feather_palm"}, - {"id": 20137, "synset": "fan_palm.n.01", "name": "fan_palm"}, - {"id": 20138, "synset": "palmetto.n.01", "name": "palmetto"}, - {"id": 20139, "synset": "coyol.n.01", "name": "coyol"}, - {"id": 20140, "synset": "grugru.n.01", "name": "grugru"}, - {"id": 20141, "synset": "areca.n.01", "name": "areca"}, - {"id": 20142, "synset": "betel_palm.n.01", "name": "betel_palm"}, - {"id": 20143, "synset": "sugar_palm.n.01", "name": "sugar_palm"}, - {"id": 20144, "synset": "piassava_palm.n.01", "name": "piassava_palm"}, - {"id": 20145, "synset": "coquilla_nut.n.01", "name": "coquilla_nut"}, - {"id": 20146, "synset": "palmyra.n.01", "name": "palmyra"}, - {"id": 20147, "synset": "calamus.n.01", "name": "calamus"}, - {"id": 20148, "synset": "rattan.n.01", "name": "rattan"}, - {"id": 20149, "synset": "lawyer_cane.n.01", "name": "lawyer_cane"}, - {"id": 20150, "synset": "fishtail_palm.n.01", "name": "fishtail_palm"}, - {"id": 20151, "synset": "wine_palm.n.01", "name": "wine_palm"}, - {"id": 20152, "synset": "wax_palm.n.03", "name": "wax_palm"}, - {"id": 20153, "synset": "coconut.n.03", "name": "coconut"}, - {"id": 20154, "synset": "carnauba.n.02", "name": "carnauba"}, - {"id": 20155, "synset": "caranday.n.01", "name": "caranday"}, - {"id": 20156, "synset": "corozo.n.01", "name": "corozo"}, - {"id": 20157, "synset": "gebang_palm.n.01", "name": "gebang_palm"}, - {"id": 20158, "synset": "latanier.n.01", "name": "latanier"}, - {"id": 20159, "synset": "talipot.n.01", "name": "talipot"}, - {"id": 20160, "synset": "oil_palm.n.01", "name": "oil_palm"}, - {"id": 20161, "synset": "african_oil_palm.n.01", "name": "African_oil_palm"}, - {"id": 20162, "synset": "american_oil_palm.n.01", "name": "American_oil_palm"}, - {"id": 20163, "synset": "palm_nut.n.01", "name": "palm_nut"}, - {"id": 20164, "synset": "cabbage_palm.n.04", "name": "cabbage_palm"}, - {"id": 20165, "synset": "cabbage_palm.n.03", "name": "cabbage_palm"}, - {"id": 20166, "synset": "true_sago_palm.n.01", "name": "true_sago_palm"}, - {"id": 20167, "synset": "nipa_palm.n.01", "name": "nipa_palm"}, - {"id": 20168, "synset": "babassu.n.01", "name": "babassu"}, - {"id": 20169, "synset": "babassu_nut.n.01", "name": "babassu_nut"}, - {"id": 20170, "synset": "cohune_palm.n.01", "name": "cohune_palm"}, - {"id": 20171, "synset": "cohune_nut.n.01", "name": "cohune_nut"}, - {"id": 20172, "synset": "date_palm.n.01", "name": "date_palm"}, - {"id": 20173, "synset": "ivory_palm.n.01", "name": "ivory_palm"}, - {"id": 20174, "synset": "raffia_palm.n.01", "name": "raffia_palm"}, - {"id": 20175, "synset": "bamboo_palm.n.02", "name": "bamboo_palm"}, - {"id": 20176, "synset": "lady_palm.n.01", "name": "lady_palm"}, - {"id": 20177, "synset": "miniature_fan_palm.n.01", "name": "miniature_fan_palm"}, - {"id": 20178, "synset": "reed_rhapis.n.01", "name": "reed_rhapis"}, - {"id": 20179, "synset": "royal_palm.n.01", "name": "royal_palm"}, - {"id": 20180, "synset": "cabbage_palm.n.02", "name": "cabbage_palm"}, - {"id": 20181, "synset": "cabbage_palmetto.n.01", "name": "cabbage_palmetto"}, - {"id": 20182, "synset": "saw_palmetto.n.01", "name": "saw_palmetto"}, - {"id": 20183, "synset": "thatch_palm.n.01", "name": "thatch_palm"}, - {"id": 20184, "synset": "key_palm.n.01", "name": "key_palm"}, - {"id": 20185, "synset": "english_plantain.n.01", "name": "English_plantain"}, - {"id": 20186, "synset": "broad-leaved_plantain.n.02", "name": "broad-leaved_plantain"}, - {"id": 20187, "synset": "hoary_plantain.n.02", "name": "hoary_plantain"}, - {"id": 20188, "synset": "fleawort.n.01", "name": "fleawort"}, - {"id": 20189, "synset": "rugel's_plantain.n.01", "name": "rugel's_plantain"}, - {"id": 20190, "synset": "hoary_plantain.n.01", "name": "hoary_plantain"}, - {"id": 20191, "synset": "buckwheat.n.01", "name": "buckwheat"}, - {"id": 20192, "synset": "prince's-feather.n.01", "name": "prince's-feather"}, - {"id": 20193, "synset": "eriogonum.n.01", "name": "eriogonum"}, - {"id": 20194, "synset": "umbrella_plant.n.02", "name": "umbrella_plant"}, - {"id": 20195, "synset": "wild_buckwheat.n.01", "name": "wild_buckwheat"}, - {"id": 20196, "synset": "rhubarb.n.02", "name": "rhubarb"}, - {"id": 20197, "synset": "himalayan_rhubarb.n.01", "name": "Himalayan_rhubarb"}, - {"id": 20198, "synset": "pie_plant.n.01", "name": "pie_plant"}, - {"id": 20199, "synset": "chinese_rhubarb.n.01", "name": "Chinese_rhubarb"}, - {"id": 20200, "synset": "sour_dock.n.01", "name": "sour_dock"}, - {"id": 20201, "synset": "sheep_sorrel.n.01", "name": "sheep_sorrel"}, - {"id": 20202, "synset": "bitter_dock.n.01", "name": "bitter_dock"}, - {"id": 20203, "synset": "french_sorrel.n.01", "name": "French_sorrel"}, - {"id": 20204, "synset": "yellow-eyed_grass.n.01", "name": "yellow-eyed_grass"}, - {"id": 20205, "synset": "commelina.n.01", "name": "commelina"}, - {"id": 20206, "synset": "spiderwort.n.01", "name": "spiderwort"}, - {"id": 20207, "synset": "pineapple.n.01", "name": "pineapple"}, - {"id": 20208, "synset": "pipewort.n.01", "name": "pipewort"}, - {"id": 20209, "synset": "water_hyacinth.n.01", "name": "water_hyacinth"}, - {"id": 20210, "synset": "water_star_grass.n.01", "name": "water_star_grass"}, - {"id": 20211, "synset": "naiad.n.01", "name": "naiad"}, - {"id": 20212, "synset": "water_plantain.n.01", "name": "water_plantain"}, - { - "id": 20213, - "synset": "narrow-leaved_water_plantain.n.01", - "name": "narrow-leaved_water_plantain", - }, - {"id": 20214, "synset": "hydrilla.n.01", "name": "hydrilla"}, - {"id": 20215, "synset": "american_frogbit.n.01", "name": "American_frogbit"}, - {"id": 20216, "synset": "waterweed.n.01", "name": "waterweed"}, - {"id": 20217, "synset": "canadian_pondweed.n.01", "name": "Canadian_pondweed"}, - {"id": 20218, "synset": "tape_grass.n.01", "name": "tape_grass"}, - {"id": 20219, "synset": "pondweed.n.01", "name": "pondweed"}, - {"id": 20220, "synset": "curled_leaf_pondweed.n.01", "name": "curled_leaf_pondweed"}, - {"id": 20221, "synset": "loddon_pondweed.n.01", "name": "loddon_pondweed"}, - {"id": 20222, "synset": "frog's_lettuce.n.01", "name": "frog's_lettuce"}, - {"id": 20223, "synset": "arrow_grass.n.01", "name": "arrow_grass"}, - {"id": 20224, "synset": "horned_pondweed.n.01", "name": "horned_pondweed"}, - {"id": 20225, "synset": "eelgrass.n.01", "name": "eelgrass"}, - {"id": 20226, "synset": "rose.n.01", "name": "rose"}, - {"id": 20227, "synset": "hip.n.05", "name": "hip"}, - {"id": 20228, "synset": "banksia_rose.n.01", "name": "banksia_rose"}, - {"id": 20229, "synset": "damask_rose.n.01", "name": "damask_rose"}, - {"id": 20230, "synset": "sweetbrier.n.01", "name": "sweetbrier"}, - {"id": 20231, "synset": "cherokee_rose.n.01", "name": "Cherokee_rose"}, - {"id": 20232, "synset": "musk_rose.n.01", "name": "musk_rose"}, - {"id": 20233, "synset": "agrimonia.n.01", "name": "agrimonia"}, - {"id": 20234, "synset": "harvest-lice.n.01", "name": "harvest-lice"}, - {"id": 20235, "synset": "fragrant_agrimony.n.01", "name": "fragrant_agrimony"}, - {"id": 20236, "synset": "alderleaf_juneberry.n.01", "name": "alderleaf_Juneberry"}, - {"id": 20237, "synset": "flowering_quince.n.01", "name": "flowering_quince"}, - {"id": 20238, "synset": "japonica.n.02", "name": "japonica"}, - {"id": 20239, "synset": "coco_plum.n.01", "name": "coco_plum"}, - {"id": 20240, "synset": "cotoneaster.n.01", "name": "cotoneaster"}, - {"id": 20241, "synset": "cotoneaster_dammeri.n.01", "name": "Cotoneaster_dammeri"}, - {"id": 20242, "synset": "cotoneaster_horizontalis.n.01", "name": "Cotoneaster_horizontalis"}, - {"id": 20243, "synset": "parsley_haw.n.01", "name": "parsley_haw"}, - {"id": 20244, "synset": "scarlet_haw.n.01", "name": "scarlet_haw"}, - {"id": 20245, "synset": "blackthorn.n.02", "name": "blackthorn"}, - {"id": 20246, "synset": "cockspur_thorn.n.01", "name": "cockspur_thorn"}, - {"id": 20247, "synset": "mayhaw.n.01", "name": "mayhaw"}, - {"id": 20248, "synset": "red_haw.n.02", "name": "red_haw"}, - {"id": 20249, "synset": "red_haw.n.01", "name": "red_haw"}, - {"id": 20250, "synset": "quince.n.01", "name": "quince"}, - {"id": 20251, "synset": "mountain_avens.n.01", "name": "mountain_avens"}, - {"id": 20252, "synset": "loquat.n.01", "name": "loquat"}, - {"id": 20253, "synset": "beach_strawberry.n.01", "name": "beach_strawberry"}, - {"id": 20254, "synset": "virginia_strawberry.n.01", "name": "Virginia_strawberry"}, - {"id": 20255, "synset": "avens.n.01", "name": "avens"}, - {"id": 20256, "synset": "yellow_avens.n.02", "name": "yellow_avens"}, - {"id": 20257, "synset": "yellow_avens.n.01", "name": "yellow_avens"}, - {"id": 20258, "synset": "prairie_smoke.n.01", "name": "prairie_smoke"}, - {"id": 20259, "synset": "bennet.n.01", "name": "bennet"}, - {"id": 20260, "synset": "toyon.n.01", "name": "toyon"}, - {"id": 20261, "synset": "apple_tree.n.01", "name": "apple_tree"}, - {"id": 20262, "synset": "apple.n.02", "name": "apple"}, - {"id": 20263, "synset": "wild_apple.n.01", "name": "wild_apple"}, - {"id": 20264, "synset": "crab_apple.n.01", "name": "crab_apple"}, - {"id": 20265, "synset": "siberian_crab.n.01", "name": "Siberian_crab"}, - {"id": 20266, "synset": "wild_crab.n.01", "name": "wild_crab"}, - {"id": 20267, "synset": "american_crab_apple.n.01", "name": "American_crab_apple"}, - {"id": 20268, "synset": "oregon_crab_apple.n.01", "name": "Oregon_crab_apple"}, - {"id": 20269, "synset": "southern_crab_apple.n.01", "name": "Southern_crab_apple"}, - {"id": 20270, "synset": "iowa_crab.n.01", "name": "Iowa_crab"}, - {"id": 20271, "synset": "bechtel_crab.n.01", "name": "Bechtel_crab"}, - {"id": 20272, "synset": "medlar.n.02", "name": "medlar"}, - {"id": 20273, "synset": "cinquefoil.n.01", "name": "cinquefoil"}, - {"id": 20274, "synset": "silverweed.n.02", "name": "silverweed"}, - {"id": 20275, "synset": "salad_burnet.n.01", "name": "salad_burnet"}, - {"id": 20276, "synset": "plum.n.01", "name": "plum"}, - {"id": 20277, "synset": "wild_plum.n.01", "name": "wild_plum"}, - {"id": 20278, "synset": "allegheny_plum.n.01", "name": "Allegheny_plum"}, - {"id": 20279, "synset": "american_red_plum.n.01", "name": "American_red_plum"}, - {"id": 20280, "synset": "chickasaw_plum.n.01", "name": "chickasaw_plum"}, - {"id": 20281, "synset": "beach_plum.n.01", "name": "beach_plum"}, - {"id": 20282, "synset": "common_plum.n.01", "name": "common_plum"}, - {"id": 20283, "synset": "bullace.n.01", "name": "bullace"}, - {"id": 20284, "synset": "damson_plum.n.02", "name": "damson_plum"}, - {"id": 20285, "synset": "big-tree_plum.n.01", "name": "big-tree_plum"}, - {"id": 20286, "synset": "canada_plum.n.01", "name": "Canada_plum"}, - {"id": 20287, "synset": "plumcot.n.01", "name": "plumcot"}, - {"id": 20288, "synset": "apricot.n.01", "name": "apricot"}, - {"id": 20289, "synset": "japanese_apricot.n.01", "name": "Japanese_apricot"}, - {"id": 20290, "synset": "common_apricot.n.01", "name": "common_apricot"}, - {"id": 20291, "synset": "purple_apricot.n.01", "name": "purple_apricot"}, - {"id": 20292, "synset": "cherry.n.02", "name": "cherry"}, - {"id": 20293, "synset": "wild_cherry.n.02", "name": "wild_cherry"}, - {"id": 20294, "synset": "wild_cherry.n.01", "name": "wild_cherry"}, - {"id": 20295, "synset": "sweet_cherry.n.01", "name": "sweet_cherry"}, - {"id": 20296, "synset": "heart_cherry.n.01", "name": "heart_cherry"}, - {"id": 20297, "synset": "gean.n.01", "name": "gean"}, - {"id": 20298, "synset": "capulin.n.01", "name": "capulin"}, - {"id": 20299, "synset": "cherry_laurel.n.02", "name": "cherry_laurel"}, - {"id": 20300, "synset": "cherry_plum.n.01", "name": "cherry_plum"}, - {"id": 20301, "synset": "sour_cherry.n.01", "name": "sour_cherry"}, - {"id": 20302, "synset": "amarelle.n.01", "name": "amarelle"}, - {"id": 20303, "synset": "morello.n.01", "name": "morello"}, - {"id": 20304, "synset": "marasca.n.01", "name": "marasca"}, - {"id": 20305, "synset": "almond_tree.n.01", "name": "almond_tree"}, - {"id": 20306, "synset": "almond.n.01", "name": "almond"}, - {"id": 20307, "synset": "bitter_almond.n.01", "name": "bitter_almond"}, - {"id": 20308, "synset": "jordan_almond.n.01", "name": "jordan_almond"}, - {"id": 20309, "synset": "dwarf_flowering_almond.n.01", "name": "dwarf_flowering_almond"}, - {"id": 20310, "synset": "holly-leaved_cherry.n.01", "name": "holly-leaved_cherry"}, - {"id": 20311, "synset": "fuji.n.01", "name": "fuji"}, - {"id": 20312, "synset": "flowering_almond.n.02", "name": "flowering_almond"}, - {"id": 20313, "synset": "cherry_laurel.n.01", "name": "cherry_laurel"}, - {"id": 20314, "synset": "catalina_cherry.n.01", "name": "Catalina_cherry"}, - {"id": 20315, "synset": "bird_cherry.n.01", "name": "bird_cherry"}, - {"id": 20316, "synset": "hagberry_tree.n.01", "name": "hagberry_tree"}, - {"id": 20317, "synset": "hagberry.n.01", "name": "hagberry"}, - {"id": 20318, "synset": "pin_cherry.n.01", "name": "pin_cherry"}, - {"id": 20319, "synset": "peach.n.01", "name": "peach"}, - {"id": 20320, "synset": "nectarine.n.01", "name": "nectarine"}, - {"id": 20321, "synset": "sand_cherry.n.01", "name": "sand_cherry"}, - {"id": 20322, "synset": "japanese_plum.n.01", "name": "Japanese_plum"}, - {"id": 20323, "synset": "black_cherry.n.01", "name": "black_cherry"}, - {"id": 20324, "synset": "flowering_cherry.n.01", "name": "flowering_cherry"}, - {"id": 20325, "synset": "oriental_cherry.n.01", "name": "oriental_cherry"}, - {"id": 20326, "synset": "japanese_flowering_cherry.n.01", "name": "Japanese_flowering_cherry"}, - {"id": 20327, "synset": "sierra_plum.n.01", "name": "Sierra_plum"}, - {"id": 20328, "synset": "rosebud_cherry.n.01", "name": "rosebud_cherry"}, - {"id": 20329, "synset": "russian_almond.n.01", "name": "Russian_almond"}, - {"id": 20330, "synset": "flowering_almond.n.01", "name": "flowering_almond"}, - {"id": 20331, "synset": "chokecherry.n.02", "name": "chokecherry"}, - {"id": 20332, "synset": "chokecherry.n.01", "name": "chokecherry"}, - {"id": 20333, "synset": "western_chokecherry.n.01", "name": "western_chokecherry"}, - {"id": 20334, "synset": "pyracantha.n.01", "name": "Pyracantha"}, - {"id": 20335, "synset": "pear.n.02", "name": "pear"}, - {"id": 20336, "synset": "fruit_tree.n.01", "name": "fruit_tree"}, - {"id": 20337, "synset": "bramble_bush.n.01", "name": "bramble_bush"}, - {"id": 20338, "synset": "lawyerbush.n.01", "name": "lawyerbush"}, - {"id": 20339, "synset": "stone_bramble.n.01", "name": "stone_bramble"}, - {"id": 20340, "synset": "sand_blackberry.n.01", "name": "sand_blackberry"}, - {"id": 20341, "synset": "boysenberry.n.01", "name": "boysenberry"}, - {"id": 20342, "synset": "loganberry.n.01", "name": "loganberry"}, - {"id": 20343, "synset": "american_dewberry.n.02", "name": "American_dewberry"}, - {"id": 20344, "synset": "northern_dewberry.n.01", "name": "Northern_dewberry"}, - {"id": 20345, "synset": "southern_dewberry.n.01", "name": "Southern_dewberry"}, - {"id": 20346, "synset": "swamp_dewberry.n.01", "name": "swamp_dewberry"}, - {"id": 20347, "synset": "european_dewberry.n.01", "name": "European_dewberry"}, - {"id": 20348, "synset": "raspberry.n.01", "name": "raspberry"}, - {"id": 20349, "synset": "wild_raspberry.n.01", "name": "wild_raspberry"}, - {"id": 20350, "synset": "american_raspberry.n.01", "name": "American_raspberry"}, - {"id": 20351, "synset": "black_raspberry.n.01", "name": "black_raspberry"}, - {"id": 20352, "synset": "salmonberry.n.03", "name": "salmonberry"}, - {"id": 20353, "synset": "salmonberry.n.02", "name": "salmonberry"}, - {"id": 20354, "synset": "wineberry.n.01", "name": "wineberry"}, - {"id": 20355, "synset": "mountain_ash.n.01", "name": "mountain_ash"}, - {"id": 20356, "synset": "rowan.n.01", "name": "rowan"}, - {"id": 20357, "synset": "rowanberry.n.01", "name": "rowanberry"}, - {"id": 20358, "synset": "american_mountain_ash.n.01", "name": "American_mountain_ash"}, - {"id": 20359, "synset": "western_mountain_ash.n.01", "name": "Western_mountain_ash"}, - {"id": 20360, "synset": "service_tree.n.01", "name": "service_tree"}, - {"id": 20361, "synset": "wild_service_tree.n.01", "name": "wild_service_tree"}, - {"id": 20362, "synset": "spirea.n.02", "name": "spirea"}, - {"id": 20363, "synset": "bridal_wreath.n.02", "name": "bridal_wreath"}, - {"id": 20364, "synset": "madderwort.n.01", "name": "madderwort"}, - {"id": 20365, "synset": "indian_madder.n.01", "name": "Indian_madder"}, - {"id": 20366, "synset": "madder.n.01", "name": "madder"}, - {"id": 20367, "synset": "woodruff.n.02", "name": "woodruff"}, - {"id": 20368, "synset": "dagame.n.01", "name": "dagame"}, - {"id": 20369, "synset": "blolly.n.01", "name": "blolly"}, - {"id": 20370, "synset": "coffee.n.02", "name": "coffee"}, - {"id": 20371, "synset": "arabian_coffee.n.01", "name": "Arabian_coffee"}, - {"id": 20372, "synset": "liberian_coffee.n.01", "name": "Liberian_coffee"}, - {"id": 20373, "synset": "robusta_coffee.n.01", "name": "robusta_coffee"}, - {"id": 20374, "synset": "cinchona.n.02", "name": "cinchona"}, - {"id": 20375, "synset": "cartagena_bark.n.01", "name": "Cartagena_bark"}, - {"id": 20376, "synset": "calisaya.n.01", "name": "calisaya"}, - {"id": 20377, "synset": "cinchona_tree.n.01", "name": "cinchona_tree"}, - {"id": 20378, "synset": "cinchona.n.01", "name": "cinchona"}, - {"id": 20379, "synset": "bedstraw.n.01", "name": "bedstraw"}, - {"id": 20380, "synset": "sweet_woodruff.n.01", "name": "sweet_woodruff"}, - {"id": 20381, "synset": "northern_bedstraw.n.01", "name": "Northern_bedstraw"}, - {"id": 20382, "synset": "yellow_bedstraw.n.01", "name": "yellow_bedstraw"}, - {"id": 20383, "synset": "wild_licorice.n.01", "name": "wild_licorice"}, - {"id": 20384, "synset": "cleavers.n.01", "name": "cleavers"}, - {"id": 20385, "synset": "wild_madder.n.01", "name": "wild_madder"}, - {"id": 20386, "synset": "cape_jasmine.n.01", "name": "cape_jasmine"}, - {"id": 20387, "synset": "genipa.n.01", "name": "genipa"}, - {"id": 20388, "synset": "genipap_fruit.n.01", "name": "genipap_fruit"}, - {"id": 20389, "synset": "hamelia.n.01", "name": "hamelia"}, - {"id": 20390, "synset": "scarlet_bush.n.01", "name": "scarlet_bush"}, - {"id": 20391, "synset": "lemonwood.n.02", "name": "lemonwood"}, - {"id": 20392, "synset": "negro_peach.n.01", "name": "negro_peach"}, - {"id": 20393, "synset": "wild_medlar.n.01", "name": "wild_medlar"}, - {"id": 20394, "synset": "spanish_tamarind.n.01", "name": "Spanish_tamarind"}, - {"id": 20395, "synset": "abelia.n.01", "name": "abelia"}, - {"id": 20396, "synset": "bush_honeysuckle.n.02", "name": "bush_honeysuckle"}, - {"id": 20397, "synset": "american_twinflower.n.01", "name": "American_twinflower"}, - {"id": 20398, "synset": "honeysuckle.n.01", "name": "honeysuckle"}, - {"id": 20399, "synset": "american_fly_honeysuckle.n.01", "name": "American_fly_honeysuckle"}, - {"id": 20400, "synset": "italian_honeysuckle.n.01", "name": "Italian_honeysuckle"}, - {"id": 20401, "synset": "yellow_honeysuckle.n.01", "name": "yellow_honeysuckle"}, - {"id": 20402, "synset": "hairy_honeysuckle.n.01", "name": "hairy_honeysuckle"}, - {"id": 20403, "synset": "japanese_honeysuckle.n.01", "name": "Japanese_honeysuckle"}, - {"id": 20404, "synset": "hall's_honeysuckle.n.01", "name": "Hall's_honeysuckle"}, - {"id": 20405, "synset": "morrow's_honeysuckle.n.01", "name": "Morrow's_honeysuckle"}, - {"id": 20406, "synset": "woodbine.n.02", "name": "woodbine"}, - {"id": 20407, "synset": "trumpet_honeysuckle.n.01", "name": "trumpet_honeysuckle"}, - {"id": 20408, "synset": "european_fly_honeysuckle.n.01", "name": "European_fly_honeysuckle"}, - {"id": 20409, "synset": "swamp_fly_honeysuckle.n.01", "name": "swamp_fly_honeysuckle"}, - {"id": 20410, "synset": "snowberry.n.01", "name": "snowberry"}, - {"id": 20411, "synset": "coralberry.n.01", "name": "coralberry"}, - {"id": 20412, "synset": "blue_elder.n.01", "name": "blue_elder"}, - {"id": 20413, "synset": "dwarf_elder.n.01", "name": "dwarf_elder"}, - {"id": 20414, "synset": "american_red_elder.n.01", "name": "American_red_elder"}, - {"id": 20415, "synset": "european_red_elder.n.01", "name": "European_red_elder"}, - {"id": 20416, "synset": "feverroot.n.01", "name": "feverroot"}, - {"id": 20417, "synset": "cranberry_bush.n.01", "name": "cranberry_bush"}, - {"id": 20418, "synset": "wayfaring_tree.n.01", "name": "wayfaring_tree"}, - {"id": 20419, "synset": "guelder_rose.n.01", "name": "guelder_rose"}, - {"id": 20420, "synset": "arrow_wood.n.01", "name": "arrow_wood"}, - {"id": 20421, "synset": "black_haw.n.02", "name": "black_haw"}, - {"id": 20422, "synset": "weigela.n.01", "name": "weigela"}, - {"id": 20423, "synset": "teasel.n.01", "name": "teasel"}, - {"id": 20424, "synset": "common_teasel.n.01", "name": "common_teasel"}, - {"id": 20425, "synset": "fuller's_teasel.n.01", "name": "fuller's_teasel"}, - {"id": 20426, "synset": "wild_teasel.n.01", "name": "wild_teasel"}, - {"id": 20427, "synset": "scabious.n.01", "name": "scabious"}, - {"id": 20428, "synset": "sweet_scabious.n.01", "name": "sweet_scabious"}, - {"id": 20429, "synset": "field_scabious.n.01", "name": "field_scabious"}, - {"id": 20430, "synset": "jewelweed.n.01", "name": "jewelweed"}, - {"id": 20431, "synset": "geranium.n.01", "name": "geranium"}, - {"id": 20432, "synset": "cranesbill.n.01", "name": "cranesbill"}, - {"id": 20433, "synset": "wild_geranium.n.01", "name": "wild_geranium"}, - {"id": 20434, "synset": "meadow_cranesbill.n.01", "name": "meadow_cranesbill"}, - {"id": 20435, "synset": "richardson's_geranium.n.01", "name": "Richardson's_geranium"}, - {"id": 20436, "synset": "herb_robert.n.01", "name": "herb_robert"}, - {"id": 20437, "synset": "sticky_geranium.n.01", "name": "sticky_geranium"}, - {"id": 20438, "synset": "dove's_foot_geranium.n.01", "name": "dove's_foot_geranium"}, - {"id": 20439, "synset": "rose_geranium.n.01", "name": "rose_geranium"}, - {"id": 20440, "synset": "fish_geranium.n.01", "name": "fish_geranium"}, - {"id": 20441, "synset": "ivy_geranium.n.01", "name": "ivy_geranium"}, - {"id": 20442, "synset": "apple_geranium.n.01", "name": "apple_geranium"}, - {"id": 20443, "synset": "lemon_geranium.n.01", "name": "lemon_geranium"}, - {"id": 20444, "synset": "storksbill.n.01", "name": "storksbill"}, - {"id": 20445, "synset": "musk_clover.n.01", "name": "musk_clover"}, - {"id": 20446, "synset": "incense_tree.n.01", "name": "incense_tree"}, - {"id": 20447, "synset": "elephant_tree.n.01", "name": "elephant_tree"}, - {"id": 20448, "synset": "gumbo-limbo.n.01", "name": "gumbo-limbo"}, - {"id": 20449, "synset": "boswellia_carteri.n.01", "name": "Boswellia_carteri"}, - {"id": 20450, "synset": "salai.n.01", "name": "salai"}, - {"id": 20451, "synset": "balm_of_gilead.n.03", "name": "balm_of_gilead"}, - {"id": 20452, "synset": "myrrh_tree.n.01", "name": "myrrh_tree"}, - {"id": 20453, "synset": "protium_heptaphyllum.n.01", "name": "Protium_heptaphyllum"}, - {"id": 20454, "synset": "protium_guianense.n.01", "name": "Protium_guianense"}, - {"id": 20455, "synset": "water_starwort.n.01", "name": "water_starwort"}, - {"id": 20456, "synset": "barbados_cherry.n.01", "name": "barbados_cherry"}, - {"id": 20457, "synset": "mahogany.n.02", "name": "mahogany"}, - {"id": 20458, "synset": "chinaberry.n.02", "name": "chinaberry"}, - {"id": 20459, "synset": "neem.n.01", "name": "neem"}, - {"id": 20460, "synset": "neem_seed.n.01", "name": "neem_seed"}, - {"id": 20461, "synset": "spanish_cedar.n.01", "name": "Spanish_cedar"}, - {"id": 20462, "synset": "satinwood.n.03", "name": "satinwood"}, - {"id": 20463, "synset": "african_scented_mahogany.n.01", "name": "African_scented_mahogany"}, - {"id": 20464, "synset": "silver_ash.n.01", "name": "silver_ash"}, - {"id": 20465, "synset": "native_beech.n.01", "name": "native_beech"}, - {"id": 20466, "synset": "bunji-bunji.n.01", "name": "bunji-bunji"}, - {"id": 20467, "synset": "african_mahogany.n.01", "name": "African_mahogany"}, - {"id": 20468, "synset": "lanseh_tree.n.01", "name": "lanseh_tree"}, - {"id": 20469, "synset": "true_mahogany.n.01", "name": "true_mahogany"}, - {"id": 20470, "synset": "honduras_mahogany.n.01", "name": "Honduras_mahogany"}, - {"id": 20471, "synset": "philippine_mahogany.n.02", "name": "Philippine_mahogany"}, - {"id": 20472, "synset": "caracolito.n.01", "name": "caracolito"}, - {"id": 20473, "synset": "common_wood_sorrel.n.01", "name": "common_wood_sorrel"}, - {"id": 20474, "synset": "bermuda_buttercup.n.01", "name": "Bermuda_buttercup"}, - {"id": 20475, "synset": "creeping_oxalis.n.01", "name": "creeping_oxalis"}, - {"id": 20476, "synset": "goatsfoot.n.01", "name": "goatsfoot"}, - {"id": 20477, "synset": "violet_wood_sorrel.n.01", "name": "violet_wood_sorrel"}, - {"id": 20478, "synset": "oca.n.01", "name": "oca"}, - {"id": 20479, "synset": "carambola.n.01", "name": "carambola"}, - {"id": 20480, "synset": "bilimbi.n.01", "name": "bilimbi"}, - {"id": 20481, "synset": "milkwort.n.01", "name": "milkwort"}, - {"id": 20482, "synset": "senega.n.02", "name": "senega"}, - {"id": 20483, "synset": "orange_milkwort.n.01", "name": "orange_milkwort"}, - {"id": 20484, "synset": "flowering_wintergreen.n.01", "name": "flowering_wintergreen"}, - {"id": 20485, "synset": "seneca_snakeroot.n.01", "name": "Seneca_snakeroot"}, - {"id": 20486, "synset": "common_milkwort.n.01", "name": "common_milkwort"}, - {"id": 20487, "synset": "rue.n.01", "name": "rue"}, - {"id": 20488, "synset": "citrus.n.02", "name": "citrus"}, - {"id": 20489, "synset": "orange.n.03", "name": "orange"}, - {"id": 20490, "synset": "sour_orange.n.01", "name": "sour_orange"}, - {"id": 20491, "synset": "bergamot.n.01", "name": "bergamot"}, - {"id": 20492, "synset": "pomelo.n.01", "name": "pomelo"}, - {"id": 20493, "synset": "citron.n.02", "name": "citron"}, - {"id": 20494, "synset": "grapefruit.n.01", "name": "grapefruit"}, - {"id": 20495, "synset": "mandarin.n.01", "name": "mandarin"}, - {"id": 20496, "synset": "tangerine.n.01", "name": "tangerine"}, - {"id": 20497, "synset": "satsuma.n.01", "name": "satsuma"}, - {"id": 20498, "synset": "sweet_orange.n.02", "name": "sweet_orange"}, - {"id": 20499, "synset": "temple_orange.n.01", "name": "temple_orange"}, - {"id": 20500, "synset": "tangelo.n.01", "name": "tangelo"}, - {"id": 20501, "synset": "rangpur.n.01", "name": "rangpur"}, - {"id": 20502, "synset": "lemon.n.03", "name": "lemon"}, - {"id": 20503, "synset": "sweet_lemon.n.01", "name": "sweet_lemon"}, - {"id": 20504, "synset": "lime.n.04", "name": "lime"}, - {"id": 20505, "synset": "citrange.n.01", "name": "citrange"}, - {"id": 20506, "synset": "fraxinella.n.01", "name": "fraxinella"}, - {"id": 20507, "synset": "kumquat.n.01", "name": "kumquat"}, - {"id": 20508, "synset": "marumi.n.01", "name": "marumi"}, - {"id": 20509, "synset": "nagami.n.01", "name": "nagami"}, - {"id": 20510, "synset": "cork_tree.n.01", "name": "cork_tree"}, - {"id": 20511, "synset": "trifoliate_orange.n.01", "name": "trifoliate_orange"}, - {"id": 20512, "synset": "prickly_ash.n.01", "name": "prickly_ash"}, - {"id": 20513, "synset": "toothache_tree.n.01", "name": "toothache_tree"}, - {"id": 20514, "synset": "hercules'-club.n.01", "name": "Hercules'-club"}, - {"id": 20515, "synset": "bitterwood_tree.n.01", "name": "bitterwood_tree"}, - {"id": 20516, "synset": "marupa.n.01", "name": "marupa"}, - {"id": 20517, "synset": "paradise_tree.n.01", "name": "paradise_tree"}, - {"id": 20518, "synset": "ailanthus.n.01", "name": "ailanthus"}, - {"id": 20519, "synset": "tree_of_heaven.n.01", "name": "tree_of_heaven"}, - {"id": 20520, "synset": "wild_mango.n.01", "name": "wild_mango"}, - {"id": 20521, "synset": "pepper_tree.n.02", "name": "pepper_tree"}, - {"id": 20522, "synset": "jamaica_quassia.n.02", "name": "Jamaica_quassia"}, - {"id": 20523, "synset": "quassia.n.02", "name": "quassia"}, - {"id": 20524, "synset": "nasturtium.n.01", "name": "nasturtium"}, - {"id": 20525, "synset": "garden_nasturtium.n.01", "name": "garden_nasturtium"}, - {"id": 20526, "synset": "bush_nasturtium.n.01", "name": "bush_nasturtium"}, - {"id": 20527, "synset": "canarybird_flower.n.01", "name": "canarybird_flower"}, - {"id": 20528, "synset": "bean_caper.n.01", "name": "bean_caper"}, - {"id": 20529, "synset": "palo_santo.n.01", "name": "palo_santo"}, - {"id": 20530, "synset": "lignum_vitae.n.02", "name": "lignum_vitae"}, - {"id": 20531, "synset": "creosote_bush.n.01", "name": "creosote_bush"}, - {"id": 20532, "synset": "caltrop.n.01", "name": "caltrop"}, - {"id": 20533, "synset": "willow.n.01", "name": "willow"}, - {"id": 20534, "synset": "osier.n.02", "name": "osier"}, - {"id": 20535, "synset": "white_willow.n.01", "name": "white_willow"}, - {"id": 20536, "synset": "silver_willow.n.01", "name": "silver_willow"}, - {"id": 20537, "synset": "golden_willow.n.01", "name": "golden_willow"}, - {"id": 20538, "synset": "cricket-bat_willow.n.01", "name": "cricket-bat_willow"}, - {"id": 20539, "synset": "arctic_willow.n.01", "name": "arctic_willow"}, - {"id": 20540, "synset": "weeping_willow.n.01", "name": "weeping_willow"}, - {"id": 20541, "synset": "wisconsin_weeping_willow.n.01", "name": "Wisconsin_weeping_willow"}, - {"id": 20542, "synset": "pussy_willow.n.01", "name": "pussy_willow"}, - {"id": 20543, "synset": "sallow.n.01", "name": "sallow"}, - {"id": 20544, "synset": "goat_willow.n.01", "name": "goat_willow"}, - {"id": 20545, "synset": "peachleaf_willow.n.01", "name": "peachleaf_willow"}, - {"id": 20546, "synset": "almond_willow.n.01", "name": "almond_willow"}, - {"id": 20547, "synset": "hoary_willow.n.01", "name": "hoary_willow"}, - {"id": 20548, "synset": "crack_willow.n.01", "name": "crack_willow"}, - {"id": 20549, "synset": "prairie_willow.n.01", "name": "prairie_willow"}, - {"id": 20550, "synset": "dwarf_willow.n.01", "name": "dwarf_willow"}, - {"id": 20551, "synset": "grey_willow.n.01", "name": "grey_willow"}, - {"id": 20552, "synset": "arroyo_willow.n.01", "name": "arroyo_willow"}, - {"id": 20553, "synset": "shining_willow.n.01", "name": "shining_willow"}, - {"id": 20554, "synset": "swamp_willow.n.01", "name": "swamp_willow"}, - {"id": 20555, "synset": "bay_willow.n.01", "name": "bay_willow"}, - {"id": 20556, "synset": "purple_willow.n.01", "name": "purple_willow"}, - {"id": 20557, "synset": "balsam_willow.n.01", "name": "balsam_willow"}, - {"id": 20558, "synset": "creeping_willow.n.01", "name": "creeping_willow"}, - {"id": 20559, "synset": "sitka_willow.n.01", "name": "Sitka_willow"}, - {"id": 20560, "synset": "dwarf_grey_willow.n.01", "name": "dwarf_grey_willow"}, - {"id": 20561, "synset": "bearberry_willow.n.01", "name": "bearberry_willow"}, - {"id": 20562, "synset": "common_osier.n.01", "name": "common_osier"}, - {"id": 20563, "synset": "poplar.n.02", "name": "poplar"}, - {"id": 20564, "synset": "balsam_poplar.n.01", "name": "balsam_poplar"}, - {"id": 20565, "synset": "white_poplar.n.01", "name": "white_poplar"}, - {"id": 20566, "synset": "grey_poplar.n.01", "name": "grey_poplar"}, - {"id": 20567, "synset": "black_poplar.n.01", "name": "black_poplar"}, - {"id": 20568, "synset": "lombardy_poplar.n.01", "name": "Lombardy_poplar"}, - {"id": 20569, "synset": "cottonwood.n.01", "name": "cottonwood"}, - {"id": 20570, "synset": "eastern_cottonwood.n.01", "name": "Eastern_cottonwood"}, - {"id": 20571, "synset": "black_cottonwood.n.02", "name": "black_cottonwood"}, - {"id": 20572, "synset": "swamp_cottonwood.n.01", "name": "swamp_cottonwood"}, - {"id": 20573, "synset": "aspen.n.01", "name": "aspen"}, - {"id": 20574, "synset": "quaking_aspen.n.01", "name": "quaking_aspen"}, - {"id": 20575, "synset": "american_quaking_aspen.n.01", "name": "American_quaking_aspen"}, - {"id": 20576, "synset": "canadian_aspen.n.01", "name": "Canadian_aspen"}, - {"id": 20577, "synset": "sandalwood_tree.n.01", "name": "sandalwood_tree"}, - {"id": 20578, "synset": "quandong.n.01", "name": "quandong"}, - {"id": 20579, "synset": "rabbitwood.n.01", "name": "rabbitwood"}, - {"id": 20580, "synset": "loranthaceae.n.01", "name": "Loranthaceae"}, - {"id": 20581, "synset": "mistletoe.n.03", "name": "mistletoe"}, - {"id": 20582, "synset": "american_mistletoe.n.02", "name": "American_mistletoe"}, - {"id": 20583, "synset": "mistletoe.n.02", "name": "mistletoe"}, - {"id": 20584, "synset": "american_mistletoe.n.01", "name": "American_mistletoe"}, - {"id": 20585, "synset": "aalii.n.01", "name": "aalii"}, - {"id": 20586, "synset": "soapberry.n.01", "name": "soapberry"}, - {"id": 20587, "synset": "wild_china_tree.n.01", "name": "wild_China_tree"}, - {"id": 20588, "synset": "china_tree.n.01", "name": "China_tree"}, - {"id": 20589, "synset": "akee.n.01", "name": "akee"}, - {"id": 20590, "synset": "soapberry_vine.n.01", "name": "soapberry_vine"}, - {"id": 20591, "synset": "heartseed.n.01", "name": "heartseed"}, - {"id": 20592, "synset": "balloon_vine.n.01", "name": "balloon_vine"}, - {"id": 20593, "synset": "longan.n.01", "name": "longan"}, - {"id": 20594, "synset": "harpullia.n.01", "name": "harpullia"}, - {"id": 20595, "synset": "harpulla.n.01", "name": "harpulla"}, - {"id": 20596, "synset": "moreton_bay_tulipwood.n.01", "name": "Moreton_Bay_tulipwood"}, - {"id": 20597, "synset": "litchi.n.01", "name": "litchi"}, - {"id": 20598, "synset": "spanish_lime.n.01", "name": "Spanish_lime"}, - {"id": 20599, "synset": "rambutan.n.01", "name": "rambutan"}, - {"id": 20600, "synset": "pulasan.n.01", "name": "pulasan"}, - {"id": 20601, "synset": "pachysandra.n.01", "name": "pachysandra"}, - {"id": 20602, "synset": "allegheny_spurge.n.01", "name": "Allegheny_spurge"}, - {"id": 20603, "synset": "bittersweet.n.02", "name": "bittersweet"}, - {"id": 20604, "synset": "spindle_tree.n.01", "name": "spindle_tree"}, - {"id": 20605, "synset": "winged_spindle_tree.n.01", "name": "winged_spindle_tree"}, - {"id": 20606, "synset": "wahoo.n.02", "name": "wahoo"}, - {"id": 20607, "synset": "strawberry_bush.n.01", "name": "strawberry_bush"}, - {"id": 20608, "synset": "evergreen_bittersweet.n.01", "name": "evergreen_bittersweet"}, - {"id": 20609, "synset": "cyrilla.n.01", "name": "cyrilla"}, - {"id": 20610, "synset": "titi.n.01", "name": "titi"}, - {"id": 20611, "synset": "crowberry.n.01", "name": "crowberry"}, - {"id": 20612, "synset": "maple.n.02", "name": "maple"}, - {"id": 20613, "synset": "silver_maple.n.01", "name": "silver_maple"}, - {"id": 20614, "synset": "sugar_maple.n.01", "name": "sugar_maple"}, - {"id": 20615, "synset": "red_maple.n.01", "name": "red_maple"}, - {"id": 20616, "synset": "moosewood.n.01", "name": "moosewood"}, - {"id": 20617, "synset": "oregon_maple.n.01", "name": "Oregon_maple"}, - {"id": 20618, "synset": "dwarf_maple.n.01", "name": "dwarf_maple"}, - {"id": 20619, "synset": "mountain_maple.n.01", "name": "mountain_maple"}, - {"id": 20620, "synset": "vine_maple.n.01", "name": "vine_maple"}, - {"id": 20621, "synset": "hedge_maple.n.01", "name": "hedge_maple"}, - {"id": 20622, "synset": "norway_maple.n.01", "name": "Norway_maple"}, - {"id": 20623, "synset": "sycamore.n.03", "name": "sycamore"}, - {"id": 20624, "synset": "box_elder.n.01", "name": "box_elder"}, - {"id": 20625, "synset": "california_box_elder.n.01", "name": "California_box_elder"}, - {"id": 20626, "synset": "pointed-leaf_maple.n.01", "name": "pointed-leaf_maple"}, - {"id": 20627, "synset": "japanese_maple.n.02", "name": "Japanese_maple"}, - {"id": 20628, "synset": "japanese_maple.n.01", "name": "Japanese_maple"}, - {"id": 20629, "synset": "holly.n.01", "name": "holly"}, - {"id": 20630, "synset": "chinese_holly.n.01", "name": "Chinese_holly"}, - {"id": 20631, "synset": "bearberry.n.02", "name": "bearberry"}, - {"id": 20632, "synset": "inkberry.n.01", "name": "inkberry"}, - {"id": 20633, "synset": "mate.n.07", "name": "mate"}, - {"id": 20634, "synset": "american_holly.n.01", "name": "American_holly"}, - {"id": 20635, "synset": "low_gallberry_holly.n.01", "name": "low_gallberry_holly"}, - {"id": 20636, "synset": "tall_gallberry_holly.n.01", "name": "tall_gallberry_holly"}, - {"id": 20637, "synset": "yaupon_holly.n.01", "name": "yaupon_holly"}, - {"id": 20638, "synset": "deciduous_holly.n.01", "name": "deciduous_holly"}, - {"id": 20639, "synset": "juneberry_holly.n.01", "name": "juneberry_holly"}, - {"id": 20640, "synset": "largeleaf_holly.n.01", "name": "largeleaf_holly"}, - {"id": 20641, "synset": "geogia_holly.n.01", "name": "Geogia_holly"}, - {"id": 20642, "synset": "common_winterberry_holly.n.01", "name": "common_winterberry_holly"}, - {"id": 20643, "synset": "smooth_winterberry_holly.n.01", "name": "smooth_winterberry_holly"}, - {"id": 20644, "synset": "cashew.n.01", "name": "cashew"}, - {"id": 20645, "synset": "goncalo_alves.n.01", "name": "goncalo_alves"}, - {"id": 20646, "synset": "venetian_sumac.n.01", "name": "Venetian_sumac"}, - {"id": 20647, "synset": "laurel_sumac.n.01", "name": "laurel_sumac"}, - {"id": 20648, "synset": "mango.n.01", "name": "mango"}, - {"id": 20649, "synset": "pistachio.n.01", "name": "pistachio"}, - {"id": 20650, "synset": "terebinth.n.01", "name": "terebinth"}, - {"id": 20651, "synset": "mastic.n.03", "name": "mastic"}, - {"id": 20652, "synset": "australian_sumac.n.01", "name": "Australian_sumac"}, - {"id": 20653, "synset": "sumac.n.02", "name": "sumac"}, - {"id": 20654, "synset": "smooth_sumac.n.01", "name": "smooth_sumac"}, - {"id": 20655, "synset": "sugar-bush.n.01", "name": "sugar-bush"}, - {"id": 20656, "synset": "staghorn_sumac.n.01", "name": "staghorn_sumac"}, - {"id": 20657, "synset": "squawbush.n.01", "name": "squawbush"}, - {"id": 20658, "synset": "aroeira_blanca.n.01", "name": "aroeira_blanca"}, - {"id": 20659, "synset": "pepper_tree.n.01", "name": "pepper_tree"}, - {"id": 20660, "synset": "brazilian_pepper_tree.n.01", "name": "Brazilian_pepper_tree"}, - {"id": 20661, "synset": "hog_plum.n.01", "name": "hog_plum"}, - {"id": 20662, "synset": "mombin.n.01", "name": "mombin"}, - {"id": 20663, "synset": "poison_ash.n.01", "name": "poison_ash"}, - {"id": 20664, "synset": "poison_ivy.n.02", "name": "poison_ivy"}, - {"id": 20665, "synset": "western_poison_oak.n.01", "name": "western_poison_oak"}, - {"id": 20666, "synset": "eastern_poison_oak.n.01", "name": "eastern_poison_oak"}, - {"id": 20667, "synset": "varnish_tree.n.02", "name": "varnish_tree"}, - {"id": 20668, "synset": "horse_chestnut.n.01", "name": "horse_chestnut"}, - {"id": 20669, "synset": "buckeye.n.01", "name": "buckeye"}, - {"id": 20670, "synset": "sweet_buckeye.n.01", "name": "sweet_buckeye"}, - {"id": 20671, "synset": "ohio_buckeye.n.01", "name": "Ohio_buckeye"}, - {"id": 20672, "synset": "dwarf_buckeye.n.01", "name": "dwarf_buckeye"}, - {"id": 20673, "synset": "red_buckeye.n.01", "name": "red_buckeye"}, - {"id": 20674, "synset": "particolored_buckeye.n.01", "name": "particolored_buckeye"}, - {"id": 20675, "synset": "ebony.n.03", "name": "ebony"}, - {"id": 20676, "synset": "marblewood.n.02", "name": "marblewood"}, - {"id": 20677, "synset": "marblewood.n.01", "name": "marblewood"}, - {"id": 20678, "synset": "persimmon.n.01", "name": "persimmon"}, - {"id": 20679, "synset": "japanese_persimmon.n.01", "name": "Japanese_persimmon"}, - {"id": 20680, "synset": "american_persimmon.n.01", "name": "American_persimmon"}, - {"id": 20681, "synset": "date_plum.n.01", "name": "date_plum"}, - {"id": 20682, "synset": "buckthorn.n.02", "name": "buckthorn"}, - {"id": 20683, "synset": "southern_buckthorn.n.01", "name": "southern_buckthorn"}, - {"id": 20684, "synset": "false_buckthorn.n.01", "name": "false_buckthorn"}, - {"id": 20685, "synset": "star_apple.n.01", "name": "star_apple"}, - {"id": 20686, "synset": "satinleaf.n.01", "name": "satinleaf"}, - {"id": 20687, "synset": "balata.n.02", "name": "balata"}, - {"id": 20688, "synset": "sapodilla.n.01", "name": "sapodilla"}, - {"id": 20689, "synset": "gutta-percha_tree.n.02", "name": "gutta-percha_tree"}, - {"id": 20690, "synset": "gutta-percha_tree.n.01", "name": "gutta-percha_tree"}, - {"id": 20691, "synset": "canistel.n.01", "name": "canistel"}, - {"id": 20692, "synset": "marmalade_tree.n.01", "name": "marmalade_tree"}, - {"id": 20693, "synset": "sweetleaf.n.01", "name": "sweetleaf"}, - {"id": 20694, "synset": "asiatic_sweetleaf.n.01", "name": "Asiatic_sweetleaf"}, - {"id": 20695, "synset": "styrax.n.01", "name": "styrax"}, - {"id": 20696, "synset": "snowbell.n.01", "name": "snowbell"}, - {"id": 20697, "synset": "japanese_snowbell.n.01", "name": "Japanese_snowbell"}, - {"id": 20698, "synset": "texas_snowbell.n.01", "name": "Texas_snowbell"}, - {"id": 20699, "synset": "silver-bell_tree.n.01", "name": "silver-bell_tree"}, - {"id": 20700, "synset": "carnivorous_plant.n.01", "name": "carnivorous_plant"}, - {"id": 20701, "synset": "pitcher_plant.n.01", "name": "pitcher_plant"}, - {"id": 20702, "synset": "common_pitcher_plant.n.01", "name": "common_pitcher_plant"}, - {"id": 20703, "synset": "hooded_pitcher_plant.n.01", "name": "hooded_pitcher_plant"}, - {"id": 20704, "synset": "huntsman's_horn.n.01", "name": "huntsman's_horn"}, - {"id": 20705, "synset": "tropical_pitcher_plant.n.01", "name": "tropical_pitcher_plant"}, - {"id": 20706, "synset": "sundew.n.01", "name": "sundew"}, - {"id": 20707, "synset": "venus's_flytrap.n.01", "name": "Venus's_flytrap"}, - {"id": 20708, "synset": "waterwheel_plant.n.01", "name": "waterwheel_plant"}, - {"id": 20709, "synset": "drosophyllum_lusitanicum.n.01", "name": "Drosophyllum_lusitanicum"}, - {"id": 20710, "synset": "roridula.n.01", "name": "roridula"}, - {"id": 20711, "synset": "australian_pitcher_plant.n.01", "name": "Australian_pitcher_plant"}, - {"id": 20712, "synset": "sedum.n.01", "name": "sedum"}, - {"id": 20713, "synset": "stonecrop.n.01", "name": "stonecrop"}, - {"id": 20714, "synset": "rose-root.n.01", "name": "rose-root"}, - {"id": 20715, "synset": "orpine.n.01", "name": "orpine"}, - {"id": 20716, "synset": "pinwheel.n.01", "name": "pinwheel"}, - {"id": 20717, "synset": "christmas_bush.n.01", "name": "Christmas_bush"}, - {"id": 20718, "synset": "hortensia.n.01", "name": "hortensia"}, - {"id": 20719, "synset": "fall-blooming_hydrangea.n.01", "name": "fall-blooming_hydrangea"}, - {"id": 20720, "synset": "carpenteria.n.01", "name": "carpenteria"}, - {"id": 20721, "synset": "decumary.n.01", "name": "decumary"}, - {"id": 20722, "synset": "deutzia.n.01", "name": "deutzia"}, - {"id": 20723, "synset": "philadelphus.n.01", "name": "philadelphus"}, - {"id": 20724, "synset": "mock_orange.n.01", "name": "mock_orange"}, - {"id": 20725, "synset": "saxifrage.n.01", "name": "saxifrage"}, - {"id": 20726, "synset": "yellow_mountain_saxifrage.n.01", "name": "yellow_mountain_saxifrage"}, - {"id": 20727, "synset": "meadow_saxifrage.n.01", "name": "meadow_saxifrage"}, - {"id": 20728, "synset": "mossy_saxifrage.n.01", "name": "mossy_saxifrage"}, - {"id": 20729, "synset": "western_saxifrage.n.01", "name": "western_saxifrage"}, - {"id": 20730, "synset": "purple_saxifrage.n.01", "name": "purple_saxifrage"}, - {"id": 20731, "synset": "star_saxifrage.n.01", "name": "star_saxifrage"}, - {"id": 20732, "synset": "strawberry_geranium.n.01", "name": "strawberry_geranium"}, - {"id": 20733, "synset": "astilbe.n.01", "name": "astilbe"}, - {"id": 20734, "synset": "false_goatsbeard.n.01", "name": "false_goatsbeard"}, - {"id": 20735, "synset": "dwarf_astilbe.n.01", "name": "dwarf_astilbe"}, - {"id": 20736, "synset": "spirea.n.01", "name": "spirea"}, - {"id": 20737, "synset": "bergenia.n.01", "name": "bergenia"}, - {"id": 20738, "synset": "coast_boykinia.n.01", "name": "coast_boykinia"}, - {"id": 20739, "synset": "golden_saxifrage.n.01", "name": "golden_saxifrage"}, - {"id": 20740, "synset": "umbrella_plant.n.01", "name": "umbrella_plant"}, - {"id": 20741, "synset": "bridal_wreath.n.01", "name": "bridal_wreath"}, - {"id": 20742, "synset": "alumroot.n.01", "name": "alumroot"}, - {"id": 20743, "synset": "coralbells.n.01", "name": "coralbells"}, - {"id": 20744, "synset": "leatherleaf_saxifrage.n.01", "name": "leatherleaf_saxifrage"}, - {"id": 20745, "synset": "woodland_star.n.01", "name": "woodland_star"}, - {"id": 20746, "synset": "prairie_star.n.01", "name": "prairie_star"}, - {"id": 20747, "synset": "miterwort.n.01", "name": "miterwort"}, - {"id": 20748, "synset": "five-point_bishop's_cap.n.01", "name": "five-point_bishop's_cap"}, - {"id": 20749, "synset": "parnassia.n.01", "name": "parnassia"}, - {"id": 20750, "synset": "bog_star.n.01", "name": "bog_star"}, - { - "id": 20751, - "synset": "fringed_grass_of_parnassus.n.01", - "name": "fringed_grass_of_Parnassus", - }, - {"id": 20752, "synset": "false_alumroot.n.01", "name": "false_alumroot"}, - {"id": 20753, "synset": "foamflower.n.01", "name": "foamflower"}, - {"id": 20754, "synset": "false_miterwort.n.01", "name": "false_miterwort"}, - {"id": 20755, "synset": "pickaback_plant.n.01", "name": "pickaback_plant"}, - {"id": 20756, "synset": "currant.n.02", "name": "currant"}, - {"id": 20757, "synset": "black_currant.n.01", "name": "black_currant"}, - {"id": 20758, "synset": "white_currant.n.01", "name": "white_currant"}, - {"id": 20759, "synset": "gooseberry.n.01", "name": "gooseberry"}, - {"id": 20760, "synset": "plane_tree.n.01", "name": "plane_tree"}, - {"id": 20761, "synset": "london_plane.n.01", "name": "London_plane"}, - {"id": 20762, "synset": "american_sycamore.n.01", "name": "American_sycamore"}, - {"id": 20763, "synset": "oriental_plane.n.01", "name": "oriental_plane"}, - {"id": 20764, "synset": "california_sycamore.n.01", "name": "California_sycamore"}, - {"id": 20765, "synset": "arizona_sycamore.n.01", "name": "Arizona_sycamore"}, - {"id": 20766, "synset": "greek_valerian.n.01", "name": "Greek_valerian"}, - {"id": 20767, "synset": "northern_jacob's_ladder.n.01", "name": "northern_Jacob's_ladder"}, - {"id": 20768, "synset": "skunkweed.n.01", "name": "skunkweed"}, - {"id": 20769, "synset": "phlox.n.01", "name": "phlox"}, - {"id": 20770, "synset": "moss_pink.n.02", "name": "moss_pink"}, - {"id": 20771, "synset": "evening-snow.n.01", "name": "evening-snow"}, - {"id": 20772, "synset": "acanthus.n.01", "name": "acanthus"}, - {"id": 20773, "synset": "bear's_breech.n.01", "name": "bear's_breech"}, - {"id": 20774, "synset": "caricature_plant.n.01", "name": "caricature_plant"}, - {"id": 20775, "synset": "black-eyed_susan.n.01", "name": "black-eyed_Susan"}, - {"id": 20776, "synset": "catalpa.n.01", "name": "catalpa"}, - {"id": 20777, "synset": "catalpa_bignioides.n.01", "name": "Catalpa_bignioides"}, - {"id": 20778, "synset": "catalpa_speciosa.n.01", "name": "Catalpa_speciosa"}, - {"id": 20779, "synset": "desert_willow.n.01", "name": "desert_willow"}, - {"id": 20780, "synset": "calabash.n.02", "name": "calabash"}, - {"id": 20781, "synset": "calabash.n.01", "name": "calabash"}, - {"id": 20782, "synset": "borage.n.01", "name": "borage"}, - {"id": 20783, "synset": "common_amsinckia.n.01", "name": "common_amsinckia"}, - {"id": 20784, "synset": "anchusa.n.01", "name": "anchusa"}, - {"id": 20785, "synset": "bugloss.n.01", "name": "bugloss"}, - {"id": 20786, "synset": "cape_forget-me-not.n.02", "name": "cape_forget-me-not"}, - {"id": 20787, "synset": "cape_forget-me-not.n.01", "name": "cape_forget-me-not"}, - {"id": 20788, "synset": "spanish_elm.n.02", "name": "Spanish_elm"}, - {"id": 20789, "synset": "princewood.n.01", "name": "princewood"}, - {"id": 20790, "synset": "chinese_forget-me-not.n.01", "name": "Chinese_forget-me-not"}, - {"id": 20791, "synset": "hound's-tongue.n.02", "name": "hound's-tongue"}, - {"id": 20792, "synset": "hound's-tongue.n.01", "name": "hound's-tongue"}, - {"id": 20793, "synset": "blueweed.n.01", "name": "blueweed"}, - {"id": 20794, "synset": "beggar's_lice.n.01", "name": "beggar's_lice"}, - {"id": 20795, "synset": "gromwell.n.01", "name": "gromwell"}, - {"id": 20796, "synset": "puccoon.n.01", "name": "puccoon"}, - {"id": 20797, "synset": "virginia_bluebell.n.01", "name": "Virginia_bluebell"}, - {"id": 20798, "synset": "garden_forget-me-not.n.01", "name": "garden_forget-me-not"}, - {"id": 20799, "synset": "forget-me-not.n.01", "name": "forget-me-not"}, - {"id": 20800, "synset": "false_gromwell.n.01", "name": "false_gromwell"}, - {"id": 20801, "synset": "comfrey.n.01", "name": "comfrey"}, - {"id": 20802, "synset": "common_comfrey.n.01", "name": "common_comfrey"}, - {"id": 20803, "synset": "convolvulus.n.01", "name": "convolvulus"}, - {"id": 20804, "synset": "bindweed.n.01", "name": "bindweed"}, - {"id": 20805, "synset": "field_bindweed.n.01", "name": "field_bindweed"}, - {"id": 20806, "synset": "scammony.n.03", "name": "scammony"}, - {"id": 20807, "synset": "silverweed.n.01", "name": "silverweed"}, - {"id": 20808, "synset": "dodder.n.01", "name": "dodder"}, - {"id": 20809, "synset": "dichondra.n.01", "name": "dichondra"}, - {"id": 20810, "synset": "cypress_vine.n.01", "name": "cypress_vine"}, - {"id": 20811, "synset": "moonflower.n.01", "name": "moonflower"}, - {"id": 20812, "synset": "wild_potato_vine.n.01", "name": "wild_potato_vine"}, - {"id": 20813, "synset": "red_morning-glory.n.01", "name": "red_morning-glory"}, - {"id": 20814, "synset": "man-of-the-earth.n.01", "name": "man-of-the-earth"}, - {"id": 20815, "synset": "scammony.n.01", "name": "scammony"}, - {"id": 20816, "synset": "japanese_morning_glory.n.01", "name": "Japanese_morning_glory"}, - { - "id": 20817, - "synset": "imperial_japanese_morning_glory.n.01", - "name": "imperial_Japanese_morning_glory", - }, - {"id": 20818, "synset": "gesneriad.n.01", "name": "gesneriad"}, - {"id": 20819, "synset": "gesneria.n.01", "name": "gesneria"}, - {"id": 20820, "synset": "achimenes.n.01", "name": "achimenes"}, - {"id": 20821, "synset": "aeschynanthus.n.01", "name": "aeschynanthus"}, - {"id": 20822, "synset": "lace-flower_vine.n.01", "name": "lace-flower_vine"}, - {"id": 20823, "synset": "columnea.n.01", "name": "columnea"}, - {"id": 20824, "synset": "episcia.n.01", "name": "episcia"}, - {"id": 20825, "synset": "gloxinia.n.01", "name": "gloxinia"}, - {"id": 20826, "synset": "canterbury_bell.n.01", "name": "Canterbury_bell"}, - {"id": 20827, "synset": "kohleria.n.01", "name": "kohleria"}, - {"id": 20828, "synset": "african_violet.n.01", "name": "African_violet"}, - {"id": 20829, "synset": "streptocarpus.n.01", "name": "streptocarpus"}, - {"id": 20830, "synset": "cape_primrose.n.01", "name": "Cape_primrose"}, - {"id": 20831, "synset": "waterleaf.n.01", "name": "waterleaf"}, - {"id": 20832, "synset": "virginia_waterleaf.n.01", "name": "Virginia_waterleaf"}, - {"id": 20833, "synset": "yellow_bells.n.01", "name": "yellow_bells"}, - {"id": 20834, "synset": "yerba_santa.n.01", "name": "yerba_santa"}, - {"id": 20835, "synset": "nemophila.n.01", "name": "nemophila"}, - {"id": 20836, "synset": "baby_blue-eyes.n.01", "name": "baby_blue-eyes"}, - {"id": 20837, "synset": "five-spot.n.02", "name": "five-spot"}, - {"id": 20838, "synset": "scorpionweed.n.01", "name": "scorpionweed"}, - {"id": 20839, "synset": "california_bluebell.n.02", "name": "California_bluebell"}, - {"id": 20840, "synset": "california_bluebell.n.01", "name": "California_bluebell"}, - {"id": 20841, "synset": "fiddleneck.n.01", "name": "fiddleneck"}, - {"id": 20842, "synset": "fiesta_flower.n.01", "name": "fiesta_flower"}, - {"id": 20843, "synset": "basil_thyme.n.01", "name": "basil_thyme"}, - {"id": 20844, "synset": "giant_hyssop.n.01", "name": "giant_hyssop"}, - {"id": 20845, "synset": "yellow_giant_hyssop.n.01", "name": "yellow_giant_hyssop"}, - {"id": 20846, "synset": "anise_hyssop.n.01", "name": "anise_hyssop"}, - {"id": 20847, "synset": "mexican_hyssop.n.01", "name": "Mexican_hyssop"}, - {"id": 20848, "synset": "bugle.n.02", "name": "bugle"}, - {"id": 20849, "synset": "creeping_bugle.n.01", "name": "creeping_bugle"}, - {"id": 20850, "synset": "erect_bugle.n.01", "name": "erect_bugle"}, - {"id": 20851, "synset": "pyramid_bugle.n.01", "name": "pyramid_bugle"}, - {"id": 20852, "synset": "wood_mint.n.01", "name": "wood_mint"}, - {"id": 20853, "synset": "hairy_wood_mint.n.01", "name": "hairy_wood_mint"}, - {"id": 20854, "synset": "downy_wood_mint.n.01", "name": "downy_wood_mint"}, - {"id": 20855, "synset": "calamint.n.01", "name": "calamint"}, - {"id": 20856, "synset": "common_calamint.n.01", "name": "common_calamint"}, - {"id": 20857, "synset": "large-flowered_calamint.n.01", "name": "large-flowered_calamint"}, - {"id": 20858, "synset": "lesser_calamint.n.01", "name": "lesser_calamint"}, - {"id": 20859, "synset": "wild_basil.n.01", "name": "wild_basil"}, - {"id": 20860, "synset": "horse_balm.n.01", "name": "horse_balm"}, - {"id": 20861, "synset": "coleus.n.01", "name": "coleus"}, - {"id": 20862, "synset": "country_borage.n.01", "name": "country_borage"}, - {"id": 20863, "synset": "painted_nettle.n.01", "name": "painted_nettle"}, - {"id": 20864, "synset": "apalachicola_rosemary.n.01", "name": "Apalachicola_rosemary"}, - {"id": 20865, "synset": "dragonhead.n.01", "name": "dragonhead"}, - {"id": 20866, "synset": "elsholtzia.n.01", "name": "elsholtzia"}, - {"id": 20867, "synset": "hemp_nettle.n.01", "name": "hemp_nettle"}, - {"id": 20868, "synset": "ground_ivy.n.01", "name": "ground_ivy"}, - {"id": 20869, "synset": "pennyroyal.n.02", "name": "pennyroyal"}, - {"id": 20870, "synset": "hyssop.n.01", "name": "hyssop"}, - {"id": 20871, "synset": "dead_nettle.n.02", "name": "dead_nettle"}, - {"id": 20872, "synset": "white_dead_nettle.n.01", "name": "white_dead_nettle"}, - {"id": 20873, "synset": "henbit.n.01", "name": "henbit"}, - {"id": 20874, "synset": "english_lavender.n.01", "name": "English_lavender"}, - {"id": 20875, "synset": "french_lavender.n.02", "name": "French_lavender"}, - {"id": 20876, "synset": "spike_lavender.n.01", "name": "spike_lavender"}, - {"id": 20877, "synset": "dagga.n.01", "name": "dagga"}, - {"id": 20878, "synset": "lion's-ear.n.01", "name": "lion's-ear"}, - {"id": 20879, "synset": "motherwort.n.01", "name": "motherwort"}, - {"id": 20880, "synset": "pitcher_sage.n.02", "name": "pitcher_sage"}, - {"id": 20881, "synset": "bugleweed.n.01", "name": "bugleweed"}, - {"id": 20882, "synset": "water_horehound.n.01", "name": "water_horehound"}, - {"id": 20883, "synset": "gipsywort.n.01", "name": "gipsywort"}, - {"id": 20884, "synset": "origanum.n.01", "name": "origanum"}, - {"id": 20885, "synset": "oregano.n.01", "name": "oregano"}, - {"id": 20886, "synset": "sweet_marjoram.n.01", "name": "sweet_marjoram"}, - {"id": 20887, "synset": "horehound.n.01", "name": "horehound"}, - {"id": 20888, "synset": "common_horehound.n.01", "name": "common_horehound"}, - {"id": 20889, "synset": "lemon_balm.n.01", "name": "lemon_balm"}, - {"id": 20890, "synset": "corn_mint.n.01", "name": "corn_mint"}, - {"id": 20891, "synset": "water-mint.n.01", "name": "water-mint"}, - {"id": 20892, "synset": "bergamot_mint.n.02", "name": "bergamot_mint"}, - {"id": 20893, "synset": "horsemint.n.03", "name": "horsemint"}, - {"id": 20894, "synset": "peppermint.n.01", "name": "peppermint"}, - {"id": 20895, "synset": "spearmint.n.01", "name": "spearmint"}, - {"id": 20896, "synset": "apple_mint.n.01", "name": "apple_mint"}, - {"id": 20897, "synset": "pennyroyal.n.01", "name": "pennyroyal"}, - {"id": 20898, "synset": "yerba_buena.n.01", "name": "yerba_buena"}, - {"id": 20899, "synset": "molucca_balm.n.01", "name": "molucca_balm"}, - {"id": 20900, "synset": "monarda.n.01", "name": "monarda"}, - {"id": 20901, "synset": "bee_balm.n.02", "name": "bee_balm"}, - {"id": 20902, "synset": "horsemint.n.02", "name": "horsemint"}, - {"id": 20903, "synset": "bee_balm.n.01", "name": "bee_balm"}, - {"id": 20904, "synset": "lemon_mint.n.01", "name": "lemon_mint"}, - {"id": 20905, "synset": "plains_lemon_monarda.n.01", "name": "plains_lemon_monarda"}, - {"id": 20906, "synset": "basil_balm.n.01", "name": "basil_balm"}, - {"id": 20907, "synset": "mustang_mint.n.01", "name": "mustang_mint"}, - {"id": 20908, "synset": "catmint.n.01", "name": "catmint"}, - {"id": 20909, "synset": "basil.n.01", "name": "basil"}, - {"id": 20910, "synset": "beefsteak_plant.n.01", "name": "beefsteak_plant"}, - {"id": 20911, "synset": "phlomis.n.01", "name": "phlomis"}, - {"id": 20912, "synset": "jerusalem_sage.n.01", "name": "Jerusalem_sage"}, - {"id": 20913, "synset": "physostegia.n.01", "name": "physostegia"}, - {"id": 20914, "synset": "plectranthus.n.01", "name": "plectranthus"}, - {"id": 20915, "synset": "patchouli.n.01", "name": "patchouli"}, - {"id": 20916, "synset": "self-heal.n.01", "name": "self-heal"}, - {"id": 20917, "synset": "mountain_mint.n.01", "name": "mountain_mint"}, - {"id": 20918, "synset": "rosemary.n.01", "name": "rosemary"}, - {"id": 20919, "synset": "clary_sage.n.01", "name": "clary_sage"}, - {"id": 20920, "synset": "purple_sage.n.01", "name": "purple_sage"}, - {"id": 20921, "synset": "cancerweed.n.01", "name": "cancerweed"}, - {"id": 20922, "synset": "common_sage.n.01", "name": "common_sage"}, - {"id": 20923, "synset": "meadow_clary.n.01", "name": "meadow_clary"}, - {"id": 20924, "synset": "clary.n.01", "name": "clary"}, - {"id": 20925, "synset": "pitcher_sage.n.01", "name": "pitcher_sage"}, - {"id": 20926, "synset": "mexican_mint.n.01", "name": "Mexican_mint"}, - {"id": 20927, "synset": "wild_sage.n.01", "name": "wild_sage"}, - {"id": 20928, "synset": "savory.n.01", "name": "savory"}, - {"id": 20929, "synset": "summer_savory.n.01", "name": "summer_savory"}, - {"id": 20930, "synset": "winter_savory.n.01", "name": "winter_savory"}, - {"id": 20931, "synset": "skullcap.n.02", "name": "skullcap"}, - {"id": 20932, "synset": "blue_pimpernel.n.01", "name": "blue_pimpernel"}, - {"id": 20933, "synset": "hedge_nettle.n.02", "name": "hedge_nettle"}, - {"id": 20934, "synset": "hedge_nettle.n.01", "name": "hedge_nettle"}, - {"id": 20935, "synset": "germander.n.01", "name": "germander"}, - {"id": 20936, "synset": "american_germander.n.01", "name": "American_germander"}, - {"id": 20937, "synset": "cat_thyme.n.01", "name": "cat_thyme"}, - {"id": 20938, "synset": "wood_sage.n.01", "name": "wood_sage"}, - {"id": 20939, "synset": "thyme.n.01", "name": "thyme"}, - {"id": 20940, "synset": "common_thyme.n.01", "name": "common_thyme"}, - {"id": 20941, "synset": "wild_thyme.n.01", "name": "wild_thyme"}, - {"id": 20942, "synset": "blue_curls.n.01", "name": "blue_curls"}, - {"id": 20943, "synset": "turpentine_camphor_weed.n.01", "name": "turpentine_camphor_weed"}, - {"id": 20944, "synset": "bastard_pennyroyal.n.01", "name": "bastard_pennyroyal"}, - {"id": 20945, "synset": "bladderwort.n.01", "name": "bladderwort"}, - {"id": 20946, "synset": "butterwort.n.01", "name": "butterwort"}, - {"id": 20947, "synset": "genlisea.n.01", "name": "genlisea"}, - {"id": 20948, "synset": "martynia.n.01", "name": "martynia"}, - {"id": 20949, "synset": "common_unicorn_plant.n.01", "name": "common_unicorn_plant"}, - {"id": 20950, "synset": "sand_devil's_claw.n.01", "name": "sand_devil's_claw"}, - {"id": 20951, "synset": "sweet_unicorn_plant.n.01", "name": "sweet_unicorn_plant"}, - {"id": 20952, "synset": "figwort.n.01", "name": "figwort"}, - {"id": 20953, "synset": "snapdragon.n.01", "name": "snapdragon"}, - {"id": 20954, "synset": "white_snapdragon.n.01", "name": "white_snapdragon"}, - {"id": 20955, "synset": "yellow_twining_snapdragon.n.01", "name": "yellow_twining_snapdragon"}, - {"id": 20956, "synset": "mediterranean_snapdragon.n.01", "name": "Mediterranean_snapdragon"}, - {"id": 20957, "synset": "kitten-tails.n.01", "name": "kitten-tails"}, - {"id": 20958, "synset": "alpine_besseya.n.01", "name": "Alpine_besseya"}, - {"id": 20959, "synset": "false_foxglove.n.02", "name": "false_foxglove"}, - {"id": 20960, "synset": "false_foxglove.n.01", "name": "false_foxglove"}, - {"id": 20961, "synset": "calceolaria.n.01", "name": "calceolaria"}, - {"id": 20962, "synset": "indian_paintbrush.n.02", "name": "Indian_paintbrush"}, - {"id": 20963, "synset": "desert_paintbrush.n.01", "name": "desert_paintbrush"}, - {"id": 20964, "synset": "giant_red_paintbrush.n.01", "name": "giant_red_paintbrush"}, - {"id": 20965, "synset": "great_plains_paintbrush.n.01", "name": "great_plains_paintbrush"}, - {"id": 20966, "synset": "sulfur_paintbrush.n.01", "name": "sulfur_paintbrush"}, - {"id": 20967, "synset": "shellflower.n.01", "name": "shellflower"}, - {"id": 20968, "synset": "maiden_blue-eyed_mary.n.01", "name": "maiden_blue-eyed_Mary"}, - {"id": 20969, "synset": "blue-eyed_mary.n.01", "name": "blue-eyed_Mary"}, - {"id": 20970, "synset": "foxglove.n.01", "name": "foxglove"}, - {"id": 20971, "synset": "common_foxglove.n.01", "name": "common_foxglove"}, - {"id": 20972, "synset": "yellow_foxglove.n.01", "name": "yellow_foxglove"}, - {"id": 20973, "synset": "gerardia.n.01", "name": "gerardia"}, - {"id": 20974, "synset": "blue_toadflax.n.01", "name": "blue_toadflax"}, - {"id": 20975, "synset": "toadflax.n.01", "name": "toadflax"}, - {"id": 20976, "synset": "golden-beard_penstemon.n.01", "name": "golden-beard_penstemon"}, - {"id": 20977, "synset": "scarlet_bugler.n.01", "name": "scarlet_bugler"}, - {"id": 20978, "synset": "red_shrubby_penstemon.n.01", "name": "red_shrubby_penstemon"}, - {"id": 20979, "synset": "platte_river_penstemon.n.01", "name": "Platte_River_penstemon"}, - {"id": 20980, "synset": "hot-rock_penstemon.n.01", "name": "hot-rock_penstemon"}, - {"id": 20981, "synset": "jones'_penstemon.n.01", "name": "Jones'_penstemon"}, - {"id": 20982, "synset": "shrubby_penstemon.n.01", "name": "shrubby_penstemon"}, - {"id": 20983, "synset": "narrow-leaf_penstemon.n.01", "name": "narrow-leaf_penstemon"}, - {"id": 20984, "synset": "balloon_flower.n.01", "name": "balloon_flower"}, - {"id": 20985, "synset": "parry's_penstemon.n.01", "name": "Parry's_penstemon"}, - {"id": 20986, "synset": "rock_penstemon.n.01", "name": "rock_penstemon"}, - {"id": 20987, "synset": "rydberg's_penstemon.n.01", "name": "Rydberg's_penstemon"}, - {"id": 20988, "synset": "cascade_penstemon.n.01", "name": "cascade_penstemon"}, - {"id": 20989, "synset": "whipple's_penstemon.n.01", "name": "Whipple's_penstemon"}, - {"id": 20990, "synset": "moth_mullein.n.01", "name": "moth_mullein"}, - {"id": 20991, "synset": "white_mullein.n.01", "name": "white_mullein"}, - {"id": 20992, "synset": "purple_mullein.n.01", "name": "purple_mullein"}, - {"id": 20993, "synset": "common_mullein.n.01", "name": "common_mullein"}, - {"id": 20994, "synset": "veronica.n.01", "name": "veronica"}, - {"id": 20995, "synset": "field_speedwell.n.01", "name": "field_speedwell"}, - {"id": 20996, "synset": "brooklime.n.02", "name": "brooklime"}, - {"id": 20997, "synset": "corn_speedwell.n.01", "name": "corn_speedwell"}, - {"id": 20998, "synset": "brooklime.n.01", "name": "brooklime"}, - {"id": 20999, "synset": "germander_speedwell.n.01", "name": "germander_speedwell"}, - {"id": 21000, "synset": "water_speedwell.n.01", "name": "water_speedwell"}, - {"id": 21001, "synset": "common_speedwell.n.01", "name": "common_speedwell"}, - {"id": 21002, "synset": "purslane_speedwell.n.01", "name": "purslane_speedwell"}, - {"id": 21003, "synset": "thyme-leaved_speedwell.n.01", "name": "thyme-leaved_speedwell"}, - {"id": 21004, "synset": "nightshade.n.01", "name": "nightshade"}, - {"id": 21005, "synset": "horse_nettle.n.01", "name": "horse_nettle"}, - {"id": 21006, "synset": "african_holly.n.01", "name": "African_holly"}, - {"id": 21007, "synset": "potato_vine.n.02", "name": "potato_vine"}, - {"id": 21008, "synset": "garden_huckleberry.n.01", "name": "garden_huckleberry"}, - {"id": 21009, "synset": "naranjilla.n.01", "name": "naranjilla"}, - {"id": 21010, "synset": "potato_vine.n.01", "name": "potato_vine"}, - {"id": 21011, "synset": "potato_tree.n.01", "name": "potato_tree"}, - {"id": 21012, "synset": "belladonna.n.01", "name": "belladonna"}, - {"id": 21013, "synset": "bush_violet.n.01", "name": "bush_violet"}, - {"id": 21014, "synset": "lady-of-the-night.n.01", "name": "lady-of-the-night"}, - {"id": 21015, "synset": "angel's_trumpet.n.02", "name": "angel's_trumpet"}, - {"id": 21016, "synset": "angel's_trumpet.n.01", "name": "angel's_trumpet"}, - {"id": 21017, "synset": "red_angel's_trumpet.n.01", "name": "red_angel's_trumpet"}, - {"id": 21018, "synset": "cone_pepper.n.01", "name": "cone_pepper"}, - {"id": 21019, "synset": "bird_pepper.n.01", "name": "bird_pepper"}, - {"id": 21020, "synset": "day_jessamine.n.01", "name": "day_jessamine"}, - {"id": 21021, "synset": "night_jasmine.n.01", "name": "night_jasmine"}, - {"id": 21022, "synset": "tree_tomato.n.01", "name": "tree_tomato"}, - {"id": 21023, "synset": "thorn_apple.n.01", "name": "thorn_apple"}, - {"id": 21024, "synset": "jimsonweed.n.01", "name": "jimsonweed"}, - {"id": 21025, "synset": "pichi.n.01", "name": "pichi"}, - {"id": 21026, "synset": "henbane.n.01", "name": "henbane"}, - {"id": 21027, "synset": "egyptian_henbane.n.01", "name": "Egyptian_henbane"}, - {"id": 21028, "synset": "matrimony_vine.n.01", "name": "matrimony_vine"}, - {"id": 21029, "synset": "common_matrimony_vine.n.01", "name": "common_matrimony_vine"}, - {"id": 21030, "synset": "christmasberry.n.01", "name": "Christmasberry"}, - {"id": 21031, "synset": "plum_tomato.n.01", "name": "plum_tomato"}, - {"id": 21032, "synset": "mandrake.n.02", "name": "mandrake"}, - {"id": 21033, "synset": "mandrake_root.n.01", "name": "mandrake_root"}, - {"id": 21034, "synset": "apple_of_peru.n.01", "name": "apple_of_Peru"}, - {"id": 21035, "synset": "flowering_tobacco.n.01", "name": "flowering_tobacco"}, - {"id": 21036, "synset": "common_tobacco.n.01", "name": "common_tobacco"}, - {"id": 21037, "synset": "wild_tobacco.n.01", "name": "wild_tobacco"}, - {"id": 21038, "synset": "cupflower.n.02", "name": "cupflower"}, - {"id": 21039, "synset": "whitecup.n.01", "name": "whitecup"}, - {"id": 21040, "synset": "petunia.n.01", "name": "petunia"}, - {"id": 21041, "synset": "large_white_petunia.n.01", "name": "large_white_petunia"}, - {"id": 21042, "synset": "violet-flowered_petunia.n.01", "name": "violet-flowered_petunia"}, - {"id": 21043, "synset": "hybrid_petunia.n.01", "name": "hybrid_petunia"}, - {"id": 21044, "synset": "cape_gooseberry.n.01", "name": "cape_gooseberry"}, - {"id": 21045, "synset": "strawberry_tomato.n.01", "name": "strawberry_tomato"}, - {"id": 21046, "synset": "tomatillo.n.02", "name": "tomatillo"}, - {"id": 21047, "synset": "tomatillo.n.01", "name": "tomatillo"}, - {"id": 21048, "synset": "yellow_henbane.n.01", "name": "yellow_henbane"}, - {"id": 21049, "synset": "cock's_eggs.n.01", "name": "cock's_eggs"}, - {"id": 21050, "synset": "salpiglossis.n.01", "name": "salpiglossis"}, - {"id": 21051, "synset": "painted_tongue.n.01", "name": "painted_tongue"}, - {"id": 21052, "synset": "butterfly_flower.n.01", "name": "butterfly_flower"}, - {"id": 21053, "synset": "scopolia_carniolica.n.01", "name": "Scopolia_carniolica"}, - {"id": 21054, "synset": "chalice_vine.n.01", "name": "chalice_vine"}, - {"id": 21055, "synset": "verbena.n.01", "name": "verbena"}, - {"id": 21056, "synset": "lantana.n.01", "name": "lantana"}, - {"id": 21057, "synset": "black_mangrove.n.02", "name": "black_mangrove"}, - {"id": 21058, "synset": "white_mangrove.n.01", "name": "white_mangrove"}, - {"id": 21059, "synset": "black_mangrove.n.01", "name": "black_mangrove"}, - {"id": 21060, "synset": "teak.n.02", "name": "teak"}, - {"id": 21061, "synset": "spurge.n.01", "name": "spurge"}, - {"id": 21062, "synset": "sun_spurge.n.01", "name": "sun_spurge"}, - {"id": 21063, "synset": "petty_spurge.n.01", "name": "petty_spurge"}, - {"id": 21064, "synset": "medusa's_head.n.01", "name": "medusa's_head"}, - {"id": 21065, "synset": "wild_spurge.n.01", "name": "wild_spurge"}, - {"id": 21066, "synset": "snow-on-the-mountain.n.01", "name": "snow-on-the-mountain"}, - {"id": 21067, "synset": "cypress_spurge.n.01", "name": "cypress_spurge"}, - {"id": 21068, "synset": "leafy_spurge.n.01", "name": "leafy_spurge"}, - {"id": 21069, "synset": "hairy_spurge.n.01", "name": "hairy_spurge"}, - {"id": 21070, "synset": "poinsettia.n.01", "name": "poinsettia"}, - {"id": 21071, "synset": "japanese_poinsettia.n.01", "name": "Japanese_poinsettia"}, - {"id": 21072, "synset": "fire-on-the-mountain.n.01", "name": "fire-on-the-mountain"}, - {"id": 21073, "synset": "wood_spurge.n.01", "name": "wood_spurge"}, - {"id": 21074, "synset": "dwarf_spurge.n.01", "name": "dwarf_spurge"}, - {"id": 21075, "synset": "scarlet_plume.n.01", "name": "scarlet_plume"}, - {"id": 21076, "synset": "naboom.n.01", "name": "naboom"}, - {"id": 21077, "synset": "crown_of_thorns.n.02", "name": "crown_of_thorns"}, - {"id": 21078, "synset": "toothed_spurge.n.01", "name": "toothed_spurge"}, - {"id": 21079, "synset": "three-seeded_mercury.n.01", "name": "three-seeded_mercury"}, - {"id": 21080, "synset": "croton.n.02", "name": "croton"}, - {"id": 21081, "synset": "cascarilla.n.01", "name": "cascarilla"}, - {"id": 21082, "synset": "cascarilla_bark.n.01", "name": "cascarilla_bark"}, - {"id": 21083, "synset": "castor-oil_plant.n.01", "name": "castor-oil_plant"}, - {"id": 21084, "synset": "spurge_nettle.n.01", "name": "spurge_nettle"}, - {"id": 21085, "synset": "physic_nut.n.01", "name": "physic_nut"}, - {"id": 21086, "synset": "para_rubber_tree.n.01", "name": "Para_rubber_tree"}, - {"id": 21087, "synset": "cassava.n.03", "name": "cassava"}, - {"id": 21088, "synset": "bitter_cassava.n.01", "name": "bitter_cassava"}, - {"id": 21089, "synset": "cassava.n.02", "name": "cassava"}, - {"id": 21090, "synset": "sweet_cassava.n.01", "name": "sweet_cassava"}, - {"id": 21091, "synset": "candlenut.n.01", "name": "candlenut"}, - {"id": 21092, "synset": "tung_tree.n.01", "name": "tung_tree"}, - {"id": 21093, "synset": "slipper_spurge.n.01", "name": "slipper_spurge"}, - {"id": 21094, "synset": "candelilla.n.01", "name": "candelilla"}, - {"id": 21095, "synset": "jewbush.n.01", "name": "Jewbush"}, - {"id": 21096, "synset": "jumping_bean.n.01", "name": "jumping_bean"}, - {"id": 21097, "synset": "camellia.n.01", "name": "camellia"}, - {"id": 21098, "synset": "japonica.n.01", "name": "japonica"}, - {"id": 21099, "synset": "umbellifer.n.01", "name": "umbellifer"}, - {"id": 21100, "synset": "wild_parsley.n.01", "name": "wild_parsley"}, - {"id": 21101, "synset": "fool's_parsley.n.01", "name": "fool's_parsley"}, - {"id": 21102, "synset": "dill.n.01", "name": "dill"}, - {"id": 21103, "synset": "angelica.n.01", "name": "angelica"}, - {"id": 21104, "synset": "garden_angelica.n.01", "name": "garden_angelica"}, - {"id": 21105, "synset": "wild_angelica.n.01", "name": "wild_angelica"}, - {"id": 21106, "synset": "chervil.n.01", "name": "chervil"}, - {"id": 21107, "synset": "cow_parsley.n.01", "name": "cow_parsley"}, - {"id": 21108, "synset": "wild_celery.n.01", "name": "wild_celery"}, - {"id": 21109, "synset": "astrantia.n.01", "name": "astrantia"}, - {"id": 21110, "synset": "greater_masterwort.n.01", "name": "greater_masterwort"}, - {"id": 21111, "synset": "caraway.n.01", "name": "caraway"}, - {"id": 21112, "synset": "whorled_caraway.n.01", "name": "whorled_caraway"}, - {"id": 21113, "synset": "water_hemlock.n.01", "name": "water_hemlock"}, - {"id": 21114, "synset": "spotted_cowbane.n.01", "name": "spotted_cowbane"}, - {"id": 21115, "synset": "hemlock.n.02", "name": "hemlock"}, - {"id": 21116, "synset": "earthnut.n.02", "name": "earthnut"}, - {"id": 21117, "synset": "cumin.n.01", "name": "cumin"}, - {"id": 21118, "synset": "wild_carrot.n.01", "name": "wild_carrot"}, - {"id": 21119, "synset": "eryngo.n.01", "name": "eryngo"}, - {"id": 21120, "synset": "sea_holly.n.01", "name": "sea_holly"}, - {"id": 21121, "synset": "button_snakeroot.n.02", "name": "button_snakeroot"}, - {"id": 21122, "synset": "rattlesnake_master.n.01", "name": "rattlesnake_master"}, - {"id": 21123, "synset": "fennel.n.01", "name": "fennel"}, - {"id": 21124, "synset": "common_fennel.n.01", "name": "common_fennel"}, - {"id": 21125, "synset": "florence_fennel.n.01", "name": "Florence_fennel"}, - {"id": 21126, "synset": "cow_parsnip.n.01", "name": "cow_parsnip"}, - {"id": 21127, "synset": "lovage.n.01", "name": "lovage"}, - {"id": 21128, "synset": "sweet_cicely.n.01", "name": "sweet_cicely"}, - {"id": 21129, "synset": "water_fennel.n.01", "name": "water_fennel"}, - {"id": 21130, "synset": "parsnip.n.02", "name": "parsnip"}, - {"id": 21131, "synset": "cultivated_parsnip.n.01", "name": "cultivated_parsnip"}, - {"id": 21132, "synset": "wild_parsnip.n.01", "name": "wild_parsnip"}, - {"id": 21133, "synset": "parsley.n.01", "name": "parsley"}, - {"id": 21134, "synset": "italian_parsley.n.01", "name": "Italian_parsley"}, - {"id": 21135, "synset": "hamburg_parsley.n.01", "name": "Hamburg_parsley"}, - {"id": 21136, "synset": "anise.n.01", "name": "anise"}, - {"id": 21137, "synset": "sanicle.n.01", "name": "sanicle"}, - {"id": 21138, "synset": "purple_sanicle.n.01", "name": "purple_sanicle"}, - {"id": 21139, "synset": "european_sanicle.n.01", "name": "European_sanicle"}, - {"id": 21140, "synset": "water_parsnip.n.01", "name": "water_parsnip"}, - {"id": 21141, "synset": "greater_water_parsnip.n.01", "name": "greater_water_parsnip"}, - {"id": 21142, "synset": "skirret.n.01", "name": "skirret"}, - {"id": 21143, "synset": "dogwood.n.01", "name": "dogwood"}, - {"id": 21144, "synset": "common_white_dogwood.n.01", "name": "common_white_dogwood"}, - {"id": 21145, "synset": "red_osier.n.01", "name": "red_osier"}, - {"id": 21146, "synset": "silky_dogwood.n.02", "name": "silky_dogwood"}, - {"id": 21147, "synset": "silky_cornel.n.01", "name": "silky_cornel"}, - {"id": 21148, "synset": "common_european_dogwood.n.01", "name": "common_European_dogwood"}, - {"id": 21149, "synset": "bunchberry.n.01", "name": "bunchberry"}, - {"id": 21150, "synset": "cornelian_cherry.n.01", "name": "cornelian_cherry"}, - {"id": 21151, "synset": "puka.n.01", "name": "puka"}, - {"id": 21152, "synset": "kapuka.n.01", "name": "kapuka"}, - {"id": 21153, "synset": "valerian.n.01", "name": "valerian"}, - {"id": 21154, "synset": "common_valerian.n.01", "name": "common_valerian"}, - {"id": 21155, "synset": "common_corn_salad.n.01", "name": "common_corn_salad"}, - {"id": 21156, "synset": "red_valerian.n.01", "name": "red_valerian"}, - {"id": 21157, "synset": "filmy_fern.n.02", "name": "filmy_fern"}, - {"id": 21158, "synset": "bristle_fern.n.01", "name": "bristle_fern"}, - {"id": 21159, "synset": "hare's-foot_bristle_fern.n.01", "name": "hare's-foot_bristle_fern"}, - {"id": 21160, "synset": "killarney_fern.n.01", "name": "Killarney_fern"}, - {"id": 21161, "synset": "kidney_fern.n.01", "name": "kidney_fern"}, - {"id": 21162, "synset": "flowering_fern.n.02", "name": "flowering_fern"}, - {"id": 21163, "synset": "royal_fern.n.01", "name": "royal_fern"}, - {"id": 21164, "synset": "interrupted_fern.n.01", "name": "interrupted_fern"}, - {"id": 21165, "synset": "crape_fern.n.01", "name": "crape_fern"}, - {"id": 21166, "synset": "crepe_fern.n.01", "name": "crepe_fern"}, - {"id": 21167, "synset": "curly_grass.n.01", "name": "curly_grass"}, - {"id": 21168, "synset": "pine_fern.n.01", "name": "pine_fern"}, - {"id": 21169, "synset": "climbing_fern.n.01", "name": "climbing_fern"}, - {"id": 21170, "synset": "creeping_fern.n.01", "name": "creeping_fern"}, - {"id": 21171, "synset": "climbing_maidenhair.n.01", "name": "climbing_maidenhair"}, - {"id": 21172, "synset": "scented_fern.n.02", "name": "scented_fern"}, - {"id": 21173, "synset": "clover_fern.n.01", "name": "clover_fern"}, - {"id": 21174, "synset": "nardoo.n.01", "name": "nardoo"}, - {"id": 21175, "synset": "water_clover.n.01", "name": "water_clover"}, - {"id": 21176, "synset": "pillwort.n.01", "name": "pillwort"}, - {"id": 21177, "synset": "regnellidium.n.01", "name": "regnellidium"}, - {"id": 21178, "synset": "floating-moss.n.01", "name": "floating-moss"}, - {"id": 21179, "synset": "mosquito_fern.n.01", "name": "mosquito_fern"}, - {"id": 21180, "synset": "adder's_tongue.n.01", "name": "adder's_tongue"}, - {"id": 21181, "synset": "ribbon_fern.n.03", "name": "ribbon_fern"}, - {"id": 21182, "synset": "grape_fern.n.01", "name": "grape_fern"}, - {"id": 21183, "synset": "daisyleaf_grape_fern.n.01", "name": "daisyleaf_grape_fern"}, - {"id": 21184, "synset": "leathery_grape_fern.n.01", "name": "leathery_grape_fern"}, - {"id": 21185, "synset": "rattlesnake_fern.n.01", "name": "rattlesnake_fern"}, - {"id": 21186, "synset": "flowering_fern.n.01", "name": "flowering_fern"}, - {"id": 21187, "synset": "powdery_mildew.n.01", "name": "powdery_mildew"}, - {"id": 21188, "synset": "dutch_elm_fungus.n.01", "name": "Dutch_elm_fungus"}, - {"id": 21189, "synset": "ergot.n.02", "name": "ergot"}, - {"id": 21190, "synset": "rye_ergot.n.01", "name": "rye_ergot"}, - {"id": 21191, "synset": "black_root_rot_fungus.n.01", "name": "black_root_rot_fungus"}, - {"id": 21192, "synset": "dead-man's-fingers.n.01", "name": "dead-man's-fingers"}, - {"id": 21193, "synset": "sclerotinia.n.01", "name": "sclerotinia"}, - {"id": 21194, "synset": "brown_cup.n.01", "name": "brown_cup"}, - {"id": 21195, "synset": "earthball.n.01", "name": "earthball"}, - {"id": 21196, "synset": "scleroderma_citrinum.n.01", "name": "Scleroderma_citrinum"}, - {"id": 21197, "synset": "scleroderma_flavidium.n.01", "name": "Scleroderma_flavidium"}, - {"id": 21198, "synset": "scleroderma_bovista.n.01", "name": "Scleroderma_bovista"}, - {"id": 21199, "synset": "podaxaceae.n.01", "name": "Podaxaceae"}, - {"id": 21200, "synset": "stalked_puffball.n.02", "name": "stalked_puffball"}, - {"id": 21201, "synset": "stalked_puffball.n.01", "name": "stalked_puffball"}, - {"id": 21202, "synset": "false_truffle.n.01", "name": "false_truffle"}, - {"id": 21203, "synset": "rhizopogon_idahoensis.n.01", "name": "Rhizopogon_idahoensis"}, - {"id": 21204, "synset": "truncocolumella_citrina.n.01", "name": "Truncocolumella_citrina"}, - {"id": 21205, "synset": "mucor.n.01", "name": "mucor"}, - {"id": 21206, "synset": "rhizopus.n.01", "name": "rhizopus"}, - {"id": 21207, "synset": "bread_mold.n.01", "name": "bread_mold"}, - {"id": 21208, "synset": "slime_mold.n.01", "name": "slime_mold"}, - {"id": 21209, "synset": "true_slime_mold.n.01", "name": "true_slime_mold"}, - {"id": 21210, "synset": "cellular_slime_mold.n.01", "name": "cellular_slime_mold"}, - {"id": 21211, "synset": "dictostylium.n.01", "name": "dictostylium"}, - {"id": 21212, "synset": "pond-scum_parasite.n.01", "name": "pond-scum_parasite"}, - {"id": 21213, "synset": "potato_wart_fungus.n.01", "name": "potato_wart_fungus"}, - {"id": 21214, "synset": "white_fungus.n.01", "name": "white_fungus"}, - {"id": 21215, "synset": "water_mold.n.01", "name": "water_mold"}, - {"id": 21216, "synset": "downy_mildew.n.01", "name": "downy_mildew"}, - {"id": 21217, "synset": "blue_mold_fungus.n.01", "name": "blue_mold_fungus"}, - {"id": 21218, "synset": "onion_mildew.n.01", "name": "onion_mildew"}, - {"id": 21219, "synset": "tobacco_mildew.n.01", "name": "tobacco_mildew"}, - {"id": 21220, "synset": "white_rust.n.01", "name": "white_rust"}, - {"id": 21221, "synset": "pythium.n.01", "name": "pythium"}, - {"id": 21222, "synset": "damping_off_fungus.n.01", "name": "damping_off_fungus"}, - {"id": 21223, "synset": "phytophthora_citrophthora.n.01", "name": "Phytophthora_citrophthora"}, - {"id": 21224, "synset": "phytophthora_infestans.n.01", "name": "Phytophthora_infestans"}, - {"id": 21225, "synset": "clubroot_fungus.n.01", "name": "clubroot_fungus"}, - {"id": 21226, "synset": "geglossaceae.n.01", "name": "Geglossaceae"}, - {"id": 21227, "synset": "sarcosomataceae.n.01", "name": "Sarcosomataceae"}, - {"id": 21228, "synset": "rufous_rubber_cup.n.01", "name": "Rufous_rubber_cup"}, - {"id": 21229, "synset": "devil's_cigar.n.01", "name": "devil's_cigar"}, - {"id": 21230, "synset": "devil's_urn.n.01", "name": "devil's_urn"}, - {"id": 21231, "synset": "truffle.n.01", "name": "truffle"}, - {"id": 21232, "synset": "club_fungus.n.01", "name": "club_fungus"}, - {"id": 21233, "synset": "coral_fungus.n.01", "name": "coral_fungus"}, - {"id": 21234, "synset": "tooth_fungus.n.01", "name": "tooth_fungus"}, - {"id": 21235, "synset": "lichen.n.02", "name": "lichen"}, - {"id": 21236, "synset": "ascolichen.n.01", "name": "ascolichen"}, - {"id": 21237, "synset": "basidiolichen.n.01", "name": "basidiolichen"}, - {"id": 21238, "synset": "lecanora.n.01", "name": "lecanora"}, - {"id": 21239, "synset": "manna_lichen.n.01", "name": "manna_lichen"}, - {"id": 21240, "synset": "archil.n.02", "name": "archil"}, - {"id": 21241, "synset": "roccella.n.01", "name": "roccella"}, - {"id": 21242, "synset": "beard_lichen.n.01", "name": "beard_lichen"}, - {"id": 21243, "synset": "horsehair_lichen.n.01", "name": "horsehair_lichen"}, - {"id": 21244, "synset": "reindeer_moss.n.01", "name": "reindeer_moss"}, - {"id": 21245, "synset": "crottle.n.01", "name": "crottle"}, - {"id": 21246, "synset": "iceland_moss.n.01", "name": "Iceland_moss"}, - {"id": 21247, "synset": "fungus.n.01", "name": "fungus"}, - {"id": 21248, "synset": "promycelium.n.01", "name": "promycelium"}, - {"id": 21249, "synset": "true_fungus.n.01", "name": "true_fungus"}, - {"id": 21250, "synset": "basidiomycete.n.01", "name": "basidiomycete"}, - {"id": 21251, "synset": "mushroom.n.03", "name": "mushroom"}, - {"id": 21252, "synset": "agaric.n.02", "name": "agaric"}, - {"id": 21253, "synset": "mushroom.n.01", "name": "mushroom"}, - {"id": 21254, "synset": "toadstool.n.01", "name": "toadstool"}, - {"id": 21255, "synset": "horse_mushroom.n.01", "name": "horse_mushroom"}, - {"id": 21256, "synset": "meadow_mushroom.n.01", "name": "meadow_mushroom"}, - {"id": 21257, "synset": "shiitake.n.01", "name": "shiitake"}, - {"id": 21258, "synset": "scaly_lentinus.n.01", "name": "scaly_lentinus"}, - {"id": 21259, "synset": "royal_agaric.n.01", "name": "royal_agaric"}, - {"id": 21260, "synset": "false_deathcap.n.01", "name": "false_deathcap"}, - {"id": 21261, "synset": "fly_agaric.n.01", "name": "fly_agaric"}, - {"id": 21262, "synset": "death_cap.n.01", "name": "death_cap"}, - {"id": 21263, "synset": "blushing_mushroom.n.01", "name": "blushing_mushroom"}, - {"id": 21264, "synset": "destroying_angel.n.01", "name": "destroying_angel"}, - {"id": 21265, "synset": "chanterelle.n.01", "name": "chanterelle"}, - {"id": 21266, "synset": "floccose_chanterelle.n.01", "name": "floccose_chanterelle"}, - {"id": 21267, "synset": "pig's_ears.n.01", "name": "pig's_ears"}, - {"id": 21268, "synset": "cinnabar_chanterelle.n.01", "name": "cinnabar_chanterelle"}, - {"id": 21269, "synset": "jack-o-lantern_fungus.n.01", "name": "jack-o-lantern_fungus"}, - {"id": 21270, "synset": "inky_cap.n.01", "name": "inky_cap"}, - {"id": 21271, "synset": "shaggymane.n.01", "name": "shaggymane"}, - {"id": 21272, "synset": "milkcap.n.01", "name": "milkcap"}, - {"id": 21273, "synset": "fairy-ring_mushroom.n.01", "name": "fairy-ring_mushroom"}, - {"id": 21274, "synset": "fairy_ring.n.01", "name": "fairy_ring"}, - {"id": 21275, "synset": "oyster_mushroom.n.01", "name": "oyster_mushroom"}, - {"id": 21276, "synset": "olive-tree_agaric.n.01", "name": "olive-tree_agaric"}, - {"id": 21277, "synset": "pholiota_astragalina.n.01", "name": "Pholiota_astragalina"}, - {"id": 21278, "synset": "pholiota_aurea.n.01", "name": "Pholiota_aurea"}, - {"id": 21279, "synset": "pholiota_destruens.n.01", "name": "Pholiota_destruens"}, - {"id": 21280, "synset": "pholiota_flammans.n.01", "name": "Pholiota_flammans"}, - {"id": 21281, "synset": "pholiota_flavida.n.01", "name": "Pholiota_flavida"}, - {"id": 21282, "synset": "nameko.n.01", "name": "nameko"}, - { - "id": 21283, - "synset": "pholiota_squarrosa-adiposa.n.01", - "name": "Pholiota_squarrosa-adiposa", - }, - {"id": 21284, "synset": "pholiota_squarrosa.n.01", "name": "Pholiota_squarrosa"}, - {"id": 21285, "synset": "pholiota_squarrosoides.n.01", "name": "Pholiota_squarrosoides"}, - {"id": 21286, "synset": "stropharia_ambigua.n.01", "name": "Stropharia_ambigua"}, - {"id": 21287, "synset": "stropharia_hornemannii.n.01", "name": "Stropharia_hornemannii"}, - { - "id": 21288, - "synset": "stropharia_rugoso-annulata.n.01", - "name": "Stropharia_rugoso-annulata", - }, - {"id": 21289, "synset": "gill_fungus.n.01", "name": "gill_fungus"}, - {"id": 21290, "synset": "entoloma_lividum.n.01", "name": "Entoloma_lividum"}, - {"id": 21291, "synset": "entoloma_aprile.n.01", "name": "Entoloma_aprile"}, - {"id": 21292, "synset": "chlorophyllum_molybdites.n.01", "name": "Chlorophyllum_molybdites"}, - {"id": 21293, "synset": "lepiota.n.01", "name": "lepiota"}, - {"id": 21294, "synset": "parasol_mushroom.n.01", "name": "parasol_mushroom"}, - {"id": 21295, "synset": "poisonous_parasol.n.01", "name": "poisonous_parasol"}, - {"id": 21296, "synset": "lepiota_naucina.n.01", "name": "Lepiota_naucina"}, - {"id": 21297, "synset": "lepiota_rhacodes.n.01", "name": "Lepiota_rhacodes"}, - {"id": 21298, "synset": "american_parasol.n.01", "name": "American_parasol"}, - {"id": 21299, "synset": "lepiota_rubrotincta.n.01", "name": "Lepiota_rubrotincta"}, - {"id": 21300, "synset": "lepiota_clypeolaria.n.01", "name": "Lepiota_clypeolaria"}, - {"id": 21301, "synset": "onion_stem.n.01", "name": "onion_stem"}, - {"id": 21302, "synset": "pink_disease_fungus.n.01", "name": "pink_disease_fungus"}, - {"id": 21303, "synset": "bottom_rot_fungus.n.01", "name": "bottom_rot_fungus"}, - {"id": 21304, "synset": "potato_fungus.n.01", "name": "potato_fungus"}, - {"id": 21305, "synset": "coffee_fungus.n.01", "name": "coffee_fungus"}, - {"id": 21306, "synset": "blewits.n.01", "name": "blewits"}, - {"id": 21307, "synset": "sandy_mushroom.n.01", "name": "sandy_mushroom"}, - {"id": 21308, "synset": "tricholoma_pessundatum.n.01", "name": "Tricholoma_pessundatum"}, - {"id": 21309, "synset": "tricholoma_sejunctum.n.01", "name": "Tricholoma_sejunctum"}, - {"id": 21310, "synset": "man-on-a-horse.n.01", "name": "man-on-a-horse"}, - {"id": 21311, "synset": "tricholoma_venenata.n.01", "name": "Tricholoma_venenata"}, - {"id": 21312, "synset": "tricholoma_pardinum.n.01", "name": "Tricholoma_pardinum"}, - {"id": 21313, "synset": "tricholoma_vaccinum.n.01", "name": "Tricholoma_vaccinum"}, - {"id": 21314, "synset": "tricholoma_aurantium.n.01", "name": "Tricholoma_aurantium"}, - {"id": 21315, "synset": "volvaria_bombycina.n.01", "name": "Volvaria_bombycina"}, - {"id": 21316, "synset": "pluteus_aurantiorugosus.n.01", "name": "Pluteus_aurantiorugosus"}, - {"id": 21317, "synset": "pluteus_magnus.n.01", "name": "Pluteus_magnus"}, - {"id": 21318, "synset": "deer_mushroom.n.01", "name": "deer_mushroom"}, - {"id": 21319, "synset": "straw_mushroom.n.01", "name": "straw_mushroom"}, - {"id": 21320, "synset": "volvariella_bombycina.n.01", "name": "Volvariella_bombycina"}, - {"id": 21321, "synset": "clitocybe_clavipes.n.01", "name": "Clitocybe_clavipes"}, - {"id": 21322, "synset": "clitocybe_dealbata.n.01", "name": "Clitocybe_dealbata"}, - {"id": 21323, "synset": "clitocybe_inornata.n.01", "name": "Clitocybe_inornata"}, - {"id": 21324, "synset": "clitocybe_robusta.n.01", "name": "Clitocybe_robusta"}, - {"id": 21325, "synset": "clitocybe_irina.n.01", "name": "Clitocybe_irina"}, - {"id": 21326, "synset": "clitocybe_subconnexa.n.01", "name": "Clitocybe_subconnexa"}, - {"id": 21327, "synset": "winter_mushroom.n.01", "name": "winter_mushroom"}, - {"id": 21328, "synset": "mycelium.n.01", "name": "mycelium"}, - {"id": 21329, "synset": "sclerotium.n.02", "name": "sclerotium"}, - {"id": 21330, "synset": "sac_fungus.n.01", "name": "sac_fungus"}, - {"id": 21331, "synset": "ascomycete.n.01", "name": "ascomycete"}, - {"id": 21332, "synset": "clavicipitaceae.n.01", "name": "Clavicipitaceae"}, - {"id": 21333, "synset": "grainy_club.n.01", "name": "grainy_club"}, - {"id": 21334, "synset": "yeast.n.02", "name": "yeast"}, - {"id": 21335, "synset": "baker's_yeast.n.01", "name": "baker's_yeast"}, - {"id": 21336, "synset": "wine-maker's_yeast.n.01", "name": "wine-maker's_yeast"}, - {"id": 21337, "synset": "aspergillus_fumigatus.n.01", "name": "Aspergillus_fumigatus"}, - {"id": 21338, "synset": "brown_root_rot_fungus.n.01", "name": "brown_root_rot_fungus"}, - {"id": 21339, "synset": "discomycete.n.01", "name": "discomycete"}, - {"id": 21340, "synset": "leotia_lubrica.n.01", "name": "Leotia_lubrica"}, - {"id": 21341, "synset": "mitrula_elegans.n.01", "name": "Mitrula_elegans"}, - {"id": 21342, "synset": "sarcoscypha_coccinea.n.01", "name": "Sarcoscypha_coccinea"}, - {"id": 21343, "synset": "caloscypha_fulgens.n.01", "name": "Caloscypha_fulgens"}, - {"id": 21344, "synset": "aleuria_aurantia.n.01", "name": "Aleuria_aurantia"}, - {"id": 21345, "synset": "elf_cup.n.01", "name": "elf_cup"}, - {"id": 21346, "synset": "peziza_domicilina.n.01", "name": "Peziza_domicilina"}, - {"id": 21347, "synset": "blood_cup.n.01", "name": "blood_cup"}, - {"id": 21348, "synset": "urnula_craterium.n.01", "name": "Urnula_craterium"}, - {"id": 21349, "synset": "galiella_rufa.n.01", "name": "Galiella_rufa"}, - {"id": 21350, "synset": "jafnea_semitosta.n.01", "name": "Jafnea_semitosta"}, - {"id": 21351, "synset": "morel.n.01", "name": "morel"}, - {"id": 21352, "synset": "common_morel.n.01", "name": "common_morel"}, - {"id": 21353, "synset": "disciotis_venosa.n.01", "name": "Disciotis_venosa"}, - {"id": 21354, "synset": "verpa.n.01", "name": "Verpa"}, - {"id": 21355, "synset": "verpa_bohemica.n.01", "name": "Verpa_bohemica"}, - {"id": 21356, "synset": "verpa_conica.n.01", "name": "Verpa_conica"}, - {"id": 21357, "synset": "black_morel.n.01", "name": "black_morel"}, - {"id": 21358, "synset": "morchella_crassipes.n.01", "name": "Morchella_crassipes"}, - {"id": 21359, "synset": "morchella_semilibera.n.01", "name": "Morchella_semilibera"}, - {"id": 21360, "synset": "wynnea_americana.n.01", "name": "Wynnea_americana"}, - {"id": 21361, "synset": "wynnea_sparassoides.n.01", "name": "Wynnea_sparassoides"}, - {"id": 21362, "synset": "false_morel.n.01", "name": "false_morel"}, - {"id": 21363, "synset": "lorchel.n.01", "name": "lorchel"}, - {"id": 21364, "synset": "helvella.n.01", "name": "helvella"}, - {"id": 21365, "synset": "helvella_crispa.n.01", "name": "Helvella_crispa"}, - {"id": 21366, "synset": "helvella_acetabulum.n.01", "name": "Helvella_acetabulum"}, - {"id": 21367, "synset": "helvella_sulcata.n.01", "name": "Helvella_sulcata"}, - {"id": 21368, "synset": "discina.n.01", "name": "discina"}, - {"id": 21369, "synset": "gyromitra.n.01", "name": "gyromitra"}, - {"id": 21370, "synset": "gyromitra_californica.n.01", "name": "Gyromitra_californica"}, - {"id": 21371, "synset": "gyromitra_sphaerospora.n.01", "name": "Gyromitra_sphaerospora"}, - {"id": 21372, "synset": "gyromitra_esculenta.n.01", "name": "Gyromitra_esculenta"}, - {"id": 21373, "synset": "gyromitra_infula.n.01", "name": "Gyromitra_infula"}, - {"id": 21374, "synset": "gyromitra_fastigiata.n.01", "name": "Gyromitra_fastigiata"}, - {"id": 21375, "synset": "gyromitra_gigas.n.01", "name": "Gyromitra_gigas"}, - {"id": 21376, "synset": "gasteromycete.n.01", "name": "gasteromycete"}, - {"id": 21377, "synset": "stinkhorn.n.01", "name": "stinkhorn"}, - {"id": 21378, "synset": "common_stinkhorn.n.01", "name": "common_stinkhorn"}, - {"id": 21379, "synset": "phallus_ravenelii.n.01", "name": "Phallus_ravenelii"}, - {"id": 21380, "synset": "dog_stinkhorn.n.01", "name": "dog_stinkhorn"}, - {"id": 21381, "synset": "calostoma_lutescens.n.01", "name": "Calostoma_lutescens"}, - {"id": 21382, "synset": "calostoma_cinnabarina.n.01", "name": "Calostoma_cinnabarina"}, - {"id": 21383, "synset": "calostoma_ravenelii.n.01", "name": "Calostoma_ravenelii"}, - {"id": 21384, "synset": "stinky_squid.n.01", "name": "stinky_squid"}, - {"id": 21385, "synset": "puffball.n.01", "name": "puffball"}, - {"id": 21386, "synset": "giant_puffball.n.01", "name": "giant_puffball"}, - {"id": 21387, "synset": "earthstar.n.01", "name": "earthstar"}, - {"id": 21388, "synset": "geastrum_coronatum.n.01", "name": "Geastrum_coronatum"}, - {"id": 21389, "synset": "radiigera_fuscogleba.n.01", "name": "Radiigera_fuscogleba"}, - {"id": 21390, "synset": "astreus_pteridis.n.01", "name": "Astreus_pteridis"}, - {"id": 21391, "synset": "astreus_hygrometricus.n.01", "name": "Astreus_hygrometricus"}, - {"id": 21392, "synset": "bird's-nest_fungus.n.01", "name": "bird's-nest_fungus"}, - {"id": 21393, "synset": "gastrocybe_lateritia.n.01", "name": "Gastrocybe_lateritia"}, - {"id": 21394, "synset": "macowanites_americanus.n.01", "name": "Macowanites_americanus"}, - {"id": 21395, "synset": "polypore.n.01", "name": "polypore"}, - {"id": 21396, "synset": "bracket_fungus.n.01", "name": "bracket_fungus"}, - {"id": 21397, "synset": "albatrellus_dispansus.n.01", "name": "Albatrellus_dispansus"}, - {"id": 21398, "synset": "albatrellus_ovinus.n.01", "name": "Albatrellus_ovinus"}, - {"id": 21399, "synset": "neolentinus_ponderosus.n.01", "name": "Neolentinus_ponderosus"}, - {"id": 21400, "synset": "oligoporus_leucospongia.n.01", "name": "Oligoporus_leucospongia"}, - {"id": 21401, "synset": "polyporus_tenuiculus.n.01", "name": "Polyporus_tenuiculus"}, - {"id": 21402, "synset": "hen-of-the-woods.n.01", "name": "hen-of-the-woods"}, - {"id": 21403, "synset": "polyporus_squamosus.n.01", "name": "Polyporus_squamosus"}, - {"id": 21404, "synset": "beefsteak_fungus.n.01", "name": "beefsteak_fungus"}, - {"id": 21405, "synset": "agaric.n.01", "name": "agaric"}, - {"id": 21406, "synset": "bolete.n.01", "name": "bolete"}, - {"id": 21407, "synset": "boletus_chrysenteron.n.01", "name": "Boletus_chrysenteron"}, - {"id": 21408, "synset": "boletus_edulis.n.01", "name": "Boletus_edulis"}, - {"id": 21409, "synset": "frost's_bolete.n.01", "name": "Frost's_bolete"}, - {"id": 21410, "synset": "boletus_luridus.n.01", "name": "Boletus_luridus"}, - {"id": 21411, "synset": "boletus_mirabilis.n.01", "name": "Boletus_mirabilis"}, - {"id": 21412, "synset": "boletus_pallidus.n.01", "name": "Boletus_pallidus"}, - {"id": 21413, "synset": "boletus_pulcherrimus.n.01", "name": "Boletus_pulcherrimus"}, - {"id": 21414, "synset": "boletus_pulverulentus.n.01", "name": "Boletus_pulverulentus"}, - {"id": 21415, "synset": "boletus_roxanae.n.01", "name": "Boletus_roxanae"}, - {"id": 21416, "synset": "boletus_subvelutipes.n.01", "name": "Boletus_subvelutipes"}, - {"id": 21417, "synset": "boletus_variipes.n.01", "name": "Boletus_variipes"}, - {"id": 21418, "synset": "boletus_zelleri.n.01", "name": "Boletus_zelleri"}, - {"id": 21419, "synset": "fuscoboletinus_paluster.n.01", "name": "Fuscoboletinus_paluster"}, - {"id": 21420, "synset": "fuscoboletinus_serotinus.n.01", "name": "Fuscoboletinus_serotinus"}, - {"id": 21421, "synset": "leccinum_fibrillosum.n.01", "name": "Leccinum_fibrillosum"}, - {"id": 21422, "synset": "suillus_albivelatus.n.01", "name": "Suillus_albivelatus"}, - {"id": 21423, "synset": "old-man-of-the-woods.n.01", "name": "old-man-of-the-woods"}, - {"id": 21424, "synset": "boletellus_russellii.n.01", "name": "Boletellus_russellii"}, - {"id": 21425, "synset": "jelly_fungus.n.01", "name": "jelly_fungus"}, - {"id": 21426, "synset": "snow_mushroom.n.01", "name": "snow_mushroom"}, - {"id": 21427, "synset": "witches'_butter.n.01", "name": "witches'_butter"}, - {"id": 21428, "synset": "tremella_foliacea.n.01", "name": "Tremella_foliacea"}, - {"id": 21429, "synset": "tremella_reticulata.n.01", "name": "Tremella_reticulata"}, - {"id": 21430, "synset": "jew's-ear.n.01", "name": "Jew's-ear"}, - {"id": 21431, "synset": "rust.n.04", "name": "rust"}, - {"id": 21432, "synset": "aecium.n.01", "name": "aecium"}, - {"id": 21433, "synset": "flax_rust.n.01", "name": "flax_rust"}, - {"id": 21434, "synset": "blister_rust.n.02", "name": "blister_rust"}, - {"id": 21435, "synset": "wheat_rust.n.01", "name": "wheat_rust"}, - {"id": 21436, "synset": "apple_rust.n.01", "name": "apple_rust"}, - {"id": 21437, "synset": "smut.n.03", "name": "smut"}, - {"id": 21438, "synset": "covered_smut.n.01", "name": "covered_smut"}, - {"id": 21439, "synset": "loose_smut.n.02", "name": "loose_smut"}, - {"id": 21440, "synset": "cornsmut.n.01", "name": "cornsmut"}, - {"id": 21441, "synset": "boil_smut.n.01", "name": "boil_smut"}, - {"id": 21442, "synset": "sphacelotheca.n.01", "name": "Sphacelotheca"}, - {"id": 21443, "synset": "head_smut.n.01", "name": "head_smut"}, - {"id": 21444, "synset": "bunt.n.04", "name": "bunt"}, - {"id": 21445, "synset": "bunt.n.03", "name": "bunt"}, - {"id": 21446, "synset": "onion_smut.n.01", "name": "onion_smut"}, - {"id": 21447, "synset": "flag_smut_fungus.n.01", "name": "flag_smut_fungus"}, - {"id": 21448, "synset": "wheat_flag_smut.n.01", "name": "wheat_flag_smut"}, - {"id": 21449, "synset": "felt_fungus.n.01", "name": "felt_fungus"}, - {"id": 21450, "synset": "waxycap.n.01", "name": "waxycap"}, - {"id": 21451, "synset": "hygrocybe_acutoconica.n.01", "name": "Hygrocybe_acutoconica"}, - {"id": 21452, "synset": "hygrophorus_borealis.n.01", "name": "Hygrophorus_borealis"}, - {"id": 21453, "synset": "hygrophorus_caeruleus.n.01", "name": "Hygrophorus_caeruleus"}, - {"id": 21454, "synset": "hygrophorus_inocybiformis.n.01", "name": "Hygrophorus_inocybiformis"}, - {"id": 21455, "synset": "hygrophorus_kauffmanii.n.01", "name": "Hygrophorus_kauffmanii"}, - {"id": 21456, "synset": "hygrophorus_marzuolus.n.01", "name": "Hygrophorus_marzuolus"}, - {"id": 21457, "synset": "hygrophorus_purpurascens.n.01", "name": "Hygrophorus_purpurascens"}, - {"id": 21458, "synset": "hygrophorus_russula.n.01", "name": "Hygrophorus_russula"}, - {"id": 21459, "synset": "hygrophorus_sordidus.n.01", "name": "Hygrophorus_sordidus"}, - {"id": 21460, "synset": "hygrophorus_tennesseensis.n.01", "name": "Hygrophorus_tennesseensis"}, - {"id": 21461, "synset": "hygrophorus_turundus.n.01", "name": "Hygrophorus_turundus"}, - { - "id": 21462, - "synset": "neohygrophorus_angelesianus.n.01", - "name": "Neohygrophorus_angelesianus", - }, - {"id": 21463, "synset": "cortinarius_armillatus.n.01", "name": "Cortinarius_armillatus"}, - {"id": 21464, "synset": "cortinarius_atkinsonianus.n.01", "name": "Cortinarius_atkinsonianus"}, - {"id": 21465, "synset": "cortinarius_corrugatus.n.01", "name": "Cortinarius_corrugatus"}, - {"id": 21466, "synset": "cortinarius_gentilis.n.01", "name": "Cortinarius_gentilis"}, - {"id": 21467, "synset": "cortinarius_mutabilis.n.01", "name": "Cortinarius_mutabilis"}, - { - "id": 21468, - "synset": "cortinarius_semisanguineus.n.01", - "name": "Cortinarius_semisanguineus", - }, - {"id": 21469, "synset": "cortinarius_subfoetidus.n.01", "name": "Cortinarius_subfoetidus"}, - {"id": 21470, "synset": "cortinarius_violaceus.n.01", "name": "Cortinarius_violaceus"}, - {"id": 21471, "synset": "gymnopilus_spectabilis.n.01", "name": "Gymnopilus_spectabilis"}, - {"id": 21472, "synset": "gymnopilus_validipes.n.01", "name": "Gymnopilus_validipes"}, - {"id": 21473, "synset": "gymnopilus_ventricosus.n.01", "name": "Gymnopilus_ventricosus"}, - {"id": 21474, "synset": "mold.n.05", "name": "mold"}, - {"id": 21475, "synset": "mildew.n.02", "name": "mildew"}, - {"id": 21476, "synset": "verticillium.n.01", "name": "verticillium"}, - {"id": 21477, "synset": "monilia.n.01", "name": "monilia"}, - {"id": 21478, "synset": "candida.n.01", "name": "candida"}, - {"id": 21479, "synset": "candida_albicans.n.01", "name": "Candida_albicans"}, - {"id": 21480, "synset": "blastomycete.n.01", "name": "blastomycete"}, - {"id": 21481, "synset": "yellow_spot_fungus.n.01", "name": "yellow_spot_fungus"}, - {"id": 21482, "synset": "green_smut_fungus.n.01", "name": "green_smut_fungus"}, - {"id": 21483, "synset": "dry_rot.n.02", "name": "dry_rot"}, - {"id": 21484, "synset": "rhizoctinia.n.01", "name": "rhizoctinia"}, - {"id": 21485, "synset": "houseplant.n.01", "name": "houseplant"}, - {"id": 21486, "synset": "bedder.n.01", "name": "bedder"}, - {"id": 21487, "synset": "succulent.n.01", "name": "succulent"}, - {"id": 21488, "synset": "cultivar.n.01", "name": "cultivar"}, - {"id": 21489, "synset": "weed.n.01", "name": "weed"}, - {"id": 21490, "synset": "wort.n.01", "name": "wort"}, - {"id": 21491, "synset": "brier.n.02", "name": "brier"}, - {"id": 21492, "synset": "aril.n.01", "name": "aril"}, - {"id": 21493, "synset": "sporophyll.n.01", "name": "sporophyll"}, - {"id": 21494, "synset": "sporangium.n.01", "name": "sporangium"}, - {"id": 21495, "synset": "sporangiophore.n.01", "name": "sporangiophore"}, - {"id": 21496, "synset": "ascus.n.01", "name": "ascus"}, - {"id": 21497, "synset": "ascospore.n.01", "name": "ascospore"}, - {"id": 21498, "synset": "arthrospore.n.02", "name": "arthrospore"}, - {"id": 21499, "synset": "eusporangium.n.01", "name": "eusporangium"}, - {"id": 21500, "synset": "tetrasporangium.n.01", "name": "tetrasporangium"}, - {"id": 21501, "synset": "gametangium.n.01", "name": "gametangium"}, - {"id": 21502, "synset": "sorus.n.02", "name": "sorus"}, - {"id": 21503, "synset": "sorus.n.01", "name": "sorus"}, - {"id": 21504, "synset": "partial_veil.n.01", "name": "partial_veil"}, - {"id": 21505, "synset": "lignum.n.01", "name": "lignum"}, - {"id": 21506, "synset": "vascular_ray.n.01", "name": "vascular_ray"}, - {"id": 21507, "synset": "phloem.n.01", "name": "phloem"}, - {"id": 21508, "synset": "evergreen.n.01", "name": "evergreen"}, - {"id": 21509, "synset": "deciduous_plant.n.01", "name": "deciduous_plant"}, - {"id": 21510, "synset": "poisonous_plant.n.01", "name": "poisonous_plant"}, - {"id": 21511, "synset": "vine.n.01", "name": "vine"}, - {"id": 21512, "synset": "creeper.n.01", "name": "creeper"}, - {"id": 21513, "synset": "tendril.n.01", "name": "tendril"}, - {"id": 21514, "synset": "root_climber.n.01", "name": "root_climber"}, - {"id": 21515, "synset": "lignosae.n.01", "name": "lignosae"}, - {"id": 21516, "synset": "arborescent_plant.n.01", "name": "arborescent_plant"}, - {"id": 21517, "synset": "snag.n.02", "name": "snag"}, - {"id": 21518, "synset": "tree.n.01", "name": "tree"}, - {"id": 21519, "synset": "timber_tree.n.01", "name": "timber_tree"}, - {"id": 21520, "synset": "treelet.n.01", "name": "treelet"}, - {"id": 21521, "synset": "arbor.n.01", "name": "arbor"}, - {"id": 21522, "synset": "bean_tree.n.01", "name": "bean_tree"}, - {"id": 21523, "synset": "pollard.n.01", "name": "pollard"}, - {"id": 21524, "synset": "sapling.n.01", "name": "sapling"}, - {"id": 21525, "synset": "shade_tree.n.01", "name": "shade_tree"}, - {"id": 21526, "synset": "gymnospermous_tree.n.01", "name": "gymnospermous_tree"}, - {"id": 21527, "synset": "conifer.n.01", "name": "conifer"}, - {"id": 21528, "synset": "angiospermous_tree.n.01", "name": "angiospermous_tree"}, - {"id": 21529, "synset": "nut_tree.n.01", "name": "nut_tree"}, - {"id": 21530, "synset": "spice_tree.n.01", "name": "spice_tree"}, - {"id": 21531, "synset": "fever_tree.n.01", "name": "fever_tree"}, - {"id": 21532, "synset": "stump.n.01", "name": "stump"}, - {"id": 21533, "synset": "bonsai.n.01", "name": "bonsai"}, - {"id": 21534, "synset": "ming_tree.n.02", "name": "ming_tree"}, - {"id": 21535, "synset": "ming_tree.n.01", "name": "ming_tree"}, - {"id": 21536, "synset": "undershrub.n.01", "name": "undershrub"}, - {"id": 21537, "synset": "subshrub.n.01", "name": "subshrub"}, - {"id": 21538, "synset": "bramble.n.01", "name": "bramble"}, - {"id": 21539, "synset": "liana.n.01", "name": "liana"}, - {"id": 21540, "synset": "geophyte.n.01", "name": "geophyte"}, - {"id": 21541, "synset": "desert_plant.n.01", "name": "desert_plant"}, - {"id": 21542, "synset": "mesophyte.n.01", "name": "mesophyte"}, - {"id": 21543, "synset": "marsh_plant.n.01", "name": "marsh_plant"}, - {"id": 21544, "synset": "hemiepiphyte.n.01", "name": "hemiepiphyte"}, - {"id": 21545, "synset": "strangler.n.01", "name": "strangler"}, - {"id": 21546, "synset": "lithophyte.n.01", "name": "lithophyte"}, - {"id": 21547, "synset": "saprobe.n.01", "name": "saprobe"}, - {"id": 21548, "synset": "autophyte.n.01", "name": "autophyte"}, - {"id": 21549, "synset": "root.n.01", "name": "root"}, - {"id": 21550, "synset": "taproot.n.01", "name": "taproot"}, - {"id": 21551, "synset": "prop_root.n.01", "name": "prop_root"}, - {"id": 21552, "synset": "prophyll.n.01", "name": "prophyll"}, - {"id": 21553, "synset": "rootstock.n.02", "name": "rootstock"}, - {"id": 21554, "synset": "quickset.n.01", "name": "quickset"}, - {"id": 21555, "synset": "stolon.n.01", "name": "stolon"}, - {"id": 21556, "synset": "tuberous_plant.n.01", "name": "tuberous_plant"}, - {"id": 21557, "synset": "rhizome.n.01", "name": "rhizome"}, - {"id": 21558, "synset": "rachis.n.01", "name": "rachis"}, - {"id": 21559, "synset": "caudex.n.02", "name": "caudex"}, - {"id": 21560, "synset": "cladode.n.01", "name": "cladode"}, - {"id": 21561, "synset": "receptacle.n.02", "name": "receptacle"}, - {"id": 21562, "synset": "scape.n.01", "name": "scape"}, - {"id": 21563, "synset": "umbel.n.01", "name": "umbel"}, - {"id": 21564, "synset": "petiole.n.01", "name": "petiole"}, - {"id": 21565, "synset": "peduncle.n.02", "name": "peduncle"}, - {"id": 21566, "synset": "pedicel.n.01", "name": "pedicel"}, - {"id": 21567, "synset": "flower_cluster.n.01", "name": "flower_cluster"}, - {"id": 21568, "synset": "raceme.n.01", "name": "raceme"}, - {"id": 21569, "synset": "panicle.n.01", "name": "panicle"}, - {"id": 21570, "synset": "thyrse.n.01", "name": "thyrse"}, - {"id": 21571, "synset": "cyme.n.01", "name": "cyme"}, - {"id": 21572, "synset": "cymule.n.01", "name": "cymule"}, - {"id": 21573, "synset": "glomerule.n.01", "name": "glomerule"}, - {"id": 21574, "synset": "scorpioid_cyme.n.01", "name": "scorpioid_cyme"}, - {"id": 21575, "synset": "ear.n.05", "name": "ear"}, - {"id": 21576, "synset": "spadix.n.01", "name": "spadix"}, - {"id": 21577, "synset": "bulbous_plant.n.01", "name": "bulbous_plant"}, - {"id": 21578, "synset": "bulbil.n.01", "name": "bulbil"}, - {"id": 21579, "synset": "cormous_plant.n.01", "name": "cormous_plant"}, - {"id": 21580, "synset": "fruit.n.01", "name": "fruit"}, - {"id": 21581, "synset": "fruitlet.n.01", "name": "fruitlet"}, - {"id": 21582, "synset": "seed.n.01", "name": "seed"}, - {"id": 21583, "synset": "bean.n.02", "name": "bean"}, - {"id": 21584, "synset": "nut.n.01", "name": "nut"}, - {"id": 21585, "synset": "nutlet.n.01", "name": "nutlet"}, - {"id": 21586, "synset": "kernel.n.01", "name": "kernel"}, - {"id": 21587, "synset": "syconium.n.01", "name": "syconium"}, - {"id": 21588, "synset": "berry.n.02", "name": "berry"}, - {"id": 21589, "synset": "aggregate_fruit.n.01", "name": "aggregate_fruit"}, - {"id": 21590, "synset": "simple_fruit.n.01", "name": "simple_fruit"}, - {"id": 21591, "synset": "acinus.n.01", "name": "acinus"}, - {"id": 21592, "synset": "drupe.n.01", "name": "drupe"}, - {"id": 21593, "synset": "drupelet.n.01", "name": "drupelet"}, - {"id": 21594, "synset": "pome.n.01", "name": "pome"}, - {"id": 21595, "synset": "pod.n.02", "name": "pod"}, - {"id": 21596, "synset": "loment.n.01", "name": "loment"}, - {"id": 21597, "synset": "pyxidium.n.01", "name": "pyxidium"}, - {"id": 21598, "synset": "husk.n.02", "name": "husk"}, - {"id": 21599, "synset": "cornhusk.n.01", "name": "cornhusk"}, - {"id": 21600, "synset": "pod.n.01", "name": "pod"}, - {"id": 21601, "synset": "accessory_fruit.n.01", "name": "accessory_fruit"}, - {"id": 21602, "synset": "buckthorn.n.01", "name": "buckthorn"}, - {"id": 21603, "synset": "buckthorn_berry.n.01", "name": "buckthorn_berry"}, - {"id": 21604, "synset": "cascara_buckthorn.n.01", "name": "cascara_buckthorn"}, - {"id": 21605, "synset": "cascara.n.01", "name": "cascara"}, - {"id": 21606, "synset": "carolina_buckthorn.n.01", "name": "Carolina_buckthorn"}, - {"id": 21607, "synset": "coffeeberry.n.01", "name": "coffeeberry"}, - {"id": 21608, "synset": "redberry.n.01", "name": "redberry"}, - {"id": 21609, "synset": "nakedwood.n.01", "name": "nakedwood"}, - {"id": 21610, "synset": "jujube.n.01", "name": "jujube"}, - {"id": 21611, "synset": "christ's-thorn.n.01", "name": "Christ's-thorn"}, - {"id": 21612, "synset": "hazel.n.01", "name": "hazel"}, - {"id": 21613, "synset": "fox_grape.n.01", "name": "fox_grape"}, - {"id": 21614, "synset": "muscadine.n.01", "name": "muscadine"}, - {"id": 21615, "synset": "vinifera.n.01", "name": "vinifera"}, - {"id": 21616, "synset": "pinot_blanc.n.01", "name": "Pinot_blanc"}, - {"id": 21617, "synset": "sauvignon_grape.n.01", "name": "Sauvignon_grape"}, - {"id": 21618, "synset": "sauvignon_blanc.n.01", "name": "Sauvignon_blanc"}, - {"id": 21619, "synset": "muscadet.n.01", "name": "Muscadet"}, - {"id": 21620, "synset": "riesling.n.01", "name": "Riesling"}, - {"id": 21621, "synset": "zinfandel.n.01", "name": "Zinfandel"}, - {"id": 21622, "synset": "chenin_blanc.n.01", "name": "Chenin_blanc"}, - {"id": 21623, "synset": "malvasia.n.01", "name": "malvasia"}, - {"id": 21624, "synset": "verdicchio.n.01", "name": "Verdicchio"}, - {"id": 21625, "synset": "boston_ivy.n.01", "name": "Boston_ivy"}, - {"id": 21626, "synset": "virginia_creeper.n.01", "name": "Virginia_creeper"}, - {"id": 21627, "synset": "true_pepper.n.01", "name": "true_pepper"}, - {"id": 21628, "synset": "betel.n.01", "name": "betel"}, - {"id": 21629, "synset": "cubeb.n.01", "name": "cubeb"}, - {"id": 21630, "synset": "schizocarp.n.01", "name": "schizocarp"}, - {"id": 21631, "synset": "peperomia.n.01", "name": "peperomia"}, - {"id": 21632, "synset": "watermelon_begonia.n.01", "name": "watermelon_begonia"}, - {"id": 21633, "synset": "yerba_mansa.n.01", "name": "yerba_mansa"}, - {"id": 21634, "synset": "pinna.n.01", "name": "pinna"}, - {"id": 21635, "synset": "frond.n.01", "name": "frond"}, - {"id": 21636, "synset": "bract.n.01", "name": "bract"}, - {"id": 21637, "synset": "bracteole.n.01", "name": "bracteole"}, - {"id": 21638, "synset": "involucre.n.01", "name": "involucre"}, - {"id": 21639, "synset": "glume.n.01", "name": "glume"}, - {"id": 21640, "synset": "palmate_leaf.n.01", "name": "palmate_leaf"}, - {"id": 21641, "synset": "pinnate_leaf.n.01", "name": "pinnate_leaf"}, - {"id": 21642, "synset": "bijugate_leaf.n.01", "name": "bijugate_leaf"}, - {"id": 21643, "synset": "decompound_leaf.n.01", "name": "decompound_leaf"}, - {"id": 21644, "synset": "acuminate_leaf.n.01", "name": "acuminate_leaf"}, - {"id": 21645, "synset": "deltoid_leaf.n.01", "name": "deltoid_leaf"}, - {"id": 21646, "synset": "ensiform_leaf.n.01", "name": "ensiform_leaf"}, - {"id": 21647, "synset": "linear_leaf.n.01", "name": "linear_leaf"}, - {"id": 21648, "synset": "lyrate_leaf.n.01", "name": "lyrate_leaf"}, - {"id": 21649, "synset": "obtuse_leaf.n.01", "name": "obtuse_leaf"}, - {"id": 21650, "synset": "oblanceolate_leaf.n.01", "name": "oblanceolate_leaf"}, - {"id": 21651, "synset": "pandurate_leaf.n.01", "name": "pandurate_leaf"}, - {"id": 21652, "synset": "reniform_leaf.n.01", "name": "reniform_leaf"}, - {"id": 21653, "synset": "spatulate_leaf.n.01", "name": "spatulate_leaf"}, - {"id": 21654, "synset": "even-pinnate_leaf.n.01", "name": "even-pinnate_leaf"}, - {"id": 21655, "synset": "odd-pinnate_leaf.n.01", "name": "odd-pinnate_leaf"}, - {"id": 21656, "synset": "pedate_leaf.n.01", "name": "pedate_leaf"}, - {"id": 21657, "synset": "crenate_leaf.n.01", "name": "crenate_leaf"}, - {"id": 21658, "synset": "dentate_leaf.n.01", "name": "dentate_leaf"}, - {"id": 21659, "synset": "denticulate_leaf.n.01", "name": "denticulate_leaf"}, - {"id": 21660, "synset": "erose_leaf.n.01", "name": "erose_leaf"}, - {"id": 21661, "synset": "runcinate_leaf.n.01", "name": "runcinate_leaf"}, - {"id": 21662, "synset": "prickly-edged_leaf.n.01", "name": "prickly-edged_leaf"}, - {"id": 21663, "synset": "deadwood.n.01", "name": "deadwood"}, - {"id": 21664, "synset": "haulm.n.01", "name": "haulm"}, - {"id": 21665, "synset": "branchlet.n.01", "name": "branchlet"}, - {"id": 21666, "synset": "osier.n.01", "name": "osier"}, - {"id": 21667, "synset": "giant_scrambling_fern.n.01", "name": "giant_scrambling_fern"}, - {"id": 21668, "synset": "umbrella_fern.n.01", "name": "umbrella_fern"}, - {"id": 21669, "synset": "floating_fern.n.02", "name": "floating_fern"}, - {"id": 21670, "synset": "polypody.n.01", "name": "polypody"}, - {"id": 21671, "synset": "licorice_fern.n.01", "name": "licorice_fern"}, - {"id": 21672, "synset": "grey_polypody.n.01", "name": "grey_polypody"}, - {"id": 21673, "synset": "leatherleaf.n.01", "name": "leatherleaf"}, - {"id": 21674, "synset": "rock_polypody.n.01", "name": "rock_polypody"}, - {"id": 21675, "synset": "common_polypody.n.01", "name": "common_polypody"}, - {"id": 21676, "synset": "bear's-paw_fern.n.01", "name": "bear's-paw_fern"}, - {"id": 21677, "synset": "strap_fern.n.01", "name": "strap_fern"}, - {"id": 21678, "synset": "florida_strap_fern.n.01", "name": "Florida_strap_fern"}, - {"id": 21679, "synset": "basket_fern.n.02", "name": "basket_fern"}, - {"id": 21680, "synset": "snake_polypody.n.01", "name": "snake_polypody"}, - {"id": 21681, "synset": "climbing_bird's_nest_fern.n.01", "name": "climbing_bird's_nest_fern"}, - {"id": 21682, "synset": "golden_polypody.n.01", "name": "golden_polypody"}, - {"id": 21683, "synset": "staghorn_fern.n.01", "name": "staghorn_fern"}, - {"id": 21684, "synset": "south_american_staghorn.n.01", "name": "South_American_staghorn"}, - {"id": 21685, "synset": "common_staghorn_fern.n.01", "name": "common_staghorn_fern"}, - {"id": 21686, "synset": "felt_fern.n.01", "name": "felt_fern"}, - {"id": 21687, "synset": "potato_fern.n.02", "name": "potato_fern"}, - {"id": 21688, "synset": "myrmecophyte.n.01", "name": "myrmecophyte"}, - {"id": 21689, "synset": "grass_fern.n.01", "name": "grass_fern"}, - {"id": 21690, "synset": "spleenwort.n.01", "name": "spleenwort"}, - {"id": 21691, "synset": "black_spleenwort.n.01", "name": "black_spleenwort"}, - {"id": 21692, "synset": "bird's_nest_fern.n.01", "name": "bird's_nest_fern"}, - {"id": 21693, "synset": "ebony_spleenwort.n.01", "name": "ebony_spleenwort"}, - {"id": 21694, "synset": "black-stem_spleenwort.n.01", "name": "black-stem_spleenwort"}, - {"id": 21695, "synset": "walking_fern.n.01", "name": "walking_fern"}, - {"id": 21696, "synset": "green_spleenwort.n.01", "name": "green_spleenwort"}, - {"id": 21697, "synset": "mountain_spleenwort.n.01", "name": "mountain_spleenwort"}, - {"id": 21698, "synset": "lobed_spleenwort.n.01", "name": "lobed_spleenwort"}, - {"id": 21699, "synset": "lanceolate_spleenwort.n.01", "name": "lanceolate_spleenwort"}, - {"id": 21700, "synset": "hart's-tongue.n.02", "name": "hart's-tongue"}, - {"id": 21701, "synset": "scale_fern.n.01", "name": "scale_fern"}, - {"id": 21702, "synset": "scolopendrium.n.01", "name": "scolopendrium"}, - {"id": 21703, "synset": "deer_fern.n.01", "name": "deer_fern"}, - {"id": 21704, "synset": "doodia.n.01", "name": "doodia"}, - {"id": 21705, "synset": "chain_fern.n.01", "name": "chain_fern"}, - {"id": 21706, "synset": "virginia_chain_fern.n.01", "name": "Virginia_chain_fern"}, - {"id": 21707, "synset": "silver_tree_fern.n.01", "name": "silver_tree_fern"}, - {"id": 21708, "synset": "davallia.n.01", "name": "davallia"}, - {"id": 21709, "synset": "hare's-foot_fern.n.01", "name": "hare's-foot_fern"}, - { - "id": 21710, - "synset": "canary_island_hare's_foot_fern.n.01", - "name": "Canary_Island_hare's_foot_fern", - }, - {"id": 21711, "synset": "squirrel's-foot_fern.n.01", "name": "squirrel's-foot_fern"}, - {"id": 21712, "synset": "bracken.n.01", "name": "bracken"}, - {"id": 21713, "synset": "soft_tree_fern.n.01", "name": "soft_tree_fern"}, - {"id": 21714, "synset": "scythian_lamb.n.01", "name": "Scythian_lamb"}, - {"id": 21715, "synset": "false_bracken.n.01", "name": "false_bracken"}, - {"id": 21716, "synset": "thyrsopteris.n.01", "name": "thyrsopteris"}, - {"id": 21717, "synset": "shield_fern.n.01", "name": "shield_fern"}, - {"id": 21718, "synset": "broad_buckler-fern.n.01", "name": "broad_buckler-fern"}, - {"id": 21719, "synset": "fragrant_cliff_fern.n.01", "name": "fragrant_cliff_fern"}, - {"id": 21720, "synset": "goldie's_fern.n.01", "name": "Goldie's_fern"}, - {"id": 21721, "synset": "wood_fern.n.01", "name": "wood_fern"}, - {"id": 21722, "synset": "male_fern.n.01", "name": "male_fern"}, - {"id": 21723, "synset": "marginal_wood_fern.n.01", "name": "marginal_wood_fern"}, - {"id": 21724, "synset": "mountain_male_fern.n.01", "name": "mountain_male_fern"}, - {"id": 21725, "synset": "lady_fern.n.01", "name": "lady_fern"}, - {"id": 21726, "synset": "alpine_lady_fern.n.01", "name": "Alpine_lady_fern"}, - {"id": 21727, "synset": "silvery_spleenwort.n.02", "name": "silvery_spleenwort"}, - {"id": 21728, "synset": "holly_fern.n.02", "name": "holly_fern"}, - {"id": 21729, "synset": "bladder_fern.n.01", "name": "bladder_fern"}, - {"id": 21730, "synset": "brittle_bladder_fern.n.01", "name": "brittle_bladder_fern"}, - {"id": 21731, "synset": "mountain_bladder_fern.n.01", "name": "mountain_bladder_fern"}, - {"id": 21732, "synset": "bulblet_fern.n.01", "name": "bulblet_fern"}, - {"id": 21733, "synset": "silvery_spleenwort.n.01", "name": "silvery_spleenwort"}, - {"id": 21734, "synset": "oak_fern.n.01", "name": "oak_fern"}, - {"id": 21735, "synset": "limestone_fern.n.01", "name": "limestone_fern"}, - {"id": 21736, "synset": "ostrich_fern.n.01", "name": "ostrich_fern"}, - {"id": 21737, "synset": "hart's-tongue.n.01", "name": "hart's-tongue"}, - {"id": 21738, "synset": "sensitive_fern.n.01", "name": "sensitive_fern"}, - {"id": 21739, "synset": "christmas_fern.n.01", "name": "Christmas_fern"}, - {"id": 21740, "synset": "holly_fern.n.01", "name": "holly_fern"}, - {"id": 21741, "synset": "braun's_holly_fern.n.01", "name": "Braun's_holly_fern"}, - {"id": 21742, "synset": "western_holly_fern.n.01", "name": "western_holly_fern"}, - {"id": 21743, "synset": "soft_shield_fern.n.01", "name": "soft_shield_fern"}, - {"id": 21744, "synset": "leather_fern.n.02", "name": "leather_fern"}, - {"id": 21745, "synset": "button_fern.n.02", "name": "button_fern"}, - {"id": 21746, "synset": "indian_button_fern.n.01", "name": "Indian_button_fern"}, - {"id": 21747, "synset": "woodsia.n.01", "name": "woodsia"}, - {"id": 21748, "synset": "rusty_woodsia.n.01", "name": "rusty_woodsia"}, - {"id": 21749, "synset": "alpine_woodsia.n.01", "name": "Alpine_woodsia"}, - {"id": 21750, "synset": "smooth_woodsia.n.01", "name": "smooth_woodsia"}, - {"id": 21751, "synset": "boston_fern.n.01", "name": "Boston_fern"}, - {"id": 21752, "synset": "basket_fern.n.01", "name": "basket_fern"}, - {"id": 21753, "synset": "golden_fern.n.02", "name": "golden_fern"}, - {"id": 21754, "synset": "maidenhair.n.01", "name": "maidenhair"}, - {"id": 21755, "synset": "common_maidenhair.n.01", "name": "common_maidenhair"}, - {"id": 21756, "synset": "american_maidenhair_fern.n.01", "name": "American_maidenhair_fern"}, - {"id": 21757, "synset": "bermuda_maidenhair.n.01", "name": "Bermuda_maidenhair"}, - {"id": 21758, "synset": "brittle_maidenhair.n.01", "name": "brittle_maidenhair"}, - {"id": 21759, "synset": "farley_maidenhair.n.01", "name": "Farley_maidenhair"}, - {"id": 21760, "synset": "annual_fern.n.01", "name": "annual_fern"}, - {"id": 21761, "synset": "lip_fern.n.01", "name": "lip_fern"}, - {"id": 21762, "synset": "smooth_lip_fern.n.01", "name": "smooth_lip_fern"}, - {"id": 21763, "synset": "lace_fern.n.01", "name": "lace_fern"}, - {"id": 21764, "synset": "wooly_lip_fern.n.01", "name": "wooly_lip_fern"}, - {"id": 21765, "synset": "southwestern_lip_fern.n.01", "name": "southwestern_lip_fern"}, - {"id": 21766, "synset": "bamboo_fern.n.01", "name": "bamboo_fern"}, - {"id": 21767, "synset": "american_rock_brake.n.01", "name": "American_rock_brake"}, - {"id": 21768, "synset": "european_parsley_fern.n.01", "name": "European_parsley_fern"}, - {"id": 21769, "synset": "hand_fern.n.01", "name": "hand_fern"}, - {"id": 21770, "synset": "cliff_brake.n.01", "name": "cliff_brake"}, - {"id": 21771, "synset": "coffee_fern.n.01", "name": "coffee_fern"}, - {"id": 21772, "synset": "purple_rock_brake.n.01", "name": "purple_rock_brake"}, - {"id": 21773, "synset": "bird's-foot_fern.n.01", "name": "bird's-foot_fern"}, - {"id": 21774, "synset": "button_fern.n.01", "name": "button_fern"}, - {"id": 21775, "synset": "silver_fern.n.02", "name": "silver_fern"}, - {"id": 21776, "synset": "golden_fern.n.01", "name": "golden_fern"}, - {"id": 21777, "synset": "gold_fern.n.01", "name": "gold_fern"}, - {"id": 21778, "synset": "pteris_cretica.n.01", "name": "Pteris_cretica"}, - {"id": 21779, "synset": "spider_brake.n.01", "name": "spider_brake"}, - {"id": 21780, "synset": "ribbon_fern.n.01", "name": "ribbon_fern"}, - {"id": 21781, "synset": "potato_fern.n.01", "name": "potato_fern"}, - {"id": 21782, "synset": "angiopteris.n.01", "name": "angiopteris"}, - {"id": 21783, "synset": "skeleton_fork_fern.n.01", "name": "skeleton_fork_fern"}, - {"id": 21784, "synset": "horsetail.n.01", "name": "horsetail"}, - {"id": 21785, "synset": "common_horsetail.n.01", "name": "common_horsetail"}, - {"id": 21786, "synset": "swamp_horsetail.n.01", "name": "swamp_horsetail"}, - {"id": 21787, "synset": "scouring_rush.n.01", "name": "scouring_rush"}, - {"id": 21788, "synset": "marsh_horsetail.n.01", "name": "marsh_horsetail"}, - {"id": 21789, "synset": "wood_horsetail.n.01", "name": "wood_horsetail"}, - {"id": 21790, "synset": "variegated_horsetail.n.01", "name": "variegated_horsetail"}, - {"id": 21791, "synset": "club_moss.n.01", "name": "club_moss"}, - {"id": 21792, "synset": "shining_clubmoss.n.01", "name": "shining_clubmoss"}, - {"id": 21793, "synset": "alpine_clubmoss.n.01", "name": "alpine_clubmoss"}, - {"id": 21794, "synset": "fir_clubmoss.n.01", "name": "fir_clubmoss"}, - {"id": 21795, "synset": "ground_cedar.n.01", "name": "ground_cedar"}, - {"id": 21796, "synset": "ground_fir.n.01", "name": "ground_fir"}, - {"id": 21797, "synset": "foxtail_grass.n.01", "name": "foxtail_grass"}, - {"id": 21798, "synset": "spikemoss.n.01", "name": "spikemoss"}, - {"id": 21799, "synset": "meadow_spikemoss.n.01", "name": "meadow_spikemoss"}, - {"id": 21800, "synset": "desert_selaginella.n.01", "name": "desert_selaginella"}, - {"id": 21801, "synset": "resurrection_plant.n.01", "name": "resurrection_plant"}, - {"id": 21802, "synset": "florida_selaginella.n.01", "name": "florida_selaginella"}, - {"id": 21803, "synset": "quillwort.n.01", "name": "quillwort"}, - {"id": 21804, "synset": "earthtongue.n.01", "name": "earthtongue"}, - {"id": 21805, "synset": "snuffbox_fern.n.01", "name": "snuffbox_fern"}, - {"id": 21806, "synset": "christella.n.01", "name": "christella"}, - {"id": 21807, "synset": "mountain_fern.n.01", "name": "mountain_fern"}, - {"id": 21808, "synset": "new_york_fern.n.01", "name": "New_York_fern"}, - {"id": 21809, "synset": "massachusetts_fern.n.01", "name": "Massachusetts_fern"}, - {"id": 21810, "synset": "beech_fern.n.01", "name": "beech_fern"}, - {"id": 21811, "synset": "broad_beech_fern.n.01", "name": "broad_beech_fern"}, - {"id": 21812, "synset": "long_beech_fern.n.01", "name": "long_beech_fern"}, - {"id": 21813, "synset": "shoestring_fungus.n.01", "name": "shoestring_fungus"}, - {"id": 21814, "synset": "armillaria_caligata.n.01", "name": "Armillaria_caligata"}, - {"id": 21815, "synset": "armillaria_ponderosa.n.01", "name": "Armillaria_ponderosa"}, - {"id": 21816, "synset": "armillaria_zelleri.n.01", "name": "Armillaria_zelleri"}, - {"id": 21817, "synset": "honey_mushroom.n.01", "name": "honey_mushroom"}, - {"id": 21818, "synset": "milkweed.n.01", "name": "milkweed"}, - {"id": 21819, "synset": "white_milkweed.n.01", "name": "white_milkweed"}, - {"id": 21820, "synset": "poke_milkweed.n.01", "name": "poke_milkweed"}, - {"id": 21821, "synset": "swamp_milkweed.n.01", "name": "swamp_milkweed"}, - {"id": 21822, "synset": "mead's_milkweed.n.01", "name": "Mead's_milkweed"}, - {"id": 21823, "synset": "purple_silkweed.n.01", "name": "purple_silkweed"}, - {"id": 21824, "synset": "showy_milkweed.n.01", "name": "showy_milkweed"}, - {"id": 21825, "synset": "poison_milkweed.n.01", "name": "poison_milkweed"}, - {"id": 21826, "synset": "butterfly_weed.n.01", "name": "butterfly_weed"}, - {"id": 21827, "synset": "whorled_milkweed.n.01", "name": "whorled_milkweed"}, - {"id": 21828, "synset": "cruel_plant.n.01", "name": "cruel_plant"}, - {"id": 21829, "synset": "wax_plant.n.01", "name": "wax_plant"}, - {"id": 21830, "synset": "silk_vine.n.01", "name": "silk_vine"}, - {"id": 21831, "synset": "stapelia.n.01", "name": "stapelia"}, - {"id": 21832, "synset": "stapelias_asterias.n.01", "name": "Stapelias_asterias"}, - {"id": 21833, "synset": "stephanotis.n.01", "name": "stephanotis"}, - {"id": 21834, "synset": "madagascar_jasmine.n.01", "name": "Madagascar_jasmine"}, - {"id": 21835, "synset": "negro_vine.n.01", "name": "negro_vine"}, - {"id": 21836, "synset": "zygospore.n.01", "name": "zygospore"}, - {"id": 21837, "synset": "tree_of_knowledge.n.01", "name": "tree_of_knowledge"}, - {"id": 21838, "synset": "orangery.n.01", "name": "orangery"}, - {"id": 21839, "synset": "pocketbook.n.01", "name": "pocketbook"}, - {"id": 21840, "synset": "shit.n.04", "name": "shit"}, - {"id": 21841, "synset": "cordage.n.01", "name": "cordage"}, - {"id": 21842, "synset": "yard.n.01", "name": "yard"}, - {"id": 21843, "synset": "extremum.n.02", "name": "extremum"}, - {"id": 21844, "synset": "leaf_shape.n.01", "name": "leaf_shape"}, - {"id": 21845, "synset": "equilateral.n.01", "name": "equilateral"}, - {"id": 21846, "synset": "figure.n.06", "name": "figure"}, - {"id": 21847, "synset": "pencil.n.03", "name": "pencil"}, - {"id": 21848, "synset": "plane_figure.n.01", "name": "plane_figure"}, - {"id": 21849, "synset": "solid_figure.n.01", "name": "solid_figure"}, - {"id": 21850, "synset": "line.n.04", "name": "line"}, - {"id": 21851, "synset": "bulb.n.04", "name": "bulb"}, - {"id": 21852, "synset": "convex_shape.n.01", "name": "convex_shape"}, - {"id": 21853, "synset": "concave_shape.n.01", "name": "concave_shape"}, - {"id": 21854, "synset": "cylinder.n.01", "name": "cylinder"}, - {"id": 21855, "synset": "round_shape.n.01", "name": "round_shape"}, - {"id": 21856, "synset": "heart.n.07", "name": "heart"}, - {"id": 21857, "synset": "polygon.n.01", "name": "polygon"}, - {"id": 21858, "synset": "convex_polygon.n.01", "name": "convex_polygon"}, - {"id": 21859, "synset": "concave_polygon.n.01", "name": "concave_polygon"}, - {"id": 21860, "synset": "reentrant_polygon.n.01", "name": "reentrant_polygon"}, - {"id": 21861, "synset": "amorphous_shape.n.01", "name": "amorphous_shape"}, - {"id": 21862, "synset": "closed_curve.n.01", "name": "closed_curve"}, - {"id": 21863, "synset": "simple_closed_curve.n.01", "name": "simple_closed_curve"}, - {"id": 21864, "synset": "s-shape.n.01", "name": "S-shape"}, - {"id": 21865, "synset": "wave.n.07", "name": "wave"}, - {"id": 21866, "synset": "extrados.n.01", "name": "extrados"}, - {"id": 21867, "synset": "hook.n.02", "name": "hook"}, - {"id": 21868, "synset": "envelope.n.03", "name": "envelope"}, - {"id": 21869, "synset": "bight.n.02", "name": "bight"}, - {"id": 21870, "synset": "diameter.n.02", "name": "diameter"}, - {"id": 21871, "synset": "cone.n.02", "name": "cone"}, - {"id": 21872, "synset": "funnel.n.01", "name": "funnel"}, - {"id": 21873, "synset": "oblong.n.01", "name": "oblong"}, - {"id": 21874, "synset": "circle.n.01", "name": "circle"}, - {"id": 21875, "synset": "circle.n.03", "name": "circle"}, - {"id": 21876, "synset": "equator.n.02", "name": "equator"}, - {"id": 21877, "synset": "scallop.n.01", "name": "scallop"}, - {"id": 21878, "synset": "ring.n.02", "name": "ring"}, - {"id": 21879, "synset": "loop.n.02", "name": "loop"}, - {"id": 21880, "synset": "bight.n.01", "name": "bight"}, - {"id": 21881, "synset": "helix.n.01", "name": "helix"}, - {"id": 21882, "synset": "element_of_a_cone.n.01", "name": "element_of_a_cone"}, - {"id": 21883, "synset": "element_of_a_cylinder.n.01", "name": "element_of_a_cylinder"}, - {"id": 21884, "synset": "ellipse.n.01", "name": "ellipse"}, - {"id": 21885, "synset": "quadrate.n.02", "name": "quadrate"}, - {"id": 21886, "synset": "triangle.n.01", "name": "triangle"}, - {"id": 21887, "synset": "acute_triangle.n.01", "name": "acute_triangle"}, - {"id": 21888, "synset": "isosceles_triangle.n.01", "name": "isosceles_triangle"}, - {"id": 21889, "synset": "obtuse_triangle.n.01", "name": "obtuse_triangle"}, - {"id": 21890, "synset": "right_triangle.n.01", "name": "right_triangle"}, - {"id": 21891, "synset": "scalene_triangle.n.01", "name": "scalene_triangle"}, - {"id": 21892, "synset": "parallel.n.03", "name": "parallel"}, - {"id": 21893, "synset": "trapezoid.n.01", "name": "trapezoid"}, - {"id": 21894, "synset": "star.n.05", "name": "star"}, - {"id": 21895, "synset": "pentagon.n.03", "name": "pentagon"}, - {"id": 21896, "synset": "hexagon.n.01", "name": "hexagon"}, - {"id": 21897, "synset": "heptagon.n.01", "name": "heptagon"}, - {"id": 21898, "synset": "octagon.n.01", "name": "octagon"}, - {"id": 21899, "synset": "nonagon.n.01", "name": "nonagon"}, - {"id": 21900, "synset": "decagon.n.01", "name": "decagon"}, - {"id": 21901, "synset": "rhombus.n.01", "name": "rhombus"}, - {"id": 21902, "synset": "spherical_polygon.n.01", "name": "spherical_polygon"}, - {"id": 21903, "synset": "spherical_triangle.n.01", "name": "spherical_triangle"}, - {"id": 21904, "synset": "convex_polyhedron.n.01", "name": "convex_polyhedron"}, - {"id": 21905, "synset": "concave_polyhedron.n.01", "name": "concave_polyhedron"}, - {"id": 21906, "synset": "cuboid.n.01", "name": "cuboid"}, - {"id": 21907, "synset": "quadrangular_prism.n.01", "name": "quadrangular_prism"}, - {"id": 21908, "synset": "bell.n.05", "name": "bell"}, - {"id": 21909, "synset": "angular_distance.n.01", "name": "angular_distance"}, - {"id": 21910, "synset": "true_anomaly.n.01", "name": "true_anomaly"}, - {"id": 21911, "synset": "spherical_angle.n.01", "name": "spherical_angle"}, - {"id": 21912, "synset": "angle_of_refraction.n.01", "name": "angle_of_refraction"}, - {"id": 21913, "synset": "acute_angle.n.01", "name": "acute_angle"}, - {"id": 21914, "synset": "groove.n.01", "name": "groove"}, - {"id": 21915, "synset": "rut.n.01", "name": "rut"}, - {"id": 21916, "synset": "bulge.n.01", "name": "bulge"}, - {"id": 21917, "synset": "belly.n.03", "name": "belly"}, - {"id": 21918, "synset": "bow.n.05", "name": "bow"}, - {"id": 21919, "synset": "crescent.n.01", "name": "crescent"}, - {"id": 21920, "synset": "ellipsoid.n.01", "name": "ellipsoid"}, - {"id": 21921, "synset": "hypotenuse.n.01", "name": "hypotenuse"}, - {"id": 21922, "synset": "balance.n.04", "name": "balance"}, - {"id": 21923, "synset": "conformation.n.01", "name": "conformation"}, - {"id": 21924, "synset": "symmetry.n.02", "name": "symmetry"}, - {"id": 21925, "synset": "spheroid.n.01", "name": "spheroid"}, - {"id": 21926, "synset": "spherule.n.01", "name": "spherule"}, - {"id": 21927, "synset": "toroid.n.01", "name": "toroid"}, - {"id": 21928, "synset": "column.n.04", "name": "column"}, - {"id": 21929, "synset": "barrel.n.03", "name": "barrel"}, - {"id": 21930, "synset": "pipe.n.03", "name": "pipe"}, - {"id": 21931, "synset": "pellet.n.01", "name": "pellet"}, - {"id": 21932, "synset": "bolus.n.01", "name": "bolus"}, - {"id": 21933, "synset": "dewdrop.n.01", "name": "dewdrop"}, - {"id": 21934, "synset": "ridge.n.02", "name": "ridge"}, - {"id": 21935, "synset": "rim.n.01", "name": "rim"}, - {"id": 21936, "synset": "taper.n.01", "name": "taper"}, - {"id": 21937, "synset": "boundary.n.02", "name": "boundary"}, - {"id": 21938, "synset": "incisure.n.01", "name": "incisure"}, - {"id": 21939, "synset": "notch.n.01", "name": "notch"}, - {"id": 21940, "synset": "wrinkle.n.01", "name": "wrinkle"}, - {"id": 21941, "synset": "dermatoglyphic.n.01", "name": "dermatoglyphic"}, - {"id": 21942, "synset": "frown_line.n.01", "name": "frown_line"}, - {"id": 21943, "synset": "line_of_life.n.01", "name": "line_of_life"}, - {"id": 21944, "synset": "line_of_heart.n.01", "name": "line_of_heart"}, - {"id": 21945, "synset": "crevice.n.01", "name": "crevice"}, - {"id": 21946, "synset": "cleft.n.01", "name": "cleft"}, - {"id": 21947, "synset": "roulette.n.01", "name": "roulette"}, - {"id": 21948, "synset": "node.n.01", "name": "node"}, - {"id": 21949, "synset": "tree.n.02", "name": "tree"}, - {"id": 21950, "synset": "stemma.n.01", "name": "stemma"}, - {"id": 21951, "synset": "brachium.n.01", "name": "brachium"}, - {"id": 21952, "synset": "fork.n.03", "name": "fork"}, - {"id": 21953, "synset": "block.n.03", "name": "block"}, - {"id": 21954, "synset": "ovoid.n.01", "name": "ovoid"}, - {"id": 21955, "synset": "tetrahedron.n.01", "name": "tetrahedron"}, - {"id": 21956, "synset": "pentahedron.n.01", "name": "pentahedron"}, - {"id": 21957, "synset": "hexahedron.n.01", "name": "hexahedron"}, - {"id": 21958, "synset": "regular_polyhedron.n.01", "name": "regular_polyhedron"}, - {"id": 21959, "synset": "polyhedral_angle.n.01", "name": "polyhedral_angle"}, - {"id": 21960, "synset": "cube.n.01", "name": "cube"}, - {"id": 21961, "synset": "truncated_pyramid.n.01", "name": "truncated_pyramid"}, - {"id": 21962, "synset": "truncated_cone.n.01", "name": "truncated_cone"}, - {"id": 21963, "synset": "tail.n.03", "name": "tail"}, - {"id": 21964, "synset": "tongue.n.03", "name": "tongue"}, - {"id": 21965, "synset": "trapezohedron.n.01", "name": "trapezohedron"}, - {"id": 21966, "synset": "wedge.n.01", "name": "wedge"}, - {"id": 21967, "synset": "keel.n.01", "name": "keel"}, - {"id": 21968, "synset": "place.n.06", "name": "place"}, - {"id": 21969, "synset": "herpes.n.01", "name": "herpes"}, - {"id": 21970, "synset": "chlamydia.n.01", "name": "chlamydia"}, - {"id": 21971, "synset": "wall.n.04", "name": "wall"}, - {"id": 21972, "synset": "micronutrient.n.01", "name": "micronutrient"}, - {"id": 21973, "synset": "chyme.n.01", "name": "chyme"}, - {"id": 21974, "synset": "ragweed_pollen.n.01", "name": "ragweed_pollen"}, - {"id": 21975, "synset": "pina_cloth.n.01", "name": "pina_cloth"}, - { - "id": 21976, - "synset": "chlorobenzylidenemalononitrile.n.01", - "name": "chlorobenzylidenemalononitrile", - }, - {"id": 21977, "synset": "carbon.n.01", "name": "carbon"}, - {"id": 21978, "synset": "charcoal.n.01", "name": "charcoal"}, - {"id": 21979, "synset": "rock.n.02", "name": "rock"}, - {"id": 21980, "synset": "gravel.n.01", "name": "gravel"}, - {"id": 21981, "synset": "aflatoxin.n.01", "name": "aflatoxin"}, - {"id": 21982, "synset": "alpha-tocopheral.n.01", "name": "alpha-tocopheral"}, - {"id": 21983, "synset": "leopard.n.01", "name": "leopard"}, - {"id": 21984, "synset": "bricks_and_mortar.n.01", "name": "bricks_and_mortar"}, - {"id": 21985, "synset": "lagging.n.01", "name": "lagging"}, - {"id": 21986, "synset": "hydraulic_cement.n.01", "name": "hydraulic_cement"}, - {"id": 21987, "synset": "choline.n.01", "name": "choline"}, - {"id": 21988, "synset": "concrete.n.01", "name": "concrete"}, - {"id": 21989, "synset": "glass_wool.n.01", "name": "glass_wool"}, - {"id": 21990, "synset": "soil.n.02", "name": "soil"}, - {"id": 21991, "synset": "high_explosive.n.01", "name": "high_explosive"}, - {"id": 21992, "synset": "litter.n.02", "name": "litter"}, - {"id": 21993, "synset": "fish_meal.n.01", "name": "fish_meal"}, - {"id": 21994, "synset": "greek_fire.n.01", "name": "Greek_fire"}, - {"id": 21995, "synset": "culture_medium.n.01", "name": "culture_medium"}, - {"id": 21996, "synset": "agar.n.01", "name": "agar"}, - {"id": 21997, "synset": "blood_agar.n.01", "name": "blood_agar"}, - {"id": 21998, "synset": "hip_tile.n.01", "name": "hip_tile"}, - {"id": 21999, "synset": "hyacinth.n.01", "name": "hyacinth"}, - {"id": 22000, "synset": "hydroxide_ion.n.01", "name": "hydroxide_ion"}, - {"id": 22001, "synset": "ice.n.01", "name": "ice"}, - {"id": 22002, "synset": "inositol.n.01", "name": "inositol"}, - {"id": 22003, "synset": "linoleum.n.01", "name": "linoleum"}, - {"id": 22004, "synset": "lithia_water.n.01", "name": "lithia_water"}, - {"id": 22005, "synset": "lodestone.n.01", "name": "lodestone"}, - {"id": 22006, "synset": "pantothenic_acid.n.01", "name": "pantothenic_acid"}, - {"id": 22007, "synset": "paper.n.01", "name": "paper"}, - {"id": 22008, "synset": "papyrus.n.01", "name": "papyrus"}, - {"id": 22009, "synset": "pantile.n.01", "name": "pantile"}, - {"id": 22010, "synset": "blacktop.n.01", "name": "blacktop"}, - {"id": 22011, "synset": "tarmacadam.n.01", "name": "tarmacadam"}, - {"id": 22012, "synset": "paving.n.01", "name": "paving"}, - {"id": 22013, "synset": "plaster.n.01", "name": "plaster"}, - {"id": 22014, "synset": "poison_gas.n.01", "name": "poison_gas"}, - {"id": 22015, "synset": "ridge_tile.n.01", "name": "ridge_tile"}, - {"id": 22016, "synset": "roughcast.n.01", "name": "roughcast"}, - {"id": 22017, "synset": "sand.n.01", "name": "sand"}, - {"id": 22018, "synset": "spackle.n.01", "name": "spackle"}, - {"id": 22019, "synset": "render.n.01", "name": "render"}, - {"id": 22020, "synset": "wattle_and_daub.n.01", "name": "wattle_and_daub"}, - {"id": 22021, "synset": "stucco.n.01", "name": "stucco"}, - {"id": 22022, "synset": "tear_gas.n.01", "name": "tear_gas"}, - {"id": 22023, "synset": "linseed.n.01", "name": "linseed"}, - {"id": 22024, "synset": "vitamin.n.01", "name": "vitamin"}, - {"id": 22025, "synset": "fat-soluble_vitamin.n.01", "name": "fat-soluble_vitamin"}, - {"id": 22026, "synset": "water-soluble_vitamin.n.01", "name": "water-soluble_vitamin"}, - {"id": 22027, "synset": "vitamin_a.n.01", "name": "vitamin_A"}, - {"id": 22028, "synset": "vitamin_a1.n.01", "name": "vitamin_A1"}, - {"id": 22029, "synset": "vitamin_a2.n.01", "name": "vitamin_A2"}, - {"id": 22030, "synset": "b-complex_vitamin.n.01", "name": "B-complex_vitamin"}, - {"id": 22031, "synset": "vitamin_b1.n.01", "name": "vitamin_B1"}, - {"id": 22032, "synset": "vitamin_b12.n.01", "name": "vitamin_B12"}, - {"id": 22033, "synset": "vitamin_b2.n.01", "name": "vitamin_B2"}, - {"id": 22034, "synset": "vitamin_b6.n.01", "name": "vitamin_B6"}, - {"id": 22035, "synset": "vitamin_bc.n.01", "name": "vitamin_Bc"}, - {"id": 22036, "synset": "niacin.n.01", "name": "niacin"}, - {"id": 22037, "synset": "vitamin_d.n.01", "name": "vitamin_D"}, - {"id": 22038, "synset": "vitamin_e.n.01", "name": "vitamin_E"}, - {"id": 22039, "synset": "biotin.n.01", "name": "biotin"}, - {"id": 22040, "synset": "vitamin_k.n.01", "name": "vitamin_K"}, - {"id": 22041, "synset": "vitamin_k1.n.01", "name": "vitamin_K1"}, - {"id": 22042, "synset": "vitamin_k3.n.01", "name": "vitamin_K3"}, - {"id": 22043, "synset": "vitamin_p.n.01", "name": "vitamin_P"}, - {"id": 22044, "synset": "vitamin_c.n.01", "name": "vitamin_C"}, - {"id": 22045, "synset": "planking.n.01", "name": "planking"}, - {"id": 22046, "synset": "chipboard.n.01", "name": "chipboard"}, - {"id": 22047, "synset": "knothole.n.01", "name": "knothole"}, -] diff --git a/dimos/models/Detic/detic/data/datasets/lvis_v1.py b/dimos/models/Detic/detic/data/datasets/lvis_v1.py deleted file mode 100644 index 659a5fbbc0..0000000000 --- a/dimos/models/Detic/detic/data/datasets/lvis_v1.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import logging -import os - -from detectron2.data import DatasetCatalog, MetadataCatalog -from detectron2.data.datasets.lvis import get_lvis_instances_meta -from detectron2.structures import BoxMode -from fvcore.common.file_io import PathManager -from fvcore.common.timer import Timer -from typing import Optional - -logger = logging.getLogger(__name__) - -__all__ = ["custom_load_lvis_json", "custom_register_lvis_instances"] - - -def custom_register_lvis_instances(name: str, metadata, json_file, image_root) -> None: - """ """ - DatasetCatalog.register(name, lambda: custom_load_lvis_json(json_file, image_root, name)) - MetadataCatalog.get(name).set( - json_file=json_file, image_root=image_root, evaluator_type="lvis", **metadata - ) - - -def custom_load_lvis_json(json_file, image_root, dataset_name: Optional[str]=None): - """ - Modifications: - use `file_name` - convert neg_category_ids - add pos_category_ids - """ - from lvis import LVIS - - json_file = PathManager.get_local_path(json_file) - - timer = Timer() - lvis_api = LVIS(json_file) - if timer.seconds() > 1: - logger.info(f"Loading {json_file} takes {timer.seconds():.2f} seconds.") - - catid2contid = { - x["id"]: i - for i, x in enumerate(sorted(lvis_api.dataset["categories"], key=lambda x: x["id"])) - } - if len(lvis_api.dataset["categories"]) == 1203: - for x in lvis_api.dataset["categories"]: - assert catid2contid[x["id"]] == x["id"] - 1 - img_ids = sorted(lvis_api.imgs.keys()) - imgs = lvis_api.load_imgs(img_ids) - anns = [lvis_api.img_ann_map[img_id] for img_id in img_ids] - - ann_ids = [ann["id"] for anns_per_image in anns for ann in anns_per_image] - assert len(set(ann_ids)) == len(ann_ids), f"Annotation ids in '{json_file}' are not unique" - - imgs_anns = list(zip(imgs, anns, strict=False)) - logger.info(f"Loaded {len(imgs_anns)} images in the LVIS v1 format from {json_file}") - - dataset_dicts = [] - - for img_dict, anno_dict_list in imgs_anns: - record = {} - if "file_name" in img_dict: - file_name = img_dict["file_name"] - if img_dict["file_name"].startswith("COCO"): - file_name = file_name[-16:] - record["file_name"] = os.path.join(image_root, file_name) - elif "coco_url" in img_dict: - # e.g., http://images.cocodataset.org/train2017/000000391895.jpg - file_name = img_dict["coco_url"][30:] - record["file_name"] = os.path.join(image_root, file_name) - elif "tar_index" in img_dict: - record["tar_index"] = img_dict["tar_index"] - - record["height"] = img_dict["height"] - record["width"] = img_dict["width"] - record["not_exhaustive_category_ids"] = img_dict.get("not_exhaustive_category_ids", []) - record["neg_category_ids"] = img_dict.get("neg_category_ids", []) - # NOTE: modified by Xingyi: convert to 0-based - record["neg_category_ids"] = [catid2contid[x] for x in record["neg_category_ids"]] - if "pos_category_ids" in img_dict: - record["pos_category_ids"] = [ - catid2contid[x] for x in img_dict.get("pos_category_ids", []) - ] - if "captions" in img_dict: - record["captions"] = img_dict["captions"] - if "caption_features" in img_dict: - record["caption_features"] = img_dict["caption_features"] - image_id = record["image_id"] = img_dict["id"] - - objs = [] - for anno in anno_dict_list: - assert anno["image_id"] == image_id - if anno.get("iscrowd", 0) > 0: - continue - obj = {"bbox": anno["bbox"], "bbox_mode": BoxMode.XYWH_ABS} - obj["category_id"] = catid2contid[anno["category_id"]] - if "segmentation" in anno: - segm = anno["segmentation"] - valid_segm = [poly for poly in segm if len(poly) % 2 == 0 and len(poly) >= 6] - # assert len(segm) == len( - # valid_segm - # ), "Annotation contains an invalid polygon with < 3 points" - if not len(segm) == len(valid_segm): - print("Annotation contains an invalid polygon with < 3 points") - assert len(segm) > 0 - obj["segmentation"] = segm - objs.append(obj) - record["annotations"] = objs - dataset_dicts.append(record) - - return dataset_dicts - - -_CUSTOM_SPLITS_LVIS = { - "lvis_v1_train+coco": ("coco/", "lvis/lvis_v1_train+coco_mask.json"), - "lvis_v1_train_norare": ("coco/", "lvis/lvis_v1_train_norare.json"), -} - - -for key, (image_root, json_file) in _CUSTOM_SPLITS_LVIS.items(): - custom_register_lvis_instances( - key, - get_lvis_instances_meta(key), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) - - -def get_lvis_22k_meta(): - from .lvis_22k_categories import CATEGORIES - - cat_ids = [k["id"] for k in CATEGORIES] - assert min(cat_ids) == 1 and max(cat_ids) == len(cat_ids), ( - "Category ids are not in [1, #categories], as expected" - ) - # Ensure that the category list is sorted by id - lvis_categories = sorted(CATEGORIES, key=lambda x: x["id"]) - thing_classes = [k["name"] for k in lvis_categories] - meta = {"thing_classes": thing_classes} - return meta - - -_CUSTOM_SPLITS_LVIS_22K = { - "lvis_v1_train_22k": ("coco/", "lvis/lvis_v1_train_lvis-22k.json"), -} - -for key, (image_root, json_file) in _CUSTOM_SPLITS_LVIS_22K.items(): - custom_register_lvis_instances( - key, - get_lvis_22k_meta(), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/detic/data/datasets/objects365.py b/dimos/models/Detic/detic/data/datasets/objects365.py deleted file mode 100644 index 236e609287..0000000000 --- a/dimos/models/Detic/detic/data/datasets/objects365.py +++ /dev/null @@ -1,781 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import os - -from detectron2.data.datasets.register_coco import register_coco_instances - -# categories_v2 = [ -# {'id': 1, 'name': 'Person'}, -# {'id': 2, 'name': 'Sneakers'}, -# {'id': 3, 'name': 'Chair'}, -# {'id': 4, 'name': 'Other Shoes'}, -# {'id': 5, 'name': 'Hat'}, -# {'id': 6, 'name': 'Car'}, -# {'id': 7, 'name': 'Lamp'}, -# {'id': 8, 'name': 'Glasses'}, -# {'id': 9, 'name': 'Bottle'}, -# {'id': 10, 'name': 'Desk'}, -# {'id': 11, 'name': 'Cup'}, -# {'id': 12, 'name': 'Street Lights'}, -# {'id': 13, 'name': 'Cabinet/shelf'}, -# {'id': 14, 'name': 'Handbag/Satchel'}, -# {'id': 15, 'name': 'Bracelet'}, -# {'id': 16, 'name': 'Plate'}, -# {'id': 17, 'name': 'Picture/Frame'}, -# {'id': 18, 'name': 'Helmet'}, -# {'id': 19, 'name': 'Book'}, -# {'id': 20, 'name': 'Gloves'}, -# {'id': 21, 'name': 'Storage box'}, -# {'id': 22, 'name': 'Boat'}, -# {'id': 23, 'name': 'Leather Shoes'}, -# {'id': 24, 'name': 'Flower'}, -# {'id': 25, 'name': 'Bench'}, -# {'id': 26, 'name': 'Potted Plant'}, -# {'id': 27, 'name': 'Bowl/Basin'}, -# {'id': 28, 'name': 'Flag'}, -# {'id': 29, 'name': 'Pillow'}, -# {'id': 30, 'name': 'Boots'}, -# {'id': 31, 'name': 'Vase'}, -# {'id': 32, 'name': 'Microphone'}, -# {'id': 33, 'name': 'Necklace'}, -# {'id': 34, 'name': 'Ring'}, -# {'id': 35, 'name': 'SUV'}, -# {'id': 36, 'name': 'Wine Glass'}, -# {'id': 37, 'name': 'Belt'}, -# {'id': 38, 'name': 'Moniter/TV'}, -# {'id': 39, 'name': 'Backpack'}, -# {'id': 40, 'name': 'Umbrella'}, -# {'id': 41, 'name': 'Traffic Light'}, -# {'id': 42, 'name': 'Speaker'}, -# {'id': 43, 'name': 'Watch'}, -# {'id': 44, 'name': 'Tie'}, -# {'id': 45, 'name': 'Trash bin Can'}, -# {'id': 46, 'name': 'Slippers'}, -# {'id': 47, 'name': 'Bicycle'}, -# {'id': 48, 'name': 'Stool'}, -# {'id': 49, 'name': 'Barrel/bucket'}, -# {'id': 50, 'name': 'Van'}, -# {'id': 51, 'name': 'Couch'}, -# {'id': 52, 'name': 'Sandals'}, -# {'id': 53, 'name': 'Bakset'}, -# {'id': 54, 'name': 'Drum'}, -# {'id': 55, 'name': 'Pen/Pencil'}, -# {'id': 56, 'name': 'Bus'}, -# {'id': 57, 'name': 'Wild Bird'}, -# {'id': 58, 'name': 'High Heels'}, -# {'id': 59, 'name': 'Motorcycle'}, -# {'id': 60, 'name': 'Guitar'}, -# {'id': 61, 'name': 'Carpet'}, -# {'id': 62, 'name': 'Cell Phone'}, -# {'id': 63, 'name': 'Bread'}, -# {'id': 64, 'name': 'Camera'}, -# {'id': 65, 'name': 'Canned'}, -# {'id': 66, 'name': 'Truck'}, -# {'id': 67, 'name': 'Traffic cone'}, -# {'id': 68, 'name': 'Cymbal'}, -# {'id': 69, 'name': 'Lifesaver'}, -# {'id': 70, 'name': 'Towel'}, -# {'id': 71, 'name': 'Stuffed Toy'}, -# {'id': 72, 'name': 'Candle'}, -# {'id': 73, 'name': 'Sailboat'}, -# {'id': 74, 'name': 'Laptop'}, -# {'id': 75, 'name': 'Awning'}, -# {'id': 76, 'name': 'Bed'}, -# {'id': 77, 'name': 'Faucet'}, -# {'id': 78, 'name': 'Tent'}, -# {'id': 79, 'name': 'Horse'}, -# {'id': 80, 'name': 'Mirror'}, -# {'id': 81, 'name': 'Power outlet'}, -# {'id': 82, 'name': 'Sink'}, -# {'id': 83, 'name': 'Apple'}, -# {'id': 84, 'name': 'Air Conditioner'}, -# {'id': 85, 'name': 'Knife'}, -# {'id': 86, 'name': 'Hockey Stick'}, -# {'id': 87, 'name': 'Paddle'}, -# {'id': 88, 'name': 'Pickup Truck'}, -# {'id': 89, 'name': 'Fork'}, -# {'id': 90, 'name': 'Traffic Sign'}, -# {'id': 91, 'name': 'Ballon'}, -# {'id': 92, 'name': 'Tripod'}, -# {'id': 93, 'name': 'Dog'}, -# {'id': 94, 'name': 'Spoon'}, -# {'id': 95, 'name': 'Clock'}, -# {'id': 96, 'name': 'Pot'}, -# {'id': 97, 'name': 'Cow'}, -# {'id': 98, 'name': 'Cake'}, -# {'id': 99, 'name': 'Dinning Table'}, -# {'id': 100, 'name': 'Sheep'}, -# {'id': 101, 'name': 'Hanger'}, -# {'id': 102, 'name': 'Blackboard/Whiteboard'}, -# {'id': 103, 'name': 'Napkin'}, -# {'id': 104, 'name': 'Other Fish'}, -# {'id': 105, 'name': 'Orange/Tangerine'}, -# {'id': 106, 'name': 'Toiletry'}, -# {'id': 107, 'name': 'Keyboard'}, -# {'id': 108, 'name': 'Tomato'}, -# {'id': 109, 'name': 'Lantern'}, -# {'id': 110, 'name': 'Machinery Vehicle'}, -# {'id': 111, 'name': 'Fan'}, -# {'id': 112, 'name': 'Green Vegetables'}, -# {'id': 113, 'name': 'Banana'}, -# {'id': 114, 'name': 'Baseball Glove'}, -# {'id': 115, 'name': 'Airplane'}, -# {'id': 116, 'name': 'Mouse'}, -# {'id': 117, 'name': 'Train'}, -# {'id': 118, 'name': 'Pumpkin'}, -# {'id': 119, 'name': 'Soccer'}, -# {'id': 120, 'name': 'Skiboard'}, -# {'id': 121, 'name': 'Luggage'}, -# {'id': 122, 'name': 'Nightstand'}, -# {'id': 123, 'name': 'Tea pot'}, -# {'id': 124, 'name': 'Telephone'}, -# {'id': 125, 'name': 'Trolley'}, -# {'id': 126, 'name': 'Head Phone'}, -# {'id': 127, 'name': 'Sports Car'}, -# {'id': 128, 'name': 'Stop Sign'}, -# {'id': 129, 'name': 'Dessert'}, -# {'id': 130, 'name': 'Scooter'}, -# {'id': 131, 'name': 'Stroller'}, -# {'id': 132, 'name': 'Crane'}, -# {'id': 133, 'name': 'Remote'}, -# {'id': 134, 'name': 'Refrigerator'}, -# {'id': 135, 'name': 'Oven'}, -# {'id': 136, 'name': 'Lemon'}, -# {'id': 137, 'name': 'Duck'}, -# {'id': 138, 'name': 'Baseball Bat'}, -# {'id': 139, 'name': 'Surveillance Camera'}, -# {'id': 140, 'name': 'Cat'}, -# {'id': 141, 'name': 'Jug'}, -# {'id': 142, 'name': 'Broccoli'}, -# {'id': 143, 'name': 'Piano'}, -# {'id': 144, 'name': 'Pizza'}, -# {'id': 145, 'name': 'Elephant'}, -# {'id': 146, 'name': 'Skateboard'}, -# {'id': 147, 'name': 'Surfboard'}, -# {'id': 148, 'name': 'Gun'}, -# {'id': 149, 'name': 'Skating and Skiing shoes'}, -# {'id': 150, 'name': 'Gas stove'}, -# {'id': 151, 'name': 'Donut'}, -# {'id': 152, 'name': 'Bow Tie'}, -# {'id': 153, 'name': 'Carrot'}, -# {'id': 154, 'name': 'Toilet'}, -# {'id': 155, 'name': 'Kite'}, -# {'id': 156, 'name': 'Strawberry'}, -# {'id': 157, 'name': 'Other Balls'}, -# {'id': 158, 'name': 'Shovel'}, -# {'id': 159, 'name': 'Pepper'}, -# {'id': 160, 'name': 'Computer Box'}, -# {'id': 161, 'name': 'Toilet Paper'}, -# {'id': 162, 'name': 'Cleaning Products'}, -# {'id': 163, 'name': 'Chopsticks'}, -# {'id': 164, 'name': 'Microwave'}, -# {'id': 165, 'name': 'Pigeon'}, -# {'id': 166, 'name': 'Baseball'}, -# {'id': 167, 'name': 'Cutting/chopping Board'}, -# {'id': 168, 'name': 'Coffee Table'}, -# {'id': 169, 'name': 'Side Table'}, -# {'id': 170, 'name': 'Scissors'}, -# {'id': 171, 'name': 'Marker'}, -# {'id': 172, 'name': 'Pie'}, -# {'id': 173, 'name': 'Ladder'}, -# {'id': 174, 'name': 'Snowboard'}, -# {'id': 175, 'name': 'Cookies'}, -# {'id': 176, 'name': 'Radiator'}, -# {'id': 177, 'name': 'Fire Hydrant'}, -# {'id': 178, 'name': 'Basketball'}, -# {'id': 179, 'name': 'Zebra'}, -# {'id': 180, 'name': 'Grape'}, -# {'id': 181, 'name': 'Giraffe'}, -# {'id': 182, 'name': 'Potato'}, -# {'id': 183, 'name': 'Sausage'}, -# {'id': 184, 'name': 'Tricycle'}, -# {'id': 185, 'name': 'Violin'}, -# {'id': 186, 'name': 'Egg'}, -# {'id': 187, 'name': 'Fire Extinguisher'}, -# {'id': 188, 'name': 'Candy'}, -# {'id': 189, 'name': 'Fire Truck'}, -# {'id': 190, 'name': 'Billards'}, -# {'id': 191, 'name': 'Converter'}, -# {'id': 192, 'name': 'Bathtub'}, -# {'id': 193, 'name': 'Wheelchair'}, -# {'id': 194, 'name': 'Golf Club'}, -# {'id': 195, 'name': 'Briefcase'}, -# {'id': 196, 'name': 'Cucumber'}, -# {'id': 197, 'name': 'Cigar/Cigarette '}, -# {'id': 198, 'name': 'Paint Brush'}, -# {'id': 199, 'name': 'Pear'}, -# {'id': 200, 'name': 'Heavy Truck'}, -# {'id': 201, 'name': 'Hamburger'}, -# {'id': 202, 'name': 'Extractor'}, -# {'id': 203, 'name': 'Extention Cord'}, -# {'id': 204, 'name': 'Tong'}, -# {'id': 205, 'name': 'Tennis Racket'}, -# {'id': 206, 'name': 'Folder'}, -# {'id': 207, 'name': 'American Football'}, -# {'id': 208, 'name': 'earphone'}, -# {'id': 209, 'name': 'Mask'}, -# {'id': 210, 'name': 'Kettle'}, -# {'id': 211, 'name': 'Tennis'}, -# {'id': 212, 'name': 'Ship'}, -# {'id': 213, 'name': 'Swing'}, -# {'id': 214, 'name': 'Coffee Machine'}, -# {'id': 215, 'name': 'Slide'}, -# {'id': 216, 'name': 'Carriage'}, -# {'id': 217, 'name': 'Onion'}, -# {'id': 218, 'name': 'Green beans'}, -# {'id': 219, 'name': 'Projector'}, -# {'id': 220, 'name': 'Frisbee'}, -# {'id': 221, 'name': 'Washing Machine/Drying Machine'}, -# {'id': 222, 'name': 'Chicken'}, -# {'id': 223, 'name': 'Printer'}, -# {'id': 224, 'name': 'Watermelon'}, -# {'id': 225, 'name': 'Saxophone'}, -# {'id': 226, 'name': 'Tissue'}, -# {'id': 227, 'name': 'Toothbrush'}, -# {'id': 228, 'name': 'Ice cream'}, -# {'id': 229, 'name': 'Hotair ballon'}, -# {'id': 230, 'name': 'Cello'}, -# {'id': 231, 'name': 'French Fries'}, -# {'id': 232, 'name': 'Scale'}, -# {'id': 233, 'name': 'Trophy'}, -# {'id': 234, 'name': 'Cabbage'}, -# {'id': 235, 'name': 'Hot dog'}, -# {'id': 236, 'name': 'Blender'}, -# {'id': 237, 'name': 'Peach'}, -# {'id': 238, 'name': 'Rice'}, -# {'id': 239, 'name': 'Wallet/Purse'}, -# {'id': 240, 'name': 'Volleyball'}, -# {'id': 241, 'name': 'Deer'}, -# {'id': 242, 'name': 'Goose'}, -# {'id': 243, 'name': 'Tape'}, -# {'id': 244, 'name': 'Tablet'}, -# {'id': 245, 'name': 'Cosmetics'}, -# {'id': 246, 'name': 'Trumpet'}, -# {'id': 247, 'name': 'Pineapple'}, -# {'id': 248, 'name': 'Golf Ball'}, -# {'id': 249, 'name': 'Ambulance'}, -# {'id': 250, 'name': 'Parking meter'}, -# {'id': 251, 'name': 'Mango'}, -# {'id': 252, 'name': 'Key'}, -# {'id': 253, 'name': 'Hurdle'}, -# {'id': 254, 'name': 'Fishing Rod'}, -# {'id': 255, 'name': 'Medal'}, -# {'id': 256, 'name': 'Flute'}, -# {'id': 257, 'name': 'Brush'}, -# {'id': 258, 'name': 'Penguin'}, -# {'id': 259, 'name': 'Megaphone'}, -# {'id': 260, 'name': 'Corn'}, -# {'id': 261, 'name': 'Lettuce'}, -# {'id': 262, 'name': 'Garlic'}, -# {'id': 263, 'name': 'Swan'}, -# {'id': 264, 'name': 'Helicopter'}, -# {'id': 265, 'name': 'Green Onion'}, -# {'id': 266, 'name': 'Sandwich'}, -# {'id': 267, 'name': 'Nuts'}, -# {'id': 268, 'name': 'Speed Limit Sign'}, -# {'id': 269, 'name': 'Induction Cooker'}, -# {'id': 270, 'name': 'Broom'}, -# {'id': 271, 'name': 'Trombone'}, -# {'id': 272, 'name': 'Plum'}, -# {'id': 273, 'name': 'Rickshaw'}, -# {'id': 274, 'name': 'Goldfish'}, -# {'id': 275, 'name': 'Kiwi fruit'}, -# {'id': 276, 'name': 'Router/modem'}, -# {'id': 277, 'name': 'Poker Card'}, -# {'id': 278, 'name': 'Toaster'}, -# {'id': 279, 'name': 'Shrimp'}, -# {'id': 280, 'name': 'Sushi'}, -# {'id': 281, 'name': 'Cheese'}, -# {'id': 282, 'name': 'Notepaper'}, -# {'id': 283, 'name': 'Cherry'}, -# {'id': 284, 'name': 'Pliers'}, -# {'id': 285, 'name': 'CD'}, -# {'id': 286, 'name': 'Pasta'}, -# {'id': 287, 'name': 'Hammer'}, -# {'id': 288, 'name': 'Cue'}, -# {'id': 289, 'name': 'Avocado'}, -# {'id': 290, 'name': 'Hamimelon'}, -# {'id': 291, 'name': 'Flask'}, -# {'id': 292, 'name': 'Mushroon'}, -# {'id': 293, 'name': 'Screwdriver'}, -# {'id': 294, 'name': 'Soap'}, -# {'id': 295, 'name': 'Recorder'}, -# {'id': 296, 'name': 'Bear'}, -# {'id': 297, 'name': 'Eggplant'}, -# {'id': 298, 'name': 'Board Eraser'}, -# {'id': 299, 'name': 'Coconut'}, -# {'id': 300, 'name': 'Tape Measur/ Ruler'}, -# {'id': 301, 'name': 'Pig'}, -# {'id': 302, 'name': 'Showerhead'}, -# {'id': 303, 'name': 'Globe'}, -# {'id': 304, 'name': 'Chips'}, -# {'id': 305, 'name': 'Steak'}, -# {'id': 306, 'name': 'Crosswalk Sign'}, -# {'id': 307, 'name': 'Stapler'}, -# {'id': 308, 'name': 'Campel'}, -# {'id': 309, 'name': 'Formula 1 '}, -# {'id': 310, 'name': 'Pomegranate'}, -# {'id': 311, 'name': 'Dishwasher'}, -# {'id': 312, 'name': 'Crab'}, -# {'id': 313, 'name': 'Hoverboard'}, -# {'id': 314, 'name': 'Meat ball'}, -# {'id': 315, 'name': 'Rice Cooker'}, -# {'id': 316, 'name': 'Tuba'}, -# {'id': 317, 'name': 'Calculator'}, -# {'id': 318, 'name': 'Papaya'}, -# {'id': 319, 'name': 'Antelope'}, -# {'id': 320, 'name': 'Parrot'}, -# {'id': 321, 'name': 'Seal'}, -# {'id': 322, 'name': 'Buttefly'}, -# {'id': 323, 'name': 'Dumbbell'}, -# {'id': 324, 'name': 'Donkey'}, -# {'id': 325, 'name': 'Lion'}, -# {'id': 326, 'name': 'Urinal'}, -# {'id': 327, 'name': 'Dolphin'}, -# {'id': 328, 'name': 'Electric Drill'}, -# {'id': 329, 'name': 'Hair Dryer'}, -# {'id': 330, 'name': 'Egg tart'}, -# {'id': 331, 'name': 'Jellyfish'}, -# {'id': 332, 'name': 'Treadmill'}, -# {'id': 333, 'name': 'Lighter'}, -# {'id': 334, 'name': 'Grapefruit'}, -# {'id': 335, 'name': 'Game board'}, -# {'id': 336, 'name': 'Mop'}, -# {'id': 337, 'name': 'Radish'}, -# {'id': 338, 'name': 'Baozi'}, -# {'id': 339, 'name': 'Target'}, -# {'id': 340, 'name': 'French'}, -# {'id': 341, 'name': 'Spring Rolls'}, -# {'id': 342, 'name': 'Monkey'}, -# {'id': 343, 'name': 'Rabbit'}, -# {'id': 344, 'name': 'Pencil Case'}, -# {'id': 345, 'name': 'Yak'}, -# {'id': 346, 'name': 'Red Cabbage'}, -# {'id': 347, 'name': 'Binoculars'}, -# {'id': 348, 'name': 'Asparagus'}, -# {'id': 349, 'name': 'Barbell'}, -# {'id': 350, 'name': 'Scallop'}, -# {'id': 351, 'name': 'Noddles'}, -# {'id': 352, 'name': 'Comb'}, -# {'id': 353, 'name': 'Dumpling'}, -# {'id': 354, 'name': 'Oyster'}, -# {'id': 355, 'name': 'Table Teniis paddle'}, -# {'id': 356, 'name': 'Cosmetics Brush/Eyeliner Pencil'}, -# {'id': 357, 'name': 'Chainsaw'}, -# {'id': 358, 'name': 'Eraser'}, -# {'id': 359, 'name': 'Lobster'}, -# {'id': 360, 'name': 'Durian'}, -# {'id': 361, 'name': 'Okra'}, -# {'id': 362, 'name': 'Lipstick'}, -# {'id': 363, 'name': 'Cosmetics Mirror'}, -# {'id': 364, 'name': 'Curling'}, -# {'id': 365, 'name': 'Table Tennis '}, -# ] - -""" -The official Objects365 category names contains typos. -Below is a manual fix. -""" -categories_v2_fix = [ - {"id": 1, "name": "Person"}, - {"id": 2, "name": "Sneakers"}, - {"id": 3, "name": "Chair"}, - {"id": 4, "name": "Other Shoes"}, - {"id": 5, "name": "Hat"}, - {"id": 6, "name": "Car"}, - {"id": 7, "name": "Lamp"}, - {"id": 8, "name": "Glasses"}, - {"id": 9, "name": "Bottle"}, - {"id": 10, "name": "Desk"}, - {"id": 11, "name": "Cup"}, - {"id": 12, "name": "Street Lights"}, - {"id": 13, "name": "Cabinet/shelf"}, - {"id": 14, "name": "Handbag/Satchel"}, - {"id": 15, "name": "Bracelet"}, - {"id": 16, "name": "Plate"}, - {"id": 17, "name": "Picture/Frame"}, - {"id": 18, "name": "Helmet"}, - {"id": 19, "name": "Book"}, - {"id": 20, "name": "Gloves"}, - {"id": 21, "name": "Storage box"}, - {"id": 22, "name": "Boat"}, - {"id": 23, "name": "Leather Shoes"}, - {"id": 24, "name": "Flower"}, - {"id": 25, "name": "Bench"}, - {"id": 26, "name": "Potted Plant"}, - {"id": 27, "name": "Bowl/Basin"}, - {"id": 28, "name": "Flag"}, - {"id": 29, "name": "Pillow"}, - {"id": 30, "name": "Boots"}, - {"id": 31, "name": "Vase"}, - {"id": 32, "name": "Microphone"}, - {"id": 33, "name": "Necklace"}, - {"id": 34, "name": "Ring"}, - {"id": 35, "name": "SUV"}, - {"id": 36, "name": "Wine Glass"}, - {"id": 37, "name": "Belt"}, - {"id": 38, "name": "Monitor/TV"}, - {"id": 39, "name": "Backpack"}, - {"id": 40, "name": "Umbrella"}, - {"id": 41, "name": "Traffic Light"}, - {"id": 42, "name": "Speaker"}, - {"id": 43, "name": "Watch"}, - {"id": 44, "name": "Tie"}, - {"id": 45, "name": "Trash bin Can"}, - {"id": 46, "name": "Slippers"}, - {"id": 47, "name": "Bicycle"}, - {"id": 48, "name": "Stool"}, - {"id": 49, "name": "Barrel/bucket"}, - {"id": 50, "name": "Van"}, - {"id": 51, "name": "Couch"}, - {"id": 52, "name": "Sandals"}, - {"id": 53, "name": "Basket"}, - {"id": 54, "name": "Drum"}, - {"id": 55, "name": "Pen/Pencil"}, - {"id": 56, "name": "Bus"}, - {"id": 57, "name": "Wild Bird"}, - {"id": 58, "name": "High Heels"}, - {"id": 59, "name": "Motorcycle"}, - {"id": 60, "name": "Guitar"}, - {"id": 61, "name": "Carpet"}, - {"id": 62, "name": "Cell Phone"}, - {"id": 63, "name": "Bread"}, - {"id": 64, "name": "Camera"}, - {"id": 65, "name": "Canned"}, - {"id": 66, "name": "Truck"}, - {"id": 67, "name": "Traffic cone"}, - {"id": 68, "name": "Cymbal"}, - {"id": 69, "name": "Lifesaver"}, - {"id": 70, "name": "Towel"}, - {"id": 71, "name": "Stuffed Toy"}, - {"id": 72, "name": "Candle"}, - {"id": 73, "name": "Sailboat"}, - {"id": 74, "name": "Laptop"}, - {"id": 75, "name": "Awning"}, - {"id": 76, "name": "Bed"}, - {"id": 77, "name": "Faucet"}, - {"id": 78, "name": "Tent"}, - {"id": 79, "name": "Horse"}, - {"id": 80, "name": "Mirror"}, - {"id": 81, "name": "Power outlet"}, - {"id": 82, "name": "Sink"}, - {"id": 83, "name": "Apple"}, - {"id": 84, "name": "Air Conditioner"}, - {"id": 85, "name": "Knife"}, - {"id": 86, "name": "Hockey Stick"}, - {"id": 87, "name": "Paddle"}, - {"id": 88, "name": "Pickup Truck"}, - {"id": 89, "name": "Fork"}, - {"id": 90, "name": "Traffic Sign"}, - {"id": 91, "name": "Ballon"}, - {"id": 92, "name": "Tripod"}, - {"id": 93, "name": "Dog"}, - {"id": 94, "name": "Spoon"}, - {"id": 95, "name": "Clock"}, - {"id": 96, "name": "Pot"}, - {"id": 97, "name": "Cow"}, - {"id": 98, "name": "Cake"}, - {"id": 99, "name": "Dining Table"}, - {"id": 100, "name": "Sheep"}, - {"id": 101, "name": "Hanger"}, - {"id": 102, "name": "Blackboard/Whiteboard"}, - {"id": 103, "name": "Napkin"}, - {"id": 104, "name": "Other Fish"}, - {"id": 105, "name": "Orange/Tangerine"}, - {"id": 106, "name": "Toiletry"}, - {"id": 107, "name": "Keyboard"}, - {"id": 108, "name": "Tomato"}, - {"id": 109, "name": "Lantern"}, - {"id": 110, "name": "Machinery Vehicle"}, - {"id": 111, "name": "Fan"}, - {"id": 112, "name": "Green Vegetables"}, - {"id": 113, "name": "Banana"}, - {"id": 114, "name": "Baseball Glove"}, - {"id": 115, "name": "Airplane"}, - {"id": 116, "name": "Mouse"}, - {"id": 117, "name": "Train"}, - {"id": 118, "name": "Pumpkin"}, - {"id": 119, "name": "Soccer"}, - {"id": 120, "name": "Skiboard"}, - {"id": 121, "name": "Luggage"}, - {"id": 122, "name": "Nightstand"}, - {"id": 123, "name": "Teapot"}, - {"id": 124, "name": "Telephone"}, - {"id": 125, "name": "Trolley"}, - {"id": 126, "name": "Head Phone"}, - {"id": 127, "name": "Sports Car"}, - {"id": 128, "name": "Stop Sign"}, - {"id": 129, "name": "Dessert"}, - {"id": 130, "name": "Scooter"}, - {"id": 131, "name": "Stroller"}, - {"id": 132, "name": "Crane"}, - {"id": 133, "name": "Remote"}, - {"id": 134, "name": "Refrigerator"}, - {"id": 135, "name": "Oven"}, - {"id": 136, "name": "Lemon"}, - {"id": 137, "name": "Duck"}, - {"id": 138, "name": "Baseball Bat"}, - {"id": 139, "name": "Surveillance Camera"}, - {"id": 140, "name": "Cat"}, - {"id": 141, "name": "Jug"}, - {"id": 142, "name": "Broccoli"}, - {"id": 143, "name": "Piano"}, - {"id": 144, "name": "Pizza"}, - {"id": 145, "name": "Elephant"}, - {"id": 146, "name": "Skateboard"}, - {"id": 147, "name": "Surfboard"}, - {"id": 148, "name": "Gun"}, - {"id": 149, "name": "Skating and Skiing shoes"}, - {"id": 150, "name": "Gas stove"}, - {"id": 151, "name": "Donut"}, - {"id": 152, "name": "Bow Tie"}, - {"id": 153, "name": "Carrot"}, - {"id": 154, "name": "Toilet"}, - {"id": 155, "name": "Kite"}, - {"id": 156, "name": "Strawberry"}, - {"id": 157, "name": "Other Balls"}, - {"id": 158, "name": "Shovel"}, - {"id": 159, "name": "Pepper"}, - {"id": 160, "name": "Computer Box"}, - {"id": 161, "name": "Toilet Paper"}, - {"id": 162, "name": "Cleaning Products"}, - {"id": 163, "name": "Chopsticks"}, - {"id": 164, "name": "Microwave"}, - {"id": 165, "name": "Pigeon"}, - {"id": 166, "name": "Baseball"}, - {"id": 167, "name": "Cutting/chopping Board"}, - {"id": 168, "name": "Coffee Table"}, - {"id": 169, "name": "Side Table"}, - {"id": 170, "name": "Scissors"}, - {"id": 171, "name": "Marker"}, - {"id": 172, "name": "Pie"}, - {"id": 173, "name": "Ladder"}, - {"id": 174, "name": "Snowboard"}, - {"id": 175, "name": "Cookies"}, - {"id": 176, "name": "Radiator"}, - {"id": 177, "name": "Fire Hydrant"}, - {"id": 178, "name": "Basketball"}, - {"id": 179, "name": "Zebra"}, - {"id": 180, "name": "Grape"}, - {"id": 181, "name": "Giraffe"}, - {"id": 182, "name": "Potato"}, - {"id": 183, "name": "Sausage"}, - {"id": 184, "name": "Tricycle"}, - {"id": 185, "name": "Violin"}, - {"id": 186, "name": "Egg"}, - {"id": 187, "name": "Fire Extinguisher"}, - {"id": 188, "name": "Candy"}, - {"id": 189, "name": "Fire Truck"}, - {"id": 190, "name": "Billards"}, - {"id": 191, "name": "Converter"}, - {"id": 192, "name": "Bathtub"}, - {"id": 193, "name": "Wheelchair"}, - {"id": 194, "name": "Golf Club"}, - {"id": 195, "name": "Briefcase"}, - {"id": 196, "name": "Cucumber"}, - {"id": 197, "name": "Cigar/Cigarette "}, - {"id": 198, "name": "Paint Brush"}, - {"id": 199, "name": "Pear"}, - {"id": 200, "name": "Heavy Truck"}, - {"id": 201, "name": "Hamburger"}, - {"id": 202, "name": "Extractor"}, - {"id": 203, "name": "Extension Cord"}, - {"id": 204, "name": "Tong"}, - {"id": 205, "name": "Tennis Racket"}, - {"id": 206, "name": "Folder"}, - {"id": 207, "name": "American Football"}, - {"id": 208, "name": "earphone"}, - {"id": 209, "name": "Mask"}, - {"id": 210, "name": "Kettle"}, - {"id": 211, "name": "Tennis"}, - {"id": 212, "name": "Ship"}, - {"id": 213, "name": "Swing"}, - {"id": 214, "name": "Coffee Machine"}, - {"id": 215, "name": "Slide"}, - {"id": 216, "name": "Carriage"}, - {"id": 217, "name": "Onion"}, - {"id": 218, "name": "Green beans"}, - {"id": 219, "name": "Projector"}, - {"id": 220, "name": "Frisbee"}, - {"id": 221, "name": "Washing Machine/Drying Machine"}, - {"id": 222, "name": "Chicken"}, - {"id": 223, "name": "Printer"}, - {"id": 224, "name": "Watermelon"}, - {"id": 225, "name": "Saxophone"}, - {"id": 226, "name": "Tissue"}, - {"id": 227, "name": "Toothbrush"}, - {"id": 228, "name": "Ice cream"}, - {"id": 229, "name": "Hot air balloon"}, - {"id": 230, "name": "Cello"}, - {"id": 231, "name": "French Fries"}, - {"id": 232, "name": "Scale"}, - {"id": 233, "name": "Trophy"}, - {"id": 234, "name": "Cabbage"}, - {"id": 235, "name": "Hot dog"}, - {"id": 236, "name": "Blender"}, - {"id": 237, "name": "Peach"}, - {"id": 238, "name": "Rice"}, - {"id": 239, "name": "Wallet/Purse"}, - {"id": 240, "name": "Volleyball"}, - {"id": 241, "name": "Deer"}, - {"id": 242, "name": "Goose"}, - {"id": 243, "name": "Tape"}, - {"id": 244, "name": "Tablet"}, - {"id": 245, "name": "Cosmetics"}, - {"id": 246, "name": "Trumpet"}, - {"id": 247, "name": "Pineapple"}, - {"id": 248, "name": "Golf Ball"}, - {"id": 249, "name": "Ambulance"}, - {"id": 250, "name": "Parking meter"}, - {"id": 251, "name": "Mango"}, - {"id": 252, "name": "Key"}, - {"id": 253, "name": "Hurdle"}, - {"id": 254, "name": "Fishing Rod"}, - {"id": 255, "name": "Medal"}, - {"id": 256, "name": "Flute"}, - {"id": 257, "name": "Brush"}, - {"id": 258, "name": "Penguin"}, - {"id": 259, "name": "Megaphone"}, - {"id": 260, "name": "Corn"}, - {"id": 261, "name": "Lettuce"}, - {"id": 262, "name": "Garlic"}, - {"id": 263, "name": "Swan"}, - {"id": 264, "name": "Helicopter"}, - {"id": 265, "name": "Green Onion"}, - {"id": 266, "name": "Sandwich"}, - {"id": 267, "name": "Nuts"}, - {"id": 268, "name": "Speed Limit Sign"}, - {"id": 269, "name": "Induction Cooker"}, - {"id": 270, "name": "Broom"}, - {"id": 271, "name": "Trombone"}, - {"id": 272, "name": "Plum"}, - {"id": 273, "name": "Rickshaw"}, - {"id": 274, "name": "Goldfish"}, - {"id": 275, "name": "Kiwi fruit"}, - {"id": 276, "name": "Router/modem"}, - {"id": 277, "name": "Poker Card"}, - {"id": 278, "name": "Toaster"}, - {"id": 279, "name": "Shrimp"}, - {"id": 280, "name": "Sushi"}, - {"id": 281, "name": "Cheese"}, - {"id": 282, "name": "Notepaper"}, - {"id": 283, "name": "Cherry"}, - {"id": 284, "name": "Pliers"}, - {"id": 285, "name": "CD"}, - {"id": 286, "name": "Pasta"}, - {"id": 287, "name": "Hammer"}, - {"id": 288, "name": "Cue"}, - {"id": 289, "name": "Avocado"}, - {"id": 290, "name": "Hami melon"}, - {"id": 291, "name": "Flask"}, - {"id": 292, "name": "Mushroom"}, - {"id": 293, "name": "Screwdriver"}, - {"id": 294, "name": "Soap"}, - {"id": 295, "name": "Recorder"}, - {"id": 296, "name": "Bear"}, - {"id": 297, "name": "Eggplant"}, - {"id": 298, "name": "Board Eraser"}, - {"id": 299, "name": "Coconut"}, - {"id": 300, "name": "Tape Measure/ Ruler"}, - {"id": 301, "name": "Pig"}, - {"id": 302, "name": "Showerhead"}, - {"id": 303, "name": "Globe"}, - {"id": 304, "name": "Chips"}, - {"id": 305, "name": "Steak"}, - {"id": 306, "name": "Crosswalk Sign"}, - {"id": 307, "name": "Stapler"}, - {"id": 308, "name": "Camel"}, - {"id": 309, "name": "Formula 1 "}, - {"id": 310, "name": "Pomegranate"}, - {"id": 311, "name": "Dishwasher"}, - {"id": 312, "name": "Crab"}, - {"id": 313, "name": "Hoverboard"}, - {"id": 314, "name": "Meatball"}, - {"id": 315, "name": "Rice Cooker"}, - {"id": 316, "name": "Tuba"}, - {"id": 317, "name": "Calculator"}, - {"id": 318, "name": "Papaya"}, - {"id": 319, "name": "Antelope"}, - {"id": 320, "name": "Parrot"}, - {"id": 321, "name": "Seal"}, - {"id": 322, "name": "Butterfly"}, - {"id": 323, "name": "Dumbbell"}, - {"id": 324, "name": "Donkey"}, - {"id": 325, "name": "Lion"}, - {"id": 326, "name": "Urinal"}, - {"id": 327, "name": "Dolphin"}, - {"id": 328, "name": "Electric Drill"}, - {"id": 329, "name": "Hair Dryer"}, - {"id": 330, "name": "Egg tart"}, - {"id": 331, "name": "Jellyfish"}, - {"id": 332, "name": "Treadmill"}, - {"id": 333, "name": "Lighter"}, - {"id": 334, "name": "Grapefruit"}, - {"id": 335, "name": "Game board"}, - {"id": 336, "name": "Mop"}, - {"id": 337, "name": "Radish"}, - {"id": 338, "name": "Baozi"}, - {"id": 339, "name": "Target"}, - {"id": 340, "name": "French"}, - {"id": 341, "name": "Spring Rolls"}, - {"id": 342, "name": "Monkey"}, - {"id": 343, "name": "Rabbit"}, - {"id": 344, "name": "Pencil Case"}, - {"id": 345, "name": "Yak"}, - {"id": 346, "name": "Red Cabbage"}, - {"id": 347, "name": "Binoculars"}, - {"id": 348, "name": "Asparagus"}, - {"id": 349, "name": "Barbell"}, - {"id": 350, "name": "Scallop"}, - {"id": 351, "name": "Noddles"}, - {"id": 352, "name": "Comb"}, - {"id": 353, "name": "Dumpling"}, - {"id": 354, "name": "Oyster"}, - {"id": 355, "name": "Table Tennis paddle"}, - {"id": 356, "name": "Cosmetics Brush/Eyeliner Pencil"}, - {"id": 357, "name": "Chainsaw"}, - {"id": 358, "name": "Eraser"}, - {"id": 359, "name": "Lobster"}, - {"id": 360, "name": "Durian"}, - {"id": 361, "name": "Okra"}, - {"id": 362, "name": "Lipstick"}, - {"id": 363, "name": "Cosmetics Mirror"}, - {"id": 364, "name": "Curling"}, - {"id": 365, "name": "Table Tennis "}, -] - - -def _get_builtin_metadata(): - id_to_name = {x["id"]: x["name"] for x in categories_v2_fix} - thing_dataset_id_to_contiguous_id = { - x["id"]: i for i, x in enumerate(sorted(categories_v2_fix, key=lambda x: x["id"])) - } - thing_classes = [id_to_name[k] for k in sorted(id_to_name)] - return { - "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes, - } - - -_PREDEFINED_SPLITS_OBJECTS365 = { - "objects365_v2_train": ( - "objects365/train", - "objects365/annotations/zhiyuan_objv2_train_fixname_fixmiss.json", - ), - # 80,000 images, 1,240,587 annotations - "objects365_v2_val": ( - "objects365/val", - "objects365/annotations/zhiyuan_objv2_val_fixname.json", - ), - "objects365_v2_val_rare": ( - "objects365/val", - "objects365/annotations/zhiyuan_objv2_val_fixname_rare.json", - ), -} - -for key, (image_root, json_file) in _PREDEFINED_SPLITS_OBJECTS365.items(): - register_coco_instances( - key, - _get_builtin_metadata(), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/detic/data/datasets/oid.py b/dimos/models/Detic/detic/data/datasets/oid.py deleted file mode 100644 index 0308a8da1d..0000000000 --- a/dimos/models/Detic/detic/data/datasets/oid.py +++ /dev/null @@ -1,544 +0,0 @@ -# Part of the code is from https://github.com/xingyizhou/UniDet/blob/master/projects/UniDet/unidet/data/datasets/oid.py -# Copyright (c) Facebook, Inc. and its affiliates. -import os - -from .register_oid import register_oid_instances - -categories = [ - {"id": 1, "name": "Infant bed", "freebase_id": "/m/061hd_"}, - {"id": 2, "name": "Rose", "freebase_id": "/m/06m11"}, - {"id": 3, "name": "Flag", "freebase_id": "/m/03120"}, - {"id": 4, "name": "Flashlight", "freebase_id": "/m/01kb5b"}, - {"id": 5, "name": "Sea turtle", "freebase_id": "/m/0120dh"}, - {"id": 6, "name": "Camera", "freebase_id": "/m/0dv5r"}, - {"id": 7, "name": "Animal", "freebase_id": "/m/0jbk"}, - {"id": 8, "name": "Glove", "freebase_id": "/m/0174n1"}, - {"id": 9, "name": "Crocodile", "freebase_id": "/m/09f_2"}, - {"id": 10, "name": "Cattle", "freebase_id": "/m/01xq0k1"}, - {"id": 11, "name": "House", "freebase_id": "/m/03jm5"}, - {"id": 12, "name": "Guacamole", "freebase_id": "/m/02g30s"}, - {"id": 13, "name": "Penguin", "freebase_id": "/m/05z6w"}, - {"id": 14, "name": "Vehicle registration plate", "freebase_id": "/m/01jfm_"}, - {"id": 15, "name": "Bench", "freebase_id": "/m/076lb9"}, - {"id": 16, "name": "Ladybug", "freebase_id": "/m/0gj37"}, - {"id": 17, "name": "Human nose", "freebase_id": "/m/0k0pj"}, - {"id": 18, "name": "Watermelon", "freebase_id": "/m/0kpqd"}, - {"id": 19, "name": "Flute", "freebase_id": "/m/0l14j_"}, - {"id": 20, "name": "Butterfly", "freebase_id": "/m/0cyf8"}, - {"id": 21, "name": "Washing machine", "freebase_id": "/m/0174k2"}, - {"id": 22, "name": "Raccoon", "freebase_id": "/m/0dq75"}, - {"id": 23, "name": "Segway", "freebase_id": "/m/076bq"}, - {"id": 24, "name": "Taco", "freebase_id": "/m/07crc"}, - {"id": 25, "name": "Jellyfish", "freebase_id": "/m/0d8zb"}, - {"id": 26, "name": "Cake", "freebase_id": "/m/0fszt"}, - {"id": 27, "name": "Pen", "freebase_id": "/m/0k1tl"}, - {"id": 28, "name": "Cannon", "freebase_id": "/m/020kz"}, - {"id": 29, "name": "Bread", "freebase_id": "/m/09728"}, - {"id": 30, "name": "Tree", "freebase_id": "/m/07j7r"}, - {"id": 31, "name": "Shellfish", "freebase_id": "/m/0fbdv"}, - {"id": 32, "name": "Bed", "freebase_id": "/m/03ssj5"}, - {"id": 33, "name": "Hamster", "freebase_id": "/m/03qrc"}, - {"id": 34, "name": "Hat", "freebase_id": "/m/02dl1y"}, - {"id": 35, "name": "Toaster", "freebase_id": "/m/01k6s3"}, - {"id": 36, "name": "Sombrero", "freebase_id": "/m/02jfl0"}, - {"id": 37, "name": "Tiara", "freebase_id": "/m/01krhy"}, - {"id": 38, "name": "Bowl", "freebase_id": "/m/04kkgm"}, - {"id": 39, "name": "Dragonfly", "freebase_id": "/m/0ft9s"}, - {"id": 40, "name": "Moths and butterflies", "freebase_id": "/m/0d_2m"}, - {"id": 41, "name": "Antelope", "freebase_id": "/m/0czz2"}, - {"id": 42, "name": "Vegetable", "freebase_id": "/m/0f4s2w"}, - {"id": 43, "name": "Torch", "freebase_id": "/m/07dd4"}, - {"id": 44, "name": "Building", "freebase_id": "/m/0cgh4"}, - {"id": 45, "name": "Power plugs and sockets", "freebase_id": "/m/03bbps"}, - {"id": 46, "name": "Blender", "freebase_id": "/m/02pjr4"}, - {"id": 47, "name": "Billiard table", "freebase_id": "/m/04p0qw"}, - {"id": 48, "name": "Cutting board", "freebase_id": "/m/02pdsw"}, - {"id": 49, "name": "Bronze sculpture", "freebase_id": "/m/01yx86"}, - {"id": 50, "name": "Turtle", "freebase_id": "/m/09dzg"}, - {"id": 51, "name": "Broccoli", "freebase_id": "/m/0hkxq"}, - {"id": 52, "name": "Tiger", "freebase_id": "/m/07dm6"}, - {"id": 53, "name": "Mirror", "freebase_id": "/m/054_l"}, - {"id": 54, "name": "Bear", "freebase_id": "/m/01dws"}, - {"id": 55, "name": "Zucchini", "freebase_id": "/m/027pcv"}, - {"id": 56, "name": "Dress", "freebase_id": "/m/01d40f"}, - {"id": 57, "name": "Volleyball", "freebase_id": "/m/02rgn06"}, - {"id": 58, "name": "Guitar", "freebase_id": "/m/0342h"}, - {"id": 59, "name": "Reptile", "freebase_id": "/m/06bt6"}, - {"id": 60, "name": "Golf cart", "freebase_id": "/m/0323sq"}, - {"id": 61, "name": "Tart", "freebase_id": "/m/02zvsm"}, - {"id": 62, "name": "Fedora", "freebase_id": "/m/02fq_6"}, - {"id": 63, "name": "Carnivore", "freebase_id": "/m/01lrl"}, - {"id": 64, "name": "Car", "freebase_id": "/m/0k4j"}, - {"id": 65, "name": "Lighthouse", "freebase_id": "/m/04h7h"}, - {"id": 66, "name": "Coffeemaker", "freebase_id": "/m/07xyvk"}, - {"id": 67, "name": "Food processor", "freebase_id": "/m/03y6mg"}, - {"id": 68, "name": "Truck", "freebase_id": "/m/07r04"}, - {"id": 69, "name": "Bookcase", "freebase_id": "/m/03__z0"}, - {"id": 70, "name": "Surfboard", "freebase_id": "/m/019w40"}, - {"id": 71, "name": "Footwear", "freebase_id": "/m/09j5n"}, - {"id": 72, "name": "Bench", "freebase_id": "/m/0cvnqh"}, - {"id": 73, "name": "Necklace", "freebase_id": "/m/01llwg"}, - {"id": 74, "name": "Flower", "freebase_id": "/m/0c9ph5"}, - {"id": 75, "name": "Radish", "freebase_id": "/m/015x5n"}, - {"id": 76, "name": "Marine mammal", "freebase_id": "/m/0gd2v"}, - {"id": 77, "name": "Frying pan", "freebase_id": "/m/04v6l4"}, - {"id": 78, "name": "Tap", "freebase_id": "/m/02jz0l"}, - {"id": 79, "name": "Peach", "freebase_id": "/m/0dj6p"}, - {"id": 80, "name": "Knife", "freebase_id": "/m/04ctx"}, - {"id": 81, "name": "Handbag", "freebase_id": "/m/080hkjn"}, - {"id": 82, "name": "Laptop", "freebase_id": "/m/01c648"}, - {"id": 83, "name": "Tent", "freebase_id": "/m/01j61q"}, - {"id": 84, "name": "Ambulance", "freebase_id": "/m/012n7d"}, - {"id": 85, "name": "Christmas tree", "freebase_id": "/m/025nd"}, - {"id": 86, "name": "Eagle", "freebase_id": "/m/09csl"}, - {"id": 87, "name": "Limousine", "freebase_id": "/m/01lcw4"}, - {"id": 88, "name": "Kitchen & dining room table", "freebase_id": "/m/0h8n5zk"}, - {"id": 89, "name": "Polar bear", "freebase_id": "/m/0633h"}, - {"id": 90, "name": "Tower", "freebase_id": "/m/01fdzj"}, - {"id": 91, "name": "Football", "freebase_id": "/m/01226z"}, - {"id": 92, "name": "Willow", "freebase_id": "/m/0mw_6"}, - {"id": 93, "name": "Human head", "freebase_id": "/m/04hgtk"}, - {"id": 94, "name": "Stop sign", "freebase_id": "/m/02pv19"}, - {"id": 95, "name": "Banana", "freebase_id": "/m/09qck"}, - {"id": 96, "name": "Mixer", "freebase_id": "/m/063rgb"}, - {"id": 97, "name": "Binoculars", "freebase_id": "/m/0lt4_"}, - {"id": 98, "name": "Dessert", "freebase_id": "/m/0270h"}, - {"id": 99, "name": "Bee", "freebase_id": "/m/01h3n"}, - {"id": 100, "name": "Chair", "freebase_id": "/m/01mzpv"}, - {"id": 101, "name": "Wood-burning stove", "freebase_id": "/m/04169hn"}, - {"id": 102, "name": "Flowerpot", "freebase_id": "/m/0fm3zh"}, - {"id": 103, "name": "Beaker", "freebase_id": "/m/0d20w4"}, - {"id": 104, "name": "Oyster", "freebase_id": "/m/0_cp5"}, - {"id": 105, "name": "Woodpecker", "freebase_id": "/m/01dy8n"}, - {"id": 106, "name": "Harp", "freebase_id": "/m/03m5k"}, - {"id": 107, "name": "Bathtub", "freebase_id": "/m/03dnzn"}, - {"id": 108, "name": "Wall clock", "freebase_id": "/m/0h8mzrc"}, - {"id": 109, "name": "Sports uniform", "freebase_id": "/m/0h8mhzd"}, - {"id": 110, "name": "Rhinoceros", "freebase_id": "/m/03d443"}, - {"id": 111, "name": "Beehive", "freebase_id": "/m/01gllr"}, - {"id": 112, "name": "Cupboard", "freebase_id": "/m/0642b4"}, - {"id": 113, "name": "Chicken", "freebase_id": "/m/09b5t"}, - {"id": 114, "name": "Man", "freebase_id": "/m/04yx4"}, - {"id": 115, "name": "Blue jay", "freebase_id": "/m/01f8m5"}, - {"id": 116, "name": "Cucumber", "freebase_id": "/m/015x4r"}, - {"id": 117, "name": "Balloon", "freebase_id": "/m/01j51"}, - {"id": 118, "name": "Kite", "freebase_id": "/m/02zt3"}, - {"id": 119, "name": "Fireplace", "freebase_id": "/m/03tw93"}, - {"id": 120, "name": "Lantern", "freebase_id": "/m/01jfsr"}, - {"id": 121, "name": "Missile", "freebase_id": "/m/04ylt"}, - {"id": 122, "name": "Book", "freebase_id": "/m/0bt_c3"}, - {"id": 123, "name": "Spoon", "freebase_id": "/m/0cmx8"}, - {"id": 124, "name": "Grapefruit", "freebase_id": "/m/0hqkz"}, - {"id": 125, "name": "Squirrel", "freebase_id": "/m/071qp"}, - {"id": 126, "name": "Orange", "freebase_id": "/m/0cyhj_"}, - {"id": 127, "name": "Coat", "freebase_id": "/m/01xygc"}, - {"id": 128, "name": "Punching bag", "freebase_id": "/m/0420v5"}, - {"id": 129, "name": "Zebra", "freebase_id": "/m/0898b"}, - {"id": 130, "name": "Billboard", "freebase_id": "/m/01knjb"}, - {"id": 131, "name": "Bicycle", "freebase_id": "/m/0199g"}, - {"id": 132, "name": "Door handle", "freebase_id": "/m/03c7gz"}, - {"id": 133, "name": "Mechanical fan", "freebase_id": "/m/02x984l"}, - {"id": 134, "name": "Ring binder", "freebase_id": "/m/04zwwv"}, - {"id": 135, "name": "Table", "freebase_id": "/m/04bcr3"}, - {"id": 136, "name": "Parrot", "freebase_id": "/m/0gv1x"}, - {"id": 137, "name": "Sock", "freebase_id": "/m/01nq26"}, - {"id": 138, "name": "Vase", "freebase_id": "/m/02s195"}, - {"id": 139, "name": "Weapon", "freebase_id": "/m/083kb"}, - {"id": 140, "name": "Shotgun", "freebase_id": "/m/06nrc"}, - {"id": 141, "name": "Glasses", "freebase_id": "/m/0jyfg"}, - {"id": 142, "name": "Seahorse", "freebase_id": "/m/0nybt"}, - {"id": 143, "name": "Belt", "freebase_id": "/m/0176mf"}, - {"id": 144, "name": "Watercraft", "freebase_id": "/m/01rzcn"}, - {"id": 145, "name": "Window", "freebase_id": "/m/0d4v4"}, - {"id": 146, "name": "Giraffe", "freebase_id": "/m/03bk1"}, - {"id": 147, "name": "Lion", "freebase_id": "/m/096mb"}, - {"id": 148, "name": "Tire", "freebase_id": "/m/0h9mv"}, - {"id": 149, "name": "Vehicle", "freebase_id": "/m/07yv9"}, - {"id": 150, "name": "Canoe", "freebase_id": "/m/0ph39"}, - {"id": 151, "name": "Tie", "freebase_id": "/m/01rkbr"}, - {"id": 152, "name": "Shelf", "freebase_id": "/m/0gjbg72"}, - {"id": 153, "name": "Picture frame", "freebase_id": "/m/06z37_"}, - {"id": 154, "name": "Printer", "freebase_id": "/m/01m4t"}, - {"id": 155, "name": "Human leg", "freebase_id": "/m/035r7c"}, - {"id": 156, "name": "Boat", "freebase_id": "/m/019jd"}, - {"id": 157, "name": "Slow cooker", "freebase_id": "/m/02tsc9"}, - {"id": 158, "name": "Croissant", "freebase_id": "/m/015wgc"}, - {"id": 159, "name": "Candle", "freebase_id": "/m/0c06p"}, - {"id": 160, "name": "Pancake", "freebase_id": "/m/01dwwc"}, - {"id": 161, "name": "Pillow", "freebase_id": "/m/034c16"}, - {"id": 162, "name": "Coin", "freebase_id": "/m/0242l"}, - {"id": 163, "name": "Stretcher", "freebase_id": "/m/02lbcq"}, - {"id": 164, "name": "Sandal", "freebase_id": "/m/03nfch"}, - {"id": 165, "name": "Woman", "freebase_id": "/m/03bt1vf"}, - {"id": 166, "name": "Stairs", "freebase_id": "/m/01lynh"}, - {"id": 167, "name": "Harpsichord", "freebase_id": "/m/03q5t"}, - {"id": 168, "name": "Stool", "freebase_id": "/m/0fqt361"}, - {"id": 169, "name": "Bus", "freebase_id": "/m/01bjv"}, - {"id": 170, "name": "Suitcase", "freebase_id": "/m/01s55n"}, - {"id": 171, "name": "Human mouth", "freebase_id": "/m/0283dt1"}, - {"id": 172, "name": "Juice", "freebase_id": "/m/01z1kdw"}, - {"id": 173, "name": "Skull", "freebase_id": "/m/016m2d"}, - {"id": 174, "name": "Door", "freebase_id": "/m/02dgv"}, - {"id": 175, "name": "Violin", "freebase_id": "/m/07y_7"}, - {"id": 176, "name": "Chopsticks", "freebase_id": "/m/01_5g"}, - {"id": 177, "name": "Digital clock", "freebase_id": "/m/06_72j"}, - {"id": 178, "name": "Sunflower", "freebase_id": "/m/0ftb8"}, - {"id": 179, "name": "Leopard", "freebase_id": "/m/0c29q"}, - {"id": 180, "name": "Bell pepper", "freebase_id": "/m/0jg57"}, - {"id": 181, "name": "Harbor seal", "freebase_id": "/m/02l8p9"}, - {"id": 182, "name": "Snake", "freebase_id": "/m/078jl"}, - {"id": 183, "name": "Sewing machine", "freebase_id": "/m/0llzx"}, - {"id": 184, "name": "Goose", "freebase_id": "/m/0dbvp"}, - {"id": 185, "name": "Helicopter", "freebase_id": "/m/09ct_"}, - {"id": 186, "name": "Seat belt", "freebase_id": "/m/0dkzw"}, - {"id": 187, "name": "Coffee cup", "freebase_id": "/m/02p5f1q"}, - {"id": 188, "name": "Microwave oven", "freebase_id": "/m/0fx9l"}, - {"id": 189, "name": "Hot dog", "freebase_id": "/m/01b9xk"}, - {"id": 190, "name": "Countertop", "freebase_id": "/m/0b3fp9"}, - {"id": 191, "name": "Serving tray", "freebase_id": "/m/0h8n27j"}, - {"id": 192, "name": "Dog bed", "freebase_id": "/m/0h8n6f9"}, - {"id": 193, "name": "Beer", "freebase_id": "/m/01599"}, - {"id": 194, "name": "Sunglasses", "freebase_id": "/m/017ftj"}, - {"id": 195, "name": "Golf ball", "freebase_id": "/m/044r5d"}, - {"id": 196, "name": "Waffle", "freebase_id": "/m/01dwsz"}, - {"id": 197, "name": "Palm tree", "freebase_id": "/m/0cdl1"}, - {"id": 198, "name": "Trumpet", "freebase_id": "/m/07gql"}, - {"id": 199, "name": "Ruler", "freebase_id": "/m/0hdln"}, - {"id": 200, "name": "Helmet", "freebase_id": "/m/0zvk5"}, - {"id": 201, "name": "Ladder", "freebase_id": "/m/012w5l"}, - {"id": 202, "name": "Office building", "freebase_id": "/m/021sj1"}, - {"id": 203, "name": "Tablet computer", "freebase_id": "/m/0bh9flk"}, - {"id": 204, "name": "Toilet paper", "freebase_id": "/m/09gtd"}, - {"id": 205, "name": "Pomegranate", "freebase_id": "/m/0jwn_"}, - {"id": 206, "name": "Skirt", "freebase_id": "/m/02wv6h6"}, - {"id": 207, "name": "Gas stove", "freebase_id": "/m/02wv84t"}, - {"id": 208, "name": "Cookie", "freebase_id": "/m/021mn"}, - {"id": 209, "name": "Cart", "freebase_id": "/m/018p4k"}, - {"id": 210, "name": "Raven", "freebase_id": "/m/06j2d"}, - {"id": 211, "name": "Egg", "freebase_id": "/m/033cnk"}, - {"id": 212, "name": "Burrito", "freebase_id": "/m/01j3zr"}, - {"id": 213, "name": "Goat", "freebase_id": "/m/03fwl"}, - {"id": 214, "name": "Kitchen knife", "freebase_id": "/m/058qzx"}, - {"id": 215, "name": "Skateboard", "freebase_id": "/m/06_fw"}, - {"id": 216, "name": "Salt and pepper shakers", "freebase_id": "/m/02x8cch"}, - {"id": 217, "name": "Lynx", "freebase_id": "/m/04g2r"}, - {"id": 218, "name": "Boot", "freebase_id": "/m/01b638"}, - {"id": 219, "name": "Platter", "freebase_id": "/m/099ssp"}, - {"id": 220, "name": "Ski", "freebase_id": "/m/071p9"}, - {"id": 221, "name": "Swimwear", "freebase_id": "/m/01gkx_"}, - {"id": 222, "name": "Swimming pool", "freebase_id": "/m/0b_rs"}, - {"id": 223, "name": "Drinking straw", "freebase_id": "/m/03v5tg"}, - {"id": 224, "name": "Wrench", "freebase_id": "/m/01j5ks"}, - {"id": 225, "name": "Drum", "freebase_id": "/m/026t6"}, - {"id": 226, "name": "Ant", "freebase_id": "/m/0_k2"}, - {"id": 227, "name": "Human ear", "freebase_id": "/m/039xj_"}, - {"id": 228, "name": "Headphones", "freebase_id": "/m/01b7fy"}, - {"id": 229, "name": "Fountain", "freebase_id": "/m/0220r2"}, - {"id": 230, "name": "Bird", "freebase_id": "/m/015p6"}, - {"id": 231, "name": "Jeans", "freebase_id": "/m/0fly7"}, - {"id": 232, "name": "Television", "freebase_id": "/m/07c52"}, - {"id": 233, "name": "Crab", "freebase_id": "/m/0n28_"}, - {"id": 234, "name": "Microphone", "freebase_id": "/m/0hg7b"}, - {"id": 235, "name": "Home appliance", "freebase_id": "/m/019dx1"}, - {"id": 236, "name": "Snowplow", "freebase_id": "/m/04vv5k"}, - {"id": 237, "name": "Beetle", "freebase_id": "/m/020jm"}, - {"id": 238, "name": "Artichoke", "freebase_id": "/m/047v4b"}, - {"id": 239, "name": "Jet ski", "freebase_id": "/m/01xs3r"}, - {"id": 240, "name": "Stationary bicycle", "freebase_id": "/m/03kt2w"}, - {"id": 241, "name": "Human hair", "freebase_id": "/m/03q69"}, - {"id": 242, "name": "Brown bear", "freebase_id": "/m/01dxs"}, - {"id": 243, "name": "Starfish", "freebase_id": "/m/01h8tj"}, - {"id": 244, "name": "Fork", "freebase_id": "/m/0dt3t"}, - {"id": 245, "name": "Lobster", "freebase_id": "/m/0cjq5"}, - {"id": 246, "name": "Corded phone", "freebase_id": "/m/0h8lkj8"}, - {"id": 247, "name": "Drink", "freebase_id": "/m/0271t"}, - {"id": 248, "name": "Saucer", "freebase_id": "/m/03q5c7"}, - {"id": 249, "name": "Carrot", "freebase_id": "/m/0fj52s"}, - {"id": 250, "name": "Insect", "freebase_id": "/m/03vt0"}, - {"id": 251, "name": "Clock", "freebase_id": "/m/01x3z"}, - {"id": 252, "name": "Castle", "freebase_id": "/m/0d5gx"}, - {"id": 253, "name": "Tennis racket", "freebase_id": "/m/0h8my_4"}, - {"id": 254, "name": "Ceiling fan", "freebase_id": "/m/03ldnb"}, - {"id": 255, "name": "Asparagus", "freebase_id": "/m/0cjs7"}, - {"id": 256, "name": "Jaguar", "freebase_id": "/m/0449p"}, - {"id": 257, "name": "Musical instrument", "freebase_id": "/m/04szw"}, - {"id": 258, "name": "Train", "freebase_id": "/m/07jdr"}, - {"id": 259, "name": "Cat", "freebase_id": "/m/01yrx"}, - {"id": 260, "name": "Rifle", "freebase_id": "/m/06c54"}, - {"id": 261, "name": "Dumbbell", "freebase_id": "/m/04h8sr"}, - {"id": 262, "name": "Mobile phone", "freebase_id": "/m/050k8"}, - {"id": 263, "name": "Taxi", "freebase_id": "/m/0pg52"}, - {"id": 264, "name": "Shower", "freebase_id": "/m/02f9f_"}, - {"id": 265, "name": "Pitcher", "freebase_id": "/m/054fyh"}, - {"id": 266, "name": "Lemon", "freebase_id": "/m/09k_b"}, - {"id": 267, "name": "Invertebrate", "freebase_id": "/m/03xxp"}, - {"id": 268, "name": "Turkey", "freebase_id": "/m/0jly1"}, - {"id": 269, "name": "High heels", "freebase_id": "/m/06k2mb"}, - {"id": 270, "name": "Bust", "freebase_id": "/m/04yqq2"}, - {"id": 271, "name": "Elephant", "freebase_id": "/m/0bwd_0j"}, - {"id": 272, "name": "Scarf", "freebase_id": "/m/02h19r"}, - {"id": 273, "name": "Barrel", "freebase_id": "/m/02zn6n"}, - {"id": 274, "name": "Trombone", "freebase_id": "/m/07c6l"}, - {"id": 275, "name": "Pumpkin", "freebase_id": "/m/05zsy"}, - {"id": 276, "name": "Box", "freebase_id": "/m/025dyy"}, - {"id": 277, "name": "Tomato", "freebase_id": "/m/07j87"}, - {"id": 278, "name": "Frog", "freebase_id": "/m/09ld4"}, - {"id": 279, "name": "Bidet", "freebase_id": "/m/01vbnl"}, - {"id": 280, "name": "Human face", "freebase_id": "/m/0dzct"}, - {"id": 281, "name": "Houseplant", "freebase_id": "/m/03fp41"}, - {"id": 282, "name": "Van", "freebase_id": "/m/0h2r6"}, - {"id": 283, "name": "Shark", "freebase_id": "/m/0by6g"}, - {"id": 284, "name": "Ice cream", "freebase_id": "/m/0cxn2"}, - {"id": 285, "name": "Swim cap", "freebase_id": "/m/04tn4x"}, - {"id": 286, "name": "Falcon", "freebase_id": "/m/0f6wt"}, - {"id": 287, "name": "Ostrich", "freebase_id": "/m/05n4y"}, - {"id": 288, "name": "Handgun", "freebase_id": "/m/0gxl3"}, - {"id": 289, "name": "Whiteboard", "freebase_id": "/m/02d9qx"}, - {"id": 290, "name": "Lizard", "freebase_id": "/m/04m9y"}, - {"id": 291, "name": "Pasta", "freebase_id": "/m/05z55"}, - {"id": 292, "name": "Snowmobile", "freebase_id": "/m/01x3jk"}, - {"id": 293, "name": "Light bulb", "freebase_id": "/m/0h8l4fh"}, - {"id": 294, "name": "Window blind", "freebase_id": "/m/031b6r"}, - {"id": 295, "name": "Muffin", "freebase_id": "/m/01tcjp"}, - {"id": 296, "name": "Pretzel", "freebase_id": "/m/01f91_"}, - {"id": 297, "name": "Computer monitor", "freebase_id": "/m/02522"}, - {"id": 298, "name": "Horn", "freebase_id": "/m/0319l"}, - {"id": 299, "name": "Furniture", "freebase_id": "/m/0c_jw"}, - {"id": 300, "name": "Sandwich", "freebase_id": "/m/0l515"}, - {"id": 301, "name": "Fox", "freebase_id": "/m/0306r"}, - {"id": 302, "name": "Convenience store", "freebase_id": "/m/0crjs"}, - {"id": 303, "name": "Fish", "freebase_id": "/m/0ch_cf"}, - {"id": 304, "name": "Fruit", "freebase_id": "/m/02xwb"}, - {"id": 305, "name": "Earrings", "freebase_id": "/m/01r546"}, - {"id": 306, "name": "Curtain", "freebase_id": "/m/03rszm"}, - {"id": 307, "name": "Grape", "freebase_id": "/m/0388q"}, - {"id": 308, "name": "Sofa bed", "freebase_id": "/m/03m3pdh"}, - {"id": 309, "name": "Horse", "freebase_id": "/m/03k3r"}, - {"id": 310, "name": "Luggage and bags", "freebase_id": "/m/0hf58v5"}, - {"id": 311, "name": "Desk", "freebase_id": "/m/01y9k5"}, - {"id": 312, "name": "Crutch", "freebase_id": "/m/05441v"}, - {"id": 313, "name": "Bicycle helmet", "freebase_id": "/m/03p3bw"}, - {"id": 314, "name": "Tick", "freebase_id": "/m/0175cv"}, - {"id": 315, "name": "Airplane", "freebase_id": "/m/0cmf2"}, - {"id": 316, "name": "Canary", "freebase_id": "/m/0ccs93"}, - {"id": 317, "name": "Spatula", "freebase_id": "/m/02d1br"}, - {"id": 318, "name": "Watch", "freebase_id": "/m/0gjkl"}, - {"id": 319, "name": "Lily", "freebase_id": "/m/0jqgx"}, - {"id": 320, "name": "Kitchen appliance", "freebase_id": "/m/0h99cwc"}, - {"id": 321, "name": "Filing cabinet", "freebase_id": "/m/047j0r"}, - {"id": 322, "name": "Aircraft", "freebase_id": "/m/0k5j"}, - {"id": 323, "name": "Cake stand", "freebase_id": "/m/0h8n6ft"}, - {"id": 324, "name": "Candy", "freebase_id": "/m/0gm28"}, - {"id": 325, "name": "Sink", "freebase_id": "/m/0130jx"}, - {"id": 326, "name": "Mouse", "freebase_id": "/m/04rmv"}, - {"id": 327, "name": "Wine", "freebase_id": "/m/081qc"}, - {"id": 328, "name": "Wheelchair", "freebase_id": "/m/0qmmr"}, - {"id": 329, "name": "Goldfish", "freebase_id": "/m/03fj2"}, - {"id": 330, "name": "Refrigerator", "freebase_id": "/m/040b_t"}, - {"id": 331, "name": "French fries", "freebase_id": "/m/02y6n"}, - {"id": 332, "name": "Drawer", "freebase_id": "/m/0fqfqc"}, - {"id": 333, "name": "Treadmill", "freebase_id": "/m/030610"}, - {"id": 334, "name": "Picnic basket", "freebase_id": "/m/07kng9"}, - {"id": 335, "name": "Dice", "freebase_id": "/m/029b3"}, - {"id": 336, "name": "Cabbage", "freebase_id": "/m/0fbw6"}, - {"id": 337, "name": "Football helmet", "freebase_id": "/m/07qxg_"}, - {"id": 338, "name": "Pig", "freebase_id": "/m/068zj"}, - {"id": 339, "name": "Person", "freebase_id": "/m/01g317"}, - {"id": 340, "name": "Shorts", "freebase_id": "/m/01bfm9"}, - {"id": 341, "name": "Gondola", "freebase_id": "/m/02068x"}, - {"id": 342, "name": "Honeycomb", "freebase_id": "/m/0fz0h"}, - {"id": 343, "name": "Doughnut", "freebase_id": "/m/0jy4k"}, - {"id": 344, "name": "Chest of drawers", "freebase_id": "/m/05kyg_"}, - {"id": 345, "name": "Land vehicle", "freebase_id": "/m/01prls"}, - {"id": 346, "name": "Bat", "freebase_id": "/m/01h44"}, - {"id": 347, "name": "Monkey", "freebase_id": "/m/08pbxl"}, - {"id": 348, "name": "Dagger", "freebase_id": "/m/02gzp"}, - {"id": 349, "name": "Tableware", "freebase_id": "/m/04brg2"}, - {"id": 350, "name": "Human foot", "freebase_id": "/m/031n1"}, - {"id": 351, "name": "Mug", "freebase_id": "/m/02jvh9"}, - {"id": 352, "name": "Alarm clock", "freebase_id": "/m/046dlr"}, - {"id": 353, "name": "Pressure cooker", "freebase_id": "/m/0h8ntjv"}, - {"id": 354, "name": "Human hand", "freebase_id": "/m/0k65p"}, - {"id": 355, "name": "Tortoise", "freebase_id": "/m/011k07"}, - {"id": 356, "name": "Baseball glove", "freebase_id": "/m/03grzl"}, - {"id": 357, "name": "Sword", "freebase_id": "/m/06y5r"}, - {"id": 358, "name": "Pear", "freebase_id": "/m/061_f"}, - {"id": 359, "name": "Miniskirt", "freebase_id": "/m/01cmb2"}, - {"id": 360, "name": "Traffic sign", "freebase_id": "/m/01mqdt"}, - {"id": 361, "name": "Girl", "freebase_id": "/m/05r655"}, - {"id": 362, "name": "Roller skates", "freebase_id": "/m/02p3w7d"}, - {"id": 363, "name": "Dinosaur", "freebase_id": "/m/029tx"}, - {"id": 364, "name": "Porch", "freebase_id": "/m/04m6gz"}, - {"id": 365, "name": "Human beard", "freebase_id": "/m/015h_t"}, - {"id": 366, "name": "Submarine sandwich", "freebase_id": "/m/06pcq"}, - {"id": 367, "name": "Screwdriver", "freebase_id": "/m/01bms0"}, - {"id": 368, "name": "Strawberry", "freebase_id": "/m/07fbm7"}, - {"id": 369, "name": "Wine glass", "freebase_id": "/m/09tvcd"}, - {"id": 370, "name": "Seafood", "freebase_id": "/m/06nwz"}, - {"id": 371, "name": "Racket", "freebase_id": "/m/0dv9c"}, - {"id": 372, "name": "Wheel", "freebase_id": "/m/083wq"}, - {"id": 373, "name": "Sea lion", "freebase_id": "/m/0gd36"}, - {"id": 374, "name": "Toy", "freebase_id": "/m/0138tl"}, - {"id": 375, "name": "Tea", "freebase_id": "/m/07clx"}, - {"id": 376, "name": "Tennis ball", "freebase_id": "/m/05ctyq"}, - {"id": 377, "name": "Waste container", "freebase_id": "/m/0bjyj5"}, - {"id": 378, "name": "Mule", "freebase_id": "/m/0dbzx"}, - {"id": 379, "name": "Cricket ball", "freebase_id": "/m/02ctlc"}, - {"id": 380, "name": "Pineapple", "freebase_id": "/m/0fp6w"}, - {"id": 381, "name": "Coconut", "freebase_id": "/m/0djtd"}, - {"id": 382, "name": "Doll", "freebase_id": "/m/0167gd"}, - {"id": 383, "name": "Coffee table", "freebase_id": "/m/078n6m"}, - {"id": 384, "name": "Snowman", "freebase_id": "/m/0152hh"}, - {"id": 385, "name": "Lavender", "freebase_id": "/m/04gth"}, - {"id": 386, "name": "Shrimp", "freebase_id": "/m/0ll1f78"}, - {"id": 387, "name": "Maple", "freebase_id": "/m/0cffdh"}, - {"id": 388, "name": "Cowboy hat", "freebase_id": "/m/025rp__"}, - {"id": 389, "name": "Goggles", "freebase_id": "/m/02_n6y"}, - {"id": 390, "name": "Rugby ball", "freebase_id": "/m/0wdt60w"}, - {"id": 391, "name": "Caterpillar", "freebase_id": "/m/0cydv"}, - {"id": 392, "name": "Poster", "freebase_id": "/m/01n5jq"}, - {"id": 393, "name": "Rocket", "freebase_id": "/m/09rvcxw"}, - {"id": 394, "name": "Organ", "freebase_id": "/m/013y1f"}, - {"id": 395, "name": "Saxophone", "freebase_id": "/m/06ncr"}, - {"id": 396, "name": "Traffic light", "freebase_id": "/m/015qff"}, - {"id": 397, "name": "Cocktail", "freebase_id": "/m/024g6"}, - {"id": 398, "name": "Plastic bag", "freebase_id": "/m/05gqfk"}, - {"id": 399, "name": "Squash", "freebase_id": "/m/0dv77"}, - {"id": 400, "name": "Mushroom", "freebase_id": "/m/052sf"}, - {"id": 401, "name": "Hamburger", "freebase_id": "/m/0cdn1"}, - {"id": 402, "name": "Light switch", "freebase_id": "/m/03jbxj"}, - {"id": 403, "name": "Parachute", "freebase_id": "/m/0cyfs"}, - {"id": 404, "name": "Teddy bear", "freebase_id": "/m/0kmg4"}, - {"id": 405, "name": "Winter melon", "freebase_id": "/m/02cvgx"}, - {"id": 406, "name": "Deer", "freebase_id": "/m/09kx5"}, - {"id": 407, "name": "Musical keyboard", "freebase_id": "/m/057cc"}, - {"id": 408, "name": "Plumbing fixture", "freebase_id": "/m/02pkr5"}, - {"id": 409, "name": "Scoreboard", "freebase_id": "/m/057p5t"}, - {"id": 410, "name": "Baseball bat", "freebase_id": "/m/03g8mr"}, - {"id": 411, "name": "Envelope", "freebase_id": "/m/0frqm"}, - {"id": 412, "name": "Adhesive tape", "freebase_id": "/m/03m3vtv"}, - {"id": 413, "name": "Briefcase", "freebase_id": "/m/0584n8"}, - {"id": 414, "name": "Paddle", "freebase_id": "/m/014y4n"}, - {"id": 415, "name": "Bow and arrow", "freebase_id": "/m/01g3x7"}, - {"id": 416, "name": "Telephone", "freebase_id": "/m/07cx4"}, - {"id": 417, "name": "Sheep", "freebase_id": "/m/07bgp"}, - {"id": 418, "name": "Jacket", "freebase_id": "/m/032b3c"}, - {"id": 419, "name": "Boy", "freebase_id": "/m/01bl7v"}, - {"id": 420, "name": "Pizza", "freebase_id": "/m/0663v"}, - {"id": 421, "name": "Otter", "freebase_id": "/m/0cn6p"}, - {"id": 422, "name": "Office supplies", "freebase_id": "/m/02rdsp"}, - {"id": 423, "name": "Couch", "freebase_id": "/m/02crq1"}, - {"id": 424, "name": "Cello", "freebase_id": "/m/01xqw"}, - {"id": 425, "name": "Bull", "freebase_id": "/m/0cnyhnx"}, - {"id": 426, "name": "Camel", "freebase_id": "/m/01x_v"}, - {"id": 427, "name": "Ball", "freebase_id": "/m/018xm"}, - {"id": 428, "name": "Duck", "freebase_id": "/m/09ddx"}, - {"id": 429, "name": "Whale", "freebase_id": "/m/084zz"}, - {"id": 430, "name": "Shirt", "freebase_id": "/m/01n4qj"}, - {"id": 431, "name": "Tank", "freebase_id": "/m/07cmd"}, - {"id": 432, "name": "Motorcycle", "freebase_id": "/m/04_sv"}, - {"id": 433, "name": "Accordion", "freebase_id": "/m/0mkg"}, - {"id": 434, "name": "Owl", "freebase_id": "/m/09d5_"}, - {"id": 435, "name": "Porcupine", "freebase_id": "/m/0c568"}, - {"id": 436, "name": "Sun hat", "freebase_id": "/m/02wbtzl"}, - {"id": 437, "name": "Nail", "freebase_id": "/m/05bm6"}, - {"id": 438, "name": "Scissors", "freebase_id": "/m/01lsmm"}, - {"id": 439, "name": "Swan", "freebase_id": "/m/0dftk"}, - {"id": 440, "name": "Lamp", "freebase_id": "/m/0dtln"}, - {"id": 441, "name": "Crown", "freebase_id": "/m/0nl46"}, - {"id": 442, "name": "Piano", "freebase_id": "/m/05r5c"}, - {"id": 443, "name": "Sculpture", "freebase_id": "/m/06msq"}, - {"id": 444, "name": "Cheetah", "freebase_id": "/m/0cd4d"}, - {"id": 445, "name": "Oboe", "freebase_id": "/m/05kms"}, - {"id": 446, "name": "Tin can", "freebase_id": "/m/02jnhm"}, - {"id": 447, "name": "Mango", "freebase_id": "/m/0fldg"}, - {"id": 448, "name": "Tripod", "freebase_id": "/m/073bxn"}, - {"id": 449, "name": "Oven", "freebase_id": "/m/029bxz"}, - {"id": 450, "name": "Mouse", "freebase_id": "/m/020lf"}, - {"id": 451, "name": "Barge", "freebase_id": "/m/01btn"}, - {"id": 452, "name": "Coffee", "freebase_id": "/m/02vqfm"}, - {"id": 453, "name": "Snowboard", "freebase_id": "/m/06__v"}, - {"id": 454, "name": "Common fig", "freebase_id": "/m/043nyj"}, - {"id": 455, "name": "Salad", "freebase_id": "/m/0grw1"}, - {"id": 456, "name": "Marine invertebrates", "freebase_id": "/m/03hl4l9"}, - {"id": 457, "name": "Umbrella", "freebase_id": "/m/0hnnb"}, - {"id": 458, "name": "Kangaroo", "freebase_id": "/m/04c0y"}, - {"id": 459, "name": "Human arm", "freebase_id": "/m/0dzf4"}, - {"id": 460, "name": "Measuring cup", "freebase_id": "/m/07v9_z"}, - {"id": 461, "name": "Snail", "freebase_id": "/m/0f9_l"}, - {"id": 462, "name": "Loveseat", "freebase_id": "/m/0703r8"}, - {"id": 463, "name": "Suit", "freebase_id": "/m/01xyhv"}, - {"id": 464, "name": "Teapot", "freebase_id": "/m/01fh4r"}, - {"id": 465, "name": "Bottle", "freebase_id": "/m/04dr76w"}, - {"id": 466, "name": "Alpaca", "freebase_id": "/m/0pcr"}, - {"id": 467, "name": "Kettle", "freebase_id": "/m/03s_tn"}, - {"id": 468, "name": "Trousers", "freebase_id": "/m/07mhn"}, - {"id": 469, "name": "Popcorn", "freebase_id": "/m/01hrv5"}, - {"id": 470, "name": "Centipede", "freebase_id": "/m/019h78"}, - {"id": 471, "name": "Spider", "freebase_id": "/m/09kmb"}, - {"id": 472, "name": "Sparrow", "freebase_id": "/m/0h23m"}, - {"id": 473, "name": "Plate", "freebase_id": "/m/050gv4"}, - {"id": 474, "name": "Bagel", "freebase_id": "/m/01fb_0"}, - {"id": 475, "name": "Personal care", "freebase_id": "/m/02w3_ws"}, - {"id": 476, "name": "Apple", "freebase_id": "/m/014j1m"}, - {"id": 477, "name": "Brassiere", "freebase_id": "/m/01gmv2"}, - {"id": 478, "name": "Bathroom cabinet", "freebase_id": "/m/04y4h8h"}, - {"id": 479, "name": "studio couch", "freebase_id": "/m/026qbn5"}, - {"id": 480, "name": "Computer keyboard", "freebase_id": "/m/01m2v"}, - {"id": 481, "name": "Table tennis racket", "freebase_id": "/m/05_5p_0"}, - {"id": 482, "name": "Sushi", "freebase_id": "/m/07030"}, - {"id": 483, "name": "Cabinetry", "freebase_id": "/m/01s105"}, - {"id": 484, "name": "Street light", "freebase_id": "/m/033rq4"}, - {"id": 485, "name": "Towel", "freebase_id": "/m/0162_1"}, - {"id": 486, "name": "Nightstand", "freebase_id": "/m/02z51p"}, - {"id": 487, "name": "Rabbit", "freebase_id": "/m/06mf6"}, - {"id": 488, "name": "Dolphin", "freebase_id": "/m/02hj4"}, - {"id": 489, "name": "Dog", "freebase_id": "/m/0bt9lr"}, - {"id": 490, "name": "Jug", "freebase_id": "/m/08hvt4"}, - {"id": 491, "name": "Wok", "freebase_id": "/m/084rd"}, - {"id": 492, "name": "Fire hydrant", "freebase_id": "/m/01pns0"}, - {"id": 493, "name": "Human eye", "freebase_id": "/m/014sv8"}, - {"id": 494, "name": "Skyscraper", "freebase_id": "/m/079cl"}, - {"id": 495, "name": "Backpack", "freebase_id": "/m/01940j"}, - {"id": 496, "name": "Potato", "freebase_id": "/m/05vtc"}, - {"id": 497, "name": "Paper towel", "freebase_id": "/m/02w3r3"}, - {"id": 498, "name": "Lifejacket", "freebase_id": "/m/054xkw"}, - {"id": 499, "name": "Bicycle wheel", "freebase_id": "/m/01bqk0"}, - {"id": 500, "name": "Toilet", "freebase_id": "/m/09g1w"}, -] - - -def _get_builtin_metadata(cats): - {x["id"]: x["name"] for x in cats} - thing_dataset_id_to_contiguous_id = {i + 1: i for i in range(len(cats))} - thing_classes = [x["name"] for x in sorted(cats, key=lambda x: x["id"])] - return { - "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes, - } - - -_PREDEFINED_SPLITS_OID = { - # cat threshold: 500, 1500: r 170, c 151, f 179 - "oid_train": ("oid/images/", "oid/annotations/oid_challenge_2019_train_bbox.json"), - # "expanded" duplicates annotations to their father classes based on the official - # hierarchy. This is used in the official evaulation protocol. - # https://storage.googleapis.com/openimages/web/evaluation.html - "oid_val_expanded": ( - "oid/images/validation/", - "oid/annotations/oid_challenge_2019_val_expanded.json", - ), - "oid_val_expanded_rare": ( - "oid/images/validation/", - "oid/annotations/oid_challenge_2019_val_expanded_rare.json", - ), -} - - -for key, (image_root, json_file) in _PREDEFINED_SPLITS_OID.items(): - register_oid_instances( - key, - _get_builtin_metadata(categories), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/detic/data/datasets/register_oid.py b/dimos/models/Detic/detic/data/datasets/register_oid.py deleted file mode 100644 index 0739556041..0000000000 --- a/dimos/models/Detic/detic/data/datasets/register_oid.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -# Modified by Xingyi Zhou from https://github.com/facebookresearch/detectron2/blob/master/detectron2/data/datasets/coco.py -import contextlib -import io -import logging -import os - -from detectron2.data import DatasetCatalog, MetadataCatalog -from detectron2.structures import BoxMode -from fvcore.common.file_io import PathManager -from fvcore.common.timer import Timer -from typing import Optional - -logger = logging.getLogger(__name__) - -""" -This file contains functions to register a COCO-format dataset to the DatasetCatalog. -""" - -__all__ = ["register_coco_instances", "register_coco_panoptic_separated"] - - -def register_oid_instances(name: str, metadata, json_file, image_root) -> None: - """ """ - # 1. register a function which returns dicts - DatasetCatalog.register(name, lambda: load_coco_json_mem_efficient(json_file, image_root, name)) - - # 2. Optionally, add metadata about this dataset, - # since they might be useful in evaluation, visualization or logging - MetadataCatalog.get(name).set( - json_file=json_file, image_root=image_root, evaluator_type="oid", **metadata - ) - - -def load_coco_json_mem_efficient( - json_file, image_root, dataset_name: Optional[str]=None, extra_annotation_keys=None -): - """ - Actually not mem efficient - """ - from pycocotools.coco import COCO - - timer = Timer() - json_file = PathManager.get_local_path(json_file) - with contextlib.redirect_stdout(io.StringIO()): - coco_api = COCO(json_file) - if timer.seconds() > 1: - logger.info(f"Loading {json_file} takes {timer.seconds():.2f} seconds.") - - id_map = None - if dataset_name is not None: - meta = MetadataCatalog.get(dataset_name) - cat_ids = sorted(coco_api.getCatIds()) - cats = coco_api.loadCats(cat_ids) - # The categories in a custom json file may not be sorted. - thing_classes = [c["name"] for c in sorted(cats, key=lambda x: x["id"])] - meta.thing_classes = thing_classes - - if not (min(cat_ids) == 1 and max(cat_ids) == len(cat_ids)): - if "coco" not in dataset_name: - logger.warning( - """ - Category ids in annotations are not in [1, #categories]! We'll apply a mapping for you. - """ - ) - id_map = {v: i for i, v in enumerate(cat_ids)} - meta.thing_dataset_id_to_contiguous_id = id_map - - # sort indices for reproducible results - img_ids = sorted(coco_api.imgs.keys()) - imgs = coco_api.loadImgs(img_ids) - logger.info(f"Loaded {len(imgs)} images in COCO format from {json_file}") - - dataset_dicts = [] - - ann_keys = ["iscrowd", "bbox", "category_id"] + (extra_annotation_keys or []) - - for img_dict in imgs: - record = {} - record["file_name"] = os.path.join(image_root, img_dict["file_name"]) - record["height"] = img_dict["height"] - record["width"] = img_dict["width"] - image_id = record["image_id"] = img_dict["id"] - anno_dict_list = coco_api.imgToAnns[image_id] - if "neg_category_ids" in img_dict: - record["neg_category_ids"] = [id_map[x] for x in img_dict["neg_category_ids"]] - - objs = [] - for anno in anno_dict_list: - assert anno["image_id"] == image_id - - assert anno.get("ignore", 0) == 0 - - obj = {key: anno[key] for key in ann_keys if key in anno} - - segm = anno.get("segmentation", None) - if segm: # either list[list[float]] or dict(RLE) - if not isinstance(segm, dict): - # filter out invalid polygons (< 3 points) - segm = [poly for poly in segm if len(poly) % 2 == 0 and len(poly) >= 6] - if len(segm) == 0: - num_instances_without_valid_segmentation += 1 - continue # ignore this instance - obj["segmentation"] = segm - - obj["bbox_mode"] = BoxMode.XYWH_ABS - - if id_map: - obj["category_id"] = id_map[obj["category_id"]] - objs.append(obj) - record["annotations"] = objs - dataset_dicts.append(record) - - del coco_api - return dataset_dicts diff --git a/dimos/models/Detic/detic/data/tar_dataset.py b/dimos/models/Detic/detic/data/tar_dataset.py deleted file mode 100644 index 8c87a056d1..0000000000 --- a/dimos/models/Detic/detic/data/tar_dataset.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Facebook, Inc. and its affiliates. -import gzip -import io -import os - -import numpy as np -from PIL import Image -from torch.utils.data import Dataset - -try: - from PIL import UnidentifiedImageError - - unidentified_error_available = True -except ImportError: - # UnidentifiedImageError isn't available in older versions of PIL - unidentified_error_available = False - - -class DiskTarDataset(Dataset): - def __init__( - self, - tarfile_path: str="dataset/imagenet/ImageNet-21k/metadata/tar_files.npy", - tar_index_dir: str="dataset/imagenet/ImageNet-21k/metadata/tarindex_npy", - preload: bool=False, - num_synsets: str="all", - ) -> None: - """ - - preload (bool): Recommend to set preload to False when using - - num_synsets (integer or string "all"): set to small number for debugging - will load subset of dataset - """ - tar_files = np.load(tarfile_path) - - chunk_datasets = [] - dataset_lens = [] - if isinstance(num_synsets, int): - assert num_synsets < len(tar_files) - tar_files = tar_files[:num_synsets] - for tar_file in tar_files: - dataset = _TarDataset(tar_file, tar_index_dir, preload=preload) - chunk_datasets.append(dataset) - dataset_lens.append(len(dataset)) - - self.chunk_datasets = chunk_datasets - self.dataset_lens = np.array(dataset_lens).astype(np.int32) - self.dataset_cumsums = np.cumsum(self.dataset_lens) - self.num_samples = sum(self.dataset_lens) - labels = np.zeros(self.dataset_lens.sum(), dtype=np.int64) - sI = 0 - for k in range(len(self.dataset_lens)): - assert (sI + self.dataset_lens[k]) <= len(labels), ( - f"{k} {sI + self.dataset_lens[k]} vs. {len(labels)}" - ) - labels[sI : (sI + self.dataset_lens[k])] = k - sI += self.dataset_lens[k] - self.labels = labels - - def __len__(self) -> int: - return self.num_samples - - def __getitem__(self, index): - assert index >= 0 and index < len(self) - # find the dataset file we need to go to - d_index = np.searchsorted(self.dataset_cumsums, index) - - # edge case, if index is at edge of chunks, move right - if index in self.dataset_cumsums: - d_index += 1 - - assert d_index == self.labels[index], ( - f"{d_index} vs. {self.labels[index]} mismatch for {index}" - ) - - # change index to local dataset index - if d_index == 0: - local_index = index - else: - local_index = index - self.dataset_cumsums[d_index - 1] - data_bytes = self.chunk_datasets[d_index][local_index] - exception_to_catch = UnidentifiedImageError if unidentified_error_available else Exception - try: - image = Image.open(data_bytes).convert("RGB") - except exception_to_catch: - image = Image.fromarray(np.ones((224, 224, 3), dtype=np.uint8) * 128) - d_index = -1 - - # label is the dataset (synset) we indexed into - return image, d_index, index - - def __repr__(self) -> str: - st = f"DiskTarDataset(subdatasets={len(self.dataset_lens)},samples={self.num_samples})" - return st - - -class _TarDataset: - def __init__(self, filename, npy_index_dir, preload: bool=False) -> None: - # translated from - # fbcode/experimental/deeplearning/matthijs/comp_descs/tardataset.lua - self.filename = filename - self.names = [] - self.offsets = [] - self.npy_index_dir = npy_index_dir - names, offsets = self.load_index() - - self.num_samples = len(names) - if preload: - self.data = np.memmap(filename, mode="r", dtype="uint8") - self.offsets = offsets - else: - self.data = None - - def __len__(self) -> int: - return self.num_samples - - def load_index(self): - basename = os.path.basename(self.filename) - basename = os.path.splitext(basename)[0] - names = np.load(os.path.join(self.npy_index_dir, f"{basename}_names.npy")) - offsets = np.load(os.path.join(self.npy_index_dir, f"{basename}_offsets.npy")) - return names, offsets - - def __getitem__(self, idx: int): - if self.data is None: - self.data = np.memmap(self.filename, mode="r", dtype="uint8") - _, self.offsets = self.load_index() - - ofs = self.offsets[idx] * 512 - fsize = 512 * (self.offsets[idx + 1] - self.offsets[idx]) - data = self.data[ofs : ofs + fsize] - - if data[:13].tostring() == "././@LongLink": - data = data[3 * 512 :] - else: - data = data[512:] - - # just to make it more fun a few JPEGs are GZIP compressed... - # catch this case - if tuple(data[:2]) == (0x1F, 0x8B): - s = io.BytesIO(data.tostring()) - g = gzip.GzipFile(None, "r", 0, s) - sdata = g.read() - else: - sdata = data.tostring() - return io.BytesIO(sdata) diff --git a/dimos/models/Detic/detic/data/transforms/custom_augmentation_impl.py b/dimos/models/Detic/detic/data/transforms/custom_augmentation_impl.py deleted file mode 100644 index 7cabc91e0f..0000000000 --- a/dimos/models/Detic/detic/data/transforms/custom_augmentation_impl.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# Part of the code is from https://github.com/rwightman/efficientdet-pytorch/blob/master/effdet/data/transforms.py -# Modified by Xingyi Zhou -# The original code is under Apache-2.0 License -from detectron2.data.transforms.augmentation import Augmentation -import numpy as np -from PIL import Image - -from .custom_transform import EfficientDetResizeCropTransform - -__all__ = [ - "EfficientDetResizeCrop", -] - - -class EfficientDetResizeCrop(Augmentation): - """ - Scale the shorter edge to the given size, with a limit of `max_size` on the longer edge. - If `max_size` is reached, then downscale so that the longer edge does not exceed max_size. - """ - - def __init__(self, size: int, scale, interp=Image.BILINEAR) -> None: - """ """ - super().__init__() - self.target_size = (size, size) - self.scale = scale - self.interp = interp - - def get_transform(self, img): - # Select a random scale factor. - scale_factor = np.random.uniform(*self.scale) - scaled_target_height = scale_factor * self.target_size[0] - scaled_target_width = scale_factor * self.target_size[1] - # Recompute the accurate scale_factor using rounded scaled image size. - width, height = img.shape[1], img.shape[0] - img_scale_y = scaled_target_height / height - img_scale_x = scaled_target_width / width - img_scale = min(img_scale_y, img_scale_x) - - # Select non-zero random offset (x, y) if scaled image is larger than target size - scaled_h = int(height * img_scale) - scaled_w = int(width * img_scale) - offset_y = scaled_h - self.target_size[0] - offset_x = scaled_w - self.target_size[1] - offset_y = int(max(0.0, float(offset_y)) * np.random.uniform(0, 1)) - offset_x = int(max(0.0, float(offset_x)) * np.random.uniform(0, 1)) - return EfficientDetResizeCropTransform( - scaled_h, scaled_w, offset_y, offset_x, img_scale, self.target_size, self.interp - ) diff --git a/dimos/models/Detic/detic/data/transforms/custom_transform.py b/dimos/models/Detic/detic/data/transforms/custom_transform.py deleted file mode 100644 index 2017c27a5f..0000000000 --- a/dimos/models/Detic/detic/data/transforms/custom_transform.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# Part of the code is from https://github.com/rwightman/efficientdet-pytorch/blob/master/effdet/data/transforms.py -# Modified by Xingyi Zhou -# The original code is under Apache-2.0 License -from fvcore.transforms.transform import ( - Transform, -) -import numpy as np -from PIL import Image -import torch -import torch.nn.functional as F - -try: - import cv2 -except ImportError: - # OpenCV is an optional dependency at the moment - pass - -__all__ = [ - "EfficientDetResizeCropTransform", -] - - -class EfficientDetResizeCropTransform(Transform): - """ """ - - def __init__(self, scaled_h, scaled_w, offset_y, offset_x, img_scale, target_size: int, interp=None) -> None: - """ - Args: - h, w (int): original image size - new_h, new_w (int): new image size - interp: PIL interpolation methods, defaults to bilinear. - """ - # TODO decide on PIL vs opencv - super().__init__() - if interp is None: - interp = Image.BILINEAR - self._set_attributes(locals()) - - def apply_image(self, img, interp=None): - assert len(img.shape) <= 4 - - if img.dtype == np.uint8: - pil_image = Image.fromarray(img) - interp_method = interp if interp is not None else self.interp - pil_image = pil_image.resize((self.scaled_w, self.scaled_h), interp_method) - ret = np.asarray(pil_image) - right = min(self.scaled_w, self.offset_x + self.target_size[1]) - lower = min(self.scaled_h, self.offset_y + self.target_size[0]) - if len(ret.shape) <= 3: - ret = ret[self.offset_y : lower, self.offset_x : right] - else: - ret = ret[..., self.offset_y : lower, self.offset_x : right, :] - else: - # PIL only supports uint8 - img = torch.from_numpy(img) - shape = list(img.shape) - shape_4d = shape[:2] + [1] * (4 - len(shape)) + shape[2:] - img = img.view(shape_4d).permute(2, 3, 0, 1) # hw(c) -> nchw - _PIL_RESIZE_TO_INTERPOLATE_MODE = {Image.BILINEAR: "bilinear", Image.BICUBIC: "bicubic"} - mode = _PIL_RESIZE_TO_INTERPOLATE_MODE[self.interp] - img = F.interpolate(img, (self.scaled_h, self.scaled_w), mode=mode, align_corners=False) - shape[:2] = (self.scaled_h, self.scaled_w) - ret = img.permute(2, 3, 0, 1).view(shape).numpy() # nchw -> hw(c) - right = min(self.scaled_w, self.offset_x + self.target_size[1]) - lower = min(self.scaled_h, self.offset_y + self.target_size[0]) - if len(ret.shape) <= 3: - ret = ret[self.offset_y : lower, self.offset_x : right] - else: - ret = ret[..., self.offset_y : lower, self.offset_x : right, :] - return ret - - def apply_coords(self, coords): - coords[:, 0] = coords[:, 0] * self.img_scale - coords[:, 1] = coords[:, 1] * self.img_scale - coords[:, 0] -= self.offset_x - coords[:, 1] -= self.offset_y - return coords - - def apply_segmentation(self, segmentation): - segmentation = self.apply_image(segmentation, interp=Image.NEAREST) - return segmentation - - def inverse(self): - raise NotImplementedError - - def inverse_apply_coords(self, coords): - coords[:, 0] += self.offset_x - coords[:, 1] += self.offset_y - coords[:, 0] = coords[:, 0] / self.img_scale - coords[:, 1] = coords[:, 1] / self.img_scale - return coords - - def inverse_apply_box(self, box: np.ndarray) -> np.ndarray: - """ """ - idxs = np.array([(0, 1), (2, 1), (0, 3), (2, 3)]).flatten() - coords = np.asarray(box).reshape(-1, 4)[:, idxs].reshape(-1, 2) - coords = self.inverse_apply_coords(coords).reshape((-1, 4, 2)) - minxy = coords.min(axis=1) - maxxy = coords.max(axis=1) - trans_boxes = np.concatenate((minxy, maxxy), axis=1) - return trans_boxes diff --git a/dimos/models/Detic/detic/evaluation/custom_coco_eval.py b/dimos/models/Detic/detic/evaluation/custom_coco_eval.py deleted file mode 100644 index 759d885f00..0000000000 --- a/dimos/models/Detic/detic/evaluation/custom_coco_eval.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import itertools - -from detectron2.evaluation.coco_evaluation import COCOEvaluator -from detectron2.utils.logger import create_small_table -import numpy as np -from tabulate import tabulate - -from ..data.datasets.coco_zeroshot import categories_seen, categories_unseen -from typing import Optional, Sequence - - -class CustomCOCOEvaluator(COCOEvaluator): - def _derive_coco_results(self, coco_eval, iou_type, class_names: Optional[Sequence[str]]=None): - """ - Additionally plot mAP for 'seen classes' and 'unseen classes' - """ - - metrics = { - "bbox": ["AP", "AP50", "AP75", "APs", "APm", "APl"], - "segm": ["AP", "AP50", "AP75", "APs", "APm", "APl"], - "keypoints": ["AP", "AP50", "AP75", "APm", "APl"], - }[iou_type] - - if coco_eval is None: - self._logger.warn("No predictions from the model!") - return {metric: float("nan") for metric in metrics} - - # the standard metrics - results = { - metric: float(coco_eval.stats[idx] * 100 if coco_eval.stats[idx] >= 0 else "nan") - for idx, metric in enumerate(metrics) - } - self._logger.info( - f"Evaluation results for {iou_type}: \n" + create_small_table(results) - ) - if not np.isfinite(sum(results.values())): - self._logger.info("Some metrics cannot be computed and is shown as NaN.") - - if class_names is None or len(class_names) <= 1: - return results - # Compute per-category AP - # from https://github.com/facebookresearch/Detectron/blob/a6a835f5b8208c45d0dce217ce9bbda915f44df7/detectron/datasets/json_dataset_evaluator.py#L222-L252 - precisions = coco_eval.eval["precision"] - # precision has dims (iou, recall, cls, area range, max dets) - assert len(class_names) == precisions.shape[2] - - seen_names = set([x["name"] for x in categories_seen]) - unseen_names = set([x["name"] for x in categories_unseen]) - results_per_category = [] - results_per_category50 = [] - results_per_category50_seen = [] - results_per_category50_unseen = [] - for idx, name in enumerate(class_names): - # area range index 0: all area ranges - # max dets index -1: typically 100 per image - precision = precisions[:, :, idx, 0, -1] - precision = precision[precision > -1] - ap = np.mean(precision) if precision.size else float("nan") - results_per_category.append((f"{name}", float(ap * 100))) - precision50 = precisions[0, :, idx, 0, -1] - precision50 = precision50[precision50 > -1] - ap50 = np.mean(precision50) if precision50.size else float("nan") - results_per_category50.append((f"{name}", float(ap50 * 100))) - if name in seen_names: - results_per_category50_seen.append(float(ap50 * 100)) - if name in unseen_names: - results_per_category50_unseen.append(float(ap50 * 100)) - - # tabulate it - N_COLS = min(6, len(results_per_category) * 2) - results_flatten = list(itertools.chain(*results_per_category)) - results_2d = itertools.zip_longest(*[results_flatten[i::N_COLS] for i in range(N_COLS)]) - table = tabulate( - results_2d, - tablefmt="pipe", - floatfmt=".3f", - headers=["category", "AP"] * (N_COLS // 2), - numalign="left", - ) - self._logger.info(f"Per-category {iou_type} AP: \n" + table) - - N_COLS = min(6, len(results_per_category50) * 2) - results_flatten = list(itertools.chain(*results_per_category50)) - results_2d = itertools.zip_longest(*[results_flatten[i::N_COLS] for i in range(N_COLS)]) - table = tabulate( - results_2d, - tablefmt="pipe", - floatfmt=".3f", - headers=["category", "AP50"] * (N_COLS // 2), - numalign="left", - ) - self._logger.info(f"Per-category {iou_type} AP50: \n" + table) - self._logger.info( - f"Seen {iou_type} AP50: {sum(results_per_category50_seen) / len(results_per_category50_seen)}" - ) - self._logger.info( - f"Unseen {iou_type} AP50: {sum(results_per_category50_unseen) / len(results_per_category50_unseen)}" - ) - - results.update({"AP-" + name: ap for name, ap in results_per_category}) - results["AP50-seen"] = sum(results_per_category50_seen) / len(results_per_category50_seen) - results["AP50-unseen"] = sum(results_per_category50_unseen) / len( - results_per_category50_unseen - ) - return results diff --git a/dimos/models/Detic/detic/evaluation/oideval.py b/dimos/models/Detic/detic/evaluation/oideval.py deleted file mode 100644 index aa5a954aef..0000000000 --- a/dimos/models/Detic/detic/evaluation/oideval.py +++ /dev/null @@ -1,683 +0,0 @@ -# Part of the code is from https://github.com/tensorflow/models/blob/master/research/object_detection/metrics/oid_challenge_evaluation.py -# Copyright 2018 The TensorFlow Authors. All Rights Reserved. -# The original code is under Apache License, Version 2.0 (the "License"); -# Part of the code is from https://github.com/lvis-dataset/lvis-api/blob/master/lvis/eval.py -# Copyright (c) 2019, Agrim Gupta and Ross Girshick -# Modified by Xingyi Zhou -# This script re-implement OpenImages evaluation in detectron2 -# The code is from https://github.com/xingyizhou/UniDet/blob/master/projects/UniDet/unidet/evaluation/oideval.py -# The original code is under Apache-2.0 License -# Copyright (c) Facebook, Inc. and its affiliates. -from collections import OrderedDict, defaultdict -import copy -import datetime -import itertools -import json -import logging -import os - -from detectron2.data import MetadataCatalog -from detectron2.evaluation import DatasetEvaluator -from detectron2.evaluation.coco_evaluation import instances_to_coco_json -import detectron2.utils.comm as comm -from detectron2.utils.logger import create_small_table -from fvcore.common.file_io import PathManager -from lvis.lvis import LVIS -from lvis.results import LVISResults -import numpy as np -import pycocotools.mask as mask_utils -from tabulate import tabulate -import torch -from typing import Optional, Sequence - - -def compute_average_precision(precision, recall): - """Compute Average Precision according to the definition in VOCdevkit. - Precision is modified to ensure that it does not decrease as recall - decrease. - Args: - precision: A float [N, 1] numpy array of precisions - recall: A float [N, 1] numpy array of recalls - Raises: - ValueError: if the input is not of the correct format - Returns: - average_precison: The area under the precision recall curve. NaN if - precision and recall are None. - """ - if precision is None: - if recall is not None: - raise ValueError("If precision is None, recall must also be None") - return np.NAN - - if not isinstance(precision, np.ndarray) or not isinstance(recall, np.ndarray): - raise ValueError("precision and recall must be numpy array") - if precision.dtype != np.float or recall.dtype != np.float: - raise ValueError("input must be float numpy array.") - if len(precision) != len(recall): - raise ValueError("precision and recall must be of the same size.") - if not precision.size: - return 0.0 - if np.amin(precision) < 0 or np.amax(precision) > 1: - raise ValueError("Precision must be in the range of [0, 1].") - if np.amin(recall) < 0 or np.amax(recall) > 1: - raise ValueError("recall must be in the range of [0, 1].") - if not all(recall[i] <= recall[i + 1] for i in range(len(recall) - 1)): - raise ValueError("recall must be a non-decreasing array") - - recall = np.concatenate([[0], recall, [1]]) - precision = np.concatenate([[0], precision, [0]]) - - for i in range(len(precision) - 2, -1, -1): - precision[i] = np.maximum(precision[i], precision[i + 1]) - indices = np.where(recall[1:] != recall[:-1])[0] + 1 - average_precision = np.sum((recall[indices] - recall[indices - 1]) * precision[indices]) - return average_precision - - -class OIDEval: - def __init__( - self, - lvis_gt, - lvis_dt, - iou_type: str="bbox", - expand_pred_label: bool=False, - oid_hierarchy_path: str="./datasets/oid/annotations/challenge-2019-label500-hierarchy.json", - ) -> None: - """Constructor for OIDEval. - Args: - lvis_gt (LVIS class instance, or str containing path of annotation file) - lvis_dt (LVISResult class instance, or str containing path of result file, - or list of dict) - iou_type (str): segm or bbox evaluation - """ - self.logger = logging.getLogger(__name__) - - if iou_type not in ["bbox", "segm"]: - raise ValueError(f"iou_type: {iou_type} is not supported.") - - if isinstance(lvis_gt, LVIS): - self.lvis_gt = lvis_gt - elif isinstance(lvis_gt, str): - self.lvis_gt = LVIS(lvis_gt) - else: - raise TypeError(f"Unsupported type {lvis_gt} of lvis_gt.") - - if isinstance(lvis_dt, LVISResults): - self.lvis_dt = lvis_dt - elif isinstance(lvis_dt, str | list): - # self.lvis_dt = LVISResults(self.lvis_gt, lvis_dt, max_dets=-1) - self.lvis_dt = LVISResults(self.lvis_gt, lvis_dt) - else: - raise TypeError(f"Unsupported type {lvis_dt} of lvis_dt.") - - if expand_pred_label: - oid_hierarchy = json.load(open(oid_hierarchy_path)) - cat_info = self.lvis_gt.dataset["categories"] - freebase2id = {x["freebase_id"]: x["id"] for x in cat_info} - {x["id"]: x["freebase_id"] for x in cat_info} - {x["id"]: x["name"] for x in cat_info} - - fas = defaultdict(set) - - def dfs(hierarchy, cur_id): - all_childs = set() - if "Subcategory" in hierarchy: - for x in hierarchy["Subcategory"]: - childs = dfs(x, freebase2id[x["LabelName"]]) - all_childs.update(childs) - if cur_id != -1: - for c in all_childs: - fas[c].add(cur_id) - all_childs.add(cur_id) - return all_childs - - dfs(oid_hierarchy, -1) - - expanded_pred = [] - id_count = 0 - for d in self.lvis_dt.dataset["annotations"]: - cur_id = d["category_id"] - ids = [cur_id] + [x for x in fas[cur_id]] - for cat_id in ids: - new_box = copy.deepcopy(d) - id_count = id_count + 1 - new_box["id"] = id_count - new_box["category_id"] = cat_id - expanded_pred.append(new_box) - - print( - "Expanding original {} preds to {} preds".format( - len(self.lvis_dt.dataset["annotations"]), len(expanded_pred) - ) - ) - self.lvis_dt.dataset["annotations"] = expanded_pred - self.lvis_dt._create_index() - - # per-image per-category evaluation results - self.eval_imgs = defaultdict(list) - self.eval = {} # accumulated evaluation results - self._gts = defaultdict(list) # gt for evaluation - self._dts = defaultdict(list) # dt for evaluation - self.params = Params(iou_type=iou_type) # parameters - self.results = OrderedDict() - self.ious = {} # ious between all gts and dts - - self.params.img_ids = sorted(self.lvis_gt.get_img_ids()) - self.params.cat_ids = sorted(self.lvis_gt.get_cat_ids()) - - def _to_mask(self, anns, lvis) -> None: - for ann in anns: - rle = lvis.ann_to_rle(ann) - ann["segmentation"] = rle - - def _prepare(self) -> None: - """Prepare self._gts and self._dts for evaluation based on params.""" - - cat_ids = self.params.cat_ids if self.params.cat_ids else None - - gts = self.lvis_gt.load_anns( - self.lvis_gt.get_ann_ids(img_ids=self.params.img_ids, cat_ids=cat_ids) - ) - dts = self.lvis_dt.load_anns( - self.lvis_dt.get_ann_ids(img_ids=self.params.img_ids, cat_ids=cat_ids) - ) - # convert ground truth to mask if iou_type == 'segm' - if self.params.iou_type == "segm": - self._to_mask(gts, self.lvis_gt) - self._to_mask(dts, self.lvis_dt) - - for gt in gts: - self._gts[gt["image_id"], gt["category_id"]].append(gt) - - # For federated dataset evaluation we will filter out all dt for an - # image which belong to categories not present in gt and not present in - # the negative list for an image. In other words detector is not penalized - # for categories about which we don't have gt information about their - # presence or absence in an image. - img_data = self.lvis_gt.load_imgs(ids=self.params.img_ids) - # per image map of categories not present in image - img_nl = {d["id"]: d["neg_category_ids"] for d in img_data} - # per image list of categories present in image - img_pl = {d["id"]: d["pos_category_ids"] for d in img_data} - # img_pl = defaultdict(set) - for ann in gts: - # img_pl[ann["image_id"]].add(ann["category_id"]) - assert ann["category_id"] in img_pl[ann["image_id"]] - # print('check pos ids OK.') - - for dt in dts: - img_id, cat_id = dt["image_id"], dt["category_id"] - if cat_id not in img_nl[img_id] and cat_id not in img_pl[img_id]: - continue - self._dts[img_id, cat_id].append(dt) - - def evaluate(self) -> None: - """ - Run per image evaluation on given images and store results - (a list of dict) in self.eval_imgs. - """ - self.logger.info("Running per image evaluation.") - self.logger.info(f"Evaluate annotation type *{self.params.iou_type}*") - - self.params.img_ids = list(np.unique(self.params.img_ids)) - - if self.params.use_cats: - cat_ids = self.params.cat_ids - else: - cat_ids = [-1] - - self._prepare() - - self.ious = { - (img_id, cat_id): self.compute_iou(img_id, cat_id) - for img_id in self.params.img_ids - for cat_id in cat_ids - } - - # loop through images, area range, max detection number - print("Evaluating ...") - self.eval_imgs = [ - self.evaluate_img_google(img_id, cat_id, area_rng) - for cat_id in cat_ids - for area_rng in self.params.area_rng - for img_id in self.params.img_ids - ] - - def _get_gt_dt(self, img_id, cat_id): - """Create gt, dt which are list of anns/dets. If use_cats is true - only anns/dets corresponding to tuple (img_id, cat_id) will be - used. Else, all anns/dets in image are used and cat_id is not used. - """ - if self.params.use_cats: - gt = self._gts[img_id, cat_id] - dt = self._dts[img_id, cat_id] - else: - gt = [_ann for _cat_id in self.params.cat_ids for _ann in self._gts[img_id, cat_id]] - dt = [_ann for _cat_id in self.params.cat_ids for _ann in self._dts[img_id, cat_id]] - return gt, dt - - def compute_iou(self, img_id, cat_id): - gt, dt = self._get_gt_dt(img_id, cat_id) - - if len(gt) == 0 and len(dt) == 0: - return [] - - # Sort detections in decreasing order of score. - idx = np.argsort([-d["score"] for d in dt], kind="mergesort") - dt = [dt[i] for i in idx] - - # iscrowd = [int(False)] * len(gt) - iscrowd = [int("iscrowd" in g and g["iscrowd"] > 0) for g in gt] - - if self.params.iou_type == "segm": - ann_type = "segmentation" - elif self.params.iou_type == "bbox": - ann_type = "bbox" - else: - raise ValueError("Unknown iou_type for iou computation.") - gt = [g[ann_type] for g in gt] - dt = [d[ann_type] for d in dt] - - # compute iou between each dt and gt region - # will return array of shape len(dt), len(gt) - ious = mask_utils.iou(dt, gt, iscrowd) - return ious - - def evaluate_img_google(self, img_id, cat_id, area_rng): - gt, dt = self._get_gt_dt(img_id, cat_id) - if len(gt) == 0 and len(dt) == 0: - return None - - if len(dt) == 0: - return { - "image_id": img_id, - "category_id": cat_id, - "area_rng": area_rng, - "dt_ids": [], - "dt_matches": np.array([], dtype=np.int32).reshape(1, -1), - "dt_scores": [], - "dt_ignore": np.array([], dtype=np.int32).reshape(1, -1), - "num_gt": len(gt), - } - - no_crowd_inds = [i for i, g in enumerate(gt) if ("iscrowd" not in g) or g["iscrowd"] == 0] - crowd_inds = [i for i, g in enumerate(gt) if "iscrowd" in g and g["iscrowd"] == 1] - dt_idx = np.argsort([-d["score"] for d in dt], kind="mergesort") - - if len(self.ious[img_id, cat_id]) > 0: - ious = self.ious[img_id, cat_id] - iou = ious[:, no_crowd_inds] - iou = iou[dt_idx] - ioa = ious[:, crowd_inds] - ioa = ioa[dt_idx] - else: - iou = np.zeros((len(dt_idx), 0)) - ioa = np.zeros((len(dt_idx), 0)) - scores = np.array([dt[i]["score"] for i in dt_idx]) - - num_detected_boxes = len(dt) - tp_fp_labels = np.zeros(num_detected_boxes, dtype=bool) - is_matched_to_group_of = np.zeros(num_detected_boxes, dtype=bool) - - def compute_match_iou(iou) -> None: - max_overlap_gt_ids = np.argmax(iou, axis=1) - is_gt_detected = np.zeros(iou.shape[1], dtype=bool) - for i in range(num_detected_boxes): - gt_id = max_overlap_gt_ids[i] - is_evaluatable = ( - not tp_fp_labels[i] and iou[i, gt_id] >= 0.5 and not is_matched_to_group_of[i] - ) - if is_evaluatable: - if not is_gt_detected[gt_id]: - tp_fp_labels[i] = True - is_gt_detected[gt_id] = True - - def compute_match_ioa(ioa): - scores_group_of = np.zeros(ioa.shape[1], dtype=float) - tp_fp_labels_group_of = np.ones(ioa.shape[1], dtype=float) - max_overlap_group_of_gt_ids = np.argmax(ioa, axis=1) - for i in range(num_detected_boxes): - gt_id = max_overlap_group_of_gt_ids[i] - is_evaluatable = ( - not tp_fp_labels[i] and ioa[i, gt_id] >= 0.5 and not is_matched_to_group_of[i] - ) - if is_evaluatable: - is_matched_to_group_of[i] = True - scores_group_of[gt_id] = max(scores_group_of[gt_id], scores[i]) - selector = np.where((scores_group_of > 0) & (tp_fp_labels_group_of > 0)) - scores_group_of = scores_group_of[selector] - tp_fp_labels_group_of = tp_fp_labels_group_of[selector] - - return scores_group_of, tp_fp_labels_group_of - - if iou.shape[1] > 0: - compute_match_iou(iou) - - scores_box_group_of = np.ndarray([0], dtype=float) - tp_fp_labels_box_group_of = np.ndarray([0], dtype=float) - - if ioa.shape[1] > 0: - scores_box_group_of, tp_fp_labels_box_group_of = compute_match_ioa(ioa) - - valid_entries = ~is_matched_to_group_of - - scores = np.concatenate((scores[valid_entries], scores_box_group_of)) - tp_fps = np.concatenate( - (tp_fp_labels[valid_entries].astype(float), tp_fp_labels_box_group_of) - ) - - return { - "image_id": img_id, - "category_id": cat_id, - "area_rng": area_rng, - "dt_matches": np.array([1 if x > 0 else 0 for x in tp_fps], dtype=np.int32).reshape( - 1, -1 - ), - "dt_scores": [x for x in scores], - "dt_ignore": np.array([0 for x in scores], dtype=np.int32).reshape(1, -1), - "num_gt": len(gt), - } - - def accumulate(self) -> None: - """Accumulate per image evaluation results and store the result in - self.eval. - """ - self.logger.info("Accumulating evaluation results.") - - if not self.eval_imgs: - self.logger.warn("Please run evaluate first.") - - if self.params.use_cats: - cat_ids = self.params.cat_ids - else: - cat_ids = [-1] - - num_thrs = 1 - num_recalls = 1 - - num_cats = len(cat_ids) - num_area_rngs = 1 - num_imgs = len(self.params.img_ids) - - # -1 for absent categories - precision = -np.ones((num_thrs, num_recalls, num_cats, num_area_rngs)) - recall = -np.ones((num_thrs, num_cats, num_area_rngs)) - - # Initialize dt_pointers - dt_pointers = {} - for cat_idx in range(num_cats): - dt_pointers[cat_idx] = {} - for area_idx in range(num_area_rngs): - dt_pointers[cat_idx][area_idx] = {} - - # Per category evaluation - for cat_idx in range(num_cats): - Nk = cat_idx * num_area_rngs * num_imgs - for area_idx in range(num_area_rngs): - Na = area_idx * num_imgs - E = [self.eval_imgs[Nk + Na + img_idx] for img_idx in range(num_imgs)] - # Remove elements which are None - E = [e for e in E if e is not None] - if len(E) == 0: - continue - - dt_scores = np.concatenate([e["dt_scores"] for e in E], axis=0) - dt_idx = np.argsort(-dt_scores, kind="mergesort") - dt_scores = dt_scores[dt_idx] - dt_m = np.concatenate([e["dt_matches"] for e in E], axis=1)[:, dt_idx] - dt_ig = np.concatenate([e["dt_ignore"] for e in E], axis=1)[:, dt_idx] - - num_gt = sum([e["num_gt"] for e in E]) - if num_gt == 0: - continue - - tps = np.logical_and(dt_m, np.logical_not(dt_ig)) - fps = np.logical_and(np.logical_not(dt_m), np.logical_not(dt_ig)) - tp_sum = np.cumsum(tps, axis=1).astype(dtype=np.float) - fp_sum = np.cumsum(fps, axis=1).astype(dtype=np.float) - - dt_pointers[cat_idx][area_idx] = { - "tps": tps, - "fps": fps, - } - - for iou_thr_idx, (tp, fp) in enumerate(zip(tp_sum, fp_sum, strict=False)): - tp = np.array(tp) - fp = np.array(fp) - num_tp = len(tp) - rc = tp / num_gt - - if num_tp: - recall[iou_thr_idx, cat_idx, area_idx] = rc[-1] - else: - recall[iou_thr_idx, cat_idx, area_idx] = 0 - - # np.spacing(1) ~= eps - pr = tp / (fp + tp + np.spacing(1)) - pr = pr.tolist() - - for i in range(num_tp - 1, 0, -1): - if pr[i] > pr[i - 1]: - pr[i - 1] = pr[i] - - mAP = compute_average_precision( - np.array(pr, np.float).reshape(-1), np.array(rc, np.float).reshape(-1) - ) - precision[iou_thr_idx, :, cat_idx, area_idx] = mAP - - self.eval = { - "params": self.params, - "counts": [num_thrs, num_recalls, num_cats, num_area_rngs], - "date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "precision": precision, - "recall": recall, - "dt_pointers": dt_pointers, - } - - def _summarize(self, summary_type): - s = self.eval["precision"] - if len(s[s > -1]) == 0: - mean_s = -1 - else: - mean_s = np.mean(s[s > -1]) - # print(s.reshape(1, 1, -1, 1)) - return mean_s - - def summarize(self): - """Compute and display summary metrics for evaluation results.""" - if not self.eval: - raise RuntimeError("Please run accumulate() first.") - - self.results["AP50"] = self._summarize("ap") - - def run(self) -> None: - """Wrapper function which calculates the results.""" - self.evaluate() - self.accumulate() - self.summarize() - - def print_results(self) -> None: - template = " {:<18} {} @[ IoU={:<9} | area={:>6s} | maxDets={:>3d} catIds={:>3s}] = {:0.3f}" - - for key, value in self.results.items(): - max_dets = self.params.max_dets - if "AP" in key: - title = "Average Precision" - _type = "(AP)" - else: - title = "Average Recall" - _type = "(AR)" - - if len(key) > 2 and key[2].isdigit(): - iou_thr = float(key[2:]) / 100 - iou = f"{iou_thr:0.2f}" - else: - iou = f"{self.params.iou_thrs[0]:0.2f}:{self.params.iou_thrs[-1]:0.2f}" - - cat_group_name = "all" - area_rng = "all" - - print(template.format(title, _type, iou, area_rng, max_dets, cat_group_name, value)) - - def get_results(self): - if not self.results: - self.logger.warn("results is empty. Call run().") - return self.results - - -class Params: - def __init__(self, iou_type) -> None: - self.img_ids = [] - self.cat_ids = [] - # np.arange causes trouble. the data point on arange is slightly - # larger than the true value - self.iou_thrs = np.linspace( - 0.5, 0.95, int(np.round((0.95 - 0.5) / 0.05)) + 1, endpoint=True - ) - self.google_style = True - # print('Using google style PR curve') - self.iou_thrs = self.iou_thrs[:1] - self.max_dets = 1000 - - self.area_rng = [ - [0**2, 1e5**2], - ] - self.area_rng_lbl = ["all"] - self.use_cats = 1 - self.iou_type = iou_type - - -class OIDEvaluator(DatasetEvaluator): - def __init__(self, dataset_name: str, cfg, distributed, output_dir=None) -> None: - self._distributed = distributed - self._output_dir = output_dir - - self._cpu_device = torch.device("cpu") - self._logger = logging.getLogger(__name__) - - self._metadata = MetadataCatalog.get(dataset_name) - json_file = PathManager.get_local_path(self._metadata.json_file) - self._oid_api = LVIS(json_file) - # Test set json files do not contain annotations (evaluation must be - # performed using the LVIS evaluation server). - self._do_evaluation = len(self._oid_api.get_ann_ids()) > 0 - self._mask_on = cfg.MODEL.MASK_ON - - def reset(self) -> None: - self._predictions = [] - self._oid_results = [] - - def process(self, inputs, outputs) -> None: - for input, output in zip(inputs, outputs, strict=False): - prediction = {"image_id": input["image_id"]} - instances = output["instances"].to(self._cpu_device) - prediction["instances"] = instances_to_coco_json(instances, input["image_id"]) - self._predictions.append(prediction) - - def evaluate(self): - if self._distributed: - comm.synchronize() - self._predictions = comm.gather(self._predictions, dst=0) - self._predictions = list(itertools.chain(*self._predictions)) - - if not comm.is_main_process(): - return - - if len(self._predictions) == 0: - self._logger.warning("[LVISEvaluator] Did not receive valid predictions.") - return {} - - self._logger.info("Preparing results in the OID format ...") - self._oid_results = list(itertools.chain(*[x["instances"] for x in self._predictions])) - - # unmap the category ids for LVIS (from 0-indexed to 1-indexed) - for result in self._oid_results: - result["category_id"] += 1 - - PathManager.mkdirs(self._output_dir) - file_path = os.path.join(self._output_dir, "oid_instances_results.json") - self._logger.info(f"Saving results to {file_path}") - with PathManager.open(file_path, "w") as f: - f.write(json.dumps(self._oid_results)) - f.flush() - - if not self._do_evaluation: - self._logger.info("Annotations are not available for evaluation.") - return - - self._logger.info("Evaluating predictions ...") - self._results = OrderedDict() - res, mAP = _evaluate_predictions_on_oid( - self._oid_api, - file_path, - eval_seg=self._mask_on, - class_names=self._metadata.get("thing_classes"), - ) - self._results["bbox"] = res - mAP_out_path = os.path.join(self._output_dir, "oid_mAP.npy") - self._logger.info("Saving mAP to" + mAP_out_path) - np.save(mAP_out_path, mAP) - return copy.deepcopy(self._results) - - -def _evaluate_predictions_on_oid(oid_gt, oid_results_path, eval_seg: bool=False, class_names: Optional[Sequence[str]]=None): - logger = logging.getLogger(__name__) - - results = {} - oid_eval = OIDEval(oid_gt, oid_results_path, "bbox", expand_pred_label=False) - oid_eval.run() - oid_eval.print_results() - results["AP50"] = oid_eval.get_results()["AP50"] - - if eval_seg: - oid_eval = OIDEval(oid_gt, oid_results_path, "segm", expand_pred_label=False) - oid_eval.run() - oid_eval.print_results() - results["AP50_segm"] = oid_eval.get_results()["AP50"] - else: - oid_eval = OIDEval(oid_gt, oid_results_path, "bbox", expand_pred_label=True) - oid_eval.run() - oid_eval.print_results() - results["AP50_expand"] = oid_eval.get_results()["AP50"] - - mAP = np.zeros(len(class_names)) - 1 - precisions = oid_eval.eval["precision"] - assert len(class_names) == precisions.shape[2] - results_per_category = [] - id2apiid = sorted(oid_gt.get_cat_ids()) - inst_aware_ap, inst_count = 0, 0 - for idx, name in enumerate(class_names): - precision = precisions[:, :, idx, 0] - precision = precision[precision > -1] - ap = np.mean(precision) if precision.size else float("nan") - inst_num = len(oid_gt.get_ann_ids(cat_ids=[id2apiid[idx]])) - if inst_num > 0: - results_per_category.append( - ( - "{} {}".format( - name.replace(" ", "_"), - inst_num if inst_num < 1000 else f"{inst_num / 1000:.1f}k", - ), - float(ap * 100), - ) - ) - inst_aware_ap += inst_num * ap - inst_count += inst_num - mAP[idx] = ap - # logger.info("{} {} {:.2f}".format(name, inst_num, ap * 100)) - inst_aware_ap = inst_aware_ap * 100 / inst_count - N_COLS = min(6, len(results_per_category) * 2) - results_flatten = list(itertools.chain(*results_per_category)) - results_2d = itertools.zip_longest(*[results_flatten[i::N_COLS] for i in range(N_COLS)]) - table = tabulate( - results_2d, - tablefmt="pipe", - floatfmt=".3f", - headers=["category", "AP"] * (N_COLS // 2), - numalign="left", - ) - logger.info("Per-category {} AP: \n".format("bbox") + table) - logger.info("Instance-aware {} AP: {:.4f}".format("bbox", inst_aware_ap)) - - logger.info("Evaluation results for bbox: \n" + create_small_table(results)) - return results, mAP diff --git a/dimos/models/Detic/detic/modeling/backbone/swintransformer.py b/dimos/models/Detic/detic/modeling/backbone/swintransformer.py deleted file mode 100644 index b7da6328e3..0000000000 --- a/dimos/models/Detic/detic/modeling/backbone/swintransformer.py +++ /dev/null @@ -1,825 +0,0 @@ -# -------------------------------------------------------- -# Swin Transformer -# Copyright (c) 2021 Microsoft -# Licensed under The MIT License [see LICENSE for details] -# Written by Ze Liu, Yutong Lin, Yixuan Wei -# -------------------------------------------------------- - -# Copyright (c) Facebook, Inc. and its affiliates. -# Modified by Xingyi Zhou from https://github.com/SwinTransformer/Swin-Transformer-Object-Detection/blob/master/mmdet/models/backbones/swin_transformer.py - - -from centernet.modeling.backbone.bifpn import BiFPN -from centernet.modeling.backbone.fpn_p5 import LastLevelP6P7_P5 -from detectron2.layers import ShapeSpec -from detectron2.modeling.backbone.backbone import Backbone -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from detectron2.modeling.backbone.fpn import FPN -import numpy as np -from timm.models.layers import DropPath, to_2tuple, trunc_normal_ -import torch -import torch.nn as nn -import torch.nn.functional as F -import torch.utils.checkpoint as checkpoint -from typing import Optional, Sequence - -# from .checkpoint import load_checkpoint - - -class Mlp(nn.Module): - """Multilayer perceptron.""" - - def __init__( - self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop: float=0.0 - ) -> None: - super().__init__() - out_features = out_features or in_features - hidden_features = hidden_features or in_features - self.fc1 = nn.Linear(in_features, hidden_features) - self.act = act_layer() - self.fc2 = nn.Linear(hidden_features, out_features) - self.drop = nn.Dropout(drop) - - def forward(self, x): - x = self.fc1(x) - x = self.act(x) - x = self.drop(x) - x = self.fc2(x) - x = self.drop(x) - return x - - -def window_partition(x, window_size: int): - """ - Args: - x: (B, H, W, C) - window_size (int): window size - Returns: - windows: (num_windows*B, window_size, window_size, C) - """ - B, H, W, C = x.shape - x = x.view(B, H // window_size, window_size, W // window_size, window_size, C) - windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C) - return windows - - -def window_reverse(windows, window_size: int, H, W): - """ - Args: - windows: (num_windows*B, window_size, window_size, C) - window_size (int): Window size - H (int): Height of image - W (int): Width of image - Returns: - x: (B, H, W, C) - """ - B = int(windows.shape[0] / (H * W / window_size / window_size)) - x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1) - x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1) - return x - - -class WindowAttention(nn.Module): - """Window based multi-head self attention (W-MSA) module with relative position bias. - It supports both of shifted and non-shifted window. - Args: - dim (int): Number of input channels. - window_size (tuple[int]): The height and width of the window. - num_heads (int): Number of attention heads. - qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True - qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set - attn_drop (float, optional): Dropout ratio of attention weight. Default: 0.0 - proj_drop (float, optional): Dropout ratio of output. Default: 0.0 - """ - - def __init__( - self, - dim: int, - window_size: int, - num_heads: int, - qkv_bias: bool=True, - qk_scale=None, - attn_drop: float=0.0, - proj_drop: float=0.0, - ) -> None: - super().__init__() - self.dim = dim - self.window_size = window_size # Wh, Ww - self.num_heads = num_heads - head_dim = dim // num_heads - self.scale = qk_scale or head_dim**-0.5 - - # define a parameter table of relative position bias - self.relative_position_bias_table = nn.Parameter( - torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads) - ) # 2*Wh-1 * 2*Ww-1, nH - - # get pair-wise relative position index for each token inside the window - coords_h = torch.arange(self.window_size[0]) - coords_w = torch.arange(self.window_size[1]) - coords = torch.stack(torch.meshgrid([coords_h, coords_w])) # 2, Wh, Ww - coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww - relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww - relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2 - relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0 - relative_coords[:, :, 1] += self.window_size[1] - 1 - relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1 - relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww - self.register_buffer("relative_position_index", relative_position_index) - - self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) - self.attn_drop = nn.Dropout(attn_drop) - self.proj = nn.Linear(dim, dim) - self.proj_drop = nn.Dropout(proj_drop) - - trunc_normal_(self.relative_position_bias_table, std=0.02) - self.softmax = nn.Softmax(dim=-1) - - def forward(self, x, mask=None): - """Forward function. - Args: - x: input features with shape of (num_windows*B, N, C) - mask: (0/-inf) mask with shape of (num_windows, Wh*Ww, Wh*Ww) or None - """ - B_, N, C = x.shape - qkv = ( - self.qkv(x) - .reshape(B_, N, 3, self.num_heads, C // self.num_heads) - .permute(2, 0, 3, 1, 4) - ) - q, k, v = qkv[0], qkv[1], qkv[2] # make torchscript happy (cannot use tensor as tuple) - - q = q * self.scale - attn = q @ k.transpose(-2, -1) - - relative_position_bias = self.relative_position_bias_table[ - self.relative_position_index.view(-1) - ].view( - self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1 - ) # Wh*Ww,Wh*Ww,nH - relative_position_bias = relative_position_bias.permute( - 2, 0, 1 - ).contiguous() # nH, Wh*Ww, Wh*Ww - attn = attn + relative_position_bias.unsqueeze(0) - - if mask is not None: - nW = mask.shape[0] - attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0) - attn = attn.view(-1, self.num_heads, N, N) - attn = self.softmax(attn) - else: - attn = self.softmax(attn) - - attn = self.attn_drop(attn) - - x = (attn @ v).transpose(1, 2).reshape(B_, N, C) - x = self.proj(x) - x = self.proj_drop(x) - return x - - -class SwinTransformerBlock(nn.Module): - """Swin Transformer Block. - Args: - dim (int): Number of input channels. - num_heads (int): Number of attention heads. - window_size (int): Window size. - shift_size (int): Shift size for SW-MSA. - mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. - qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True - qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set. - drop (float, optional): Dropout rate. Default: 0.0 - attn_drop (float, optional): Attention dropout rate. Default: 0.0 - drop_path (float, optional): Stochastic depth rate. Default: 0.0 - act_layer (nn.Module, optional): Activation layer. Default: nn.GELU - norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm - """ - - def __init__( - self, - dim: int, - num_heads: int, - window_size: int=7, - shift_size: int=0, - mlp_ratio: float=4.0, - qkv_bias: bool=True, - qk_scale=None, - drop: float=0.0, - attn_drop: float=0.0, - drop_path: float=0.0, - act_layer=nn.GELU, - norm_layer=nn.LayerNorm, - ) -> None: - super().__init__() - self.dim = dim - self.num_heads = num_heads - self.window_size = window_size - self.shift_size = shift_size - self.mlp_ratio = mlp_ratio - assert 0 <= self.shift_size < self.window_size, "shift_size must in 0-window_size" - - self.norm1 = norm_layer(dim) - self.attn = WindowAttention( - dim, - window_size=to_2tuple(self.window_size), - num_heads=num_heads, - qkv_bias=qkv_bias, - qk_scale=qk_scale, - attn_drop=attn_drop, - proj_drop=drop, - ) - - self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() - self.norm2 = norm_layer(dim) - mlp_hidden_dim = int(dim * mlp_ratio) - self.mlp = Mlp( - in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop - ) - - self.H = None - self.W = None - - def forward(self, x, mask_matrix): - """Forward function. - Args: - x: Input feature, tensor size (B, H*W, C). - H, W: Spatial resolution of the input feature. - mask_matrix: Attention mask for cyclic shift. - """ - B, L, C = x.shape - H, W = self.H, self.W - assert L == H * W, "input feature has wrong size" - - shortcut = x - x = self.norm1(x) - x = x.view(B, H, W, C) - - # pad feature maps to multiples of window size - pad_l = pad_t = 0 - pad_r = (self.window_size - W % self.window_size) % self.window_size - pad_b = (self.window_size - H % self.window_size) % self.window_size - x = F.pad(x, (0, 0, pad_l, pad_r, pad_t, pad_b)) - _, Hp, Wp, _ = x.shape - - # cyclic shift - if self.shift_size > 0: - shifted_x = torch.roll(x, shifts=(-self.shift_size, -self.shift_size), dims=(1, 2)) - attn_mask = mask_matrix - else: - shifted_x = x - attn_mask = None - - # partition windows - x_windows = window_partition( - shifted_x, self.window_size - ) # nW*B, window_size, window_size, C - x_windows = x_windows.view( - -1, self.window_size * self.window_size, C - ) # nW*B, window_size*window_size, C - - # W-MSA/SW-MSA - attn_windows = self.attn(x_windows, mask=attn_mask) # nW*B, window_size*window_size, C - - # merge windows - attn_windows = attn_windows.view(-1, self.window_size, self.window_size, C) - shifted_x = window_reverse(attn_windows, self.window_size, Hp, Wp) # B H' W' C - - # reverse cyclic shift - if self.shift_size > 0: - x = torch.roll(shifted_x, shifts=(self.shift_size, self.shift_size), dims=(1, 2)) - else: - x = shifted_x - - if pad_r > 0 or pad_b > 0: - x = x[:, :H, :W, :].contiguous() - - x = x.view(B, H * W, C) - - # FFN - x = shortcut + self.drop_path(x) - x = x + self.drop_path(self.mlp(self.norm2(x))) - - return x - - -class PatchMerging(nn.Module): - """Patch Merging Layer - Args: - dim (int): Number of input channels. - norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm - """ - - def __init__(self, dim: int, norm_layer=nn.LayerNorm) -> None: - super().__init__() - self.dim = dim - self.reduction = nn.Linear(4 * dim, 2 * dim, bias=False) - self.norm = norm_layer(4 * dim) - - def forward(self, x, H, W): - """Forward function. - Args: - x: Input feature, tensor size (B, H*W, C). - H, W: Spatial resolution of the input feature. - """ - B, L, C = x.shape - assert L == H * W, "input feature has wrong size" - - x = x.view(B, H, W, C) - - # padding - pad_input = (H % 2 == 1) or (W % 2 == 1) - if pad_input: - x = F.pad(x, (0, 0, 0, W % 2, 0, H % 2)) - - x0 = x[:, 0::2, 0::2, :] # B H/2 W/2 C - x1 = x[:, 1::2, 0::2, :] # B H/2 W/2 C - x2 = x[:, 0::2, 1::2, :] # B H/2 W/2 C - x3 = x[:, 1::2, 1::2, :] # B H/2 W/2 C - x = torch.cat([x0, x1, x2, x3], -1) # B H/2 W/2 4*C - x = x.view(B, -1, 4 * C) # B H/2*W/2 4*C - - x = self.norm(x) - x = self.reduction(x) - - return x - - -class BasicLayer(nn.Module): - """A basic Swin Transformer layer for one stage. - Args: - dim (int): Number of feature channels - depth (int): Depths of this stage. - num_heads (int): Number of attention head. - window_size (int): Local window size. Default: 7. - mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4. - qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True - qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set. - drop (float, optional): Dropout rate. Default: 0.0 - attn_drop (float, optional): Attention dropout rate. Default: 0.0 - drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0 - norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm - downsample (nn.Module | None, optional): Downsample layer at the end of the layer. Default: None - use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False. - """ - - def __init__( - self, - dim: int, - depth: int, - num_heads: int, - window_size: int=7, - mlp_ratio: float=4.0, - qkv_bias: bool=True, - qk_scale=None, - drop: float=0.0, - attn_drop: float=0.0, - drop_path: float=0.0, - norm_layer=nn.LayerNorm, - downsample=None, - use_checkpoint: bool=False, - ) -> None: - super().__init__() - self.window_size = window_size - self.shift_size = window_size // 2 - self.depth = depth - self.use_checkpoint = use_checkpoint - - # build blocks - self.blocks = nn.ModuleList( - [ - SwinTransformerBlock( - dim=dim, - num_heads=num_heads, - window_size=window_size, - shift_size=0 if (i % 2 == 0) else window_size // 2, - mlp_ratio=mlp_ratio, - qkv_bias=qkv_bias, - qk_scale=qk_scale, - drop=drop, - attn_drop=attn_drop, - drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path, - norm_layer=norm_layer, - ) - for i in range(depth) - ] - ) - - # patch merging layer - if downsample is not None: - self.downsample = downsample(dim=dim, norm_layer=norm_layer) - else: - self.downsample = None - - def forward(self, x, H, W): - """Forward function. - Args: - x: Input feature, tensor size (B, H*W, C). - H, W: Spatial resolution of the input feature. - """ - - # calculate attention mask for SW-MSA - Hp = int(np.ceil(H / self.window_size)) * self.window_size - Wp = int(np.ceil(W / self.window_size)) * self.window_size - img_mask = torch.zeros((1, Hp, Wp, 1), device=x.device) # 1 Hp Wp 1 - h_slices = ( - slice(0, -self.window_size), - slice(-self.window_size, -self.shift_size), - slice(-self.shift_size, None), - ) - w_slices = ( - slice(0, -self.window_size), - slice(-self.window_size, -self.shift_size), - slice(-self.shift_size, None), - ) - cnt = 0 - for h in h_slices: - for w in w_slices: - img_mask[:, h, w, :] = cnt - cnt += 1 - - mask_windows = window_partition( - img_mask, self.window_size - ) # nW, window_size, window_size, 1 - mask_windows = mask_windows.view(-1, self.window_size * self.window_size) - attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) - attn_mask = attn_mask.masked_fill(attn_mask != 0, (-100.0)).masked_fill( - attn_mask == 0, 0.0 - ) - - for blk in self.blocks: - blk.H, blk.W = H, W - if self.use_checkpoint: - x = checkpoint.checkpoint(blk, x, attn_mask) - else: - x = blk(x, attn_mask) - if self.downsample is not None: - x_down = self.downsample(x, H, W) - Wh, Ww = (H + 1) // 2, (W + 1) // 2 - return x, H, W, x_down, Wh, Ww - else: - return x, H, W, x, H, W - - -class PatchEmbed(nn.Module): - """Image to Patch Embedding - Args: - patch_size (int): Patch token size. Default: 4. - in_chans (int): Number of input image channels. Default: 3. - embed_dim (int): Number of linear projection output channels. Default: 96. - norm_layer (nn.Module, optional): Normalization layer. Default: None - """ - - def __init__(self, patch_size: int=4, in_chans: int=3, embed_dim: int=96, norm_layer=None) -> None: - super().__init__() - patch_size = to_2tuple(patch_size) - self.patch_size = patch_size - - self.in_chans = in_chans - self.embed_dim = embed_dim - - self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) - if norm_layer is not None: - self.norm = norm_layer(embed_dim) - else: - self.norm = None - - def forward(self, x): - """Forward function.""" - # padding - _, _, H, W = x.size() - if W % self.patch_size[1] != 0: - x = F.pad(x, (0, self.patch_size[1] - W % self.patch_size[1])) - if H % self.patch_size[0] != 0: - x = F.pad(x, (0, 0, 0, self.patch_size[0] - H % self.patch_size[0])) - - x = self.proj(x) # B C Wh Ww - if self.norm is not None: - Wh, Ww = x.size(2), x.size(3) - x = x.flatten(2).transpose(1, 2) - x = self.norm(x) - x = x.transpose(1, 2).view(-1, self.embed_dim, Wh, Ww) - - return x - - -class SwinTransformer(Backbone): - """Swin Transformer backbone. - A PyTorch impl of : `Swin Transformer: Hierarchical Vision Transformer using Shifted Windows` - - https://arxiv.org/pdf/2103.14030 - Args: - pretrain_img_size (int): Input image size for training the pretrained model, - used in absolute postion embedding. Default 224. - patch_size (int | tuple(int)): Patch size. Default: 4. - in_chans (int): Number of input image channels. Default: 3. - embed_dim (int): Number of linear projection output channels. Default: 96. - depths (tuple[int]): Depths of each Swin Transformer stage. - num_heads (tuple[int]): Number of attention head of each stage. - window_size (int): Window size. Default: 7. - mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4. - qkv_bias (bool): If True, add a learnable bias to query, key, value. Default: True - qk_scale (float): Override default qk scale of head_dim ** -0.5 if set. - drop_rate (float): Dropout rate. - attn_drop_rate (float): Attention dropout rate. Default: 0. - drop_path_rate (float): Stochastic depth rate. Default: 0.2. - norm_layer (nn.Module): Normalization layer. Default: nn.LayerNorm. - ape (bool): If True, add absolute position embedding to the patch embedding. Default: False. - patch_norm (bool): If True, add normalization after patch embedding. Default: True. - out_indices (Sequence[int]): Output from which stages. - frozen_stages (int): Stages to be frozen (stop grad and set eval mode). - -1 means not freezing any parameters. - use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False. - """ - - def __init__( - self, - pretrain_img_size: int=224, - patch_size: int=4, - in_chans: int=3, - embed_dim: int=96, - depths: Optional[Sequence[int]]=None, - num_heads: Optional[int]=None, - window_size: int=7, - mlp_ratio: float=4.0, - qkv_bias: bool=True, - qk_scale=None, - drop_rate: float=0.0, - attn_drop_rate: float=0.0, - drop_path_rate: float=0.2, - norm_layer=nn.LayerNorm, - ape: bool=False, - patch_norm: bool=True, - out_indices=(0, 1, 2, 3), - frozen_stages=-1, - use_checkpoint: bool=False, - ) -> None: - if num_heads is None: - num_heads = [3, 6, 12, 24] - if depths is None: - depths = [2, 2, 6, 2] - super().__init__() - - self.pretrain_img_size = pretrain_img_size - self.num_layers = len(depths) - self.embed_dim = embed_dim - self.ape = ape - self.patch_norm = patch_norm - self.out_indices = out_indices - self.frozen_stages = frozen_stages - - # split image into non-overlapping patches - self.patch_embed = PatchEmbed( - patch_size=patch_size, - in_chans=in_chans, - embed_dim=embed_dim, - norm_layer=norm_layer if self.patch_norm else None, - ) - - # absolute position embedding - if self.ape: - pretrain_img_size = to_2tuple(pretrain_img_size) - patch_size = to_2tuple(patch_size) - patches_resolution = [ - pretrain_img_size[0] // patch_size[0], - pretrain_img_size[1] // patch_size[1], - ] - - self.absolute_pos_embed = nn.Parameter( - torch.zeros(1, embed_dim, patches_resolution[0], patches_resolution[1]) - ) - trunc_normal_(self.absolute_pos_embed, std=0.02) - - self.pos_drop = nn.Dropout(p=drop_rate) - - # stochastic depth - dpr = [ - x.item() for x in torch.linspace(0, drop_path_rate, sum(depths)) - ] # stochastic depth decay rule - - # build layers - self.layers = nn.ModuleList() - for i_layer in range(self.num_layers): - layer = BasicLayer( - dim=int(embed_dim * 2**i_layer), - depth=depths[i_layer], - num_heads=num_heads[i_layer], - window_size=window_size, - mlp_ratio=mlp_ratio, - qkv_bias=qkv_bias, - qk_scale=qk_scale, - drop=drop_rate, - attn_drop=attn_drop_rate, - drop_path=dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])], - norm_layer=norm_layer, - downsample=PatchMerging if (i_layer < self.num_layers - 1) else None, - use_checkpoint=use_checkpoint, - ) - self.layers.append(layer) - - num_features = [int(embed_dim * 2**i) for i in range(self.num_layers)] - self.num_features = num_features - - # add a norm layer for each output - for i_layer in out_indices: - layer = norm_layer(num_features[i_layer]) - layer_name = f"norm{i_layer}" - self.add_module(layer_name, layer) - - self._freeze_stages() - self._out_features = [f"swin{i}" for i in self.out_indices] - self._out_feature_channels = { - f"swin{i}": self.embed_dim * 2**i for i in self.out_indices - } - self._out_feature_strides = {f"swin{i}": 2 ** (i + 2) for i in self.out_indices} - self._size_devisibility = 32 - - def _freeze_stages(self) -> None: - if self.frozen_stages >= 0: - self.patch_embed.eval() - for param in self.patch_embed.parameters(): - param.requires_grad = False - - if self.frozen_stages >= 1 and self.ape: - self.absolute_pos_embed.requires_grad = False - - if self.frozen_stages >= 2: - self.pos_drop.eval() - for i in range(0, self.frozen_stages - 1): - m = self.layers[i] - m.eval() - for param in m.parameters(): - param.requires_grad = False - - def init_weights(self, pretrained: Optional[bool]=None): - """Initialize the weights in backbone. - Args: - pretrained (str, optional): Path to pre-trained weights. - Defaults to None. - """ - - def _init_weights(m) -> None: - if isinstance(m, nn.Linear): - trunc_normal_(m.weight, std=0.02) - if isinstance(m, nn.Linear) and m.bias is not None: - nn.init.constant_(m.bias, 0) - elif isinstance(m, nn.LayerNorm): - nn.init.constant_(m.bias, 0) - nn.init.constant_(m.weight, 1.0) - - if isinstance(pretrained, str): - self.apply(_init_weights) - # load_checkpoint(self, pretrained, strict=False) - elif pretrained is None: - self.apply(_init_weights) - else: - raise TypeError("pretrained must be a str or None") - - def forward(self, x): - """Forward function.""" - x = self.patch_embed(x) - - Wh, Ww = x.size(2), x.size(3) - if self.ape: - # interpolate the position embedding to the corresponding size - absolute_pos_embed = F.interpolate( - self.absolute_pos_embed, size=(Wh, Ww), mode="bicubic" - ) - x = (x + absolute_pos_embed).flatten(2).transpose(1, 2) # B Wh*Ww C - else: - x = x.flatten(2).transpose(1, 2) - x = self.pos_drop(x) - - # outs = [] - outs = {} - for i in range(self.num_layers): - layer = self.layers[i] - x_out, H, W, x, Wh, Ww = layer(x, Wh, Ww) - - if i in self.out_indices: - norm_layer = getattr(self, f"norm{i}") - x_out = norm_layer(x_out) - - out = x_out.view(-1, H, W, self.num_features[i]).permute(0, 3, 1, 2).contiguous() - # outs.append(out) - outs[f"swin{i}"] = out - - return outs - - def train(self, mode: bool=True) -> None: - """Convert the model into training mode while keep layers freezed.""" - super().train(mode) - self._freeze_stages() - - -size2config = { - "T": { - "window_size": 7, - "embed_dim": 96, - "depth": [2, 2, 6, 2], - "num_heads": [3, 6, 12, 24], - "drop_path_rate": 0.2, - "pretrained": "models/swin_tiny_patch4_window7_224.pth", - }, - "S": { - "window_size": 7, - "embed_dim": 96, - "depth": [2, 2, 18, 2], - "num_heads": [3, 6, 12, 24], - "drop_path_rate": 0.2, - "pretrained": "models/swin_small_patch4_window7_224.pth", - }, - "B": { - "window_size": 7, - "embed_dim": 128, - "depth": [2, 2, 18, 2], - "num_heads": [4, 8, 16, 32], - "drop_path_rate": 0.3, - "pretrained": "models/swin_base_patch4_window7_224.pth", - }, - "B-22k": { - "window_size": 7, - "embed_dim": 128, - "depth": [2, 2, 18, 2], - "num_heads": [4, 8, 16, 32], - "drop_path_rate": 0.3, - "pretrained": "models/swin_base_patch4_window7_224_22k.pth", - }, - "B-22k-384": { - "window_size": 12, - "embed_dim": 128, - "depth": [2, 2, 18, 2], - "num_heads": [4, 8, 16, 32], - "drop_path_rate": 0.3, - "pretrained": "models/swin_base_patch4_window12_384_22k.pth", - }, - "L-22k": { - "window_size": 7, - "embed_dim": 192, - "depth": [2, 2, 18, 2], - "num_heads": [6, 12, 24, 48], - "drop_path_rate": 0.3, # TODO (xingyi): this is unclear - "pretrained": "models/swin_large_patch4_window7_224_22k.pth", - }, - "L-22k-384": { - "window_size": 12, - "embed_dim": 192, - "depth": [2, 2, 18, 2], - "num_heads": [6, 12, 24, 48], - "drop_path_rate": 0.3, # TODO (xingyi): this is unclear - "pretrained": "models/swin_large_patch4_window12_384_22k.pth", - }, -} - - -@BACKBONE_REGISTRY.register() -def build_swintransformer_backbone(cfg, input_shape): - """ """ - config = size2config[cfg.MODEL.SWIN.SIZE] - out_indices = cfg.MODEL.SWIN.OUT_FEATURES - model = SwinTransformer( - embed_dim=config["embed_dim"], - window_size=config["window_size"], - depths=config["depth"], - num_heads=config["num_heads"], - drop_path_rate=config["drop_path_rate"], - out_indices=out_indices, - frozen_stages=-1, - use_checkpoint=cfg.MODEL.SWIN.USE_CHECKPOINT, - ) - # print('Initializing', config['pretrained']) - model.init_weights(config["pretrained"]) - return model - - -@BACKBONE_REGISTRY.register() -def build_swintransformer_fpn_backbone(cfg, input_shape: ShapeSpec): - """ """ - bottom_up = build_swintransformer_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=LastLevelP6P7_P5(out_channels, out_channels), - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - return backbone - - -@BACKBONE_REGISTRY.register() -def build_swintransformer_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ """ - bottom_up = build_swintransformer_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - backbone = BiFPN( - cfg=cfg, - bottom_up=bottom_up, - in_features=in_features, - out_channels=cfg.MODEL.BIFPN.OUT_CHANNELS, - norm=cfg.MODEL.BIFPN.NORM, - num_levels=cfg.MODEL.BIFPN.NUM_LEVELS, - num_bifpn=cfg.MODEL.BIFPN.NUM_BIFPN, - separable_conv=cfg.MODEL.BIFPN.SEPARABLE_CONV, - ) - return backbone diff --git a/dimos/models/Detic/detic/modeling/backbone/timm.py b/dimos/models/Detic/detic/modeling/backbone/timm.py deleted file mode 100644 index a15e03f875..0000000000 --- a/dimos/models/Detic/detic/modeling/backbone/timm.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. -import copy - -from detectron2.layers.batch_norm import FrozenBatchNorm2d -from detectron2.modeling.backbone import FPN, Backbone -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -import fvcore.nn.weight_init as weight_init -from timm import create_model -from timm.models.convnext import ConvNeXt, checkpoint_filter_fn, default_cfgs -from timm.models.helpers import build_model_with_cfg -from timm.models.registry import register_model -from timm.models.resnet import Bottleneck, ResNet, default_cfgs as default_cfgs_resnet -import torch -from torch import nn -import torch.nn.functional as F - - -@register_model -def convnext_tiny_21k(pretrained: bool=False, **kwargs): - model_args = dict(depths=(3, 3, 9, 3), dims=(96, 192, 384, 768), **kwargs) - cfg = default_cfgs["convnext_tiny"] - cfg["url"] = "https://dl.fbaipublicfiles.com/convnext/convnext_tiny_22k_224.pth" - model = build_model_with_cfg( - ConvNeXt, - "convnext_tiny", - pretrained, - default_cfg=cfg, - pretrained_filter_fn=checkpoint_filter_fn, - feature_cfg=dict(out_indices=(0, 1, 2, 3), flatten_sequential=True), - **model_args, - ) - return model - - -class CustomResNet(ResNet): - def __init__(self, **kwargs) -> None: - self.out_indices = kwargs.pop("out_indices") - super().__init__(**kwargs) - - def forward(self, x): - x = self.conv1(x) - x = self.bn1(x) - x = self.act1(x) - x = self.maxpool(x) - ret = [x] - x = self.layer1(x) - ret.append(x) - x = self.layer2(x) - ret.append(x) - x = self.layer3(x) - ret.append(x) - x = self.layer4(x) - ret.append(x) - return [ret[i] for i in self.out_indices] - - def load_pretrained(self, cached_file) -> None: - data = torch.load(cached_file, map_location="cpu") - if "state_dict" in data: - self.load_state_dict(data["state_dict"]) - else: - self.load_state_dict(data) - - -model_params = { - "resnet50_in21k": dict(block=Bottleneck, layers=[3, 4, 6, 3]), -} - - -def create_timm_resnet(variant, out_indices, pretrained: bool=False, **kwargs): - params = model_params[variant] - default_cfgs_resnet["resnet50_in21k"] = copy.deepcopy(default_cfgs_resnet["resnet50"]) - default_cfgs_resnet["resnet50_in21k"]["url"] = ( - "https://miil-public-eu.oss-eu-central-1.aliyuncs.com/model-zoo/ImageNet_21K_P/models/resnet50_miil_21k.pth" - ) - default_cfgs_resnet["resnet50_in21k"]["num_classes"] = 11221 - - return build_model_with_cfg( - CustomResNet, - variant, - pretrained, - default_cfg=default_cfgs_resnet[variant], - out_indices=out_indices, - pretrained_custom_load=True, - **params, - **kwargs, - ) - - -class LastLevelP6P7_P5(nn.Module): - """ """ - - def __init__(self, in_channels, out_channels) -> None: - super().__init__() - self.num_levels = 2 - self.in_feature = "p5" - self.p6 = nn.Conv2d(in_channels, out_channels, 3, 2, 1) - self.p7 = nn.Conv2d(out_channels, out_channels, 3, 2, 1) - for module in [self.p6, self.p7]: - weight_init.c2_xavier_fill(module) - - def forward(self, c5): - p6 = self.p6(c5) - p7 = self.p7(F.relu(p6)) - return [p6, p7] - - -def freeze_module(x): - """ """ - for p in x.parameters(): - p.requires_grad = False - FrozenBatchNorm2d.convert_frozen_batchnorm(x) - return x - - -class TIMM(Backbone): - def __init__(self, base_name: str, out_levels, freeze_at: int=0, norm: str="FrozenBN", pretrained: bool=False) -> None: - super().__init__() - out_indices = [x - 1 for x in out_levels] - if base_name in model_params: - self.base = create_timm_resnet(base_name, out_indices=out_indices, pretrained=False) - elif "eff" in base_name or "resnet" in base_name or "regnet" in base_name: - self.base = create_model( - base_name, features_only=True, out_indices=out_indices, pretrained=pretrained - ) - elif "convnext" in base_name: - drop_path_rate = 0.2 if ("tiny" in base_name or "small" in base_name) else 0.3 - self.base = create_model( - base_name, - features_only=True, - out_indices=out_indices, - pretrained=pretrained, - drop_path_rate=drop_path_rate, - ) - else: - assert 0, base_name - feature_info = [ - dict(num_chs=f["num_chs"], reduction=f["reduction"]) - for i, f in enumerate(self.base.feature_info) - ] - self._out_features = [f"layer{x}" for x in out_levels] - self._out_feature_channels = { - f"layer{l}": feature_info[l - 1]["num_chs"] for l in out_levels - } - self._out_feature_strides = { - f"layer{l}": feature_info[l - 1]["reduction"] for l in out_levels - } - self._size_divisibility = max(self._out_feature_strides.values()) - if "resnet" in base_name: - self.freeze(freeze_at) - if norm == "FrozenBN": - self = FrozenBatchNorm2d.convert_frozen_batchnorm(self) - - def freeze(self, freeze_at: int=0) -> None: - """ """ - if freeze_at >= 1: - print("Frezing", self.base.conv1) - self.base.conv1 = freeze_module(self.base.conv1) - if freeze_at >= 2: - print("Frezing", self.base.layer1) - self.base.layer1 = freeze_module(self.base.layer1) - - def forward(self, x): - features = self.base(x) - ret = {k: v for k, v in zip(self._out_features, features, strict=False)} - return ret - - @property - def size_divisibility(self): - return self._size_divisibility - - -@BACKBONE_REGISTRY.register() -def build_timm_backbone(cfg, input_shape): - model = TIMM( - cfg.MODEL.TIMM.BASE_NAME, - cfg.MODEL.TIMM.OUT_LEVELS, - freeze_at=cfg.MODEL.TIMM.FREEZE_AT, - norm=cfg.MODEL.TIMM.NORM, - pretrained=cfg.MODEL.TIMM.PRETRAINED, - ) - return model - - -@BACKBONE_REGISTRY.register() -def build_p67_timm_fpn_backbone(cfg, input_shape): - """ """ - bottom_up = build_timm_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=LastLevelP6P7_P5(out_channels, out_channels), - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - return backbone - - -@BACKBONE_REGISTRY.register() -def build_p35_timm_fpn_backbone(cfg, input_shape): - """ """ - bottom_up = build_timm_backbone(cfg, input_shape) - - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=None, - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - return backbone diff --git a/dimos/models/Detic/detic/modeling/debug.py b/dimos/models/Detic/detic/modeling/debug.py deleted file mode 100644 index f37849019e..0000000000 --- a/dimos/models/Detic/detic/modeling/debug.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import os - -import cv2 -import numpy as np -import torch -import torch.nn.functional as F -from typing import Optional, Sequence - -COLORS = ((np.random.rand(1300, 3) * 0.4 + 0.6) * 255).astype(np.uint8).reshape(1300, 1, 1, 3) - - -def _get_color_image(heatmap): - heatmap = heatmap.reshape(heatmap.shape[0], heatmap.shape[1], heatmap.shape[2], 1) - if heatmap.shape[0] == 1: - color_map = ( - (heatmap * np.ones((1, 1, 1, 3), np.uint8) * 255).max(axis=0).astype(np.uint8) - ) # H, W, 3 - else: - color_map = (heatmap * COLORS[: heatmap.shape[0]]).max(axis=0).astype(np.uint8) # H, W, 3 - - return color_map - - -def _blend_image(image, color_map, a: float=0.7): - color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) - ret = np.clip(image * (1 - a) + color_map * a, 0, 255).astype(np.uint8) - return ret - - -def _blend_image_heatmaps(image, color_maps, a: float=0.7): - merges = np.zeros((image.shape[0], image.shape[1], 3), np.float32) - for color_map in color_maps: - color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) - merges = np.maximum(merges, color_map) - ret = np.clip(image * (1 - a) + merges * a, 0, 255).astype(np.uint8) - return ret - - -def _decompose_level(x, shapes_per_level, N): - """ - x: LNHiWi x C - """ - x = x.view(x.shape[0], -1) - ret = [] - st = 0 - for l in range(len(shapes_per_level)): - ret.append([]) - h = shapes_per_level[l][0].int().item() - w = shapes_per_level[l][1].int().item() - for i in range(N): - ret[l].append(x[st + h * w * i : st + h * w * (i + 1)].view(h, w, -1).permute(2, 0, 1)) - st += h * w * N - return ret - - -def _imagelist_to_tensor(images): - images = [x for x in images] - image_sizes = [x.shape[-2:] for x in images] - h = max([size[0] for size in image_sizes]) - w = max([size[1] for size in image_sizes]) - S = 32 - h, w = ((h - 1) // S + 1) * S, ((w - 1) // S + 1) * S - images = [F.pad(x, (0, w - x.shape[2], 0, h - x.shape[1], 0, 0)) for x in images] - images = torch.stack(images) - return images - - -def _ind2il(ind, shapes_per_level, N): - r = ind - l = 0 - S = 0 - while r - S >= N * shapes_per_level[l][0] * shapes_per_level[l][1]: - S += N * shapes_per_level[l][0] * shapes_per_level[l][1] - l += 1 - i = (r - S) // (shapes_per_level[l][0] * shapes_per_level[l][1]) - return i, l - - -def debug_train( - images, - gt_instances, - flattened_hms, - reg_targets, - labels: Sequence[str], - pos_inds, - shapes_per_level, - locations, - strides: Sequence[int], -) -> None: - """ - images: N x 3 x H x W - flattened_hms: LNHiWi x C - shapes_per_level: L x 2 [(H_i, W_i)] - locations: LNHiWi x 2 - """ - reg_inds = torch.nonzero(reg_targets.max(dim=1)[0] > 0).squeeze(1) - N = len(images) - images = _imagelist_to_tensor(images) - repeated_locations = [torch.cat([loc] * N, dim=0) for loc in locations] - locations = torch.cat(repeated_locations, dim=0) - gt_hms = _decompose_level(flattened_hms, shapes_per_level, N) - masks = flattened_hms.new_zeros((flattened_hms.shape[0], 1)) - masks[pos_inds] = 1 - masks = _decompose_level(masks, shapes_per_level, N) - for i in range(len(images)): - image = images[i].detach().cpu().numpy().transpose(1, 2, 0) - color_maps = [] - for l in range(len(gt_hms)): - color_map = _get_color_image(gt_hms[l][i].detach().cpu().numpy()) - color_maps.append(color_map) - cv2.imshow(f"gthm_{l}", color_map) - blend = _blend_image_heatmaps(image.copy(), color_maps) - if gt_instances is not None: - bboxes = gt_instances[i].gt_boxes.tensor - for j in range(len(bboxes)): - bbox = bboxes[j] - cv2.rectangle( - blend, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - (0, 0, 255), - 3, - cv2.LINE_AA, - ) - - for j in range(len(pos_inds)): - image_id, l = _ind2il(pos_inds[j], shapes_per_level, N) - if image_id != i: - continue - loc = locations[pos_inds[j]] - cv2.drawMarker( - blend, (int(loc[0]), int(loc[1])), (0, 255, 255), markerSize=(l + 1) * 16 - ) - - for j in range(len(reg_inds)): - image_id, l = _ind2il(reg_inds[j], shapes_per_level, N) - if image_id != i: - continue - ltrb = reg_targets[reg_inds[j]] - ltrb *= strides[l] - loc = locations[reg_inds[j]] - bbox = [(loc[0] - ltrb[0]), (loc[1] - ltrb[1]), (loc[0] + ltrb[2]), (loc[1] + ltrb[3])] - cv2.rectangle( - blend, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - (255, 0, 0), - 1, - cv2.LINE_AA, - ) - cv2.circle(blend, (int(loc[0]), int(loc[1])), 2, (255, 0, 0), -1) - - cv2.imshow("blend", blend) - cv2.waitKey() - - -def debug_test( - images, - logits_pred, - reg_pred, - agn_hm_pred=None, - preds=None, - vis_thresh: float=0.3, - debug_show_name: bool=False, - mult_agn: bool=False, -) -> None: - """ - images: N x 3 x H x W - class_target: LNHiWi x C - cat_agn_heatmap: LNHiWi - shapes_per_level: L x 2 [(H_i, W_i)] - """ - if preds is None: - preds = [] - if agn_hm_pred is None: - agn_hm_pred = [] - len(images) - for i in range(len(images)): - image = images[i].detach().cpu().numpy().transpose(1, 2, 0) - image.copy().astype(np.uint8) - pred_image = image.copy().astype(np.uint8) - color_maps = [] - L = len(logits_pred) - for l in range(L): - if logits_pred[0] is not None: - stride = min(image.shape[0], image.shape[1]) / min( - logits_pred[l][i].shape[1], logits_pred[l][i].shape[2] - ) - else: - stride = min(image.shape[0], image.shape[1]) / min( - agn_hm_pred[l][i].shape[1], agn_hm_pred[l][i].shape[2] - ) - stride = stride if stride < 60 else 64 if stride < 100 else 128 - if logits_pred[0] is not None: - if mult_agn: - logits_pred[l][i] = logits_pred[l][i] * agn_hm_pred[l][i] - color_map = _get_color_image(logits_pred[l][i].detach().cpu().numpy()) - color_maps.append(color_map) - cv2.imshow(f"predhm_{l}", color_map) - - if debug_show_name: - from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - - cat2name = [x["name"] for x in LVIS_CATEGORIES] - for j in range(len(preds[i].scores) if preds is not None else 0): - if preds[i].scores[j] > vis_thresh: - bbox = ( - preds[i].proposal_boxes[j] - if preds[i].has("proposal_boxes") - else preds[i].pred_boxes[j] - ) - bbox = bbox.tensor[0].detach().cpu().numpy().astype(np.int32) - cat = int(preds[i].pred_classes[j]) if preds[i].has("pred_classes") else 0 - cl = COLORS[cat, 0, 0] - cv2.rectangle( - pred_image, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - (int(cl[0]), int(cl[1]), int(cl[2])), - 2, - cv2.LINE_AA, - ) - if debug_show_name: - txt = "{}{:.1f}".format( - cat2name[cat] if cat > 0 else "", preds[i].scores[j] - ) - font = cv2.FONT_HERSHEY_SIMPLEX - cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] - cv2.rectangle( - pred_image, - (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), - -1, - ) - cv2.putText( - pred_image, - txt, - (int(bbox[0]), int(bbox[1] - 2)), - font, - 0.5, - (0, 0, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - - if agn_hm_pred[l] is not None: - agn_hm_ = agn_hm_pred[l][i, 0, :, :, None].detach().cpu().numpy() - agn_hm_ = (agn_hm_ * np.array([255, 255, 255]).reshape(1, 1, 3)).astype(np.uint8) - cv2.imshow(f"agn_hm_{l}", agn_hm_) - blend = _blend_image_heatmaps(image.copy(), color_maps) - cv2.imshow("blend", blend) - cv2.imshow("preds", pred_image) - cv2.waitKey() - - -global cnt -cnt = 0 - - -def debug_second_stage( - images, - instances, - proposals=None, - vis_thresh: float=0.3, - save_debug: bool=False, - debug_show_name: bool=False, - image_labels: Optional[Sequence[str]]=None, - save_debug_path: str="output/save_debug/", - bgr: bool=False, -) -> None: - if image_labels is None: - image_labels = [] - images = _imagelist_to_tensor(images) - if "COCO" in save_debug_path: - from detectron2.data.datasets.builtin_meta import COCO_CATEGORIES - - cat2name = [x["name"] for x in COCO_CATEGORIES] - else: - from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - - cat2name = ["({}){}".format(x["frequency"], x["name"]) for x in LVIS_CATEGORIES] - for i in range(len(images)): - image = images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() - if bgr: - image = image[:, :, ::-1].copy() - if instances[i].has("gt_boxes"): - bboxes = instances[i].gt_boxes.tensor.cpu().numpy() - scores = np.ones(bboxes.shape[0]) - cats = instances[i].gt_classes.cpu().numpy() - else: - bboxes = instances[i].pred_boxes.tensor.cpu().numpy() - scores = instances[i].scores.cpu().numpy() - cats = instances[i].pred_classes.cpu().numpy() - for j in range(len(bboxes)): - if scores[j] > vis_thresh: - bbox = bboxes[j] - cl = COLORS[cats[j], 0, 0] - cl = (int(cl[0]), int(cl[1]), int(cl[2])) - cv2.rectangle( - image, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - cl, - 2, - cv2.LINE_AA, - ) - if debug_show_name: - cat = cats[j] - txt = "{}{:.1f}".format(cat2name[cat] if cat > 0 else "", scores[j]) - font = cv2.FONT_HERSHEY_SIMPLEX - cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] - cv2.rectangle( - image, - (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), - -1, - ) - cv2.putText( - image, - txt, - (int(bbox[0]), int(bbox[1] - 2)), - font, - 0.5, - (0, 0, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - if proposals is not None: - proposal_image = ( - images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() - ) - if bgr: - proposal_image = proposal_image.copy() - else: - proposal_image = proposal_image[:, :, ::-1].copy() - bboxes = proposals[i].proposal_boxes.tensor.cpu().numpy() - if proposals[i].has("scores"): - scores = proposals[i].scores.detach().cpu().numpy() - else: - scores = proposals[i].objectness_logits.detach().cpu().numpy() - # selected = -1 - # if proposals[i].has('image_loss'): - # selected = proposals[i].image_loss.argmin() - if proposals[i].has("selected"): - selected = proposals[i].selected - else: - selected = [-1 for _ in range(len(bboxes))] - for j in range(len(bboxes)): - if scores[j] > vis_thresh or selected[j] >= 0: - bbox = bboxes[j] - cl = (209, 159, 83) - th = 2 - if selected[j] >= 0: - cl = (0, 0, 0xA4) - th = 4 - cv2.rectangle( - proposal_image, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - cl, - th, - cv2.LINE_AA, - ) - if selected[j] >= 0 and debug_show_name: - cat = selected[j].item() - txt = f"{cat2name[cat]}" - font = cv2.FONT_HERSHEY_SIMPLEX - cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] - cv2.rectangle( - proposal_image, - (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), - -1, - ) - cv2.putText( - proposal_image, - txt, - (int(bbox[0]), int(bbox[1] - 2)), - font, - 0.5, - (0, 0, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - - if save_debug: - global cnt - cnt = (cnt + 1) % 5000 - if not os.path.exists(save_debug_path): - os.mkdir(save_debug_path) - save_name = f"{save_debug_path}/{cnt:05d}.jpg" - if i < len(image_labels): - image_label = image_labels[i] - save_name = f"{save_debug_path}/{cnt:05d}" - for x in image_label: - class_name = cat2name[x] - save_name = save_name + f"|{class_name}" - save_name = save_name + ".jpg" - cv2.imwrite(save_name, proposal_image) - else: - cv2.imshow("image", image) - if proposals is not None: - cv2.imshow("proposals", proposal_image) - cv2.waitKey() diff --git a/dimos/models/Detic/detic/modeling/meta_arch/custom_rcnn.py b/dimos/models/Detic/detic/modeling/meta_arch/custom_rcnn.py deleted file mode 100644 index 872084f7cb..0000000000 --- a/dimos/models/Detic/detic/modeling/meta_arch/custom_rcnn.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from typing import Dict, List, Optional, Tuple - -from detectron2.config import configurable -from detectron2.modeling.meta_arch.build import META_ARCH_REGISTRY -from detectron2.modeling.meta_arch.rcnn import GeneralizedRCNN -from detectron2.structures import Instances -import detectron2.utils.comm as comm -from detectron2.utils.events import get_event_storage -import torch -from torch.cuda.amp import autocast - -from ..text.text_encoder import build_text_encoder -from ..utils import get_fed_loss_inds, load_class_freq - - -@META_ARCH_REGISTRY.register() -class CustomRCNN(GeneralizedRCNN): - """ - Add image labels - """ - - @configurable - def __init__( - self, - with_image_labels: bool=False, - dataset_loss_weight=None, - fp16: bool=False, - sync_caption_batch: bool=False, - roi_head_name: str="", - cap_batch_ratio: int=4, - with_caption: bool=False, - dynamic_classifier: bool=False, - **kwargs, - ) -> None: - """ """ - if dataset_loss_weight is None: - dataset_loss_weight = [] - self.with_image_labels = with_image_labels - self.dataset_loss_weight = dataset_loss_weight - self.fp16 = fp16 - self.with_caption = with_caption - self.sync_caption_batch = sync_caption_batch - self.roi_head_name = roi_head_name - self.cap_batch_ratio = cap_batch_ratio - self.dynamic_classifier = dynamic_classifier - self.return_proposal = False - if self.dynamic_classifier: - self.freq_weight = kwargs.pop("freq_weight") - self.num_classes = kwargs.pop("num_classes") - self.num_sample_cats = kwargs.pop("num_sample_cats") - super().__init__(**kwargs) - assert self.proposal_generator is not None - if self.with_caption: - assert not self.dynamic_classifier - self.text_encoder = build_text_encoder(pretrain=True) - for v in self.text_encoder.parameters(): - v.requires_grad = False - - @classmethod - def from_config(cls, cfg): - ret = super().from_config(cfg) - ret.update( - { - "with_image_labels": cfg.WITH_IMAGE_LABELS, - "dataset_loss_weight": cfg.MODEL.DATASET_LOSS_WEIGHT, - "fp16": cfg.FP16, - "with_caption": cfg.MODEL.WITH_CAPTION, - "sync_caption_batch": cfg.MODEL.SYNC_CAPTION_BATCH, - "dynamic_classifier": cfg.MODEL.DYNAMIC_CLASSIFIER, - "roi_head_name": cfg.MODEL.ROI_HEADS.NAME, - "cap_batch_ratio": cfg.MODEL.CAP_BATCH_RATIO, - } - ) - if ret["dynamic_classifier"]: - ret["freq_weight"] = load_class_freq( - cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT - ) - ret["num_classes"] = cfg.MODEL.ROI_HEADS.NUM_CLASSES - ret["num_sample_cats"] = cfg.MODEL.NUM_SAMPLE_CATS - return ret - - def inference( - self, - batched_inputs: tuple[dict[str, torch.Tensor]], - detected_instances: list[Instances] | None = None, - do_postprocess: bool = True, - ): - assert not self.training - assert detected_instances is None - - images = self.preprocess_image(batched_inputs) - features = self.backbone(images.tensor) - proposals, _ = self.proposal_generator(images, features, None) - results, _ = self.roi_heads(images, features, proposals) - if do_postprocess: - assert not torch.jit.is_scripting(), "Scripting is not supported for postprocess." - return CustomRCNN._postprocess(results, batched_inputs, images.image_sizes) - else: - return results - - def forward(self, batched_inputs: list[dict[str, torch.Tensor]]): - """ - Add ann_type - Ignore proposal loss when training with image labels - """ - if not self.training: - return self.inference(batched_inputs) - - images = self.preprocess_image(batched_inputs) - - ann_type = "box" - gt_instances = [x["instances"].to(self.device) for x in batched_inputs] - if self.with_image_labels: - for inst, x in zip(gt_instances, batched_inputs, strict=False): - inst._ann_type = x["ann_type"] - inst._pos_category_ids = x["pos_category_ids"] - ann_types = [x["ann_type"] for x in batched_inputs] - assert len(set(ann_types)) == 1 - ann_type = ann_types[0] - if ann_type in ["prop", "proptag"]: - for t in gt_instances: - t.gt_classes *= 0 - - if self.fp16: # TODO (zhouxy): improve - with autocast(): - features = self.backbone(images.tensor.half()) - features = {k: v.float() for k, v in features.items()} - else: - features = self.backbone(images.tensor) - - cls_features, cls_inds, caption_features = None, None, None - - if self.with_caption and "caption" in ann_type: - inds = [torch.randint(len(x["captions"]), (1,))[0].item() for x in batched_inputs] - caps = [x["captions"][ind] for ind, x in zip(inds, batched_inputs, strict=False)] - caption_features = self.text_encoder(caps).float() - if self.sync_caption_batch: - caption_features = self._sync_caption_features( - caption_features, ann_type, len(batched_inputs) - ) - - if self.dynamic_classifier and ann_type != "caption": - cls_inds = self._sample_cls_inds(gt_instances, ann_type) # inds, inv_inds - ind_with_bg = [*cls_inds[0].tolist(), -1] - cls_features = ( - self.roi_heads.box_predictor[0] - .cls_score.zs_weight[:, ind_with_bg] - .permute(1, 0) - .contiguous() - ) - - classifier_info = cls_features, cls_inds, caption_features - proposals, proposal_losses = self.proposal_generator(images, features, gt_instances) - - if self.roi_head_name in ["StandardROIHeads", "CascadeROIHeads"]: - proposals, detector_losses = self.roi_heads(images, features, proposals, gt_instances) - else: - proposals, detector_losses = self.roi_heads( - images, - features, - proposals, - gt_instances, - ann_type=ann_type, - classifier_info=classifier_info, - ) - - if self.vis_period > 0: - storage = get_event_storage() - if storage.iter % self.vis_period == 0: - self.visualize_training(batched_inputs, proposals) - - losses = {} - losses.update(detector_losses) - if self.with_image_labels: - if ann_type in ["box", "prop", "proptag"]: - losses.update(proposal_losses) - else: # ignore proposal loss for non-bbox data - losses.update({k: v * 0 for k, v in proposal_losses.items()}) - else: - losses.update(proposal_losses) - if len(self.dataset_loss_weight) > 0: - dataset_sources = [x["dataset_source"] for x in batched_inputs] - assert len(set(dataset_sources)) == 1 - dataset_source = dataset_sources[0] - for k in losses: - losses[k] *= self.dataset_loss_weight[dataset_source] - - if self.return_proposal: - return proposals, losses - else: - return losses - - def _sync_caption_features(self, caption_features, ann_type, BS): - has_caption_feature = caption_features is not None - BS = (BS * self.cap_batch_ratio) if (ann_type == "box") else BS - rank = torch.full((BS, 1), comm.get_rank(), dtype=torch.float32, device=self.device) - if not has_caption_feature: - caption_features = rank.new_zeros((BS, 512)) - caption_features = torch.cat([caption_features, rank], dim=1) - global_caption_features = comm.all_gather(caption_features) - caption_features = ( - torch.cat([x.to(self.device) for x in global_caption_features], dim=0) - if has_caption_feature - else None - ) # (NB) x (D + 1) - return caption_features - - def _sample_cls_inds(self, gt_instances, ann_type: str="box"): - if ann_type == "box": - gt_classes = torch.cat([x.gt_classes for x in gt_instances]) - C = len(self.freq_weight) - freq_weight = self.freq_weight - else: - gt_classes = torch.cat( - [ - torch.tensor(x._pos_category_ids, dtype=torch.long, device=x.gt_classes.device) - for x in gt_instances - ] - ) - C = self.num_classes - freq_weight = None - assert gt_classes.max() < C, f"{gt_classes.max()} {C}" - inds = get_fed_loss_inds(gt_classes, self.num_sample_cats, C, weight=freq_weight) - cls_id_map = gt_classes.new_full((self.num_classes + 1,), len(inds)) - cls_id_map[inds] = torch.arange(len(inds), device=cls_id_map.device) - return inds, cls_id_map diff --git a/dimos/models/Detic/detic/modeling/meta_arch/d2_deformable_detr.py b/dimos/models/Detic/detic/modeling/meta_arch/d2_deformable_detr.py deleted file mode 100644 index 9c2ec8e81e..0000000000 --- a/dimos/models/Detic/detic/modeling/meta_arch/d2_deformable_detr.py +++ /dev/null @@ -1,318 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from detectron2.modeling import META_ARCH_REGISTRY, build_backbone -from detectron2.structures import Boxes, Instances -from models.backbone import Joiner -from models.deformable_detr import DeformableDETR, SetCriterion -from models.deformable_transformer import DeformableTransformer -from models.matcher import HungarianMatcher -from models.position_encoding import PositionEmbeddingSine -from models.segmentation import sigmoid_focal_loss -import torch -from torch import nn -import torch.nn.functional as F -from util.box_ops import box_cxcywh_to_xyxy, box_xyxy_to_cxcywh -from util.misc import NestedTensor, accuracy - -from ..utils import get_fed_loss_inds, load_class_freq -from typing import Sequence - -__all__ = ["DeformableDetr"] - - -class CustomSetCriterion(SetCriterion): - def __init__( - self, num_classes: int, matcher, weight_dict, losses, focal_alpha: float=0.25, use_fed_loss: bool=False - ) -> None: - super().__init__(num_classes, matcher, weight_dict, losses, focal_alpha) - self.use_fed_loss = use_fed_loss - if self.use_fed_loss: - self.register_buffer("fed_loss_weight", load_class_freq(freq_weight=0.5)) - - def loss_labels(self, outputs, targets, indices, num_boxes: int, log: bool=True): - """Classification loss (NLL) - targets dicts must contain the key "labels" containing a tensor of dim [nb_target_boxes] - """ - assert "pred_logits" in outputs - src_logits = outputs["pred_logits"] - - idx = self._get_src_permutation_idx(indices) - target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices, strict=False)]) - target_classes = torch.full( - src_logits.shape[:2], self.num_classes, dtype=torch.int64, device=src_logits.device - ) - target_classes[idx] = target_classes_o - - target_classes_onehot = torch.zeros( - [src_logits.shape[0], src_logits.shape[1], src_logits.shape[2] + 1], - dtype=src_logits.dtype, - layout=src_logits.layout, - device=src_logits.device, - ) - target_classes_onehot.scatter_(2, target_classes.unsqueeze(-1), 1) - - target_classes_onehot = target_classes_onehot[:, :, :-1] # B x N x C - if self.use_fed_loss: - inds = get_fed_loss_inds( - gt_classes=target_classes_o, - num_sample_cats=50, - weight=self.fed_loss_weight, - C=target_classes_onehot.shape[2], - ) - loss_ce = ( - sigmoid_focal_loss( - src_logits[:, :, inds], - target_classes_onehot[:, :, inds], - num_boxes, - alpha=self.focal_alpha, - gamma=2, - ) - * src_logits.shape[1] - ) - else: - loss_ce = ( - sigmoid_focal_loss( - src_logits, target_classes_onehot, num_boxes, alpha=self.focal_alpha, gamma=2 - ) - * src_logits.shape[1] - ) - losses = {"loss_ce": loss_ce} - - if log: - # TODO this should probably be a separate loss, not hacked in this one here - losses["class_error"] = 100 - accuracy(src_logits[idx], target_classes_o)[0] - return losses - - -class MaskedBackbone(nn.Module): - """This is a thin wrapper around D2's backbone to provide padding masking""" - - def __init__(self, cfg) -> None: - super().__init__() - self.backbone = build_backbone(cfg) - backbone_shape = self.backbone.output_shape() - self.feature_strides = [backbone_shape[f].stride for f in backbone_shape.keys()] - self.strides = [backbone_shape[f].stride for f in backbone_shape.keys()] - self.num_channels = [backbone_shape[x].channels for x in backbone_shape.keys()] - - def forward(self, tensor_list: NestedTensor): - xs = self.backbone(tensor_list.tensors) - out = {} - for name, x in xs.items(): - m = tensor_list.mask - assert m is not None - mask = F.interpolate(m[None].float(), size=x.shape[-2:]).to(torch.bool)[0] - out[name] = NestedTensor(x, mask) - return out - - -@META_ARCH_REGISTRY.register() -class DeformableDetr(nn.Module): - """ - Implement Deformable Detr - """ - - def __init__(self, cfg) -> None: - super().__init__() - self.with_image_labels = cfg.WITH_IMAGE_LABELS - self.weak_weight = cfg.MODEL.DETR.WEAK_WEIGHT - - self.device = torch.device(cfg.MODEL.DEVICE) - self.test_topk = cfg.TEST.DETECTIONS_PER_IMAGE - self.num_classes = cfg.MODEL.DETR.NUM_CLASSES - self.mask_on = cfg.MODEL.MASK_ON - hidden_dim = cfg.MODEL.DETR.HIDDEN_DIM - num_queries = cfg.MODEL.DETR.NUM_OBJECT_QUERIES - - # Transformer parameters: - nheads = cfg.MODEL.DETR.NHEADS - dropout = cfg.MODEL.DETR.DROPOUT - dim_feedforward = cfg.MODEL.DETR.DIM_FEEDFORWARD - enc_layers = cfg.MODEL.DETR.ENC_LAYERS - dec_layers = cfg.MODEL.DETR.DEC_LAYERS - num_feature_levels = cfg.MODEL.DETR.NUM_FEATURE_LEVELS - two_stage = cfg.MODEL.DETR.TWO_STAGE - with_box_refine = cfg.MODEL.DETR.WITH_BOX_REFINE - - # Loss parameters: - giou_weight = cfg.MODEL.DETR.GIOU_WEIGHT - l1_weight = cfg.MODEL.DETR.L1_WEIGHT - deep_supervision = cfg.MODEL.DETR.DEEP_SUPERVISION - cls_weight = cfg.MODEL.DETR.CLS_WEIGHT - focal_alpha = cfg.MODEL.DETR.FOCAL_ALPHA - - N_steps = hidden_dim // 2 - d2_backbone = MaskedBackbone(cfg) - backbone = Joiner(d2_backbone, PositionEmbeddingSine(N_steps, normalize=True)) - - transformer = DeformableTransformer( - d_model=hidden_dim, - nhead=nheads, - num_encoder_layers=enc_layers, - num_decoder_layers=dec_layers, - dim_feedforward=dim_feedforward, - dropout=dropout, - activation="relu", - return_intermediate_dec=True, - num_feature_levels=num_feature_levels, - dec_n_points=4, - enc_n_points=4, - two_stage=two_stage, - two_stage_num_proposals=num_queries, - ) - - self.detr = DeformableDETR( - backbone, - transformer, - num_classes=self.num_classes, - num_queries=num_queries, - num_feature_levels=num_feature_levels, - aux_loss=deep_supervision, - with_box_refine=with_box_refine, - two_stage=two_stage, - ) - - if self.mask_on: - assert 0, "Mask is not supported yet :(" - - matcher = HungarianMatcher( - cost_class=cls_weight, cost_bbox=l1_weight, cost_giou=giou_weight - ) - weight_dict = {"loss_ce": cls_weight, "loss_bbox": l1_weight} - weight_dict["loss_giou"] = giou_weight - if deep_supervision: - aux_weight_dict = {} - for i in range(dec_layers - 1): - aux_weight_dict.update({k + f"_{i}": v for k, v in weight_dict.items()}) - weight_dict.update(aux_weight_dict) - print("weight_dict", weight_dict) - losses = ["labels", "boxes", "cardinality"] - if self.mask_on: - losses += ["masks"] - self.criterion = CustomSetCriterion( - self.num_classes, - matcher=matcher, - weight_dict=weight_dict, - focal_alpha=focal_alpha, - losses=losses, - use_fed_loss=cfg.MODEL.DETR.USE_FED_LOSS, - ) - pixel_mean = torch.Tensor(cfg.MODEL.PIXEL_MEAN).to(self.device).view(3, 1, 1) - pixel_std = torch.Tensor(cfg.MODEL.PIXEL_STD).to(self.device).view(3, 1, 1) - self.normalizer = lambda x: (x - pixel_mean) / pixel_std - - def forward(self, batched_inputs): - """ - Args: - Returns: - dict[str: Tensor]: - mapping from a named loss to a tensor storing the loss. Used during training only. - """ - images = self.preprocess_image(batched_inputs) - output = self.detr(images) - if self.training: - gt_instances = [x["instances"].to(self.device) for x in batched_inputs] - targets = self.prepare_targets(gt_instances) - loss_dict = self.criterion(output, targets) - weight_dict = self.criterion.weight_dict - for k in loss_dict.keys(): - if k in weight_dict: - loss_dict[k] *= weight_dict[k] - if self.with_image_labels: - if batched_inputs[0]["ann_type"] in ["image", "captiontag"]: - loss_dict["loss_image"] = self.weak_weight * self._weak_loss( - output, batched_inputs - ) - else: - loss_dict["loss_image"] = images[0].new_zeros([1], dtype=torch.float32)[0] - # import pdb; pdb.set_trace() - return loss_dict - else: - image_sizes = output["pred_boxes"].new_tensor( - [(t["height"], t["width"]) for t in batched_inputs] - ) - results = self.post_process(output, image_sizes) - return results - - def prepare_targets(self, targets): - new_targets = [] - for targets_per_image in targets: - h, w = targets_per_image.image_size - image_size_xyxy = torch.as_tensor([w, h, w, h], dtype=torch.float, device=self.device) - gt_classes = targets_per_image.gt_classes - gt_boxes = targets_per_image.gt_boxes.tensor / image_size_xyxy - gt_boxes = box_xyxy_to_cxcywh(gt_boxes) - new_targets.append({"labels": gt_classes, "boxes": gt_boxes}) - if self.mask_on and hasattr(targets_per_image, "gt_masks"): - assert 0, "Mask is not supported yet :(" - gt_masks = targets_per_image.gt_masks - gt_masks = convert_coco_poly_to_mask(gt_masks.polygons, h, w) - new_targets[-1].update({"masks": gt_masks}) - return new_targets - - def post_process(self, outputs, target_sizes: Sequence[int]): - """ """ - out_logits, out_bbox = outputs["pred_logits"], outputs["pred_boxes"] - assert len(out_logits) == len(target_sizes) - assert target_sizes.shape[1] == 2 - - prob = out_logits.sigmoid() - topk_values, topk_indexes = torch.topk( - prob.view(out_logits.shape[0], -1), self.test_topk, dim=1 - ) - scores = topk_values - topk_boxes = topk_indexes // out_logits.shape[2] - labels = topk_indexes % out_logits.shape[2] - boxes = box_cxcywh_to_xyxy(out_bbox) - boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1, 1, 4)) - - # and from relative [0, 1] to absolute [0, height] coordinates - img_h, img_w = target_sizes.unbind(1) - scale_fct = torch.stack([img_w, img_h, img_w, img_h], dim=1) - boxes = boxes * scale_fct[:, None, :] - - results = [] - for s, l, b, size in zip(scores, labels, boxes, target_sizes, strict=False): - r = Instances((size[0], size[1])) - r.pred_boxes = Boxes(b) - r.scores = s - r.pred_classes = l - results.append({"instances": r}) - return results - - def preprocess_image(self, batched_inputs): - """ - Normalize, pad and batch the input images. - """ - images = [self.normalizer(x["image"].to(self.device)) for x in batched_inputs] - return images - - def _weak_loss(self, outputs, batched_inputs): - loss = 0 - for b, x in enumerate(batched_inputs): - labels = x["pos_category_ids"] - pred_logits = [outputs["pred_logits"][b]] - pred_boxes = [outputs["pred_boxes"][b]] - for xx in outputs["aux_outputs"]: - pred_logits.append(xx["pred_logits"][b]) - pred_boxes.append(xx["pred_boxes"][b]) - pred_logits = torch.stack(pred_logits, dim=0) # L x N x C - pred_boxes = torch.stack(pred_boxes, dim=0) # L x N x 4 - for label in labels: - loss += self._max_size_loss(pred_logits, pred_boxes, label) / len(labels) - loss = loss / len(batched_inputs) - return loss - - def _max_size_loss(self, logits, boxes, label: str): - """ - Inputs: - logits: L x N x C - boxes: L x N x 4 - """ - target = logits.new_zeros((logits.shape[0], logits.shape[2])) - target[:, label] = 1.0 - sizes = boxes[..., 2] * boxes[..., 3] # L x N - ind = sizes.argmax(dim=1) # L - loss = F.binary_cross_entropy_with_logits( - logits[range(len(ind)), ind], target, reduction="sum" - ) - return loss diff --git a/dimos/models/Detic/detic/modeling/roi_heads/detic_fast_rcnn.py b/dimos/models/Detic/detic/modeling/roi_heads/detic_fast_rcnn.py deleted file mode 100644 index aaa7ca233e..0000000000 --- a/dimos/models/Detic/detic/modeling/roi_heads/detic_fast_rcnn.py +++ /dev/null @@ -1,569 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import math - -from detectron2.config import configurable -from detectron2.layers import ShapeSpec, cat, nonzero_tuple -from detectron2.modeling.roi_heads.fast_rcnn import ( - FastRCNNOutputLayers, - _log_classification_stats, - fast_rcnn_inference, -) -import detectron2.utils.comm as comm -from detectron2.utils.events import get_event_storage -from fvcore.nn import giou_loss, smooth_l1_loss -import fvcore.nn.weight_init as weight_init -import torch -from torch import nn -from torch.nn import functional as F - -from ..utils import get_fed_loss_inds, load_class_freq -from .zero_shot_classifier import ZeroShotClassifier -from typing import Sequence - -__all__ = ["DeticFastRCNNOutputLayers"] - - -class DeticFastRCNNOutputLayers(FastRCNNOutputLayers): - @configurable - def __init__( - self, - input_shape: ShapeSpec, - *, - mult_proposal_score: bool=False, - cls_score=None, - sync_caption_batch: bool=False, - use_sigmoid_ce: bool=False, - use_fed_loss: bool=False, - ignore_zero_cats: bool=False, - fed_loss_num_cat: int=50, - dynamic_classifier: bool=False, - image_label_loss: str="", - use_zeroshot_cls: bool=False, - image_loss_weight: float=0.1, - with_softmax_prop: bool=False, - caption_weight: float=1.0, - neg_cap_weight: float=1.0, - add_image_box: bool=False, - debug: bool=False, - prior_prob: float=0.01, - cat_freq_path: str="", - fed_loss_freq_weight: float=0.5, - softmax_weak_loss: bool=False, - **kwargs, - ) -> None: - super().__init__( - input_shape=input_shape, - **kwargs, - ) - self.mult_proposal_score = mult_proposal_score - self.sync_caption_batch = sync_caption_batch - self.use_sigmoid_ce = use_sigmoid_ce - self.use_fed_loss = use_fed_loss - self.ignore_zero_cats = ignore_zero_cats - self.fed_loss_num_cat = fed_loss_num_cat - self.dynamic_classifier = dynamic_classifier - self.image_label_loss = image_label_loss - self.use_zeroshot_cls = use_zeroshot_cls - self.image_loss_weight = image_loss_weight - self.with_softmax_prop = with_softmax_prop - self.caption_weight = caption_weight - self.neg_cap_weight = neg_cap_weight - self.add_image_box = add_image_box - self.softmax_weak_loss = softmax_weak_loss - self.debug = debug - - if softmax_weak_loss: - assert image_label_loss in ["max_size"] - - if self.use_sigmoid_ce: - bias_value = -math.log((1 - prior_prob) / prior_prob) - nn.init.constant_(self.cls_score.bias, bias_value) - - if self.use_fed_loss or self.ignore_zero_cats: - freq_weight = load_class_freq(cat_freq_path, fed_loss_freq_weight) - self.register_buffer("freq_weight", freq_weight) - else: - self.freq_weight = None - - if self.use_fed_loss and len(self.freq_weight) < self.num_classes: - # assert self.num_classes == 11493 - print("Extending federated loss weight") - self.freq_weight = torch.cat( - [ - self.freq_weight, - self.freq_weight.new_zeros(self.num_classes - len(self.freq_weight)), - ] - ) - - assert (not self.dynamic_classifier) or (not self.use_fed_loss) - input_size = input_shape.channels * (input_shape.width or 1) * (input_shape.height or 1) - - if self.use_zeroshot_cls: - del self.cls_score - del self.bbox_pred - assert cls_score is not None - self.cls_score = cls_score - self.bbox_pred = nn.Sequential( - nn.Linear(input_size, input_size), nn.ReLU(inplace=True), nn.Linear(input_size, 4) - ) - weight_init.c2_xavier_fill(self.bbox_pred[0]) - nn.init.normal_(self.bbox_pred[-1].weight, std=0.001) - nn.init.constant_(self.bbox_pred[-1].bias, 0) - - if self.with_softmax_prop: - self.prop_score = nn.Sequential( - nn.Linear(input_size, input_size), - nn.ReLU(inplace=True), - nn.Linear(input_size, self.num_classes + 1), - ) - weight_init.c2_xavier_fill(self.prop_score[0]) - nn.init.normal_(self.prop_score[-1].weight, mean=0, std=0.001) - nn.init.constant_(self.prop_score[-1].bias, 0) - - @classmethod - def from_config(cls, cfg, input_shape): - ret = super().from_config(cfg, input_shape) - ret.update( - { - "mult_proposal_score": cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE, - "sync_caption_batch": cfg.MODEL.SYNC_CAPTION_BATCH, - "use_sigmoid_ce": cfg.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE, - "use_fed_loss": cfg.MODEL.ROI_BOX_HEAD.USE_FED_LOSS, - "ignore_zero_cats": cfg.MODEL.ROI_BOX_HEAD.IGNORE_ZERO_CATS, - "fed_loss_num_cat": cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT, - "dynamic_classifier": cfg.MODEL.DYNAMIC_CLASSIFIER, - "image_label_loss": cfg.MODEL.ROI_BOX_HEAD.IMAGE_LABEL_LOSS, - "use_zeroshot_cls": cfg.MODEL.ROI_BOX_HEAD.USE_ZEROSHOT_CLS, - "image_loss_weight": cfg.MODEL.ROI_BOX_HEAD.IMAGE_LOSS_WEIGHT, - "with_softmax_prop": cfg.MODEL.ROI_BOX_HEAD.WITH_SOFTMAX_PROP, - "caption_weight": cfg.MODEL.ROI_BOX_HEAD.CAPTION_WEIGHT, - "neg_cap_weight": cfg.MODEL.ROI_BOX_HEAD.NEG_CAP_WEIGHT, - "add_image_box": cfg.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX, - "debug": cfg.DEBUG or cfg.SAVE_DEBUG or cfg.IS_DEBUG, - "prior_prob": cfg.MODEL.ROI_BOX_HEAD.PRIOR_PROB, - "cat_freq_path": cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, - "fed_loss_freq_weight": cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT, - "softmax_weak_loss": cfg.MODEL.ROI_BOX_HEAD.SOFTMAX_WEAK_LOSS, - } - ) - if ret["use_zeroshot_cls"]: - ret["cls_score"] = ZeroShotClassifier(cfg, input_shape) - return ret - - def losses( - self, predictions, proposals, use_advanced_loss: bool=True, classifier_info=(None, None, None) - ): - """ - enable advanced loss - """ - scores, proposal_deltas = predictions - gt_classes = ( - cat([p.gt_classes for p in proposals], dim=0) if len(proposals) else torch.empty(0) - ) - num_classes = self.num_classes - if self.dynamic_classifier: - _, cls_id_map = classifier_info[1] - gt_classes = cls_id_map[gt_classes] - num_classes = scores.shape[1] - 1 - assert cls_id_map[self.num_classes] == num_classes - _log_classification_stats(scores, gt_classes) - - if len(proposals): - proposal_boxes = cat([p.proposal_boxes.tensor for p in proposals], dim=0) # Nx4 - assert not proposal_boxes.requires_grad, "Proposals should not require gradients!" - gt_boxes = cat( - [(p.gt_boxes if p.has("gt_boxes") else p.proposal_boxes).tensor for p in proposals], - dim=0, - ) - else: - proposal_boxes = gt_boxes = torch.empty((0, 4), device=proposal_deltas.device) - - if self.use_sigmoid_ce: - loss_cls = self.sigmoid_cross_entropy_loss(scores, gt_classes) - else: - loss_cls = self.softmax_cross_entropy_loss(scores, gt_classes) - return { - "loss_cls": loss_cls, - "loss_box_reg": self.box_reg_loss( - proposal_boxes, gt_boxes, proposal_deltas, gt_classes, num_classes=num_classes - ), - } - - def sigmoid_cross_entropy_loss(self, pred_class_logits, gt_classes): - if pred_class_logits.numel() == 0: - return pred_class_logits.new_zeros([1])[0] # This is more robust than .sum() * 0. - - B = pred_class_logits.shape[0] - C = pred_class_logits.shape[1] - 1 - - target = pred_class_logits.new_zeros(B, C + 1) - target[range(len(gt_classes)), gt_classes] = 1 # B x (C + 1) - target = target[:, :C] # B x C - - weight = 1 - - if self.use_fed_loss and (self.freq_weight is not None): # fedloss - appeared = get_fed_loss_inds( - gt_classes, num_sample_cats=self.fed_loss_num_cat, C=C, weight=self.freq_weight - ) - appeared_mask = appeared.new_zeros(C + 1) - appeared_mask[appeared] = 1 # C + 1 - appeared_mask = appeared_mask[:C] - fed_w = appeared_mask.view(1, C).expand(B, C) - weight = weight * fed_w.float() - if self.ignore_zero_cats and (self.freq_weight is not None): - w = (self.freq_weight.view(-1) > 1e-4).float() - weight = weight * w.view(1, C).expand(B, C) - # import pdb; pdb.set_trace() - - cls_loss = F.binary_cross_entropy_with_logits( - pred_class_logits[:, :-1], target, reduction="none" - ) # B x C - loss = torch.sum(cls_loss * weight) / B - return loss - - def softmax_cross_entropy_loss(self, pred_class_logits, gt_classes): - """ - change _no_instance handling - """ - if pred_class_logits.numel() == 0: - return pred_class_logits.new_zeros([1])[0] - - if self.ignore_zero_cats and (self.freq_weight is not None): - zero_weight = torch.cat( - [(self.freq_weight.view(-1) > 1e-4).float(), self.freq_weight.new_ones(1)] - ) # C + 1 - loss = F.cross_entropy( - pred_class_logits, gt_classes, weight=zero_weight, reduction="mean" - ) - elif self.use_fed_loss and (self.freq_weight is not None): # fedloss - C = pred_class_logits.shape[1] - 1 - appeared = get_fed_loss_inds( - gt_classes, num_sample_cats=self.fed_loss_num_cat, C=C, weight=self.freq_weight - ) - appeared_mask = appeared.new_zeros(C + 1).float() - appeared_mask[appeared] = 1.0 # C + 1 - appeared_mask[C] = 1.0 - loss = F.cross_entropy( - pred_class_logits, gt_classes, weight=appeared_mask, reduction="mean" - ) - else: - loss = F.cross_entropy(pred_class_logits, gt_classes, reduction="mean") - return loss - - def box_reg_loss(self, proposal_boxes, gt_boxes, pred_deltas, gt_classes, num_classes: int=-1): - """ - Allow custom background index - """ - num_classes = num_classes if num_classes > 0 else self.num_classes - box_dim = proposal_boxes.shape[1] # 4 or 5 - fg_inds = nonzero_tuple((gt_classes >= 0) & (gt_classes < num_classes))[0] - if pred_deltas.shape[1] == box_dim: # cls-agnostic regression - fg_pred_deltas = pred_deltas[fg_inds] - else: - fg_pred_deltas = pred_deltas.view(-1, self.num_classes, box_dim)[ - fg_inds, gt_classes[fg_inds] - ] - - if self.box_reg_loss_type == "smooth_l1": - gt_pred_deltas = self.box2box_transform.get_deltas( - proposal_boxes[fg_inds], - gt_boxes[fg_inds], - ) - loss_box_reg = smooth_l1_loss( - fg_pred_deltas, gt_pred_deltas, self.smooth_l1_beta, reduction="sum" - ) - elif self.box_reg_loss_type == "giou": - fg_pred_boxes = self.box2box_transform.apply_deltas( - fg_pred_deltas, proposal_boxes[fg_inds] - ) - loss_box_reg = giou_loss(fg_pred_boxes, gt_boxes[fg_inds], reduction="sum") - else: - raise ValueError(f"Invalid bbox reg loss type '{self.box_reg_loss_type}'") - return loss_box_reg / max(gt_classes.numel(), 1.0) - - def inference(self, predictions, proposals): - """ - enable use proposal boxes - """ - predictions = (predictions[0], predictions[1]) - boxes = self.predict_boxes(predictions, proposals) - scores = self.predict_probs(predictions, proposals) - if self.mult_proposal_score: - proposal_scores = [p.get("objectness_logits") for p in proposals] - scores = [(s * ps[:, None]) ** 0.5 for s, ps in zip(scores, proposal_scores, strict=False)] - image_shapes = [x.image_size for x in proposals] - return fast_rcnn_inference( - boxes, - scores, - image_shapes, - self.test_score_thresh, - self.test_nms_thresh, - self.test_topk_per_image, - ) - - def predict_probs(self, predictions, proposals): - """ - support sigmoid - """ - # scores, _ = predictions - scores = predictions[0] - num_inst_per_image = [len(p) for p in proposals] - if self.use_sigmoid_ce: - probs = scores.sigmoid() - else: - probs = F.softmax(scores, dim=-1) - return probs.split(num_inst_per_image, dim=0) - - def image_label_losses( - self, - predictions, - proposals, - image_labels: Sequence[str], - classifier_info=(None, None, None), - ann_type: str="image", - ): - """ - Inputs: - scores: N x (C + 1) - image_labels B x 1 - """ - num_inst_per_image = [len(p) for p in proposals] - scores = predictions[0] - scores = scores.split(num_inst_per_image, dim=0) # B x n x (C + 1) - if self.with_softmax_prop: - prop_scores = predictions[2].split(num_inst_per_image, dim=0) - else: - prop_scores = [None for _ in num_inst_per_image] - B = len(scores) - img_box_count = 0 - select_size_count = 0 - select_x_count = 0 - select_y_count = 0 - max_score_count = 0 - storage = get_event_storage() - loss = scores[0].new_zeros([1])[0] - caption_loss = scores[0].new_zeros([1])[0] - for idx, (score, labels, prop_score, p) in enumerate( - zip(scores, image_labels, prop_scores, proposals, strict=False) - ): - if score.shape[0] == 0: - loss += score.new_zeros([1])[0] - continue - if "caption" in ann_type: - score, caption_loss_img = self._caption_loss(score, classifier_info, idx, B) - caption_loss += self.caption_weight * caption_loss_img - if ann_type == "caption": - continue - - if self.debug: - p.selected = score.new_zeros((len(p),), dtype=torch.long) - 1 - for i_l, label in enumerate(labels): - if self.dynamic_classifier: - if idx == 0 and i_l == 0 and comm.is_main_process(): - storage.put_scalar("stats_label", label) - label = classifier_info[1][1][label] - assert label < score.shape[1] - if self.image_label_loss in ["wsod", "wsddn"]: - loss_i, ind = self._wsddn_loss(score, prop_score, label) - elif self.image_label_loss == "max_score": - loss_i, ind = self._max_score_loss(score, label) - elif self.image_label_loss == "max_size": - loss_i, ind = self._max_size_loss(score, label, p) - elif self.image_label_loss == "first": - loss_i, ind = self._first_loss(score, label) - elif self.image_label_loss == "image": - loss_i, ind = self._image_loss(score, label) - elif self.image_label_loss == "min_loss": - loss_i, ind = self._min_loss_loss(score, label) - else: - assert 0 - loss += loss_i / len(labels) - if type(ind) == type([]): - img_box_count = sum(ind) / len(ind) - if self.debug: - for ind_i in ind: - p.selected[ind_i] = label - else: - img_box_count = ind - select_size_count = p[ind].proposal_boxes.area() / ( - p.image_size[0] * p.image_size[1] - ) - max_score_count = score[ind, label].sigmoid() - select_x_count = ( - (p.proposal_boxes.tensor[ind, 0] + p.proposal_boxes.tensor[ind, 2]) - / 2 - / p.image_size[1] - ) - select_y_count = ( - (p.proposal_boxes.tensor[ind, 1] + p.proposal_boxes.tensor[ind, 3]) - / 2 - / p.image_size[0] - ) - if self.debug: - p.selected[ind] = label - - loss = loss / B - storage.put_scalar("stats_l_image", loss.item()) - if "caption" in ann_type: - caption_loss = caption_loss / B - loss = loss + caption_loss - storage.put_scalar("stats_l_caption", caption_loss.item()) - if comm.is_main_process(): - storage.put_scalar("pool_stats", img_box_count) - storage.put_scalar("stats_select_size", select_size_count) - storage.put_scalar("stats_select_x", select_x_count) - storage.put_scalar("stats_select_y", select_y_count) - storage.put_scalar("stats_max_label_score", max_score_count) - - return { - "image_loss": loss * self.image_loss_weight, - "loss_cls": score.new_zeros([1])[0], - "loss_box_reg": score.new_zeros([1])[0], - } - - def forward(self, x, classifier_info=(None, None, None)): - """ - enable classifier_info - """ - if x.dim() > 2: - x = torch.flatten(x, start_dim=1) - scores = [] - - if classifier_info[0] is not None: - cls_scores = self.cls_score(x, classifier=classifier_info[0]) - scores.append(cls_scores) - else: - cls_scores = self.cls_score(x) - scores.append(cls_scores) - - if classifier_info[2] is not None: - cap_cls = classifier_info[2] - if self.sync_caption_batch: - caption_scores = self.cls_score(x, classifier=cap_cls[:, :-1]) - else: - caption_scores = self.cls_score(x, classifier=cap_cls) - scores.append(caption_scores) - scores = torch.cat(scores, dim=1) # B x C' or B x N or B x (C'+N) - - proposal_deltas = self.bbox_pred(x) - if self.with_softmax_prop: - prop_score = self.prop_score(x) - return scores, proposal_deltas, prop_score - else: - return scores, proposal_deltas - - def _caption_loss(self, score, classifier_info, idx: int, B): - assert classifier_info[2] is not None - assert self.add_image_box - cls_and_cap_num = score.shape[1] - cap_num = classifier_info[2].shape[0] - score, caption_score = score.split([cls_and_cap_num - cap_num, cap_num], dim=1) - # n x (C + 1), n x B - caption_score = caption_score[-1:] # 1 x B # -1: image level box - caption_target = caption_score.new_zeros( - caption_score.shape - ) # 1 x B or 1 x MB, M: num machines - if self.sync_caption_batch: - # caption_target: 1 x MB - rank = comm.get_rank() - global_idx = B * rank + idx - assert (classifier_info[2][global_idx, -1] - rank) ** 2 < 1e-8, f"{rank} {global_idx} {classifier_info[2][global_idx, -1]} {classifier_info[2].shape} {classifier_info[2][:, -1]}" - caption_target[:, global_idx] = 1.0 - else: - assert caption_score.shape[1] == B - caption_target[:, idx] = 1.0 - caption_loss_img = F.binary_cross_entropy_with_logits( - caption_score, caption_target, reduction="none" - ) - if self.sync_caption_batch: - fg_mask = (caption_target > 0.5).float() - assert (fg_mask.sum().item() - 1.0) ** 2 < 1e-8, f"{fg_mask.shape} {fg_mask}" - pos_loss = (caption_loss_img * fg_mask).sum() - neg_loss = (caption_loss_img * (1.0 - fg_mask)).sum() - caption_loss_img = pos_loss + self.neg_cap_weight * neg_loss - else: - caption_loss_img = caption_loss_img.sum() - return score, caption_loss_img - - def _wsddn_loss(self, score, prop_score, label: str): - assert prop_score is not None - loss = 0 - final_score = score.sigmoid() * F.softmax(prop_score, dim=0) # B x (C + 1) - img_score = torch.clamp(torch.sum(final_score, dim=0), min=1e-10, max=1 - 1e-10) # (C + 1) - target = img_score.new_zeros(img_score.shape) # (C + 1) - target[label] = 1.0 - loss += F.binary_cross_entropy(img_score, target) - ind = final_score[:, label].argmax() - return loss, ind - - def _max_score_loss(self, score, label: str): - loss = 0 - target = score.new_zeros(score.shape[1]) - target[label] = 1.0 - ind = score[:, label].argmax().item() - loss += F.binary_cross_entropy_with_logits(score[ind], target, reduction="sum") - return loss, ind - - def _min_loss_loss(self, score, label: str): - loss = 0 - target = score.new_zeros(score.shape) - target[:, label] = 1.0 - with torch.no_grad(): - x = F.binary_cross_entropy_with_logits(score, target, reduction="none").sum(dim=1) # n - ind = x.argmin().item() - loss += F.binary_cross_entropy_with_logits(score[ind], target[0], reduction="sum") - return loss, ind - - def _first_loss(self, score, label: str): - loss = 0 - target = score.new_zeros(score.shape[1]) - target[label] = 1.0 - ind = 0 - loss += F.binary_cross_entropy_with_logits(score[ind], target, reduction="sum") - return loss, ind - - def _image_loss(self, score, label: str): - assert self.add_image_box - target = score.new_zeros(score.shape[1]) - target[label] = 1.0 - ind = score.shape[0] - 1 - loss = F.binary_cross_entropy_with_logits(score[ind], target, reduction="sum") - return loss, ind - - def _max_size_loss(self, score, label: str, p): - loss = 0 - target = score.new_zeros(score.shape[1]) - target[label] = 1.0 - sizes = p.proposal_boxes.area() - ind = sizes[:-1].argmax().item() if len(sizes) > 1 else 0 - if self.softmax_weak_loss: - loss += F.cross_entropy( - score[ind : ind + 1], - score.new_tensor(label, dtype=torch.long).view(1), - reduction="sum", - ) - else: - loss += F.binary_cross_entropy_with_logits(score[ind], target, reduction="sum") - return loss, ind - - -def put_label_distribution(storage, hist_name: str, hist_counts, num_classes: int) -> None: - """ """ - ht_min, ht_max = 0, num_classes - hist_edges = torch.linspace( - start=ht_min, end=ht_max, steps=num_classes + 1, dtype=torch.float32 - ) - - hist_params = dict( - tag=hist_name, - min=ht_min, - max=ht_max, - num=float(hist_counts.sum()), - sum=float((hist_counts * torch.arange(len(hist_counts))).sum()), - sum_squares=float(((hist_counts * torch.arange(len(hist_counts))) ** 2).sum()), - bucket_limits=hist_edges[1:].tolist(), - bucket_counts=hist_counts.tolist(), - global_step=storage._iter, - ) - storage._histograms.append(hist_params) diff --git a/dimos/models/Detic/detic/modeling/roi_heads/detic_roi_heads.py b/dimos/models/Detic/detic/modeling/roi_heads/detic_roi_heads.py deleted file mode 100644 index a8f1f4efe2..0000000000 --- a/dimos/models/Detic/detic/modeling/roi_heads/detic_roi_heads.py +++ /dev/null @@ -1,258 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from detectron2.config import configurable -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.modeling.roi_heads.cascade_rcnn import CascadeROIHeads, _ScaleGradient -from detectron2.modeling.roi_heads.fast_rcnn import fast_rcnn_inference -from detectron2.modeling.roi_heads.roi_heads import ROI_HEADS_REGISTRY -from detectron2.structures import Boxes, Instances -from detectron2.utils.events import get_event_storage -import torch - -from .detic_fast_rcnn import DeticFastRCNNOutputLayers -from typing import Sequence - - -@ROI_HEADS_REGISTRY.register() -class DeticCascadeROIHeads(CascadeROIHeads): - @configurable - def __init__( - self, - *, - mult_proposal_score: bool = False, - with_image_labels: bool = False, - add_image_box: bool = False, - image_box_size: float = 1.0, - ws_num_props: int = 512, - add_feature_to_prop: bool = False, - mask_weight: float = 1.0, - one_class_per_proposal: bool = False, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.mult_proposal_score = mult_proposal_score - self.with_image_labels = with_image_labels - self.add_image_box = add_image_box - self.image_box_size = image_box_size - self.ws_num_props = ws_num_props - self.add_feature_to_prop = add_feature_to_prop - self.mask_weight = mask_weight - self.one_class_per_proposal = one_class_per_proposal - - @classmethod - def from_config(cls, cfg, input_shape): - ret = super().from_config(cfg, input_shape) - ret.update( - { - "mult_proposal_score": cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE, - "with_image_labels": cfg.WITH_IMAGE_LABELS, - "add_image_box": cfg.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX, - "image_box_size": cfg.MODEL.ROI_BOX_HEAD.IMAGE_BOX_SIZE, - "ws_num_props": cfg.MODEL.ROI_BOX_HEAD.WS_NUM_PROPS, - "add_feature_to_prop": cfg.MODEL.ROI_BOX_HEAD.ADD_FEATURE_TO_PROP, - "mask_weight": cfg.MODEL.ROI_HEADS.MASK_WEIGHT, - "one_class_per_proposal": cfg.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL, - } - ) - return ret - - @classmethod - def _init_box_head(cls, cfg, input_shape): - ret = super()._init_box_head(cfg, input_shape) - del ret["box_predictors"] - cascade_bbox_reg_weights = cfg.MODEL.ROI_BOX_CASCADE_HEAD.BBOX_REG_WEIGHTS - box_predictors = [] - for box_head, bbox_reg_weights in zip(ret["box_heads"], cascade_bbox_reg_weights, strict=False): - box_predictors.append( - DeticFastRCNNOutputLayers( - cfg, - box_head.output_shape, - box2box_transform=Box2BoxTransform(weights=bbox_reg_weights), - ) - ) - ret["box_predictors"] = box_predictors - return ret - - def _forward_box( - self, features, proposals, targets=None, ann_type: str="box", classifier_info=(None, None, None) - ): - """ - Add mult proposal scores at testing - Add ann_type - """ - if (not self.training) and self.mult_proposal_score: - if len(proposals) > 0 and proposals[0].has("scores"): - proposal_scores = [p.get("scores") for p in proposals] - else: - proposal_scores = [p.get("objectness_logits") for p in proposals] - - features = [features[f] for f in self.box_in_features] - head_outputs = [] # (predictor, predictions, proposals) - prev_pred_boxes = None - image_sizes = [x.image_size for x in proposals] - - for k in range(self.num_cascade_stages): - if k > 0: - proposals = self._create_proposals_from_boxes( - prev_pred_boxes, image_sizes, logits=[p.objectness_logits for p in proposals] - ) - if self.training and ann_type in ["box"]: - proposals = self._match_and_label_boxes(proposals, k, targets) - predictions = self._run_stage(features, proposals, k, classifier_info=classifier_info) - prev_pred_boxes = self.box_predictor[k].predict_boxes( - (predictions[0], predictions[1]), proposals - ) - head_outputs.append((self.box_predictor[k], predictions, proposals)) - - if self.training: - losses = {} - storage = get_event_storage() - for stage, (predictor, predictions, proposals) in enumerate(head_outputs): - with storage.name_scope(f"stage{stage}"): - if ann_type != "box": - stage_losses = {} - if ann_type in ["image", "caption", "captiontag"]: - image_labels = [x._pos_category_ids for x in targets] - weak_losses = predictor.image_label_losses( - predictions, - proposals, - image_labels, - classifier_info=classifier_info, - ann_type=ann_type, - ) - stage_losses.update(weak_losses) - else: # supervised - stage_losses = predictor.losses( - (predictions[0], predictions[1]), - proposals, - classifier_info=classifier_info, - ) - if self.with_image_labels: - stage_losses["image_loss"] = predictions[0].new_zeros([1])[0] - losses.update({k + f"_stage{stage}": v for k, v in stage_losses.items()}) - return losses - else: - # Each is a list[Tensor] of length #image. Each tensor is Ri x (K+1) - scores_per_stage = [h[0].predict_probs(h[1], h[2]) for h in head_outputs] - scores = [ - sum(list(scores_per_image)) * (1.0 / self.num_cascade_stages) - for scores_per_image in zip(*scores_per_stage, strict=False) - ] - if self.mult_proposal_score: - scores = [(s * ps[:, None]) ** 0.5 for s, ps in zip(scores, proposal_scores, strict=False)] - if self.one_class_per_proposal: - scores = [s * (s == s[:, :-1].max(dim=1)[0][:, None]).float() for s in scores] - predictor, predictions, proposals = head_outputs[-1] - boxes = predictor.predict_boxes((predictions[0], predictions[1]), proposals) - pred_instances, _ = fast_rcnn_inference( - boxes, - scores, - image_sizes, - predictor.test_score_thresh, - predictor.test_nms_thresh, - predictor.test_topk_per_image, - ) - return pred_instances - - def forward( - self, - images, - features, - proposals, - targets=None, - ann_type: str="box", - classifier_info=(None, None, None), - ): - """ - enable debug and image labels - classifier_info is shared across the batch - """ - if self.training: - if ann_type in ["box", "prop", "proptag"]: - proposals = self.label_and_sample_proposals(proposals, targets) - else: - proposals = self.get_top_proposals(proposals) - - losses = self._forward_box( - features, proposals, targets, ann_type=ann_type, classifier_info=classifier_info - ) - if ann_type == "box" and targets[0].has("gt_masks"): - mask_losses = self._forward_mask(features, proposals) - losses.update({k: v * self.mask_weight for k, v in mask_losses.items()}) - losses.update(self._forward_keypoint(features, proposals)) - else: - losses.update( - self._get_empty_mask_loss( - features, proposals, device=proposals[0].objectness_logits.device - ) - ) - return proposals, losses - else: - pred_instances = self._forward_box(features, proposals, classifier_info=classifier_info) - pred_instances = self.forward_with_given_boxes(features, pred_instances) - return pred_instances, {} - - def get_top_proposals(self, proposals): - for i in range(len(proposals)): - proposals[i].proposal_boxes.clip(proposals[i].image_size) - proposals = [p[: self.ws_num_props] for p in proposals] - for i, p in enumerate(proposals): - p.proposal_boxes.tensor = p.proposal_boxes.tensor.detach() - if self.add_image_box: - proposals[i] = self._add_image_box(p) - return proposals - - def _add_image_box(self, p): - image_box = Instances(p.image_size) - n = 1 - h, w = p.image_size - f = self.image_box_size - image_box.proposal_boxes = Boxes( - p.proposal_boxes.tensor.new_tensor( - [ - w * (1.0 - f) / 2.0, - h * (1.0 - f) / 2.0, - w * (1.0 - (1.0 - f) / 2.0), - h * (1.0 - (1.0 - f) / 2.0), - ] - ).view(n, 4) - ) - image_box.objectness_logits = p.objectness_logits.new_ones(n) - return Instances.cat([p, image_box]) - - def _get_empty_mask_loss(self, features, proposals, device): - if self.mask_on: - return {"loss_mask": torch.zeros((1,), device=device, dtype=torch.float32)[0]} - else: - return {} - - def _create_proposals_from_boxes(self, boxes, image_sizes: Sequence[int], logits): - """ - Add objectness_logits - """ - boxes = [Boxes(b.detach()) for b in boxes] - proposals = [] - for boxes_per_image, image_size, logit in zip(boxes, image_sizes, logits, strict=False): - boxes_per_image.clip(image_size) - if self.training: - inds = boxes_per_image.nonempty() - boxes_per_image = boxes_per_image[inds] - logit = logit[inds] - prop = Instances(image_size) - prop.proposal_boxes = boxes_per_image - prop.objectness_logits = logit - proposals.append(prop) - return proposals - - def _run_stage(self, features, proposals, stage, classifier_info=(None, None, None)): - """ - Support classifier_info and add_feature_to_prop - """ - pool_boxes = [x.proposal_boxes for x in proposals] - box_features = self.box_pooler(features, pool_boxes) - box_features = _ScaleGradient.apply(box_features, 1.0 / self.num_cascade_stages) - box_features = self.box_head[stage](box_features) - if self.add_feature_to_prop: - feats_per_image = box_features.split([len(p) for p in proposals], dim=0) - for feat, p in zip(feats_per_image, proposals, strict=False): - p.feat = feat - return self.box_predictor[stage](box_features, classifier_info=classifier_info) diff --git a/dimos/models/Detic/detic/modeling/roi_heads/res5_roi_heads.py b/dimos/models/Detic/detic/modeling/roi_heads/res5_roi_heads.py deleted file mode 100644 index 642f889b5d..0000000000 --- a/dimos/models/Detic/detic/modeling/roi_heads/res5_roi_heads.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from detectron2.config import configurable -from detectron2.layers import ShapeSpec -from detectron2.modeling.roi_heads.roi_heads import ROI_HEADS_REGISTRY, Res5ROIHeads -from detectron2.structures import Boxes, Instances -import torch - -from ..debug import debug_second_stage -from .detic_fast_rcnn import DeticFastRCNNOutputLayers - - -@ROI_HEADS_REGISTRY.register() -class CustomRes5ROIHeads(Res5ROIHeads): - @configurable - def __init__(self, **kwargs) -> None: - cfg = kwargs.pop("cfg") - super().__init__(**kwargs) - stage_channel_factor = 2**3 - out_channels = cfg.MODEL.RESNETS.RES2_OUT_CHANNELS * stage_channel_factor - - self.with_image_labels = cfg.WITH_IMAGE_LABELS - self.ws_num_props = cfg.MODEL.ROI_BOX_HEAD.WS_NUM_PROPS - self.add_image_box = cfg.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX - self.add_feature_to_prop = cfg.MODEL.ROI_BOX_HEAD.ADD_FEATURE_TO_PROP - self.image_box_size = cfg.MODEL.ROI_BOX_HEAD.IMAGE_BOX_SIZE - self.box_predictor = DeticFastRCNNOutputLayers( - cfg, ShapeSpec(channels=out_channels, height=1, width=1) - ) - - self.save_debug = cfg.SAVE_DEBUG - self.save_debug_path = cfg.SAVE_DEBUG_PATH - if self.save_debug: - self.debug_show_name = cfg.DEBUG_SHOW_NAME - self.vis_thresh = cfg.VIS_THRESH - self.pixel_mean = ( - torch.Tensor(cfg.MODEL.PIXEL_MEAN).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - ) - self.pixel_std = ( - torch.Tensor(cfg.MODEL.PIXEL_STD).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - ) - self.bgr = cfg.INPUT.FORMAT == "BGR" - - @classmethod - def from_config(cls, cfg, input_shape): - ret = super().from_config(cfg, input_shape) - ret["cfg"] = cfg - return ret - - def forward( - self, - images, - features, - proposals, - targets=None, - ann_type: str="box", - classifier_info=(None, None, None), - ): - """ - enable debug and image labels - classifier_info is shared across the batch - """ - if not self.save_debug: - del images - - if self.training: - if ann_type in ["box"]: - proposals = self.label_and_sample_proposals(proposals, targets) - else: - proposals = self.get_top_proposals(proposals) - - proposal_boxes = [x.proposal_boxes for x in proposals] - box_features = self._shared_roi_transform( - [features[f] for f in self.in_features], proposal_boxes - ) - predictions = self.box_predictor( - box_features.mean(dim=[2, 3]), classifier_info=classifier_info - ) - - if self.add_feature_to_prop: - feats_per_image = box_features.mean(dim=[2, 3]).split( - [len(p) for p in proposals], dim=0 - ) - for feat, p in zip(feats_per_image, proposals, strict=False): - p.feat = feat - - if self.training: - del features - if ann_type != "box": - image_labels = [x._pos_category_ids for x in targets] - losses = self.box_predictor.image_label_losses( - predictions, - proposals, - image_labels, - classifier_info=classifier_info, - ann_type=ann_type, - ) - else: - losses = self.box_predictor.losses((predictions[0], predictions[1]), proposals) - if self.with_image_labels: - assert "image_loss" not in losses - losses["image_loss"] = predictions[0].new_zeros([1])[0] - if self.save_debug: - def denormalizer(x): - return x * self.pixel_std + self.pixel_mean - if ann_type != "box": - image_labels = [x._pos_category_ids for x in targets] - else: - image_labels = [[] for x in targets] - debug_second_stage( - [denormalizer(x.clone()) for x in images], - targets, - proposals=proposals, - save_debug=self.save_debug, - debug_show_name=self.debug_show_name, - vis_thresh=self.vis_thresh, - image_labels=image_labels, - save_debug_path=self.save_debug_path, - bgr=self.bgr, - ) - return proposals, losses - else: - pred_instances, _ = self.box_predictor.inference(predictions, proposals) - pred_instances = self.forward_with_given_boxes(features, pred_instances) - if self.save_debug: - def denormalizer(x): - return x * self.pixel_std + self.pixel_mean - debug_second_stage( - [denormalizer(x.clone()) for x in images], - pred_instances, - proposals=proposals, - save_debug=self.save_debug, - debug_show_name=self.debug_show_name, - vis_thresh=self.vis_thresh, - save_debug_path=self.save_debug_path, - bgr=self.bgr, - ) - return pred_instances, {} - - def get_top_proposals(self, proposals): - for i in range(len(proposals)): - proposals[i].proposal_boxes.clip(proposals[i].image_size) - proposals = [p[: self.ws_num_props] for p in proposals] - for i, p in enumerate(proposals): - p.proposal_boxes.tensor = p.proposal_boxes.tensor.detach() - if self.add_image_box: - proposals[i] = self._add_image_box(p) - return proposals - - def _add_image_box(self, p, use_score: bool=False): - image_box = Instances(p.image_size) - n = 1 - h, w = p.image_size - if self.image_box_size < 1.0: - f = self.image_box_size - image_box.proposal_boxes = Boxes( - p.proposal_boxes.tensor.new_tensor( - [ - w * (1.0 - f) / 2.0, - h * (1.0 - f) / 2.0, - w * (1.0 - (1.0 - f) / 2.0), - h * (1.0 - (1.0 - f) / 2.0), - ] - ).view(n, 4) - ) - else: - image_box.proposal_boxes = Boxes( - p.proposal_boxes.tensor.new_tensor([0, 0, w, h]).view(n, 4) - ) - if use_score: - image_box.scores = p.objectness_logits.new_ones(n) - image_box.pred_classes = p.objectness_logits.new_zeros(n, dtype=torch.long) - image_box.objectness_logits = p.objectness_logits.new_ones(n) - else: - image_box.objectness_logits = p.objectness_logits.new_ones(n) - return Instances.cat([p, image_box]) diff --git a/dimos/models/Detic/detic/modeling/roi_heads/zero_shot_classifier.py b/dimos/models/Detic/detic/modeling/roi_heads/zero_shot_classifier.py deleted file mode 100644 index d436e6be34..0000000000 --- a/dimos/models/Detic/detic/modeling/roi_heads/zero_shot_classifier.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from detectron2.config import configurable -from detectron2.layers import ShapeSpec -import numpy as np -import torch -from torch import nn -from torch.nn import functional as F - - -class ZeroShotClassifier(nn.Module): - @configurable - def __init__( - self, - input_shape: ShapeSpec, - *, - num_classes: int, - zs_weight_path: str, - zs_weight_dim: int = 512, - use_bias: float = 0.0, - norm_weight: bool = True, - norm_temperature: float = 50.0, - ) -> None: - super().__init__() - if isinstance(input_shape, int): # some backward compatibility - input_shape = ShapeSpec(channels=input_shape) - input_size = input_shape.channels * (input_shape.width or 1) * (input_shape.height or 1) - self.norm_weight = norm_weight - self.norm_temperature = norm_temperature - - self.use_bias = use_bias < 0 - if self.use_bias: - self.cls_bias = nn.Parameter(torch.ones(1) * use_bias) - - self.linear = nn.Linear(input_size, zs_weight_dim) - - if zs_weight_path == "rand": - zs_weight = torch.randn((zs_weight_dim, num_classes)) - nn.init.normal_(zs_weight, std=0.01) - else: - zs_weight = ( - torch.tensor(np.load(zs_weight_path), dtype=torch.float32) - .permute(1, 0) - .contiguous() - ) # D x C - zs_weight = torch.cat( - [zs_weight, zs_weight.new_zeros((zs_weight_dim, 1))], dim=1 - ) # D x (C + 1) - - if self.norm_weight: - zs_weight = F.normalize(zs_weight, p=2, dim=0) - - if zs_weight_path == "rand": - self.zs_weight = nn.Parameter(zs_weight) - else: - self.register_buffer("zs_weight", zs_weight) - - assert self.zs_weight.shape[1] == num_classes + 1, self.zs_weight.shape - - @classmethod - def from_config(cls, cfg, input_shape): - return { - "input_shape": input_shape, - "num_classes": cfg.MODEL.ROI_HEADS.NUM_CLASSES, - "zs_weight_path": cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH, - "zs_weight_dim": cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_DIM, - "use_bias": cfg.MODEL.ROI_BOX_HEAD.USE_BIAS, - "norm_weight": cfg.MODEL.ROI_BOX_HEAD.NORM_WEIGHT, - "norm_temperature": cfg.MODEL.ROI_BOX_HEAD.NORM_TEMP, - } - - def forward(self, x, classifier=None): - """ - Inputs: - x: B x D' - classifier_info: (C', C' x D) - """ - x = self.linear(x) - if classifier is not None: - zs_weight = classifier.permute(1, 0).contiguous() # D x C' - zs_weight = F.normalize(zs_weight, p=2, dim=0) if self.norm_weight else zs_weight - else: - zs_weight = self.zs_weight - if self.norm_weight: - x = self.norm_temperature * F.normalize(x, p=2, dim=1) - x = torch.mm(x, zs_weight) - if self.use_bias: - x = x + self.cls_bias - return x diff --git a/dimos/models/Detic/detic/modeling/text/text_encoder.py b/dimos/models/Detic/detic/modeling/text/text_encoder.py deleted file mode 100644 index 7c9b15bdf5..0000000000 --- a/dimos/models/Detic/detic/modeling/text/text_encoder.py +++ /dev/null @@ -1,198 +0,0 @@ -# This code is modified from https://github.com/openai/CLIP/blob/main/clip/clip.py -# Modified by Xingyi Zhou -# The original code is under MIT license -# Copyright (c) Facebook, Inc. and its affiliates. -from collections import OrderedDict -from typing import List, Union - -from clip.simple_tokenizer import SimpleTokenizer as _Tokenizer -import torch -from torch import nn - -__all__ = ["tokenize"] - -count = 0 - - -class LayerNorm(nn.LayerNorm): - """Subclass torch's LayerNorm to handle fp16.""" - - def forward(self, x: torch.Tensor): - orig_type = x.dtype - ret = super().forward(x.type(torch.float32)) - return ret.type(orig_type) - - -class QuickGELU(nn.Module): - def forward(self, x: torch.Tensor): - return x * torch.sigmoid(1.702 * x) - - -class ResidualAttentionBlock(nn.Module): - def __init__(self, d_model: int, n_head: int, attn_mask: torch.Tensor = None) -> None: - super().__init__() - - self.attn = nn.MultiheadAttention(d_model, n_head) - self.ln_1 = LayerNorm(d_model) - self.mlp = nn.Sequential( - OrderedDict( - [ - ("c_fc", nn.Linear(d_model, d_model * 4)), - ("gelu", QuickGELU()), - ("c_proj", nn.Linear(d_model * 4, d_model)), - ] - ) - ) - self.ln_2 = LayerNorm(d_model) - self.attn_mask = attn_mask - - def attention(self, x: torch.Tensor): - self.attn_mask = ( - self.attn_mask.to(dtype=x.dtype, device=x.device) - if self.attn_mask is not None - else None - ) - return self.attn(x, x, x, need_weights=False, attn_mask=self.attn_mask)[0] - - def forward(self, x: torch.Tensor): - x = x + self.attention(self.ln_1(x)) - x = x + self.mlp(self.ln_2(x)) - return x - - -class Transformer(nn.Module): - def __init__(self, width: int, layers: int, heads: int, attn_mask: torch.Tensor = None) -> None: - super().__init__() - self.width = width - self.layers = layers - self.resblocks = nn.Sequential( - *[ResidualAttentionBlock(width, heads, attn_mask) for _ in range(layers)] - ) - - def forward(self, x: torch.Tensor): - return self.resblocks(x) - - -class CLIPTEXT(nn.Module): - def __init__( - self, - embed_dim: int=512, - # text - context_length: int=77, - vocab_size: int=49408, - transformer_width: int=512, - transformer_heads: int=8, - transformer_layers: int=12, - ) -> None: - super().__init__() - - self._tokenizer = _Tokenizer() - self.context_length = context_length - - self.transformer = Transformer( - width=transformer_width, - layers=transformer_layers, - heads=transformer_heads, - attn_mask=self.build_attention_mask(), - ) - - self.vocab_size = vocab_size - self.token_embedding = nn.Embedding(vocab_size, transformer_width) - self.positional_embedding = nn.Parameter( - torch.empty(self.context_length, transformer_width) - ) - self.ln_final = LayerNorm(transformer_width) - - self.text_projection = nn.Parameter(torch.empty(transformer_width, embed_dim)) - # self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07)) - - self.initialize_parameters() - - def initialize_parameters(self) -> None: - nn.init.normal_(self.token_embedding.weight, std=0.02) - nn.init.normal_(self.positional_embedding, std=0.01) - - proj_std = (self.transformer.width**-0.5) * ((2 * self.transformer.layers) ** -0.5) - attn_std = self.transformer.width**-0.5 - fc_std = (2 * self.transformer.width) ** -0.5 - for block in self.transformer.resblocks: - nn.init.normal_(block.attn.in_proj_weight, std=attn_std) - nn.init.normal_(block.attn.out_proj.weight, std=proj_std) - nn.init.normal_(block.mlp.c_fc.weight, std=fc_std) - nn.init.normal_(block.mlp.c_proj.weight, std=proj_std) - - if self.text_projection is not None: - nn.init.normal_(self.text_projection, std=self.transformer.width**-0.5) - - def build_attention_mask(self): - # lazily create causal attention mask, with full attention between the vision tokens - # pytorch uses additive attention mask; fill with -inf - mask = torch.empty(self.context_length, self.context_length) - mask.fill_(float("-inf")) - mask.triu_(1) # zero out the lower diagonal - return mask - - @property - def device(self): - return self.text_projection.device - - @property - def dtype(self): - return self.text_projection.dtype - - def tokenize(self, texts: Union[str, list[str]], context_length: int = 77) -> torch.LongTensor: - """ """ - if isinstance(texts, str): - texts = [texts] - - sot_token = self._tokenizer.encoder["<|startoftext|>"] - eot_token = self._tokenizer.encoder["<|endoftext|>"] - all_tokens = [[sot_token, *self._tokenizer.encode(text), eot_token] for text in texts] - result = torch.zeros(len(all_tokens), context_length, dtype=torch.long) - - for i, tokens in enumerate(all_tokens): - if len(tokens) > context_length: - st = torch.randint(len(tokens) - context_length + 1, (1,))[0].item() - tokens = tokens[st : st + context_length] - # raise RuntimeError(f"Input {texts[i]} is too long for context length {context_length}") - result[i, : len(tokens)] = torch.tensor(tokens) - - return result - - def encode_text(self, text: str): - x = self.token_embedding(text).type(self.dtype) # [batch_size, n_ctx, d_model] - x = x + self.positional_embedding.type(self.dtype) - x = x.permute(1, 0, 2) # NLD -> LND - x = self.transformer(x) - x = x.permute(1, 0, 2) # LND -> NLD - x = self.ln_final(x).type(self.dtype) - # take features from the eot embedding (eot_token is the highest number in each sequence) - x = x[torch.arange(x.shape[0]), text.argmax(dim=-1)] @ self.text_projection - return x - - def forward(self, captions): - """ - captions: list of strings - """ - text = self.tokenize(captions).to(self.device) # B x L x D - features = self.encode_text(text) # B x D - return features - - -def build_text_encoder(pretrain: bool=True): - text_encoder = CLIPTEXT() - if pretrain: - import clip - - pretrained_model, _ = clip.load("ViT-B/32", device="cpu") - state_dict = pretrained_model.state_dict() - to_delete_keys = ["logit_scale", "input_resolution", "context_length", "vocab_size"] + [ - k for k in state_dict.keys() if k.startswith("visual.") - ] - for k in to_delete_keys: - if k in state_dict: - del state_dict[k] - print("Loading pretrained CLIP") - text_encoder.load_state_dict(state_dict) - # import pdb; pdb.set_trace() - return text_encoder diff --git a/dimos/models/Detic/detic/modeling/utils.py b/dimos/models/Detic/detic/modeling/utils.py deleted file mode 100644 index f24a0699a1..0000000000 --- a/dimos/models/Detic/detic/modeling/utils.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import json - -import numpy as np -import torch -from torch.nn import functional as F - - -def load_class_freq(path: str="datasets/metadata/lvis_v1_train_cat_info.json", freq_weight: float=1.0): - cat_info = json.load(open(path)) - cat_info = torch.tensor([c["image_count"] for c in sorted(cat_info, key=lambda x: x["id"])]) - freq_weight = cat_info.float() ** freq_weight - return freq_weight - - -def get_fed_loss_inds(gt_classes, num_sample_cats: int, C, weight=None): - appeared = torch.unique(gt_classes) # C' - prob = appeared.new_ones(C + 1).float() - prob[-1] = 0 - if len(appeared) < num_sample_cats: - if weight is not None: - prob[:C] = weight.float().clone() - prob[appeared] = 0 - more_appeared = torch.multinomial(prob, num_sample_cats - len(appeared), replacement=False) - appeared = torch.cat([appeared, more_appeared]) - return appeared - - -def reset_cls_test(model, cls_path, num_classes: int) -> None: - model.roi_heads.num_classes = num_classes - if type(cls_path) == str: - print("Resetting zs_weight", cls_path) - zs_weight = ( - torch.tensor(np.load(cls_path), dtype=torch.float32).permute(1, 0).contiguous() - ) # D x C - else: - zs_weight = cls_path - zs_weight = torch.cat( - [zs_weight, zs_weight.new_zeros((zs_weight.shape[0], 1))], dim=1 - ) # D x (C + 1) - if model.roi_heads.box_predictor[0].cls_score.norm_weight: - zs_weight = F.normalize(zs_weight, p=2, dim=0) - zs_weight = zs_weight.to(model.device) - for k in range(len(model.roi_heads.box_predictor)): - del model.roi_heads.box_predictor[k].cls_score.zs_weight - model.roi_heads.box_predictor[k].cls_score.zs_weight = zs_weight diff --git a/dimos/models/Detic/detic/predictor.py b/dimos/models/Detic/detic/predictor.py deleted file mode 100644 index a85941e25a..0000000000 --- a/dimos/models/Detic/detic/predictor.py +++ /dev/null @@ -1,254 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import atexit -import bisect -from collections import deque -import multiprocessing as mp - -import cv2 -from detectron2.data import MetadataCatalog -from detectron2.engine.defaults import DefaultPredictor -from detectron2.utils.video_visualizer import VideoVisualizer -from detectron2.utils.visualizer import ColorMode, Visualizer -import torch - -from .modeling.utils import reset_cls_test - - -def get_clip_embeddings(vocabulary, prompt: str="a "): - from detic.modeling.text.text_encoder import build_text_encoder - - text_encoder = build_text_encoder(pretrain=True) - text_encoder.eval() - texts = [prompt + x for x in vocabulary] - emb = text_encoder(texts).detach().permute(1, 0).contiguous().cpu() - return emb - - -BUILDIN_CLASSIFIER = { - "lvis": "datasets/metadata/lvis_v1_clip_a+cname.npy", - "objects365": "datasets/metadata/o365_clip_a+cnamefix.npy", - "openimages": "datasets/metadata/oid_clip_a+cname.npy", - "coco": "datasets/metadata/coco_clip_a+cname.npy", -} - -BUILDIN_METADATA_PATH = { - "lvis": "lvis_v1_val", - "objects365": "objects365_v2_val", - "openimages": "oid_val_expanded", - "coco": "coco_2017_val", -} - - -class VisualizationDemo: - def __init__(self, cfg, args, instance_mode=ColorMode.IMAGE, parallel: bool=False) -> None: - """ - Args: - cfg (CfgNode): - instance_mode (ColorMode): - parallel (bool): whether to run the model in different processes from visualization. - Useful since the visualization logic can be slow. - """ - if args.vocabulary == "custom": - self.metadata = MetadataCatalog.get("__unused") - self.metadata.thing_classes = args.custom_vocabulary.split(",") - classifier = get_clip_embeddings(self.metadata.thing_classes) - else: - self.metadata = MetadataCatalog.get(BUILDIN_METADATA_PATH[args.vocabulary]) - classifier = BUILDIN_CLASSIFIER[args.vocabulary] - - num_classes = len(self.metadata.thing_classes) - self.cpu_device = torch.device("cpu") - self.instance_mode = instance_mode - - self.parallel = parallel - if parallel: - num_gpu = torch.cuda.device_count() - self.predictor = AsyncPredictor(cfg, num_gpus=num_gpu) - else: - self.predictor = DefaultPredictor(cfg) - reset_cls_test(self.predictor.model, classifier, num_classes) - - def run_on_image(self, image): - """ - Args: - image (np.ndarray): an image of shape (H, W, C) (in BGR order). - This is the format used by OpenCV. - - Returns: - predictions (dict): the output of the model. - vis_output (VisImage): the visualized image output. - """ - vis_output = None - predictions = self.predictor(image) - # Convert image from OpenCV BGR format to Matplotlib RGB format. - image = image[:, :, ::-1] - visualizer = Visualizer(image, self.metadata, instance_mode=self.instance_mode) - if "panoptic_seg" in predictions: - panoptic_seg, segments_info = predictions["panoptic_seg"] - vis_output = visualizer.draw_panoptic_seg_predictions( - panoptic_seg.to(self.cpu_device), segments_info - ) - else: - if "sem_seg" in predictions: - vis_output = visualizer.draw_sem_seg( - predictions["sem_seg"].argmax(dim=0).to(self.cpu_device) - ) - if "instances" in predictions: - instances = predictions["instances"].to(self.cpu_device) - vis_output = visualizer.draw_instance_predictions(predictions=instances) - - return predictions, vis_output - - def _frame_from_video(self, video): - while video.isOpened(): - success, frame = video.read() - if success: - yield frame - else: - break - - def run_on_video(self, video): - """ - Visualizes predictions on frames of the input video. - - Args: - video (cv2.VideoCapture): a :class:`VideoCapture` object, whose source can be - either a webcam or a video file. - - Yields: - ndarray: BGR visualizations of each video frame. - """ - video_visualizer = VideoVisualizer(self.metadata, self.instance_mode) - - def process_predictions(frame, predictions): - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - if "panoptic_seg" in predictions: - panoptic_seg, segments_info = predictions["panoptic_seg"] - vis_frame = video_visualizer.draw_panoptic_seg_predictions( - frame, panoptic_seg.to(self.cpu_device), segments_info - ) - elif "instances" in predictions: - predictions = predictions["instances"].to(self.cpu_device) - vis_frame = video_visualizer.draw_instance_predictions(frame, predictions) - elif "sem_seg" in predictions: - vis_frame = video_visualizer.draw_sem_seg( - frame, predictions["sem_seg"].argmax(dim=0).to(self.cpu_device) - ) - - # Converts Matplotlib RGB format to OpenCV BGR format - vis_frame = cv2.cvtColor(vis_frame.get_image(), cv2.COLOR_RGB2BGR) - return vis_frame - - frame_gen = self._frame_from_video(video) - if self.parallel: - buffer_size = self.predictor.default_buffer_size - - frame_data = deque() - - for cnt, frame in enumerate(frame_gen): - frame_data.append(frame) - self.predictor.put(frame) - - if cnt >= buffer_size: - frame = frame_data.popleft() - predictions = self.predictor.get() - yield process_predictions(frame, predictions) - - while len(frame_data): - frame = frame_data.popleft() - predictions = self.predictor.get() - yield process_predictions(frame, predictions) - else: - for frame in frame_gen: - yield process_predictions(frame, self.predictor(frame)) - - -class AsyncPredictor: - """ - A predictor that runs the model asynchronously, possibly on >1 GPUs. - Because rendering the visualization takes considerably amount of time, - this helps improve throughput a little bit when rendering videos. - """ - - class _StopToken: - pass - - class _PredictWorker(mp.Process): - def __init__(self, cfg, task_queue, result_queue) -> None: - self.cfg = cfg - self.task_queue = task_queue - self.result_queue = result_queue - super().__init__() - - def run(self) -> None: - predictor = DefaultPredictor(self.cfg) - - while True: - task = self.task_queue.get() - if isinstance(task, AsyncPredictor._StopToken): - break - idx, data = task - result = predictor(data) - self.result_queue.put((idx, result)) - - def __init__(self, cfg, num_gpus: int = 1) -> None: - """ - Args: - cfg (CfgNode): - num_gpus (int): if 0, will run on CPU - """ - num_workers = max(num_gpus, 1) - self.task_queue = mp.Queue(maxsize=num_workers * 3) - self.result_queue = mp.Queue(maxsize=num_workers * 3) - self.procs = [] - for gpuid in range(max(num_gpus, 1)): - cfg = cfg.clone() - cfg.defrost() - cfg.MODEL.DEVICE = f"cuda:{gpuid}" if num_gpus > 0 else "cpu" - self.procs.append( - AsyncPredictor._PredictWorker(cfg, self.task_queue, self.result_queue) - ) - - self.put_idx = 0 - self.get_idx = 0 - self.result_rank = [] - self.result_data = [] - - for p in self.procs: - p.start() - atexit.register(self.shutdown) - - def put(self, image) -> None: - self.put_idx += 1 - self.task_queue.put((self.put_idx, image)) - - def get(self): - self.get_idx += 1 # the index needed for this request - if len(self.result_rank) and self.result_rank[0] == self.get_idx: - res = self.result_data[0] - del self.result_data[0], self.result_rank[0] - return res - - while True: - # make sure the results are returned in the correct order - idx, res = self.result_queue.get() - if idx == self.get_idx: - return res - insert = bisect.bisect(self.result_rank, idx) - self.result_rank.insert(insert, idx) - self.result_data.insert(insert, res) - - def __len__(self) -> int: - return self.put_idx - self.get_idx - - def __call__(self, image): - self.put(image) - return self.get() - - def shutdown(self) -> None: - for _ in self.procs: - self.task_queue.put(AsyncPredictor._StopToken()) - - @property - def default_buffer_size(self): - return len(self.procs) * 5 diff --git a/dimos/models/Detic/docs/INSTALL.md b/dimos/models/Detic/docs/INSTALL.md deleted file mode 100644 index 1d5fbc4ae1..0000000000 --- a/dimos/models/Detic/docs/INSTALL.md +++ /dev/null @@ -1,33 +0,0 @@ -# Installation - -### Requirements -- Linux or macOS with Python ≄ 3.6 -- PyTorch ≄ 1.8. - Install them together at [pytorch.org](https://pytorch.org) to make sure of this. Note, please check - PyTorch version matches that is required by Detectron2. -- Detectron2: follow [Detectron2 installation instructions](https://detectron2.readthedocs.io/tutorials/install.html). - - -### Example conda environment setup -```bash -conda create --name detic python=3.8 -y -conda activate detic -conda install pytorch torchvision torchaudio cudatoolkit=11.1 -c pytorch-lts -c nvidia - -# under your working directory -git clone git@github.com:facebookresearch/detectron2.git -cd detectron2 -pip install -e . - -cd .. -git clone https://github.com/facebookresearch/Detic.git --recurse-submodules -cd Detic -pip install -r requirements.txt -``` - -Our project uses two submodules, [CenterNet2](https://github.com/xingyizhou/CenterNet2.git) and [Deformable-DETR](https://github.com/fundamentalvision/Deformable-DETR.git). If you forget to add `--recurse-submodules`, do `git submodule init` and then `git submodule update`. To train models with Deformable-DETR (optional), we need to compile it - -``` -cd third_party/Deformable-DETR/models/ops -./make.sh -``` \ No newline at end of file diff --git a/dimos/models/Detic/docs/MODEL_ZOO.md b/dimos/models/Detic/docs/MODEL_ZOO.md deleted file mode 100644 index fe7c795197..0000000000 --- a/dimos/models/Detic/docs/MODEL_ZOO.md +++ /dev/null @@ -1,143 +0,0 @@ -# Detic model zoo - -## Introduction - -This file documents a collection of models reported in our paper. -The training time was measured on [Big Basin](https://engineering.fb.com/data-center-engineering/introducing-big-basin-our-next-generation-ai-hardware/) -servers with 8 NVIDIA V100 GPUs & NVLink. - -#### How to Read the Tables - -The "Name" column contains a link to the config file. -To train a model, run - -``` -python train_net.py --num-gpus 8 --config-file /path/to/config/name.yaml -``` - -To evaluate a model with a trained/ pretrained model, run - -``` -python train_net.py --num-gpus 8 --config-file /path/to/config/name.yaml --eval-only MODEL.WEIGHTS /path/to/weight.pth -``` - -#### Third-party ImageNet-21K Pretrained Models - -Our paper uses ImageNet-21K pretrained models that are not part of Detectron2 (ResNet-50-21K from [MIIL](https://github.com/Alibaba-MIIL/ImageNet21K) and SwinB-21K from [Swin-Transformer](https://github.com/microsoft/Swin-Transformer)). Before training, -please download the models and place them under `DETIC_ROOT/models/`, and following [this tool](../tools/convert-thirdparty-pretrained-model-to-d2.py) to convert the format. - - -## Open-vocabulary LVIS - -| Name |Training time | mask mAP | mask mAP_novel | Download | -|-----------------------|------------------|-----------|-----------------|----------| -|[Box-Supervised_C2_R50_640_4x](../configs/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.yaml) | 17h | 30.2 | 16.4 | [model](https://dl.fbaipublicfiles.com/detic/BoxSup-C2_Lbase_CLIP_R5021k_640b64_4x.pth) | -|[Detic_C2_IN-L_R50_640_4x](../configs/Detic_LbaseI_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml) | 22h | 32.4 | 24.9 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LbaseI_CLIP_R5021k_640b64_4x_ft4x_max-size.pth) | -|[Detic_C2_CCimg_R50_640_4x](../configs/Detic_LbaseCCimg_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml) | 22h | 31.0 | 19.8 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LbaseCCimg_CLIP_R5021k_640b64_4x_ft4x_max-size.pth) | -|[Detic_C2_CCcapimg_R50_640_4x](../configs/Detic_LbaseCCcapimg_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml) | 22h | 31.0 | 21.3 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LbaseCCcapimg_CLIP_R5021k_640b64_4x_ft4x_max-size.pth) | -|[Box-Supervised_C2_SwinB_896_4x](../configs/BoxSup-C2_Lbase_CLIP_SwinB_896b32_4x.yaml) | 43h | 38.4 | 21.9 | [model](https://dl.fbaipublicfiles.com/detic/BoxSup-C2_Lbase_CLIP_SwinB_896b32_4x.pth) | -|[Detic_C2_IN-L_SwinB_896_4x](../configs/Detic_LbaseI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml) | 47h | 40.7 | 33.8 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LbaseI_CLIP_SwinB_896b32_4x_ft4x_max-size.pth) | - - -#### Note - -- The open-vocabulary LVIS setup is LVIS without rare class annotations in training. We evaluate rare classes as novel classes in testing. - -- The models with `C2` are trained using our improved LVIS baseline (Appendix D of the paper), including CenterNet2 detector, Federated Loss, large-scale jittering, etc. - -- All models use [CLIP](https://github.com/openai/CLIP) embeddings as classifiers. This makes the box-supervised models have non-zero mAP on novel classes. - -- The models with `IN-L` use the overlap classes between ImageNet-21K and LVIS as image-labeled data. - -- The models with `CC` use Conception Captions. `CCimg` uses image labels extracted from the captions (using a naive text-match) as image-labeled data. `CCcapimg` additionally uses the row captions (Appendix C of the paper). - -- The Detic models are finetuned on the corresponding Box-Supervised models above (indicated by MODEL.WEIGHTS in the config files). Please train or download the Box-Supervised model and place them under `DETIC_ROOT/models/` before training the Detic models. - - -## Standard LVIS - -| Name |Training time | mask mAP | mask mAP_rare | Download | -|-----------------------|------------------|-----------|-----------------|----------| -|[Box-Supervised_C2_R50_640_4x](../configs/BoxSup-C2_L_CLIP_R5021k_640b64_4x.yaml) | 17h | 31.5 | 25.6 | [model](https://dl.fbaipublicfiles.com/detic/BoxSup-C2_L_CLIP_R5021k_640b64_4x.pth) | -|[Detic_C2_R50_640_4x](../configs/Detic_LI_CLIP_R5021k_640b64_4x_ft4x_max-size.yaml) | 22h | 33.2 | 29.7 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LI_CLIP_R5021k_640b64_4x_ft4x_max-size.pth) | -|[Box-Supervised_C2_SwinB_896_4x](../configs/BoxSup-C2_L_CLIP_SwinB_896b32_4x.yaml) | 43h | 40.7 | 35.9 | [model](https://dl.fbaipublicfiles.com/detic/BoxSup-C2_L_CLIP_SwinB_896b32_4x.pth) | -|[Detic_C2_SwinB_896_4x](../configs/Detic_LI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml) | 47h | 41.7 | 41.7 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LI_CLIP_SwinB_896b32_4x_ft4x_max-size.pth) | - - -| Name |Training time | box mAP | box mAP_rare | Download | -|-----------------------|------------------|-----------|-----------------|----------| -|[Box-Supervised_DeformDETR_R50_4x](../configs/BoxSup-DeformDETR_L_R50_4x.yaml) | 31h | 31.7 | 21.4 | [model](https://dl.fbaipublicfiles.com/detic/BoxSup-DeformDETR_L_R50_4x.pth) | -|[Detic_DeformDETR_R50_4x](../configs/Detic_DeformDETR_LI_R50_4x_ft4x.yaml) | 47h | 32.5 | 26.2 | [model](https://dl.fbaipublicfiles.com/detic/Detic_DeformDETR_LI_R50_4x_ft4x.pth) | - - -#### Note - -- All Detic models use the overlap classes between ImageNet-21K and LVIS as image-labeled data; - -- The models with `C2` are trained using our improved LVIS baseline in the paper, including CenterNet2 detector, Federated loss, large-scale jittering, etc. - -- The models with `DeformDETR` are Deformable DETR models. We train the models with Federated Loss. - -## Open-vocabulary COCO - -| Name |Training time | box mAP50 | box mAP50_novel | Download | -|-----------------------|------------------|-----------|-----------------|----------| -|[BoxSup_CLIP_R50_1x](../configs/BoxSup_OVCOCO_CLIP_R50_1x.yaml) | 12h | 39.3 | 1.3 | [model](https://dl.fbaipublicfiles.com/detic/BoxSup_OVCOCO_CLIP_R50_1x.pth) | -|[Detic_CLIP_R50_1x_image](../configs/Detic_OVCOCO_CLIP_R50_1x_max-size.yaml) | 13h | 44.7 | 24.1 | [model](https://dl.fbaipublicfiles.com/detic/Detic_OVCOCO_CLIP_R50_1x_max-size.pth) | -|[Detic_CLIP_R50_1x_caption](../configs/Detic_OVCOCO_CLIP_R50_1x_caption.yaml) | 16h | 43.8 | 21.0 | [model](https://dl.fbaipublicfiles.com/detic/Detic_OVCOCO_CLIP_R50_1x_caption.pth) | -|[Detic_CLIP_R50_1x_caption-image](../configs/Detic_OVCOCO_CLIP_R50_1x_max-size_caption.yaml) | 16h | 45.0 | 27.8 | [model](https://dl.fbaipublicfiles.com/detic/Detic_OVCOCO_CLIP_R50_1x_max-size_caption.pth) | - -#### Note - -- All models are trained with ResNet50-C4 without multi-scale augmentation. All models use CLIP embeddings as the classifier. - -- We extract class names from COCO-captions as image-labels. `Detic_CLIP_R50_1x_image` uses the max-size loss; `Detic_CLIP_R50_1x_caption` directly uses CLIP caption embedding within each mini-batch for classification; `Detic_CLIP_R50_1x_caption-image` uses both losses. - -- We report box mAP50 under the "generalized" open-vocabulary setting. - - -## Cross-dataset evaluation - - -| Name |Training time | Objects365 box mAP | OpenImages box mAP50 | Download | -|-----------------------|------------------|-----------|-----------------|----------| -|[Box-Supervised_C2_SwinB_896_4x](../configs/BoxSup-C2_L_CLIP_SwinB_896b32_4x.yaml) | 43h | 19.1 | 46.2 | [model](https://dl.fbaipublicfiles.com/detic/BoxSup-C2_L_CLIP_SwinB_896b32_4x.pth) | -|[Detic_C2_SwinB_896_4x](../configs/Detic_LI_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml) | 47h | 21.2 |53.0 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LI_CLIP_SwinB_896b32_4x_ft4x_max-size.pth) | -|[Detic_C2_SwinB_896_4x_IN-21K](../configs/Detic_LI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml) | 47h | 21.4 | 55.2 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth) | -|[Box-Supervised_C2_SwinB_896_4x+COCO](../configs/BoxSup-C2_LCOCO_CLIP_SwinB_896b32_4x.yaml) | 43h | 19.7 | 46.4 | [model](https://dl.fbaipublicfiles.com/detic/BoxSup-C2_LCOCO_CLIP_SwinB_896b32_4x.pth) | -|[Detic_C2_SwinB_896_4x_IN-21K+COCO](../configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml) | 47h | 21.6 | 54.6 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth) | - - - -#### Note - -- `Box-Supervised_C2_SwinB_896_4x` and `Detic_C2_SwinB_896_4x` are the same model in the [Standard LVIS](#standard-lvis) section, but evaluated with Objects365/ OpenImages vocabulary (i.e. CLIP embeddings of the corresponding class names as classifier). To run the evaluation on Objects365/ OpenImages, run - - ``` - python train_net.py --num-gpus 8 --config-file configs/Detic_C2_SwinB_896_4x.yaml --eval-only DATASETS.TEST "('oid_val_expanded','objects365_v2_val',)" MODEL.RESET_CLS_TESTS True MODEL.TEST_CLASSIFIERS "('datasets/metadata/oid_clip_a+cname.npy','datasets/metadata/o365_clip_a+cnamefix.npy',)" MODEL.TEST_NUM_CLASSES "(500,365)" MODEL.MASK_ON False - ``` - -- `Detic_C2_SwinB_896_4x_IN-21K` trains on the full ImageNet-22K. We additionally use a dynamic class sampling ("Modified Federated Loss" in Section 4.4) and use a larger data sampling ratio of ImageNet images (1:16 instead of 1:4). - -- `Detic_C2_SwinB_896_4x_IN-21K-COCO` is a model trained on combined LVIS-COCO and ImageNet-21K for better demo purposes. LVIS models do not detect persons well due to its federated annotation protocol. LVIS+COCO models give better visual results. - - -## Real-time models - -| Name | Run time (ms) | LVIS box mAP | Download | -|-----------------------|------------------|-----------|-----------------| -|[Detic_C2_SwinB_896_4x_IN-21K+COCO (800x1333, no threshold)](../configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml) | 115 | 44.4 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth) | -|[Detic_C2_SwinB_896_4x_IN-21K+COCO](../configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml) | 46 | 35.0 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth) | -|[Detic_C2_ConvNeXtT_896_4x_IN-21K+COCO](../configs/Detic_LCOCOI21k_CLIP_CXT21k_640b32_4x_ft4x_max-size.yaml) | 26 | 30.7 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_CXT21k_640b32_4x_ft4x_max-size.pth) | -|[Detic_C2_R5021k_896_4x_IN-21K+COCO](../configs/Detic_LCOCOI21k_CLIP_R5021k_640b32_4x_ft4x_max-size.yaml) | 23 | 29.0 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_R5021k_640b32_4x_ft4x_max-size.pth) | -|[Detic_C2_R18_896_4x_IN-21K+COCO](../configs/Detic_LCOCOI21k_CLIP_R18_640b32_4x_ft4x_max-size.yaml) | 18 | 22.1 | [model](https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_R18_640b32_4x_ft4x_max-size.pth) | - -- `Detic_C2_SwinB_896_4x_IN-21K+COCO (800x1333, thresh 0.02)` is the entry on the [Cross-dataset evaluation](#Cross-dataset evaluation) section without the mask head. All other entries use a max-size of 640 and an output score threshold of 0.3 using the following command (e.g., with R50). - - ``` - python train_net.py --config-file configs/Detic_LCOCOI21k_CLIP_R5021k_640b32_4x_ft4x_max-size.yaml --num-gpus 2 --eval-only DATASETS.TEST "('lvis_v1_val',)" MODEL.RESET_CLS_TESTS True MODEL.TEST_CLASSIFIERS "('datasets/metadata/lvis_v1_clip_a+cname.npy',)" MODEL.TEST_NUM_CLASSES "(1203,)" MODEL.MASK_ON False MODEL.WEIGHTS models/Detic_LCOCOI21k_CLIP_R5021k_640b32_4x_ft4x_max-size.pth INPUT.MIN_SIZE_TEST 640 INPUT.MAX_SIZE_TEST 640 MODEL.ROI_HEADS.SCORE_THRESH_TEST 0.3 - ``` - -- All models are trained using the same training recipe except for different backbones. -- The ConvNeXtT and Res50 models are initialized from their corresponding ImageNet-21K pretrained models. The Res18 model is initialized from its ImageNet-1K pretrained model. -- The runtimes are measured on a local workstation with a Titan RTX GPU. diff --git a/dimos/models/Detic/docs/example_output_custom.jpeg b/dimos/models/Detic/docs/example_output_custom.jpeg deleted file mode 100644 index ac6aa3fb93..0000000000 Binary files a/dimos/models/Detic/docs/example_output_custom.jpeg and /dev/null differ diff --git a/dimos/models/Detic/docs/example_output_lvis.jpeg b/dimos/models/Detic/docs/example_output_lvis.jpeg deleted file mode 100644 index 3d22122059..0000000000 Binary files a/dimos/models/Detic/docs/example_output_lvis.jpeg and /dev/null differ diff --git a/dimos/models/Detic/docs/teaser.jpeg b/dimos/models/Detic/docs/teaser.jpeg deleted file mode 100644 index 2e8fbac2f8..0000000000 Binary files a/dimos/models/Detic/docs/teaser.jpeg and /dev/null differ diff --git a/dimos/models/Detic/lazy_train_net.py b/dimos/models/Detic/lazy_train_net.py deleted file mode 100644 index 3525a1f63a..0000000000 --- a/dimos/models/Detic/lazy_train_net.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -""" -Training script using the new "LazyConfig" python config files. -This scripts reads a given python config file and runs the training or evaluation. -It can be used to train any models or dataset as long as they can be -instantiated by the recursive construction defined in the given config file. -Besides lazy construction of models, dataloader, etc., this scripts expects a -few common configuration parameters currently defined in "configs/common/train.py". -To add more complicated training logic, you can easily add other configs -in the config file and implement a new train_net.py to handle them. -""" - -import logging -import sys - -from detectron2.checkpoint import DetectionCheckpointer -from detectron2.config import LazyConfig, instantiate -from detectron2.engine import ( - AMPTrainer, - SimpleTrainer, - default_argument_parser, - default_setup, - default_writers, - hooks, - launch, -) -from detectron2.engine.defaults import create_ddp_model -from detectron2.evaluation import inference_on_dataset, print_csv_format -from detectron2.utils import comm - -sys.path.insert(0, "third_party/CenterNet2/") -sys.path.insert(0, "third_party/Deformable-DETR") -logger = logging.getLogger("detectron2") - - -def do_test(cfg, model): - if "evaluator" in cfg.dataloader: - ret = inference_on_dataset( - model, instantiate(cfg.dataloader.test), instantiate(cfg.dataloader.evaluator) - ) - print_csv_format(ret) - return ret - - -def do_train(args, cfg) -> None: - """ - Args: - cfg: an object with the following attributes: - model: instantiate to a module - dataloader.{train,test}: instantiate to dataloaders - dataloader.evaluator: instantiate to evaluator for test set - optimizer: instantaite to an optimizer - lr_multiplier: instantiate to a fvcore scheduler - train: other misc config defined in `common_train.py`, including: - output_dir (str) - init_checkpoint (str) - amp.enabled (bool) - max_iter (int) - eval_period, log_period (int) - device (str) - checkpointer (dict) - ddp (dict) - """ - model = instantiate(cfg.model) - logger = logging.getLogger("detectron2") - logger.info(f"Model:\n{model}") - model.to(cfg.train.device) - - cfg.optimizer.params.model = model - optim = instantiate(cfg.optimizer) - - train_loader = instantiate(cfg.dataloader.train) - - model = create_ddp_model(model, **cfg.train.ddp) - trainer = (AMPTrainer if cfg.train.amp.enabled else SimpleTrainer)(model, train_loader, optim) - checkpointer = DetectionCheckpointer( - model, - cfg.train.output_dir, - optimizer=optim, - trainer=trainer, - ) - train_hooks = [ - hooks.IterationTimer(), - hooks.LRScheduler(scheduler=instantiate(cfg.lr_multiplier)), - hooks.PeriodicCheckpointer(checkpointer, **cfg.train.checkpointer) - if comm.is_main_process() - else None, - hooks.EvalHook(cfg.train.eval_period, lambda: do_test(cfg, model)), - hooks.PeriodicWriter( - default_writers(cfg.train.output_dir, cfg.train.max_iter), - period=cfg.train.log_period, - ) - if comm.is_main_process() - else None, - ] - trainer.register_hooks(train_hooks) - - checkpointer.resume_or_load(cfg.train.init_checkpoint, resume=args.resume) - if args.resume and checkpointer.has_checkpoint(): - # The checkpoint stores the training iteration that just finished, thus we start - # at the next iteration - start_iter = trainer.iter + 1 - else: - start_iter = 0 - trainer.train(start_iter, cfg.train.max_iter) - - -def main(args) -> None: - cfg = LazyConfig.load(args.config_file) - cfg = LazyConfig.apply_overrides(cfg, args.opts) - default_setup(cfg, args) - - if args.eval_only: - model = instantiate(cfg.model) - model.to(cfg.train.device) - model = create_ddp_model(model) - DetectionCheckpointer(model).load(cfg.train.init_checkpoint) - print(do_test(cfg, model)) - else: - do_train(args, cfg) - - -if __name__ == "__main__": - args = default_argument_parser().parse_args() - launch( - main, - args.num_gpus, - num_machines=args.num_machines, - machine_rank=args.machine_rank, - dist_url=args.dist_url, - args=(args,), - ) diff --git a/dimos/models/Detic/predict.py b/dimos/models/Detic/predict.py deleted file mode 100644 index bf71d007a1..0000000000 --- a/dimos/models/Detic/predict.py +++ /dev/null @@ -1,102 +0,0 @@ -from pathlib import Path -import sys -import tempfile -import time - -import cog -import cv2 -from detectron2.config import get_cfg -from detectron2.data import MetadataCatalog - -# import some common detectron2 utilities -from detectron2.engine import DefaultPredictor -from detectron2.utils.visualizer import Visualizer - -# Detic libraries -sys.path.insert(0, "third_party/CenterNet2/") -from centernet.config import add_centernet_config -from detic.config import add_detic_config -from detic.modeling.text.text_encoder import build_text_encoder -from detic.modeling.utils import reset_cls_test - - -class Predictor(cog.Predictor): - def setup(self) -> None: - cfg = get_cfg() - add_centernet_config(cfg) - add_detic_config(cfg) - cfg.merge_from_file("configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml") - cfg.MODEL.WEIGHTS = "Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth" - cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5 # set threshold for this model - cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = "rand" - cfg.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL = True - self.predictor = DefaultPredictor(cfg) - self.BUILDIN_CLASSIFIER = { - "lvis": "datasets/metadata/lvis_v1_clip_a+cname.npy", - "objects365": "datasets/metadata/o365_clip_a+cnamefix.npy", - "openimages": "datasets/metadata/oid_clip_a+cname.npy", - "coco": "datasets/metadata/coco_clip_a+cname.npy", - } - self.BUILDIN_METADATA_PATH = { - "lvis": "lvis_v1_val", - "objects365": "objects365_v2_val", - "openimages": "oid_val_expanded", - "coco": "coco_2017_val", - } - - @cog.input( - "image", - type=Path, - help="input image", - ) - @cog.input( - "vocabulary", - type=str, - default="lvis", - options=["lvis", "objects365", "openimages", "coco", "custom"], - help="Choose vocabulary", - ) - @cog.input( - "custom_vocabulary", - type=str, - default=None, - help="Type your own vocabularies, separated by coma ','", - ) - def predict(self, image, vocabulary, custom_vocabulary): - image = cv2.imread(str(image)) - if not vocabulary == "custom": - metadata = MetadataCatalog.get(self.BUILDIN_METADATA_PATH[vocabulary]) - classifier = self.BUILDIN_CLASSIFIER[vocabulary] - num_classes = len(metadata.thing_classes) - reset_cls_test(self.predictor.model, classifier, num_classes) - - else: - assert custom_vocabulary is not None and len(custom_vocabulary.split(",")) > 0, ( - "Please provide your own vocabularies when vocabulary is set to 'custom'." - ) - metadata = MetadataCatalog.get(str(time.time())) - metadata.thing_classes = custom_vocabulary.split(",") - classifier = get_clip_embeddings(metadata.thing_classes) - num_classes = len(metadata.thing_classes) - reset_cls_test(self.predictor.model, classifier, num_classes) - # Reset visualization threshold - output_score_threshold = 0.3 - for cascade_stages in range(len(self.predictor.model.roi_heads.box_predictor)): - self.predictor.model.roi_heads.box_predictor[ - cascade_stages - ].test_score_thresh = output_score_threshold - - outputs = self.predictor(image) - v = Visualizer(image[:, :, ::-1], metadata) - out = v.draw_instance_predictions(outputs["instances"].to("cpu")) - out_path = Path(tempfile.mkdtemp()) / "out.png" - cv2.imwrite(str(out_path), out.get_image()[:, :, ::-1]) - return out_path - - -def get_clip_embeddings(vocabulary, prompt: str="a "): - text_encoder = build_text_encoder(pretrain=True) - text_encoder.eval() - texts = [prompt + x for x in vocabulary] - emb = text_encoder(texts).detach().permute(1, 0).contiguous().cpu() - return emb diff --git a/dimos/models/Detic/requirements.txt b/dimos/models/Detic/requirements.txt deleted file mode 100644 index 518274db24..0000000000 --- a/dimos/models/Detic/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -opencv-python -mss -timm -dataclasses -ftfy -regex -fasttext -scikit-learn -lvis -nltk -git+https://github.com/openai/CLIP.git diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/CODE_OF_CONDUCT.md b/dimos/models/Detic/third_party/CenterNet2/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 0f7ad8bfc1..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,5 +0,0 @@ -# Code of Conduct - -Facebook has adopted a Code of Conduct that we expect project participants to adhere to. -Please read the [full text](https://code.fb.com/codeofconduct/) -so that you can understand what actions will and will not be tolerated. diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/CONTRIBUTING.md b/dimos/models/Detic/third_party/CenterNet2/.github/CONTRIBUTING.md deleted file mode 100644 index 9bab709cae..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/CONTRIBUTING.md +++ /dev/null @@ -1,68 +0,0 @@ -# Contributing to detectron2 - -## Issues -We use GitHub issues to track public bugs and questions. -Please make sure to follow one of the -[issue templates](https://github.com/facebookresearch/detectron2/issues/new/choose) -when reporting any issues. - -Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe -disclosure of security bugs. In those cases, please go through the process -outlined on that page and do not file a public issue. - -## Pull Requests -We actively welcome pull requests. - -However, if you're adding any significant features (e.g. > 50 lines), please -make sure to discuss with maintainers about your motivation and proposals in an issue -before sending a PR. This is to save your time so you don't spend time on a PR that we'll not accept. - -We do not always accept new features, and we take the following -factors into consideration: - -1. Whether the same feature can be achieved without modifying detectron2. - Detectron2 is designed so that you can implement many extensions from the outside, e.g. - those in [projects](https://github.com/facebookresearch/detectron2/tree/master/projects). - * If some part of detectron2 is not extensible enough, you can also bring up a more general issue to - improve it. Such feature request may be useful to more users. -2. Whether the feature is potentially useful to a large audience (e.g. an impactful detection paper, a popular dataset, - a significant speedup, a widely useful utility), - or only to a small portion of users (e.g., a less-known paper, an improvement not in the object - detection field, a trick that's not very popular in the community, code to handle a non-standard type of data) - * Adoption of additional models, datasets, new task are by default not added to detectron2 before they - receive significant popularity in the community. - We sometimes accept such features in `projects/`, or as a link in `projects/README.md`. -3. Whether the proposed solution has a good design / interface. This can be discussed in the issue prior to PRs, or - in the form of a draft PR. -4. Whether the proposed solution adds extra mental/practical overhead to users who don't - need such feature. -5. Whether the proposed solution breaks existing APIs. - -To add a feature to an existing function/class `Func`, there are always two approaches: -(1) add new arguments to `Func`; (2) write a new `Func_with_new_feature`. -To meet the above criteria, we often prefer approach (2), because: - -1. It does not involve modifying or potentially breaking existing code. -2. It does not add overhead to users who do not need the new feature. -3. Adding new arguments to a function/class is not scalable w.r.t. all the possible new research ideas in the future. - -When sending a PR, please do: - -1. If a PR contains multiple orthogonal changes, split it to several PRs. -2. If you've added code that should be tested, add tests. -3. For PRs that need experiments (e.g. adding a new model or new methods), - you don't need to update model zoo, but do provide experiment results in the description of the PR. -4. If APIs are changed, update the documentation. -5. We use the [Google style docstrings](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html) in python. -6. Make sure your code lints with `./dev/linter.sh`. - - -## Contributor License Agreement ("CLA") -In order to accept your pull request, we need you to submit a CLA. You only need -to do this once to work on any of Facebook's open source projects. - -Complete your CLA here: - -## License -By contributing to detectron2, you agree that your contributions will be licensed -under the LICENSE file in the root directory of this source tree. diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/Detectron2-Logo-Horz.svg b/dimos/models/Detic/third_party/CenterNet2/.github/Detectron2-Logo-Horz.svg deleted file mode 100644 index eb2d643ddd..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/Detectron2-Logo-Horz.svg +++ /dev/null @@ -1 +0,0 @@ -Detectron2-Logo-Horz \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE.md b/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 5e8aaa2d37..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,5 +0,0 @@ - -Please select an issue template from -https://github.com/facebookresearch/detectron2/issues/new/choose . - -Otherwise your issue will be closed. diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/bugs.md b/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/bugs.md deleted file mode 100644 index d0235c708a..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/bugs.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: "šŸ› Bugs" -about: Report bugs in detectron2 -title: Please read & provide the following - ---- - -## Instructions To Reproduce the šŸ› Bug: -1. Full runnable code or full changes you made: -``` -If making changes to the project itself, please use output of the following command: -git rev-parse HEAD; git diff - - -``` -2. What exact command you run: -3. __Full logs__ or other relevant observations: -``` - -``` -4. please simplify the steps as much as possible so they do not require additional resources to - run, such as a private dataset. - -## Expected behavior: - -If there are no obvious error in "full logs" provided above, -please tell us the expected behavior. - -## Environment: - -Provide your environment information using the following command: -``` -wget -nc -q https://github.com/facebookresearch/detectron2/raw/main/detectron2/utils/collect_env.py && python collect_env.py -``` - -If your issue looks like an installation issue / environment issue, -please first try to solve it yourself with the instructions in -https://detectron2.readthedocs.io/tutorials/install.html#common-installation-issues diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/config.yml b/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index c60c2e1430..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -# require an issue template to be chosen -blank_issues_enabled: false - -contact_links: - - name: How-To / All Other Questions - url: https://github.com/facebookresearch/detectron2/discussions - about: Use "github discussions" for community support on general questions that don't belong to the above issue categories - - name: Detectron2 Documentation - url: https://detectron2.readthedocs.io/index.html - about: Check if your question is answered in tutorials or API docs - -# Unexpected behaviors & bugs are split to two templates. -# When they are one template, users think "it's not a bug" and don't choose the template. -# -# But the file name is still "unexpected-problems-bugs.md" so that old references -# to this issue template still works. -# It's ok since this template should be a superset of "bugs.md" (unexpected behaviors is a superset of bugs) diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/documentation.md b/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index 88214d62e5..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: "\U0001F4DA Documentation Issue" -about: Report a problem about existing documentation, comments, website or tutorials. -labels: documentation - ---- - -## šŸ“š Documentation Issue - -This issue category is for problems about existing documentation, not for asking how-to questions. - -* Provide a link to an existing documentation/comment/tutorial: - -* How should the above documentation/comment/tutorial improve: diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/feature-request.md b/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index 03a1e93d72..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: "\U0001F680Feature Request" -about: Suggest an improvement or new feature -labels: enhancement - ---- - -## šŸš€ Feature -A clear and concise description of the feature proposal. - -## Motivation & Examples - -Tell us why the feature is useful. - -Describe what the feature would look like, if it is implemented. -Best demonstrated using **code examples** in addition to words. - -## Note - -We only consider adding new features if they are relevant to many users. - -If you request implementation of research papers -- we only consider papers that have enough significance and prevalance in the object detection field. - -We do not take requests for most projects in the `projects/` directory, because they are research code release that is mainly for other researchers to reproduce results. - -"Make X faster/accurate" is not a valid feature request. "Implement a concrete feature that can make X faster/accurate" can be a valid feature request. - -Instead of adding features inside detectron2, -you can implement many features by [extending detectron2](https://detectron2.readthedocs.io/tutorials/extend.html). -The [projects/](https://github.com/facebookresearch/detectron2/tree/main/projects/) directory contains many of such examples. - diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/unexpected-problems-bugs.md b/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/unexpected-problems-bugs.md deleted file mode 100644 index 5db8f22415..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/ISSUE_TEMPLATE/unexpected-problems-bugs.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: "😩 Unexpected behaviors" -about: Report unexpected behaviors when using detectron2 -title: Please read & provide the following - ---- - -If you do not know the root cause of the problem, please post according to this template: - -## Instructions To Reproduce the Issue: - -Check https://stackoverflow.com/help/minimal-reproducible-example for how to ask good questions. -Simplify the steps to reproduce the issue using suggestions from the above link, and provide them below: - -1. Full runnable code or full changes you made: -``` -If making changes to the project itself, please use output of the following command: -git rev-parse HEAD; git diff - - -``` -2. What exact command you run: -3. __Full logs__ or other relevant observations: -``` - -``` - -## Expected behavior: - -If there are no obvious crash in "full logs" provided above, -please tell us the expected behavior. - -If you expect a model to converge / work better, we do not help with such issues, unless -a model fails to reproduce the results in detectron2 model zoo, or proves existence of bugs. - -## Environment: - -Paste the output of the following command: -``` -wget -nc -nv https://github.com/facebookresearch/detectron2/raw/main/detectron2/utils/collect_env.py && python collect_env.py -``` - -If your issue looks like an installation issue / environment issue, -please first check common issues in https://detectron2.readthedocs.io/tutorials/install.html#common-installation-issues diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/pull_request_template.md b/dimos/models/Detic/third_party/CenterNet2/.github/pull_request_template.md deleted file mode 100644 index d71729baee..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/pull_request_template.md +++ /dev/null @@ -1,10 +0,0 @@ -Thanks for your contribution! - -If you're sending a large PR (e.g., >100 lines), -please open an issue first about the feature / bug, and indicate how you want to contribute. - -We do not always accept features. -See https://detectron2.readthedocs.io/notes/contributing.html#pull-requests about how we handle PRs. - -Before submitting a PR, please run `dev/linter.sh` to lint the code. - diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/check-template.yml b/dimos/models/Detic/third_party/CenterNet2/.github/workflows/check-template.yml deleted file mode 100644 index 3caed9df3c..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/check-template.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Check issue template - -on: - issues: - types: [opened] - -jobs: - check-template: - runs-on: ubuntu-latest - # comment this out when testing with https://github.com/nektos/act - if: ${{ github.repository_owner == 'facebookresearch' }} - steps: - - uses: actions/checkout@v2 - - uses: actions/github-script@v3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - // Arguments available: - // - github: A pre-authenticated octokit/rest.js client - // - context: An object containing the context of the workflow run - // - core: A reference to the @actions/core package - // - io: A reference to the @actions/io package - const fs = require('fs'); - const editDistance = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/levenshtein.js`).getEditDistance - issue = await github.issues.get({ - owner: context.issue.owner, - repo: context.issue.repo, - issue_number: context.issue.number, - }); - const hasLabel = issue.data.labels.length > 0; - if (hasLabel || issue.state === "closed") { - // don't require template on them - core.debug("Issue " + issue.data.title + " was skipped."); - return; - } - - sameAsTemplate = function(filename, body) { - let tmpl = fs.readFileSync(`.github/ISSUE_TEMPLATE/${filename}`, 'utf8'); - tmpl = tmpl.toLowerCase().split("---").slice(2).join("").trim(); - tmpl = tmpl.replace(/(\r\n|\n|\r)/gm, ""); - let bodyr = body.replace(/(\r\n|\n|\r)/gm, ""); - let dist = editDistance(tmpl, bodyr); - return dist < 8; - }; - - checkFail = async function(msg) { - core.info("Processing '" + issue.data.title + "' with message: " + msg); - await github.issues.addLabels({ - owner: context.issue.owner, - repo: context.issue.repo, - issue_number: context.issue.number, - labels: ["needs-more-info"], - }); - await github.issues.createComment({ - owner: context.issue.owner, - repo: context.issue.repo, - issue_number: context.issue.number, - body: msg, - }); - }; - - const body = issue.data.body.toLowerCase().trim(); - - if (sameAsTemplate("bugs.md", body) || sameAsTemplate("unexpected-problems-bugs.md", body)) { - await checkFail(` - We found that not enough information is provided about this issue. - Please provide details following the [issue template](https://github.com/facebookresearch/detectron2/issues/new/choose).`) - return; - } - - const hasInstructions = body.indexOf("reproduce") != -1; - const hasEnvironment = (body.indexOf("environment") != -1) || (body.indexOf("colab") != -1) || (body.indexOf("docker") != -1); - if (hasInstructions && hasEnvironment) { - core.debug("Issue " + issue.data.title + " follows template."); - return; - } - - let message = "You've chosen to report an unexpected problem or bug. Unless you already know the root cause of it, please include details about it by filling the [issue template](https://github.com/facebookresearch/detectron2/issues/new/choose).\n"; - message += "The following information is missing: "; - if (!hasInstructions) { - message += "\"Instructions To Reproduce the Issue and __Full__ Logs\"; "; - } - if (!hasEnvironment) { - message += "\"Your Environment\"; "; - } - await checkFail(message); diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/levenshtein.js b/dimos/models/Detic/third_party/CenterNet2/.github/workflows/levenshtein.js deleted file mode 100644 index 67a5e3613c..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/levenshtein.js +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright (c) 2011 Andrei Mackenzie - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -// Compute the edit distance between the two given strings -exports.getEditDistance = function(a, b){ - if(a.length == 0) return b.length; - if(b.length == 0) return a.length; - - var matrix = []; - - // increment along the first column of each row - var i; - for(i = 0; i <= b.length; i++){ - matrix[i] = [i]; - } - - // increment each column in the first row - var j; - for(j = 0; j <= a.length; j++){ - matrix[0][j] = j; - } - - // Fill in the rest of the matrix - for(i = 1; i <= b.length; i++){ - for(j = 1; j <= a.length; j++){ - if(b.charAt(i-1) == a.charAt(j-1)){ - matrix[i][j] = matrix[i-1][j-1]; - } else { - matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, // substitution - Math.min(matrix[i][j-1] + 1, // insertion - matrix[i-1][j] + 1)); // deletion - } - } - } - - return matrix[b.length][a.length]; -}; diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/needs-reply.yml b/dimos/models/Detic/third_party/CenterNet2/.github/workflows/needs-reply.yml deleted file mode 100644 index 4affabd349..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/needs-reply.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Close/Lock issues after inactivity - -on: - schedule: - - cron: "0 0 * * *" - -jobs: - close-issues-needs-more-info: - runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'facebookresearch' }} - steps: - - name: Close old issues that need reply - uses: actions/github-script@v3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - # Modified from https://github.com/dwieeb/needs-reply - script: | - // Arguments available: - // - github: A pre-authenticated octokit/rest.js client - // - context: An object containing the context of the workflow run - // - core: A reference to the @actions/core package - // - io: A reference to the @actions/io package - const kLabelToCheck = "needs-more-info"; - const kInvalidLabel = "invalid/unrelated"; - const kDaysBeforeClose = 7; - const kMessage = "Requested information was not provided in 7 days, so we're closing this issue.\n\nPlease open new issue if information becomes available. Otherwise, use [github discussions](https://github.com/facebookresearch/detectron2/discussions) for free-form discussions." - - issues = await github.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: kLabelToCheck, - sort: 'updated', - direction: 'asc', - per_page: 30, - page: 1, - }); - issues = issues.data; - if (issues.length === 0) { - core.info('No more issues found to process. Exiting.'); - return; - } - for (const issue of issues) { - if (!!issue.pull_request) - continue; - core.info(`Processing issue #${issue.number}`); - - let updatedAt = new Date(issue.updated_at).getTime(); - const numComments = issue.comments; - const comments = await github.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - per_page: 30, - page: Math.floor((numComments - 1) / 30) + 1, // the last page - }); - const lastComments = comments.data - .map(l => new Date(l.created_at).getTime()) - .sort(); - if (lastComments.length > 0) { - updatedAt = lastComments[lastComments.length - 1]; - } - - const now = new Date().getTime(); - const daysSinceUpdated = (now - updatedAt) / 1000 / 60 / 60 / 24; - - if (daysSinceUpdated < kDaysBeforeClose) { - core.info(`Skipping #${issue.number} because it has been updated in the last ${daysSinceUpdated} days`); - continue; - } - core.info(`Closing #${issue.number} because it has not been updated in the last ${daysSinceUpdated} days`); - await github.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: kMessage, - }); - const newLabels = numComments <= 2 ? [kInvalidLabel, kLabelToCheck] : issue.labels; - await github.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: newLabels, - state: 'closed', - }); - } - - lock-issues-after-closed: - runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'facebookresearch' }} - steps: - - name: Lock closed issues that have no activity for a while - uses: dessant/lock-threads@v2 - with: - github-token: ${{ github.token }} - issue-lock-inactive-days: '300' - process-only: 'issues' - issue-exclude-labels: 'enhancement,bug,documentation' diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/remove-needs-reply.yml b/dimos/models/Detic/third_party/CenterNet2/.github/workflows/remove-needs-reply.yml deleted file mode 100644 index 1f000b28ca..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/remove-needs-reply.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Remove needs-more-info label - -on: - issue_comment: - types: [created] - issues: - types: [edited] - -jobs: - remove-needs-more-info-label: - runs-on: ubuntu-latest - # 1. issue_comment events could include PR comment, filter them out - # 2. Only trigger action if event was produced by the original author - if: ${{ !github.event.issue.pull_request && github.event.sender.login == github.event.issue.user.login }} - steps: - - name: Remove needs-more-info label - uses: octokit/request-action@v2.x - continue-on-error: true - with: - route: DELETE /repos/:repository/issues/:issue/labels/:label - repository: ${{ github.repository }} - issue: ${{ github.event.issue.number }} - label: needs-more-info - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/workflow.yml b/dimos/models/Detic/third_party/CenterNet2/.github/workflows/workflow.yml deleted file mode 100644 index 6085b32a50..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.github/workflows/workflow.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: CI -on: [push, pull_request] - -# Run linter with github actions for quick feedbacks. -# Run macos tests with github actions. Linux (CPU & GPU) tests currently runs on CircleCI -jobs: - linter: - runs-on: ubuntu-latest - # run on PRs, or commits to facebookresearch (not internal) - if: ${{ github.repository_owner == 'facebookresearch' || github.event_name == 'pull_request' }} - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.6 - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - name: Install dependencies - # flake8-bugbear flake8-comprehensions are useful but not available internally - run: | - python -m pip install --upgrade pip - python -m pip install flake8==3.8.1 isort==4.3.21 - python -m pip install black==21.4b2 - flake8 --version - - name: Lint - run: | - echo "Running isort" - isort -c -sp . - echo "Running black" - black -l 100 --check . - echo "Running flake8" - flake8 . - - macos_tests: - runs-on: macos-latest - # run on PRs, or commits to facebookresearch (not internal) - if: ${{ github.repository_owner == 'facebookresearch' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - matrix: - torch: ["1.8", "1.9", "1.10"] - include: - - torch: "1.8" - torchvision: 0.9 - - torch: "1.9" - torchvision: "0.10" - - torch: "1.10" - torchvision: "0.11.1" - env: - # point datasets to ~/.torch so it's cached by CI - DETECTRON2_DATASETS: ~/.torch/datasets - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Set up Python 3.6 - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: | - ${{ env.pythonLocation }}/lib/python3.6/site-packages - ~/.torch - key: ${{ runner.os }}-torch${{ matrix.torch }}-${{ hashFiles('setup.py') }}-20210420 - - - name: Install dependencies - run: | - python -m pip install -U pip - python -m pip install ninja opencv-python-headless onnx pytest-xdist - python -m pip install torch==${{matrix.torch}} torchvision==${{matrix.torchvision}} -f https://download.pytorch.org/whl/torch_stable.html - # install from github to get latest; install iopath first since fvcore depends on it - python -m pip install -U 'git+https://github.com/facebookresearch/iopath' - python -m pip install -U 'git+https://github.com/facebookresearch/fvcore' - - - name: Build and install - run: | - CC=clang CXX=clang++ python -m pip install -e .[all] - python -m detectron2.utils.collect_env - ./datasets/prepare_for_tests.sh - - name: Run unittests - run: python -m pytest -n 4 --durations=15 -v tests/ diff --git a/dimos/models/Detic/third_party/CenterNet2/.gitignore b/dimos/models/Detic/third_party/CenterNet2/.gitignore deleted file mode 100644 index e045ffa557..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/.gitignore +++ /dev/null @@ -1,58 +0,0 @@ -third_party/detectron2 -slurm* -# output dir -output -instant_test_output -inference_test_output - - -*.png -*.json -*.diff -# *.jpg -!/projects/DensePose/doc/images/*.jpg - -# compilation and distribution -__pycache__ -_ext -*.pyc -*.pyd -*.so -*.dll -*.egg-info/ -build/ -dist/ -wheels/ - -# pytorch/python/numpy formats -*.pth -*.pkl -*.npy -*.ts -model_ts*.txt - -# ipython/jupyter notebooks -*.ipynb -**/.ipynb_checkpoints/ - -# Editor temporaries -*.swn -*.swo -*.swp -*~ - -# editor settings -.idea -.vscode -_darcs - -# project dirs -/detectron2/model_zoo/configs -/datasets/* -!/datasets/*.* -!/datasets/lvis/ -/datasets/lvis/* -!/datasets/lvis/lvis_v1_train_cat_info.json -/projects/*/datasets -/models -/snippet diff --git a/dimos/models/Detic/third_party/CenterNet2/LICENSE b/dimos/models/Detic/third_party/CenterNet2/LICENSE deleted file mode 100644 index cd1b070674..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "[]" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/dimos/models/Detic/third_party/CenterNet2/README.md b/dimos/models/Detic/third_party/CenterNet2/README.md deleted file mode 100644 index 7ccbf8818f..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Probabilistic two-stage detection -Two-stage object detectors that use class-agnostic one-stage detectors as the proposal network. - - -

- -> [**Probabilistic two-stage detection**](http://arxiv.org/abs/2103.07461), -> Xingyi Zhou, Vladlen Koltun, Philipp Krähenbühl, -> *arXiv technical report ([arXiv 2103.07461](http://arxiv.org/abs/2103.07461))* - -Contact: [zhouxy@cs.utexas.edu](mailto:zhouxy@cs.utexas.edu). Any questions or discussions are welcomed! - -## Summary - -- Two-stage CenterNet: First stage estimates object probabilities, second stage conditionally classifies objects. - -- Resulting detector is faster and more accurate than both traditional two-stage detectors (fewer proposals required), and one-stage detectors (lighter first stage head). - -- Our best model achieves 56.4 mAP on COCO test-dev. - -- This repo also includes a detectron2-based CenterNet implementation with better accuracy (42.5 mAP at 70FPS) and a new FPN version of CenterNet (40.2 mAP with Res50_1x). - -## Main results - -All models are trained with multi-scale training, and tested with a single scale. The FPS is tested on a Titan RTX GPU. -More models and details can be found in the [MODEL_ZOO](docs/MODEL_ZOO.md). - -#### COCO - -| Model | COCO val mAP | FPS | -|-------------------------------------------|---------------|-------| -| CenterNet-S4_DLA_8x | 42.5 | 71 | -| CenterNet2_R50_1x | 42.9 | 24 | -| CenterNet2_X101-DCN_2x | 49.9 | 8 | -| CenterNet2_R2-101-DCN-BiFPN_4x+4x_1560_ST | 56.1 | 5 | -| CenterNet2_DLA-BiFPN-P5_24x_ST | 49.2 | 38 | - - -#### LVIS - -| Model | val mAP box | -| ------------------------- | ----------- | -| CenterNet2_R50_1x | 26.5 | -| CenterNet2_FedLoss_R50_1x | 28.3 | - - -#### Objects365 - -| Model | val mAP | -|-------------------------------------------|----------| -| CenterNet2_R50_1x | 22.6 | - -## Installation - -Our project is developed on [detectron2](https://github.com/facebookresearch/detectron2). Please follow the official detectron2 [installation](https://github.com/facebookresearch/detectron2/blob/master/INSTALL.md). - -We use the default detectron2 demo script. To run inference on an image folder using our pre-trained model, run - -~~~ -python demo.py --config-file configs/CenterNet2_R50_1x.yaml --input path/to/image/ --opts MODEL.WEIGHTS models/CenterNet2_R50_1x.pth -~~~ - -## Benchmark evaluation and training - -Please check detectron2 [GETTING_STARTED.md](https://github.com/facebookresearch/detectron2/blob/master/GETTING_STARTED.md) for running evaluation and training. Our config files are under `configs` and the pre-trained models are in the [MODEL_ZOO](docs/MODEL_ZOO.md). - - -## License - -Our code is under [Apache 2.0 license](LICENSE). `centernet/modeling/backbone/bifpn_fcos.py` are from [AdelaiDet](https://github.com/aim-uofa/AdelaiDet), which follows the original [non-commercial license](https://github.com/aim-uofa/AdelaiDet/blob/master/LICENSE). - -## Citation - -If you find this project useful for your research, please use the following BibTeX entry. - - @inproceedings{zhou2021probablistic, - title={Probabilistic two-stage detection}, - author={Zhou, Xingyi and Koltun, Vladlen and Kr{\"a}henb{\"u}hl, Philipp}, - booktitle={arXiv preprint arXiv:2103.07461}, - year={2021} - } diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/__init__.py b/dimos/models/Detic/third_party/CenterNet2/centernet/__init__.py deleted file mode 100644 index 5e2e7afac6..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .data.datasets import nuimages -from .data.datasets.coco import _PREDEFINED_SPLITS_COCO -from .data.datasets.objects365 import categories_v1 -from .modeling.backbone.bifpn import build_resnet_bifpn_backbone -from .modeling.backbone.bifpn_fcos import build_fcos_resnet_bifpn_backbone -from .modeling.backbone.dla import build_dla_backbone -from .modeling.backbone.dlafpn import build_dla_fpn3_backbone -from .modeling.backbone.fpn_p5 import build_p67_resnet_fpn_backbone -from .modeling.backbone.res2net import build_p67_res2net_fpn_backbone -from .modeling.dense_heads.centernet import CenterNet -from .modeling.meta_arch.centernet_detector import CenterNetDetector -from .modeling.roi_heads.custom_roi_heads import CustomCascadeROIHeads, CustomROIHeads diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/config.py b/dimos/models/Detic/third_party/CenterNet2/centernet/config.py deleted file mode 100644 index 255eb36340..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/config.py +++ /dev/null @@ -1,88 +0,0 @@ -from detectron2.config import CfgNode as CN - - -def add_centernet_config(cfg) -> None: - _C = cfg - - _C.MODEL.CENTERNET = CN() - _C.MODEL.CENTERNET.NUM_CLASSES = 80 - _C.MODEL.CENTERNET.IN_FEATURES = ["p3", "p4", "p5", "p6", "p7"] - _C.MODEL.CENTERNET.FPN_STRIDES = [8, 16, 32, 64, 128] - _C.MODEL.CENTERNET.PRIOR_PROB = 0.01 - _C.MODEL.CENTERNET.INFERENCE_TH = 0.05 - _C.MODEL.CENTERNET.CENTER_NMS = False - _C.MODEL.CENTERNET.NMS_TH_TRAIN = 0.6 - _C.MODEL.CENTERNET.NMS_TH_TEST = 0.6 - _C.MODEL.CENTERNET.PRE_NMS_TOPK_TRAIN = 1000 - _C.MODEL.CENTERNET.POST_NMS_TOPK_TRAIN = 100 - _C.MODEL.CENTERNET.PRE_NMS_TOPK_TEST = 1000 - _C.MODEL.CENTERNET.POST_NMS_TOPK_TEST = 100 - _C.MODEL.CENTERNET.NORM = "GN" - _C.MODEL.CENTERNET.USE_DEFORMABLE = False - _C.MODEL.CENTERNET.NUM_CLS_CONVS = 4 - _C.MODEL.CENTERNET.NUM_BOX_CONVS = 4 - _C.MODEL.CENTERNET.NUM_SHARE_CONVS = 0 - _C.MODEL.CENTERNET.LOC_LOSS_TYPE = "giou" - _C.MODEL.CENTERNET.SIGMOID_CLAMP = 1e-4 - _C.MODEL.CENTERNET.HM_MIN_OVERLAP = 0.8 - _C.MODEL.CENTERNET.MIN_RADIUS = 4 - _C.MODEL.CENTERNET.SOI = [[0, 80], [64, 160], [128, 320], [256, 640], [512, 10000000]] - _C.MODEL.CENTERNET.POS_WEIGHT = 1.0 - _C.MODEL.CENTERNET.NEG_WEIGHT = 1.0 - _C.MODEL.CENTERNET.REG_WEIGHT = 2.0 - _C.MODEL.CENTERNET.HM_FOCAL_BETA = 4 - _C.MODEL.CENTERNET.HM_FOCAL_ALPHA = 0.25 - _C.MODEL.CENTERNET.LOSS_GAMMA = 2.0 - _C.MODEL.CENTERNET.WITH_AGN_HM = False - _C.MODEL.CENTERNET.ONLY_PROPOSAL = False - _C.MODEL.CENTERNET.AS_PROPOSAL = False - _C.MODEL.CENTERNET.IGNORE_HIGH_FP = -1.0 - _C.MODEL.CENTERNET.MORE_POS = False - _C.MODEL.CENTERNET.MORE_POS_THRESH = 0.2 - _C.MODEL.CENTERNET.MORE_POS_TOPK = 9 - _C.MODEL.CENTERNET.NOT_NORM_REG = True - _C.MODEL.CENTERNET.NOT_NMS = False - _C.MODEL.CENTERNET.NO_REDUCE = False - - _C.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE = False - _C.MODEL.ROI_BOX_HEAD.PRIOR_PROB = 0.01 - _C.MODEL.ROI_BOX_HEAD.USE_EQL_LOSS = False - _C.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH = "datasets/lvis/lvis_v1_train_cat_info.json" - _C.MODEL.ROI_BOX_HEAD.EQL_FREQ_CAT = 200 - _C.MODEL.ROI_BOX_HEAD.USE_FED_LOSS = False - _C.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT = 50 - _C.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT = 0.5 - _C.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE = False - - _C.MODEL.BIFPN = CN() - _C.MODEL.BIFPN.NUM_LEVELS = 5 - _C.MODEL.BIFPN.NUM_BIFPN = 6 - _C.MODEL.BIFPN.NORM = "GN" - _C.MODEL.BIFPN.OUT_CHANNELS = 160 - _C.MODEL.BIFPN.SEPARABLE_CONV = False - - _C.MODEL.DLA = CN() - _C.MODEL.DLA.OUT_FEATURES = ["dla2"] - _C.MODEL.DLA.USE_DLA_UP = True - _C.MODEL.DLA.NUM_LAYERS = 34 - _C.MODEL.DLA.MS_OUTPUT = False - _C.MODEL.DLA.NORM = "BN" - _C.MODEL.DLA.DLAUP_IN_FEATURES = ["dla3", "dla4", "dla5"] - _C.MODEL.DLA.DLAUP_NODE = "conv" - - _C.SOLVER.RESET_ITER = False - _C.SOLVER.TRAIN_ITER = -1 - - _C.INPUT.CUSTOM_AUG = "" - _C.INPUT.TRAIN_SIZE = 640 - _C.INPUT.TEST_SIZE = 640 - _C.INPUT.SCALE_RANGE = (0.1, 2.0) - # 'default' for fixed short/ long edge, 'square' for max size=INPUT.SIZE - _C.INPUT.TEST_INPUT_TYPE = "default" - _C.INPUT.NOT_CLAMP_BOX = False - - _C.DEBUG = False - _C.SAVE_DEBUG = False - _C.SAVE_PTH = False - _C.VIS_THRESH = 0.3 - _C.DEBUG_SHOW_NAME = False diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_build_augmentation.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_build_augmentation.py deleted file mode 100644 index 1bcb7cee66..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_build_augmentation.py +++ /dev/null @@ -1,43 +0,0 @@ -from detectron2.data import transforms as T - -from .transforms.custom_augmentation_impl import EfficientDetResizeCrop - - -def build_custom_augmentation(cfg, is_train: bool): - """ - Create a list of default :class:`Augmentation` from config. - Now it includes resizing and flipping. - - Returns: - list[Augmentation] - """ - if cfg.INPUT.CUSTOM_AUG == "ResizeShortestEdge": - if is_train: - min_size = cfg.INPUT.MIN_SIZE_TRAIN - max_size = cfg.INPUT.MAX_SIZE_TRAIN - sample_style = cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING - else: - min_size = cfg.INPUT.MIN_SIZE_TEST - max_size = cfg.INPUT.MAX_SIZE_TEST - sample_style = "choice" - augmentation = [T.ResizeShortestEdge(min_size, max_size, sample_style)] - elif cfg.INPUT.CUSTOM_AUG == "EfficientDetResizeCrop": - if is_train: - scale = cfg.INPUT.SCALE_RANGE - size = cfg.INPUT.TRAIN_SIZE - else: - scale = (1, 1) - size = cfg.INPUT.TEST_SIZE - augmentation = [EfficientDetResizeCrop(size, scale)] - else: - assert 0, cfg.INPUT.CUSTOM_AUG - - if is_train: - augmentation.append(T.RandomFlip()) - return augmentation - - -build_custom_transform_gen = build_custom_augmentation -""" -Alias for backward-compatibility. -""" diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_dataset_dataloader.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_dataset_dataloader.py deleted file mode 100644 index a7cfdd523d..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_dataset_dataloader.py +++ /dev/null @@ -1,217 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -from collections import defaultdict -import itertools -import logging -from typing import Iterator, Sequence, Optional - -from detectron2.data.build import ( - build_batch_data_loader, - check_metadata_consistency, - filter_images_with_few_keypoints, - filter_images_with_only_crowd_annotations, - get_detection_dataset_dicts, - print_instances_class_histogram, -) -from detectron2.data.catalog import DatasetCatalog, MetadataCatalog -from detectron2.data.common import DatasetFromList, MapDataset -from detectron2.data.samplers import RepeatFactorTrainingSampler, TrainingSampler -from detectron2.utils import comm -import torch -import torch.utils.data -from torch.utils.data.sampler import Sampler - -# from .custom_build_augmentation import build_custom_augmentation - - -def build_custom_train_loader(cfg, mapper=None): - """ - Modified from detectron2.data.build.build_custom_train_loader, but supports - different samplers - """ - source_aware = cfg.DATALOADER.SOURCE_AWARE - if source_aware: - dataset_dicts = get_detection_dataset_dicts_with_source( - cfg.DATASETS.TRAIN, - filter_empty=cfg.DATALOADER.FILTER_EMPTY_ANNOTATIONS, - min_keypoints=cfg.MODEL.ROI_KEYPOINT_HEAD.MIN_KEYPOINTS_PER_IMAGE - if cfg.MODEL.KEYPOINT_ON - else 0, - proposal_files=cfg.DATASETS.PROPOSAL_FILES_TRAIN if cfg.MODEL.LOAD_PROPOSALS else None, - ) - sizes = [0 for _ in range(len(cfg.DATASETS.TRAIN))] - for d in dataset_dicts: - sizes[d["dataset_source"]] += 1 - print("dataset sizes", sizes) - else: - dataset_dicts = get_detection_dataset_dicts( - cfg.DATASETS.TRAIN, - filter_empty=cfg.DATALOADER.FILTER_EMPTY_ANNOTATIONS, - min_keypoints=cfg.MODEL.ROI_KEYPOINT_HEAD.MIN_KEYPOINTS_PER_IMAGE - if cfg.MODEL.KEYPOINT_ON - else 0, - proposal_files=cfg.DATASETS.PROPOSAL_FILES_TRAIN if cfg.MODEL.LOAD_PROPOSALS else None, - ) - dataset = DatasetFromList(dataset_dicts, copy=False) - - if mapper is None: - assert 0 - # mapper = DatasetMapper(cfg, True) - dataset = MapDataset(dataset, mapper) - - sampler_name = cfg.DATALOADER.SAMPLER_TRAIN - logger = logging.getLogger(__name__) - logger.info(f"Using training sampler {sampler_name}") - # TODO avoid if-else? - if sampler_name == "TrainingSampler": - sampler = TrainingSampler(len(dataset)) - elif sampler_name == "MultiDatasetSampler": - assert source_aware - sampler = MultiDatasetSampler(cfg, sizes, dataset_dicts) - elif sampler_name == "RepeatFactorTrainingSampler": - repeat_factors = RepeatFactorTrainingSampler.repeat_factors_from_category_frequency( - dataset_dicts, cfg.DATALOADER.REPEAT_THRESHOLD - ) - sampler = RepeatFactorTrainingSampler(repeat_factors) - elif sampler_name == "ClassAwareSampler": - sampler = ClassAwareSampler(dataset_dicts) - else: - raise ValueError(f"Unknown training sampler: {sampler_name}") - - return build_batch_data_loader( - dataset, - sampler, - cfg.SOLVER.IMS_PER_BATCH, - aspect_ratio_grouping=cfg.DATALOADER.ASPECT_RATIO_GROUPING, - num_workers=cfg.DATALOADER.NUM_WORKERS, - ) - - -class ClassAwareSampler(Sampler): - def __init__(self, dataset_dicts, seed: int | None = None) -> None: - """ - Args: - size (int): the total number of data of the underlying dataset to sample from - seed (int): the initial seed of the shuffle. Must be the same - across all workers. If None, will use a random seed shared - among workers (require synchronization among all workers). - """ - self._size = len(dataset_dicts) - assert self._size > 0 - if seed is None: - seed = comm.shared_random_seed() - self._seed = int(seed) - - self._rank = comm.get_rank() - self._world_size = comm.get_world_size() - self.weights = self._get_class_balance_factor(dataset_dicts) - - def __iter__(self) -> Iterator: - start = self._rank - yield from itertools.islice(self._infinite_indices(), start, None, self._world_size) - - def _infinite_indices(self): - g = torch.Generator() - g.manual_seed(self._seed) - while True: - ids = torch.multinomial(self.weights, self._size, generator=g, replacement=True) - yield from ids - - def _get_class_balance_factor(self, dataset_dicts, l: float=1.0): - # 1. For each category c, compute the fraction of images that contain it: f(c) - ret = [] - category_freq = defaultdict(int) - for dataset_dict in dataset_dicts: # For each image (without repeats) - cat_ids = {ann["category_id"] for ann in dataset_dict["annotations"]} - for cat_id in cat_ids: - category_freq[cat_id] += 1 - for _i, dataset_dict in enumerate(dataset_dicts): - cat_ids = {ann["category_id"] for ann in dataset_dict["annotations"]} - ret.append(sum([1.0 / (category_freq[cat_id] ** l) for cat_id in cat_ids])) - return torch.tensor(ret).float() - - -def get_detection_dataset_dicts_with_source( - dataset_names: Sequence[str], filter_empty: bool=True, min_keypoints: int=0, proposal_files=None -): - assert len(dataset_names) - dataset_dicts = [DatasetCatalog.get(dataset_name) for dataset_name in dataset_names] - for dataset_name, dicts in zip(dataset_names, dataset_dicts, strict=False): - assert len(dicts), f"Dataset '{dataset_name}' is empty!" - - for source_id, (dataset_name, dicts) in enumerate(zip(dataset_names, dataset_dicts, strict=False)): - assert len(dicts), f"Dataset '{dataset_name}' is empty!" - for d in dicts: - d["dataset_source"] = source_id - - if "annotations" in dicts[0]: - try: - class_names = MetadataCatalog.get(dataset_name).thing_classes - check_metadata_consistency("thing_classes", dataset_name) - print_instances_class_histogram(dicts, class_names) - except AttributeError: # class names are not available for this dataset - pass - - assert proposal_files is None - - dataset_dicts = list(itertools.chain.from_iterable(dataset_dicts)) - - has_instances = "annotations" in dataset_dicts[0] - if filter_empty and has_instances: - dataset_dicts = filter_images_with_only_crowd_annotations(dataset_dicts) - if min_keypoints > 0 and has_instances: - dataset_dicts = filter_images_with_few_keypoints(dataset_dicts, min_keypoints) - - return dataset_dicts - - -class MultiDatasetSampler(Sampler): - def __init__(self, cfg, sizes: Sequence[int], dataset_dicts, seed: int | None = None) -> None: - """ - Args: - size (int): the total number of data of the underlying dataset to sample from - seed (int): the initial seed of the shuffle. Must be the same - across all workers. If None, will use a random seed shared - among workers (require synchronization among all workers). - """ - self.sizes = sizes - dataset_ratio = cfg.DATALOADER.DATASET_RATIO - self._batch_size = cfg.SOLVER.IMS_PER_BATCH - assert len(dataset_ratio) == len(sizes), ( - f"length of dataset ratio {len(dataset_ratio)} should be equal to number if dataset {len(sizes)}" - ) - if seed is None: - seed = comm.shared_random_seed() - self._seed = int(seed) - self._rank = comm.get_rank() - self._world_size = comm.get_world_size() - - self._ims_per_gpu = self._batch_size // self._world_size - self.dataset_ids = torch.tensor( - [d["dataset_source"] for d in dataset_dicts], dtype=torch.long - ) - - dataset_weight = [ - torch.ones(s) * max(sizes) / s * r / sum(dataset_ratio) - for i, (r, s) in enumerate(zip(dataset_ratio, sizes, strict=False)) - ] - dataset_weight = torch.cat(dataset_weight) - self.weights = dataset_weight - self.sample_epoch_size = len(self.weights) - - def __iter__(self) -> Iterator: - start = self._rank - yield from itertools.islice(self._infinite_indices(), start, None, self._world_size) - - def _infinite_indices(self): - g = torch.Generator() - g.manual_seed(self._seed) - while True: - ids = torch.multinomial( - self.weights, self.sample_epoch_size, generator=g, replacement=True - ) - nums = [(self.dataset_ids[ids] == i).sum().int().item() for i in range(len(self.sizes))] - print("_rank, len, nums", self._rank, len(ids), nums, flush=True) - # print('_rank, len, nums, self.dataset_ids[ids[:10]], ', - # self._rank, len(ids), nums, self.dataset_ids[ids[:10]], - # flush=True) - yield from ids diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/coco.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/coco.py deleted file mode 100644 index 33ff5a6980..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/coco.py +++ /dev/null @@ -1,53 +0,0 @@ -import os - -from detectron2.data import DatasetCatalog, MetadataCatalog -from detectron2.data.datasets.builtin_meta import _get_builtin_metadata -from detectron2.data.datasets.coco import load_coco_json -from detectron2.data.datasets.register_coco import register_coco_instances - - -def register_distill_coco_instances(name: str, metadata, json_file, image_root) -> None: - """ - add extra_annotation_keys - """ - assert isinstance(name, str), name - assert isinstance(json_file, str | os.PathLike), json_file - assert isinstance(image_root, str | os.PathLike), image_root - # 1. register a function which returns dicts - DatasetCatalog.register( - name, lambda: load_coco_json(json_file, image_root, name, extra_annotation_keys=["score"]) - ) - - # 2. Optionally, add metadata about this dataset, - # since they might be useful in evaluation, visualization or logging - MetadataCatalog.get(name).set( - json_file=json_file, image_root=image_root, evaluator_type="coco", **metadata - ) - - -_PREDEFINED_SPLITS_COCO = { - "coco_2017_unlabeled": ("coco/unlabeled2017", "coco/annotations/image_info_unlabeled2017.json"), -} - -for key, (image_root, json_file) in _PREDEFINED_SPLITS_COCO.items(): - register_coco_instances( - key, - _get_builtin_metadata("coco"), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) - -_PREDEFINED_SPLITS_DISTILL_COCO = { - "coco_un_yolov4_55_0.5": ( - "coco/unlabeled2017", - "coco/annotations/yolov4_cocounlabeled_55_ann0.5.json", - ), -} - -for key, (image_root, json_file) in _PREDEFINED_SPLITS_DISTILL_COCO.items(): - register_distill_coco_instances( - key, - _get_builtin_metadata("coco"), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/nuimages.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/nuimages.py deleted file mode 100644 index fdcd40242f..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/nuimages.py +++ /dev/null @@ -1,41 +0,0 @@ -import os - -from detectron2.data.datasets.register_coco import register_coco_instances - -categories = [ - {"id": 0, "name": "car"}, - {"id": 1, "name": "truck"}, - {"id": 2, "name": "trailer"}, - {"id": 3, "name": "bus"}, - {"id": 4, "name": "construction_vehicle"}, - {"id": 5, "name": "bicycle"}, - {"id": 6, "name": "motorcycle"}, - {"id": 7, "name": "pedestrian"}, - {"id": 8, "name": "traffic_cone"}, - {"id": 9, "name": "barrier"}, -] - - -def _get_builtin_metadata(): - id_to_name = {x["id"]: x["name"] for x in categories} - thing_dataset_id_to_contiguous_id = {i: i for i in range(len(categories))} - thing_classes = [id_to_name[k] for k in sorted(id_to_name)] - return { - "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes, - } - - -_PREDEFINED_SPLITS = { - "nuimages_train": ("nuimages", "nuimages/annotations/nuimages_v1.0-train.json"), - "nuimages_val": ("nuimages", "nuimages/annotations/nuimages_v1.0-val.json"), - "nuimages_mini": ("nuimages", "nuimages/annotations/nuimages_v1.0-mini.json"), -} - -for key, (image_root, json_file) in _PREDEFINED_SPLITS.items(): - register_coco_instances( - key, - _get_builtin_metadata(), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/objects365.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/objects365.py deleted file mode 100644 index e3e8383a91..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/objects365.py +++ /dev/null @@ -1,398 +0,0 @@ -import os - -from detectron2.data.datasets.register_coco import register_coco_instances - -categories_v1 = [ - {"id": 164, "name": "cutting/chopping board"}, - {"id": 49, "name": "tie"}, - {"id": 306, "name": "crosswalk sign"}, - {"id": 145, "name": "gun"}, - {"id": 14, "name": "street lights"}, - {"id": 223, "name": "bar soap"}, - {"id": 74, "name": "wild bird"}, - {"id": 219, "name": "ice cream"}, - {"id": 37, "name": "stool"}, - {"id": 25, "name": "storage box"}, - {"id": 153, "name": "giraffe"}, - {"id": 52, "name": "pen/pencil"}, - {"id": 61, "name": "high heels"}, - {"id": 340, "name": "mangosteen"}, - {"id": 22, "name": "bracelet"}, - {"id": 155, "name": "piano"}, - {"id": 162, "name": "vent"}, - {"id": 75, "name": "laptop"}, - {"id": 236, "name": "toaster"}, - {"id": 231, "name": "fire truck"}, - {"id": 42, "name": "basket"}, - {"id": 150, "name": "zebra"}, - {"id": 124, "name": "head phone"}, - {"id": 90, "name": "sheep"}, - {"id": 322, "name": "steak"}, - {"id": 39, "name": "couch"}, - {"id": 209, "name": "toothbrush"}, - {"id": 59, "name": "bicycle"}, - {"id": 336, "name": "red cabbage"}, - {"id": 228, "name": "golf ball"}, - {"id": 120, "name": "tomato"}, - {"id": 132, "name": "computer box"}, - {"id": 8, "name": "cup"}, - {"id": 183, "name": "basketball"}, - {"id": 298, "name": "butterfly"}, - {"id": 250, "name": "garlic"}, - {"id": 12, "name": "desk"}, - {"id": 141, "name": "microwave"}, - {"id": 171, "name": "strawberry"}, - {"id": 200, "name": "kettle"}, - {"id": 63, "name": "van"}, - {"id": 300, "name": "cheese"}, - {"id": 215, "name": "marker"}, - {"id": 100, "name": "blackboard/whiteboard"}, - {"id": 186, "name": "printer"}, - {"id": 333, "name": "bread/bun"}, - {"id": 243, "name": "penguin"}, - {"id": 364, "name": "iron"}, - {"id": 180, "name": "ladder"}, - {"id": 34, "name": "flag"}, - {"id": 78, "name": "cell phone"}, - {"id": 97, "name": "fan"}, - {"id": 224, "name": "scale"}, - {"id": 151, "name": "duck"}, - {"id": 319, "name": "flute"}, - {"id": 156, "name": "stop sign"}, - {"id": 290, "name": "rickshaw"}, - {"id": 128, "name": "sailboat"}, - {"id": 165, "name": "tennis racket"}, - {"id": 241, "name": "cigar"}, - {"id": 101, "name": "balloon"}, - {"id": 308, "name": "hair drier"}, - {"id": 167, "name": "skating and skiing shoes"}, - {"id": 237, "name": "helicopter"}, - {"id": 65, "name": "sink"}, - {"id": 129, "name": "tangerine"}, - {"id": 330, "name": "crab"}, - {"id": 320, "name": "measuring cup"}, - {"id": 260, "name": "fishing rod"}, - {"id": 346, "name": "saw"}, - {"id": 216, "name": "ship"}, - {"id": 46, "name": "coffee table"}, - {"id": 194, "name": "facial mask"}, - {"id": 281, "name": "stapler"}, - {"id": 118, "name": "refrigerator"}, - {"id": 40, "name": "belt"}, - {"id": 349, "name": "starfish"}, - {"id": 87, "name": "hanger"}, - {"id": 116, "name": "baseball glove"}, - {"id": 261, "name": "cherry"}, - {"id": 334, "name": "baozi"}, - {"id": 267, "name": "screwdriver"}, - {"id": 158, "name": "converter"}, - {"id": 335, "name": "lion"}, - {"id": 170, "name": "baseball"}, - {"id": 111, "name": "skis"}, - {"id": 136, "name": "broccoli"}, - {"id": 342, "name": "eraser"}, - {"id": 337, "name": "polar bear"}, - {"id": 139, "name": "shovel"}, - {"id": 193, "name": "extension cord"}, - {"id": 284, "name": "goldfish"}, - {"id": 174, "name": "pepper"}, - {"id": 138, "name": "stroller"}, - {"id": 328, "name": "yak"}, - {"id": 83, "name": "clock"}, - {"id": 235, "name": "tricycle"}, - {"id": 248, "name": "parking meter"}, - {"id": 274, "name": "trophy"}, - {"id": 324, "name": "binoculars"}, - {"id": 51, "name": "traffic light"}, - {"id": 314, "name": "donkey"}, - {"id": 45, "name": "barrel/bucket"}, - {"id": 292, "name": "pomegranate"}, - {"id": 13, "name": "handbag"}, - {"id": 262, "name": "tablet"}, - {"id": 68, "name": "apple"}, - {"id": 226, "name": "cabbage"}, - {"id": 23, "name": "flower"}, - {"id": 58, "name": "faucet"}, - {"id": 206, "name": "tong"}, - {"id": 291, "name": "trombone"}, - {"id": 160, "name": "carrot"}, - {"id": 172, "name": "bow tie"}, - {"id": 122, "name": "tent"}, - {"id": 163, "name": "cookies"}, - {"id": 115, "name": "remote"}, - {"id": 175, "name": "coffee machine"}, - {"id": 238, "name": "green beans"}, - {"id": 233, "name": "cello"}, - {"id": 28, "name": "wine glass"}, - {"id": 295, "name": "mushroom"}, - {"id": 344, "name": "scallop"}, - {"id": 125, "name": "lantern"}, - {"id": 123, "name": "shampoo/shower gel"}, - {"id": 285, "name": "meat balls"}, - {"id": 266, "name": "key"}, - {"id": 296, "name": "calculator"}, - {"id": 168, "name": "scissors"}, - {"id": 103, "name": "cymbal"}, - {"id": 6, "name": "bottle"}, - {"id": 264, "name": "nuts"}, - {"id": 234, "name": "notepaper"}, - {"id": 211, "name": "mango"}, - {"id": 287, "name": "toothpaste"}, - {"id": 196, "name": "chopsticks"}, - {"id": 140, "name": "baseball bat"}, - {"id": 244, "name": "hurdle"}, - {"id": 195, "name": "tennis ball"}, - {"id": 144, "name": "surveillance camera"}, - {"id": 271, "name": "volleyball"}, - {"id": 94, "name": "keyboard"}, - {"id": 339, "name": "seal"}, - {"id": 11, "name": "picture/frame"}, - {"id": 348, "name": "okra"}, - {"id": 191, "name": "sausage"}, - {"id": 166, "name": "candy"}, - {"id": 62, "name": "ring"}, - {"id": 311, "name": "dolphin"}, - {"id": 273, "name": "eggplant"}, - {"id": 84, "name": "drum"}, - {"id": 143, "name": "surfboard"}, - {"id": 288, "name": "antelope"}, - {"id": 204, "name": "clutch"}, - {"id": 207, "name": "slide"}, - {"id": 43, "name": "towel/napkin"}, - {"id": 352, "name": "durian"}, - {"id": 276, "name": "board eraser"}, - {"id": 315, "name": "electric drill"}, - {"id": 312, "name": "sushi"}, - {"id": 198, "name": "pie"}, - {"id": 106, "name": "pickup truck"}, - {"id": 176, "name": "bathtub"}, - {"id": 26, "name": "vase"}, - {"id": 133, "name": "elephant"}, - {"id": 256, "name": "sandwich"}, - {"id": 327, "name": "noodles"}, - {"id": 10, "name": "glasses"}, - {"id": 109, "name": "airplane"}, - {"id": 95, "name": "tripod"}, - {"id": 247, "name": "CD"}, - {"id": 121, "name": "machinery vehicle"}, - {"id": 365, "name": "flashlight"}, - {"id": 53, "name": "microphone"}, - {"id": 270, "name": "pliers"}, - {"id": 362, "name": "chainsaw"}, - {"id": 259, "name": "bear"}, - {"id": 197, "name": "electronic stove and gas stove"}, - {"id": 89, "name": "pot/pan"}, - {"id": 220, "name": "tape"}, - {"id": 338, "name": "lighter"}, - {"id": 177, "name": "snowboard"}, - {"id": 214, "name": "violin"}, - {"id": 217, "name": "chicken"}, - {"id": 2, "name": "sneakers"}, - {"id": 161, "name": "washing machine"}, - {"id": 131, "name": "kite"}, - {"id": 354, "name": "rabbit"}, - {"id": 86, "name": "bus"}, - {"id": 275, "name": "dates"}, - {"id": 282, "name": "camel"}, - {"id": 88, "name": "nightstand"}, - {"id": 179, "name": "grapes"}, - {"id": 229, "name": "pine apple"}, - {"id": 56, "name": "necklace"}, - {"id": 18, "name": "leather shoes"}, - {"id": 358, "name": "hoverboard"}, - {"id": 345, "name": "pencil case"}, - {"id": 359, "name": "pasta"}, - {"id": 157, "name": "radiator"}, - {"id": 201, "name": "hamburger"}, - {"id": 268, "name": "globe"}, - {"id": 332, "name": "barbell"}, - {"id": 329, "name": "mop"}, - {"id": 252, "name": "horn"}, - {"id": 350, "name": "eagle"}, - {"id": 169, "name": "folder"}, - {"id": 137, "name": "toilet"}, - {"id": 5, "name": "lamp"}, - {"id": 27, "name": "bench"}, - {"id": 249, "name": "swan"}, - {"id": 76, "name": "knife"}, - {"id": 341, "name": "comb"}, - {"id": 64, "name": "watch"}, - {"id": 105, "name": "telephone"}, - {"id": 3, "name": "chair"}, - {"id": 33, "name": "boat"}, - {"id": 107, "name": "orange"}, - {"id": 60, "name": "bread"}, - {"id": 147, "name": "cat"}, - {"id": 135, "name": "gas stove"}, - {"id": 307, "name": "papaya"}, - {"id": 227, "name": "router/modem"}, - {"id": 357, "name": "asparagus"}, - {"id": 73, "name": "motorcycle"}, - {"id": 77, "name": "traffic sign"}, - {"id": 67, "name": "fish"}, - {"id": 326, "name": "radish"}, - {"id": 213, "name": "egg"}, - {"id": 203, "name": "cucumber"}, - {"id": 17, "name": "helmet"}, - {"id": 110, "name": "luggage"}, - {"id": 80, "name": "truck"}, - {"id": 199, "name": "frisbee"}, - {"id": 232, "name": "peach"}, - {"id": 1, "name": "person"}, - {"id": 29, "name": "boots"}, - {"id": 310, "name": "chips"}, - {"id": 142, "name": "skateboard"}, - {"id": 44, "name": "slippers"}, - {"id": 4, "name": "hat"}, - {"id": 178, "name": "suitcase"}, - {"id": 24, "name": "tv"}, - {"id": 119, "name": "train"}, - {"id": 82, "name": "power outlet"}, - {"id": 245, "name": "swing"}, - {"id": 15, "name": "book"}, - {"id": 294, "name": "jellyfish"}, - {"id": 192, "name": "fire extinguisher"}, - {"id": 212, "name": "deer"}, - {"id": 181, "name": "pear"}, - {"id": 347, "name": "table tennis paddle"}, - {"id": 113, "name": "trolley"}, - {"id": 91, "name": "guitar"}, - {"id": 202, "name": "golf club"}, - {"id": 221, "name": "wheelchair"}, - {"id": 254, "name": "saxophone"}, - {"id": 117, "name": "paper towel"}, - {"id": 303, "name": "race car"}, - {"id": 240, "name": "carriage"}, - {"id": 246, "name": "radio"}, - {"id": 318, "name": "parrot"}, - {"id": 251, "name": "french fries"}, - {"id": 98, "name": "dog"}, - {"id": 112, "name": "soccer"}, - {"id": 355, "name": "french horn"}, - {"id": 79, "name": "paddle"}, - {"id": 283, "name": "lettuce"}, - {"id": 9, "name": "car"}, - {"id": 258, "name": "kiwi fruit"}, - {"id": 325, "name": "llama"}, - {"id": 187, "name": "billiards"}, - {"id": 210, "name": "facial cleanser"}, - {"id": 81, "name": "cow"}, - {"id": 331, "name": "microscope"}, - {"id": 148, "name": "lemon"}, - {"id": 302, "name": "pomelo"}, - {"id": 85, "name": "fork"}, - {"id": 154, "name": "pumpkin"}, - {"id": 289, "name": "shrimp"}, - {"id": 71, "name": "teddy bear"}, - {"id": 184, "name": "potato"}, - {"id": 102, "name": "air conditioner"}, - {"id": 208, "name": "hot dog"}, - {"id": 222, "name": "plum"}, - {"id": 316, "name": "spring rolls"}, - {"id": 230, "name": "crane"}, - {"id": 149, "name": "liquid soap"}, - {"id": 55, "name": "canned"}, - {"id": 35, "name": "speaker"}, - {"id": 108, "name": "banana"}, - {"id": 297, "name": "treadmill"}, - {"id": 99, "name": "spoon"}, - {"id": 104, "name": "mouse"}, - {"id": 182, "name": "american football"}, - {"id": 299, "name": "egg tart"}, - {"id": 127, "name": "cleaning products"}, - {"id": 313, "name": "urinal"}, - {"id": 286, "name": "medal"}, - {"id": 239, "name": "brush"}, - {"id": 96, "name": "hockey"}, - {"id": 279, "name": "dumbbell"}, - {"id": 32, "name": "umbrella"}, - {"id": 272, "name": "hammer"}, - {"id": 16, "name": "plate"}, - {"id": 21, "name": "potted plant"}, - {"id": 242, "name": "earphone"}, - {"id": 70, "name": "candle"}, - {"id": 185, "name": "paint brush"}, - {"id": 48, "name": "toy"}, - {"id": 130, "name": "pizza"}, - {"id": 255, "name": "trumpet"}, - {"id": 361, "name": "hotair balloon"}, - {"id": 188, "name": "fire hydrant"}, - {"id": 50, "name": "bed"}, - {"id": 253, "name": "avocado"}, - {"id": 293, "name": "coconut"}, - {"id": 257, "name": "cue"}, - {"id": 280, "name": "hamimelon"}, - {"id": 66, "name": "horse"}, - {"id": 173, "name": "pigeon"}, - {"id": 190, "name": "projector"}, - {"id": 69, "name": "camera"}, - {"id": 30, "name": "bowl"}, - {"id": 269, "name": "broom"}, - {"id": 343, "name": "pitaya"}, - {"id": 305, "name": "tuba"}, - {"id": 309, "name": "green onion"}, - {"id": 363, "name": "lobster"}, - {"id": 225, "name": "watermelon"}, - {"id": 47, "name": "suv"}, - {"id": 31, "name": "dining table"}, - {"id": 54, "name": "sandals"}, - {"id": 351, "name": "monkey"}, - {"id": 218, "name": "onion"}, - {"id": 36, "name": "trash bin/can"}, - {"id": 20, "name": "glove"}, - {"id": 277, "name": "rice"}, - {"id": 152, "name": "sports car"}, - {"id": 360, "name": "target"}, - {"id": 205, "name": "blender"}, - {"id": 19, "name": "pillow"}, - {"id": 72, "name": "cake"}, - {"id": 93, "name": "tea pot"}, - {"id": 353, "name": "game board"}, - {"id": 38, "name": "backpack"}, - {"id": 356, "name": "ambulance"}, - {"id": 146, "name": "life saver"}, - {"id": 189, "name": "goose"}, - {"id": 278, "name": "tape measure/ruler"}, - {"id": 92, "name": "traffic cone"}, - {"id": 134, "name": "toiletries"}, - {"id": 114, "name": "oven"}, - {"id": 317, "name": "tortoise/turtle"}, - {"id": 265, "name": "corn"}, - {"id": 126, "name": "donut"}, - {"id": 57, "name": "mirror"}, - {"id": 7, "name": "cabinet/shelf"}, - {"id": 263, "name": "green vegetables"}, - {"id": 159, "name": "tissue "}, - {"id": 321, "name": "shark"}, - {"id": 301, "name": "pig"}, - {"id": 41, "name": "carpet"}, - {"id": 304, "name": "rice cooker"}, - {"id": 323, "name": "poker card"}, -] - - -def _get_builtin_metadata(version): - if version == "v1": - id_to_name = {x["id"]: x["name"] for x in categories_v1} - else: - assert 0, version - thing_dataset_id_to_contiguous_id = {i + 1: i for i in range(365)} - thing_classes = [id_to_name[k] for k in sorted(id_to_name)] - return { - "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes, - } - - -_PREDEFINED_SPLITS_OBJECTS365 = { - "objects365_train": ("objects365/train", "objects365/annotations/objects365_train.json"), - "objects365_val": ("objects365/val", "objects365/annotations/objects365_val.json"), -} - -for key, (image_root, json_file) in _PREDEFINED_SPLITS_OBJECTS365.items(): - register_coco_instances( - key, - _get_builtin_metadata("v1"), - os.path.join("datasets", json_file) if "://" not in json_file else json_file, - os.path.join("datasets", image_root), - ) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_augmentation_impl.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_augmentation_impl.py deleted file mode 100644 index f4ec0ad07f..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_augmentation_impl.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# Modified by Xingyi Zhou -""" -Implement many useful :class:`Augmentation`. -""" - -from detectron2.data.transforms.augmentation import Augmentation -import numpy as np -from PIL import Image - -from .custom_transform import EfficientDetResizeCropTransform - -__all__ = [ - "EfficientDetResizeCrop", -] - - -class EfficientDetResizeCrop(Augmentation): - """ - Scale the shorter edge to the given size, with a limit of `max_size` on the longer edge. - If `max_size` is reached, then downscale so that the longer edge does not exceed max_size. - """ - - def __init__(self, size: int, scale, interp=Image.BILINEAR) -> None: - """ - Args: - """ - super().__init__() - self.target_size = (size, size) - self.scale = scale - self.interp = interp - - def get_transform(self, img): - # Select a random scale factor. - scale_factor = np.random.uniform(*self.scale) - scaled_target_height = scale_factor * self.target_size[0] - scaled_target_width = scale_factor * self.target_size[1] - # Recompute the accurate scale_factor using rounded scaled image size. - width, height = img.shape[1], img.shape[0] - img_scale_y = scaled_target_height / height - img_scale_x = scaled_target_width / width - img_scale = min(img_scale_y, img_scale_x) - - # Select non-zero random offset (x, y) if scaled image is larger than target size - scaled_h = int(height * img_scale) - scaled_w = int(width * img_scale) - offset_y = scaled_h - self.target_size[0] - offset_x = scaled_w - self.target_size[1] - offset_y = int(max(0.0, float(offset_y)) * np.random.uniform(0, 1)) - offset_x = int(max(0.0, float(offset_x)) * np.random.uniform(0, 1)) - return EfficientDetResizeCropTransform( - scaled_h, scaled_w, offset_y, offset_x, img_scale, self.target_size, self.interp - ) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_transform.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_transform.py deleted file mode 100644 index 6635a5999b..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_transform.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# Modified by Xingyi Zhou -# File: transform.py - -from fvcore.transforms.transform import ( - Transform, -) -import numpy as np -from PIL import Image -import torch -import torch.nn.functional as F - -try: - import cv2 -except ImportError: - # OpenCV is an optional dependency at the moment - pass - -__all__ = [ - "EfficientDetResizeCropTransform", -] - - -class EfficientDetResizeCropTransform(Transform): - """ """ - - def __init__(self, scaled_h, scaled_w, offset_y, offset_x, img_scale, target_size: int, interp=None) -> None: - """ - Args: - h, w (int): original image size - new_h, new_w (int): new image size - interp: PIL interpolation methods, defaults to bilinear. - """ - # TODO decide on PIL vs opencv - super().__init__() - if interp is None: - interp = Image.BILINEAR - self._set_attributes(locals()) - - def apply_image(self, img, interp=None): - # assert img.shape[:2] == (self.h, self.w) - assert len(img.shape) <= 4 - - if img.dtype == np.uint8: - pil_image = Image.fromarray(img) - interp_method = interp if interp is not None else self.interp - pil_image = pil_image.resize((self.scaled_w, self.scaled_h), interp_method) - ret = np.asarray(pil_image) - right = min(self.scaled_w, self.offset_x + self.target_size[1]) - lower = min(self.scaled_h, self.offset_y + self.target_size[0]) - # img = img.crop((self.offset_x, self.offset_y, right, lower)) - if len(ret.shape) <= 3: - ret = ret[self.offset_y : lower, self.offset_x : right] - else: - ret = ret[..., self.offset_y : lower, self.offset_x : right, :] - else: - # PIL only supports uint8 - img = torch.from_numpy(img) - shape = list(img.shape) - shape_4d = shape[:2] + [1] * (4 - len(shape)) + shape[2:] - img = img.view(shape_4d).permute(2, 3, 0, 1) # hw(c) -> nchw - _PIL_RESIZE_TO_INTERPOLATE_MODE = {Image.BILINEAR: "bilinear", Image.BICUBIC: "bicubic"} - mode = _PIL_RESIZE_TO_INTERPOLATE_MODE[self.interp] - img = F.interpolate(img, (self.scaled_h, self.scaled_w), mode=mode, align_corners=False) - shape[:2] = (self.scaled_h, self.scaled_w) - ret = img.permute(2, 3, 0, 1).view(shape).numpy() # nchw -> hw(c) - right = min(self.scaled_w, self.offset_x + self.target_size[1]) - lower = min(self.scaled_h, self.offset_y + self.target_size[0]) - if len(ret.shape) <= 3: - ret = ret[self.offset_y : lower, self.offset_x : right] - else: - ret = ret[..., self.offset_y : lower, self.offset_x : right, :] - return ret - - def apply_coords(self, coords): - coords[:, 0] = coords[:, 0] * self.img_scale - coords[:, 1] = coords[:, 1] * self.img_scale - coords[:, 0] -= self.offset_x - coords[:, 1] -= self.offset_y - return coords - - def apply_segmentation(self, segmentation): - segmentation = self.apply_image(segmentation, interp=Image.NEAREST) - return segmentation - - def inverse(self): - raise NotImplementedError - # return ResizeTransform(self.new_h, self.new_w, self.h, self.w, self.interp) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn.py deleted file mode 100644 index 733b502da4..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn.py +++ /dev/null @@ -1,527 +0,0 @@ -# Modified from https://github.com/rwightman/efficientdet-pytorch/blob/master/effdet/efficientdet.py -# The original file is under Apache-2.0 License -from collections import OrderedDict -import math - -from detectron2.layers import Conv2d, ShapeSpec -from detectron2.layers.batch_norm import get_norm -from detectron2.modeling.backbone import Backbone -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from detectron2.modeling.backbone.resnet import build_resnet_backbone -import torch -from torch import nn - -from .dlafpn import dla34 - - -def get_fpn_config(base_reduction: int=8): - """BiFPN config with sum.""" - p = { - "nodes": [ - {"reduction": base_reduction << 3, "inputs_offsets": [3, 4]}, - {"reduction": base_reduction << 2, "inputs_offsets": [2, 5]}, - {"reduction": base_reduction << 1, "inputs_offsets": [1, 6]}, - {"reduction": base_reduction, "inputs_offsets": [0, 7]}, - {"reduction": base_reduction << 1, "inputs_offsets": [1, 7, 8]}, - {"reduction": base_reduction << 2, "inputs_offsets": [2, 6, 9]}, - {"reduction": base_reduction << 3, "inputs_offsets": [3, 5, 10]}, - {"reduction": base_reduction << 4, "inputs_offsets": [4, 11]}, - ], - "weight_method": "fastattn", - } - return p - - -def swish(x, inplace: bool = False): - """Swish - Described in: https://arxiv.org/abs/1710.05941""" - return x.mul_(x.sigmoid()) if inplace else x.mul(x.sigmoid()) - - -class Swish(nn.Module): - def __init__(self, inplace: bool = False) -> None: - super().__init__() - self.inplace = inplace - - def forward(self, x): - return swish(x, self.inplace) - - -class SequentialAppend(nn.Sequential): - def __init__(self, *args) -> None: - super().__init__(*args) - - def forward(self, x): - for module in self: - x.append(module(x)) - return x - - -class SequentialAppendLast(nn.Sequential): - def __init__(self, *args) -> None: - super().__init__(*args) - - # def forward(self, x: List[torch.Tensor]): - def forward(self, x): - for module in self: - x.append(module(x[-1])) - return x - - -class ConvBnAct2d(nn.Module): - def __init__( - self, - in_channels, - out_channels, - kernel_size: int, - stride: int=1, - dilation: int=1, - padding: str="", - bias: bool=False, - norm: str="", - act_layer=Swish, - ) -> None: - super().__init__() - # self.conv = create_conv2d( - # in_channels, out_channels, kernel_size, stride=stride, dilation=dilation, padding=padding, bias=bias) - self.conv = Conv2d( - in_channels, - out_channels, - kernel_size=kernel_size, - stride=stride, - padding=kernel_size // 2, - bias=(norm == ""), - ) - self.bn = get_norm(norm, out_channels) - self.act = None if act_layer is None else act_layer(inplace=True) - - def forward(self, x): - x = self.conv(x) - if self.bn is not None: - x = self.bn(x) - if self.act is not None: - x = self.act(x) - return x - - -class SeparableConv2d(nn.Module): - """Separable Conv""" - - def __init__( - self, - in_channels, - out_channels, - kernel_size: int=3, - stride: int=1, - dilation: int=1, - padding: str="", - bias: bool=False, - channel_multiplier: float=1.0, - pw_kernel_size: int=1, - act_layer=Swish, - norm: str="", - ) -> None: - super().__init__() - - # self.conv_dw = create_conv2d( - # in_channels, int(in_channels * channel_multiplier), kernel_size, - # stride=stride, dilation=dilation, padding=padding, depthwise=True) - - self.conv_dw = Conv2d( - in_channels, - int(in_channels * channel_multiplier), - kernel_size=kernel_size, - stride=stride, - padding=kernel_size // 2, - bias=bias, - groups=out_channels, - ) - # print('conv_dw', kernel_size, stride) - # self.conv_pw = create_conv2d( - # int(in_channels * channel_multiplier), out_channels, pw_kernel_size, padding=padding, bias=bias) - - self.conv_pw = Conv2d( - int(in_channels * channel_multiplier), - out_channels, - kernel_size=pw_kernel_size, - padding=pw_kernel_size // 2, - bias=(norm == ""), - ) - # print('conv_pw', pw_kernel_size) - - self.bn = get_norm(norm, out_channels) - self.act = None if act_layer is None else act_layer(inplace=True) - - def forward(self, x): - x = self.conv_dw(x) - x = self.conv_pw(x) - if self.bn is not None: - x = self.bn(x) - if self.act is not None: - x = self.act(x) - return x - - -class ResampleFeatureMap(nn.Sequential): - def __init__( - self, - in_channels, - out_channels, - reduction_ratio: float=1.0, - pad_type: str="", - pooling_type: str="max", - norm: str="", - apply_bn: bool=False, - conv_after_downsample: bool=False, - redundant_bias: bool=False, - ) -> None: - super().__init__() - pooling_type = pooling_type or "max" - self.in_channels = in_channels - self.out_channels = out_channels - self.reduction_ratio = reduction_ratio - self.conv_after_downsample = conv_after_downsample - - conv = None - if in_channels != out_channels: - conv = ConvBnAct2d( - in_channels, - out_channels, - kernel_size=1, - padding=pad_type, - norm=norm if apply_bn else "", - bias=not apply_bn or redundant_bias, - act_layer=None, - ) - - if reduction_ratio > 1: - stride_size = int(reduction_ratio) - if conv is not None and not self.conv_after_downsample: - self.add_module("conv", conv) - self.add_module( - "downsample", - # create_pool2d( - # pooling_type, kernel_size=stride_size + 1, stride=stride_size, padding=pad_type) - # nn.MaxPool2d(kernel_size=stride_size + 1, stride=stride_size, padding=pad_type) - nn.MaxPool2d(kernel_size=stride_size, stride=stride_size), - ) - if conv is not None and self.conv_after_downsample: - self.add_module("conv", conv) - else: - if conv is not None: - self.add_module("conv", conv) - if reduction_ratio < 1: - scale = int(1 // reduction_ratio) - self.add_module("upsample", nn.UpsamplingNearest2d(scale_factor=scale)) - - -class FpnCombine(nn.Module): - def __init__( - self, - feature_info, - fpn_config, - fpn_channels, - inputs_offsets, - target_reduction, - pad_type: str="", - pooling_type: str="max", - norm: str="", - apply_bn_for_resampling: bool=False, - conv_after_downsample: bool=False, - redundant_bias: bool=False, - weight_method: str="attn", - ) -> None: - super().__init__() - self.inputs_offsets = inputs_offsets - self.weight_method = weight_method - - self.resample = nn.ModuleDict() - for _idx, offset in enumerate(inputs_offsets): - in_channels = fpn_channels - if offset < len(feature_info): - in_channels = feature_info[offset]["num_chs"] - input_reduction = feature_info[offset]["reduction"] - else: - node_idx = offset - len(feature_info) - # print('node_idx, len', node_idx, len(fpn_config['nodes'])) - input_reduction = fpn_config["nodes"][node_idx]["reduction"] - reduction_ratio = target_reduction / input_reduction - self.resample[str(offset)] = ResampleFeatureMap( - in_channels, - fpn_channels, - reduction_ratio=reduction_ratio, - pad_type=pad_type, - pooling_type=pooling_type, - norm=norm, - apply_bn=apply_bn_for_resampling, - conv_after_downsample=conv_after_downsample, - redundant_bias=redundant_bias, - ) - - if weight_method == "attn" or weight_method == "fastattn": - # WSM - self.edge_weights = nn.Parameter(torch.ones(len(inputs_offsets)), requires_grad=True) - else: - self.edge_weights = None - - def forward(self, x): - dtype = x[0].dtype - nodes = [] - for offset in self.inputs_offsets: - input_node = x[offset] - input_node = self.resample[str(offset)](input_node) - nodes.append(input_node) - - if self.weight_method == "attn": - normalized_weights = torch.softmax(self.edge_weights.type(dtype), dim=0) - x = torch.stack(nodes, dim=-1) * normalized_weights - elif self.weight_method == "fastattn": - edge_weights = nn.functional.relu(self.edge_weights.type(dtype)) - weights_sum = torch.sum(edge_weights) - x = torch.stack( - [(nodes[i] * edge_weights[i]) / (weights_sum + 0.0001) for i in range(len(nodes))], - dim=-1, - ) - elif self.weight_method == "sum": - x = torch.stack(nodes, dim=-1) - else: - raise ValueError(f"unknown weight_method {self.weight_method}") - x = torch.sum(x, dim=-1) - return x - - -class BiFpnLayer(nn.Module): - def __init__( - self, - feature_info, - fpn_config, - fpn_channels, - num_levels: int=5, - pad_type: str="", - pooling_type: str="max", - norm: str="", - act_layer=Swish, - apply_bn_for_resampling: bool=False, - conv_after_downsample: bool=True, - conv_bn_relu_pattern: bool=False, - separable_conv: bool=True, - redundant_bias: bool=False, - ) -> None: - super().__init__() - self.fpn_config = fpn_config - self.num_levels = num_levels - self.conv_bn_relu_pattern = False - - self.feature_info = [] - self.fnode = SequentialAppend() - for i, fnode_cfg in enumerate(fpn_config["nodes"]): - # logging.debug('fnode {} : {}'.format(i, fnode_cfg)) - # print('fnode {} : {}'.format(i, fnode_cfg)) - fnode_layers = OrderedDict() - - # combine features - reduction = fnode_cfg["reduction"] - fnode_layers["combine"] = FpnCombine( - feature_info, - fpn_config, - fpn_channels, - fnode_cfg["inputs_offsets"], - target_reduction=reduction, - pad_type=pad_type, - pooling_type=pooling_type, - norm=norm, - apply_bn_for_resampling=apply_bn_for_resampling, - conv_after_downsample=conv_after_downsample, - redundant_bias=redundant_bias, - weight_method=fpn_config["weight_method"], - ) - self.feature_info.append(dict(num_chs=fpn_channels, reduction=reduction)) - - # after combine ops - after_combine = OrderedDict() - if not conv_bn_relu_pattern: - after_combine["act"] = act_layer(inplace=True) - conv_bias = redundant_bias - conv_act = None - else: - conv_bias = False - conv_act = act_layer - conv_kwargs = dict( - in_channels=fpn_channels, - out_channels=fpn_channels, - kernel_size=3, - padding=pad_type, - bias=conv_bias, - norm=norm, - act_layer=conv_act, - ) - after_combine["conv"] = ( - SeparableConv2d(**conv_kwargs) if separable_conv else ConvBnAct2d(**conv_kwargs) - ) - fnode_layers["after_combine"] = nn.Sequential(after_combine) - - self.fnode.add_module(str(i), nn.Sequential(fnode_layers)) - - self.feature_info = self.feature_info[-num_levels::] - - def forward(self, x): - x = self.fnode(x) - return x[-self.num_levels : :] - - -class BiFPN(Backbone): - def __init__( - self, - cfg, - bottom_up, - in_features, - out_channels, - norm: str="", - num_levels: int=5, - num_bifpn: int=4, - separable_conv: bool=False, - ) -> None: - super().__init__() - assert isinstance(bottom_up, Backbone) - - # Feature map strides and channels from the bottom up network (e.g. ResNet) - input_shapes = bottom_up.output_shape() - in_strides = [input_shapes[f].stride for f in in_features] - in_channels = [input_shapes[f].channels for f in in_features] - - self.num_levels = num_levels - self.num_bifpn = num_bifpn - self.bottom_up = bottom_up - self.in_features = in_features - self._size_divisibility = 128 - levels = [int(math.log2(s)) for s in in_strides] - self._out_feature_strides = {f"p{int(math.log2(s))}": s for s in in_strides} - if len(in_features) < num_levels: - for l in range(num_levels - len(in_features)): - s = l + levels[-1] - self._out_feature_strides[f"p{s + 1}"] = 2 ** (s + 1) - self._out_features = list(sorted(self._out_feature_strides.keys())) - self._out_feature_channels = {k: out_channels for k in self._out_features} - - # print('self._out_feature_strides', self._out_feature_strides) - # print('self._out_feature_channels', self._out_feature_channels) - - feature_info = [ - {"num_chs": in_channels[level], "reduction": in_strides[level]} - for level in range(len(self.in_features)) - ] - # self.config = config - fpn_config = get_fpn_config() - self.resample = SequentialAppendLast() - for level in range(num_levels): - if level < len(feature_info): - in_chs = in_channels[level] # feature_info[level]['num_chs'] - reduction = in_strides[level] # feature_info[level]['reduction'] - else: - # Adds a coarser level by downsampling the last feature map - reduction_ratio = 2 - self.resample.add_module( - str(level), - ResampleFeatureMap( - in_channels=in_chs, - out_channels=out_channels, - pad_type="same", - pooling_type=None, - norm=norm, - reduction_ratio=reduction_ratio, - apply_bn=True, - conv_after_downsample=False, - redundant_bias=False, - ), - ) - in_chs = out_channels - reduction = int(reduction * reduction_ratio) - feature_info.append(dict(num_chs=in_chs, reduction=reduction)) - - self.cell = nn.Sequential() - for rep in range(self.num_bifpn): - # logging.debug('building cell {}'.format(rep)) - # print('building cell {}'.format(rep)) - fpn_layer = BiFpnLayer( - feature_info=feature_info, - fpn_config=fpn_config, - fpn_channels=out_channels, - num_levels=self.num_levels, - pad_type="same", - pooling_type=None, - norm=norm, - act_layer=Swish, - separable_conv=separable_conv, - apply_bn_for_resampling=True, - conv_after_downsample=False, - conv_bn_relu_pattern=False, - redundant_bias=False, - ) - self.cell.add_module(str(rep), fpn_layer) - feature_info = fpn_layer.feature_info - # import pdb; pdb.set_trace() - - @property - def size_divisibility(self): - return self._size_divisibility - - def forward(self, x): - # print('input shapes', x.shape) - bottom_up_features = self.bottom_up(x) - x = [bottom_up_features[f] for f in self.in_features] - assert len(self.resample) == self.num_levels - len(x) - x = self.resample(x) - [xx.shape for xx in x] - # print('resample shapes', shapes) - x = self.cell(x) - out = {f: xx for f, xx in zip(self._out_features, x, strict=False)} - # import pdb; pdb.set_trace() - return out - - -@BACKBONE_REGISTRY.register() -def build_resnet_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = build_resnet_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - backbone = BiFPN( - cfg=cfg, - bottom_up=bottom_up, - in_features=in_features, - out_channels=cfg.MODEL.BIFPN.OUT_CHANNELS, - norm=cfg.MODEL.BIFPN.NORM, - num_levels=cfg.MODEL.BIFPN.NUM_LEVELS, - num_bifpn=cfg.MODEL.BIFPN.NUM_BIFPN, - separable_conv=cfg.MODEL.BIFPN.SEPARABLE_CONV, - ) - return backbone - - -@BACKBONE_REGISTRY.register() -def build_p37_dla_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = dla34(cfg) - in_features = cfg.MODEL.FPN.IN_FEATURES - assert cfg.MODEL.BIFPN.NUM_LEVELS == 5 - - backbone = BiFPN( - cfg=cfg, - bottom_up=bottom_up, - in_features=in_features, - out_channels=cfg.MODEL.BIFPN.OUT_CHANNELS, - norm=cfg.MODEL.BIFPN.NORM, - num_levels=cfg.MODEL.BIFPN.NUM_LEVELS, - num_bifpn=cfg.MODEL.BIFPN.NUM_BIFPN, - separable_conv=cfg.MODEL.BIFPN.SEPARABLE_CONV, - ) - return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn_fcos.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn_fcos.py deleted file mode 100644 index 27ad4e62fc..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn_fcos.py +++ /dev/null @@ -1,455 +0,0 @@ -# This file is modified from https://github.com/aim-uofa/AdelaiDet/blob/master/adet/modeling/backbone/bifpn.py -# The original file is under 2-clause BSD License for academic use, and *non-commercial use*. -from detectron2.layers import Conv2d, ShapeSpec, get_norm -from detectron2.modeling import BACKBONE_REGISTRY -from detectron2.modeling.backbone import Backbone, build_resnet_backbone -import torch -from torch import nn -import torch.nn.functional as F - -from .dlafpn import dla34 -from typing import Sequence - -__all__ = [] - - -def swish(x): - return x * x.sigmoid() - - -def split_name(name: str): - for i, c in enumerate(name): - if not c.isalpha(): - return name[:i], int(name[i:]) - raise ValueError() - - -class FeatureMapResampler(nn.Module): - def __init__(self, in_channels, out_channels, stride: int, norm: str="") -> None: - super().__init__() - if in_channels != out_channels: - self.reduction = Conv2d( - in_channels, - out_channels, - kernel_size=1, - bias=(norm == ""), - norm=get_norm(norm, out_channels), - activation=None, - ) - else: - self.reduction = None - - assert stride <= 2 - self.stride = stride - - def forward(self, x): - if self.reduction is not None: - x = self.reduction(x) - - if self.stride == 2: - x = F.max_pool2d(x, kernel_size=self.stride + 1, stride=self.stride, padding=1) - elif self.stride == 1: - pass - else: - raise NotImplementedError() - return x - - -class BackboneWithTopLevels(Backbone): - def __init__(self, backbone, out_channels, num_top_levels: int, norm: str="") -> None: - super().__init__() - self.backbone = backbone - backbone_output_shape = backbone.output_shape() - - self._out_feature_channels = { - name: shape.channels for name, shape in backbone_output_shape.items() - } - self._out_feature_strides = { - name: shape.stride for name, shape in backbone_output_shape.items() - } - self._out_features = list(self._out_feature_strides.keys()) - - last_feature_name = max(self._out_feature_strides.keys(), key=lambda x: split_name(x)[1]) - self.last_feature_name = last_feature_name - self.num_top_levels = num_top_levels - - last_channels = self._out_feature_channels[last_feature_name] - last_stride = self._out_feature_strides[last_feature_name] - - prefix, suffix = split_name(last_feature_name) - prev_channels = last_channels - for i in range(num_top_levels): - name = prefix + str(suffix + i + 1) - self.add_module(name, FeatureMapResampler(prev_channels, out_channels, 2, norm)) - prev_channels = out_channels - - self._out_feature_channels[name] = out_channels - self._out_feature_strides[name] = last_stride * 2 ** (i + 1) - self._out_features.append(name) - - def forward(self, x): - outputs = self.backbone(x) - last_features = outputs[self.last_feature_name] - prefix, suffix = split_name(self.last_feature_name) - - x = last_features - for i in range(self.num_top_levels): - name = prefix + str(suffix + i + 1) - x = self.__getattr__(name)(x) - outputs[name] = x - - return outputs - - -class SingleBiFPN(Backbone): - """ - This module implements Feature Pyramid Network. - It creates pyramid features built on top of some input feature maps. - """ - - def __init__(self, in_channels_list, out_channels, norm: str="") -> None: - """ - Args: - bottom_up (Backbone): module representing the bottom up subnetwork. - Must be a subclass of :class:`Backbone`. The multi-scale feature - maps generated by the bottom up network, and listed in `in_features`, - are used to generate FPN levels. - in_features (list[str]): names of the input feature maps coming - from the backbone to which FPN is attached. For example, if the - backbone produces ["res2", "res3", "res4"], any *contiguous* sublist - of these may be used; order must be from high to low resolution. - out_channels (int): number of channels in the output feature maps. - norm (str): the normalization to use. - """ - super().__init__() - - self.out_channels = out_channels - # build 5-levels bifpn - if len(in_channels_list) == 5: - self.nodes = [ - {"feat_level": 3, "inputs_offsets": [3, 4]}, - {"feat_level": 2, "inputs_offsets": [2, 5]}, - {"feat_level": 1, "inputs_offsets": [1, 6]}, - {"feat_level": 0, "inputs_offsets": [0, 7]}, - {"feat_level": 1, "inputs_offsets": [1, 7, 8]}, - {"feat_level": 2, "inputs_offsets": [2, 6, 9]}, - {"feat_level": 3, "inputs_offsets": [3, 5, 10]}, - {"feat_level": 4, "inputs_offsets": [4, 11]}, - ] - elif len(in_channels_list) == 3: - self.nodes = [ - {"feat_level": 1, "inputs_offsets": [1, 2]}, - {"feat_level": 0, "inputs_offsets": [0, 3]}, - {"feat_level": 1, "inputs_offsets": [1, 3, 4]}, - {"feat_level": 2, "inputs_offsets": [2, 5]}, - ] - else: - raise NotImplementedError - - node_info = [_ for _ in in_channels_list] - - num_output_connections = [0 for _ in in_channels_list] - for fnode in self.nodes: - feat_level = fnode["feat_level"] - inputs_offsets = fnode["inputs_offsets"] - inputs_offsets_str = "_".join(map(str, inputs_offsets)) - for input_offset in inputs_offsets: - num_output_connections[input_offset] += 1 - - in_channels = node_info[input_offset] - if in_channels != out_channels: - lateral_conv = Conv2d( - in_channels, out_channels, kernel_size=1, norm=get_norm(norm, out_channels) - ) - self.add_module(f"lateral_{input_offset}_f{feat_level}", lateral_conv) - node_info.append(out_channels) - num_output_connections.append(0) - - # generate attention weights - name = f"weights_f{feat_level}_{inputs_offsets_str}" - self.__setattr__( - name, - nn.Parameter( - torch.ones(len(inputs_offsets), dtype=torch.float32), requires_grad=True - ), - ) - - # generate convolutions after combination - name = f"outputs_f{feat_level}_{inputs_offsets_str}" - self.add_module( - name, - Conv2d( - out_channels, - out_channels, - kernel_size=3, - padding=1, - norm=get_norm(norm, out_channels), - bias=(norm == ""), - ), - ) - - def forward(self, feats): - """ - Args: - input (dict[str->Tensor]): mapping feature map name (e.g., "p5") to - feature map tensor for each feature level in high to low resolution order. - Returns: - dict[str->Tensor]: - mapping from feature map name to FPN feature map tensor - in high to low resolution order. Returned feature names follow the FPN - paper convention: "p", where stage has stride = 2 ** stage e.g., - ["n2", "n3", ..., "n6"]. - """ - feats = [_ for _ in feats] - num_levels = len(feats) - num_output_connections = [0 for _ in feats] - for fnode in self.nodes: - feat_level = fnode["feat_level"] - inputs_offsets = fnode["inputs_offsets"] - inputs_offsets_str = "_".join(map(str, inputs_offsets)) - input_nodes = [] - _, _, target_h, target_w = feats[feat_level].size() - for input_offset in inputs_offsets: - num_output_connections[input_offset] += 1 - input_node = feats[input_offset] - - # reduction - if input_node.size(1) != self.out_channels: - name = f"lateral_{input_offset}_f{feat_level}" - input_node = self.__getattr__(name)(input_node) - - # maybe downsample - _, _, h, w = input_node.size() - if h > target_h and w > target_w: - height_stride_size = int((h - 1) // target_h + 1) - width_stride_size = int((w - 1) // target_w + 1) - assert height_stride_size == width_stride_size == 2 - input_node = F.max_pool2d( - input_node, - kernel_size=(height_stride_size + 1, width_stride_size + 1), - stride=(height_stride_size, width_stride_size), - padding=1, - ) - elif h <= target_h and w <= target_w: - if h < target_h or w < target_w: - input_node = F.interpolate( - input_node, size=(target_h, target_w), mode="nearest" - ) - else: - raise NotImplementedError() - input_nodes.append(input_node) - - # attention - name = f"weights_f{feat_level}_{inputs_offsets_str}" - weights = F.relu(self.__getattr__(name)) - norm_weights = weights / (weights.sum() + 0.0001) - - new_node = torch.stack(input_nodes, dim=-1) - new_node = (norm_weights * new_node).sum(dim=-1) - new_node = swish(new_node) - - name = f"outputs_f{feat_level}_{inputs_offsets_str}" - feats.append(self.__getattr__(name)(new_node)) - - num_output_connections.append(0) - - output_feats = [] - for idx in range(num_levels): - for i, fnode in enumerate(reversed(self.nodes)): - if fnode["feat_level"] == idx: - output_feats.append(feats[-1 - i]) - break - else: - raise ValueError() - return output_feats - - -class BiFPN(Backbone): - """ - This module implements Feature Pyramid Network. - It creates pyramid features built on top of some input feature maps. - """ - - def __init__(self, bottom_up, in_features, out_channels, num_top_levels: int, num_repeats: int, norm: str="") -> None: - """ - Args: - bottom_up (Backbone): module representing the bottom up subnetwork. - Must be a subclass of :class:`Backbone`. The multi-scale feature - maps generated by the bottom up network, and listed in `in_features`, - are used to generate FPN levels. - in_features (list[str]): names of the input feature maps coming - from the backbone to which FPN is attached. For example, if the - backbone produces ["res2", "res3", "res4"], any *contiguous* sublist - of these may be used; order must be from high to low resolution. - out_channels (int): number of channels in the output feature maps. - num_top_levels (int): the number of the top levels (p6 or p7). - num_repeats (int): the number of repeats of BiFPN. - norm (str): the normalization to use. - """ - super().__init__() - assert isinstance(bottom_up, Backbone) - - # add extra feature levels (i.e., 6 and 7) - self.bottom_up = BackboneWithTopLevels(bottom_up, out_channels, num_top_levels, norm) - bottom_up_output_shapes = self.bottom_up.output_shape() - - in_features = sorted(in_features, key=lambda x: split_name(x)[1]) - self._size_divisibility = 128 # bottom_up_output_shapes[in_features[-1]].stride - self.out_channels = out_channels - self.min_level = split_name(in_features[0])[1] - - # add the names for top blocks - prefix, last_suffix = split_name(in_features[-1]) - for i in range(num_top_levels): - in_features.append(prefix + str(last_suffix + i + 1)) - self.in_features = in_features - - # generate output features - self._out_features = [f"p{split_name(name)[1]}" for name in in_features] - self._out_feature_strides = { - out_name: bottom_up_output_shapes[in_name].stride - for out_name, in_name in zip(self._out_features, in_features, strict=False) - } - self._out_feature_channels = {k: out_channels for k in self._out_features} - - # build bifpn - self.repeated_bifpn = nn.ModuleList() - for i in range(num_repeats): - if i == 0: - in_channels_list = [bottom_up_output_shapes[name].channels for name in in_features] - else: - in_channels_list = [self._out_feature_channels[name] for name in self._out_features] - self.repeated_bifpn.append(SingleBiFPN(in_channels_list, out_channels, norm)) - - @property - def size_divisibility(self): - return self._size_divisibility - - def forward(self, x): - """ - Args: - input (dict[str->Tensor]): mapping feature map name (e.g., "p5") to - feature map tensor for each feature level in high to low resolution order. - Returns: - dict[str->Tensor]: - mapping from feature map name to FPN feature map tensor - in high to low resolution order. Returned feature names follow the FPN - paper convention: "p", where stage has stride = 2 ** stage e.g., - ["n2", "n3", ..., "n6"]. - """ - bottom_up_features = self.bottom_up(x) - feats = [bottom_up_features[f] for f in self.in_features] - - for bifpn in self.repeated_bifpn: - feats = bifpn(feats) - - return dict(zip(self._out_features, feats, strict=False)) - - -def _assert_strides_are_log2_contiguous(strides: Sequence[int]) -> None: - """ - Assert that each stride is 2x times its preceding stride, i.e. "contiguous in log2". - """ - for i, stride in enumerate(strides[1:], 1): - assert stride == 2 * strides[i - 1], f"Strides {stride} {strides[i - 1]} are not log2 contiguous" - - -@BACKBONE_REGISTRY.register() -def build_fcos_resnet_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = build_resnet_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.BIFPN.OUT_CHANNELS - num_repeats = cfg.MODEL.BIFPN.NUM_BIFPN - top_levels = 2 - - backbone = BiFPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - num_top_levels=top_levels, - num_repeats=num_repeats, - norm=cfg.MODEL.BIFPN.NORM, - ) - return backbone - - -@BACKBONE_REGISTRY.register() -def build_p35_fcos_resnet_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = build_resnet_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.BIFPN.OUT_CHANNELS - num_repeats = cfg.MODEL.BIFPN.NUM_BIFPN - top_levels = 0 - - backbone = BiFPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - num_top_levels=top_levels, - num_repeats=num_repeats, - norm=cfg.MODEL.BIFPN.NORM, - ) - return backbone - - -@BACKBONE_REGISTRY.register() -def build_p35_fcos_dla_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = dla34(cfg) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.BIFPN.OUT_CHANNELS - num_repeats = cfg.MODEL.BIFPN.NUM_BIFPN - top_levels = 0 - - backbone = BiFPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - num_top_levels=top_levels, - num_repeats=num_repeats, - norm=cfg.MODEL.BIFPN.NORM, - ) - return backbone - - -@BACKBONE_REGISTRY.register() -def build_p37_fcos_dla_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = dla34(cfg) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.BIFPN.OUT_CHANNELS - num_repeats = cfg.MODEL.BIFPN.NUM_BIFPN - assert cfg.MODEL.BIFPN.NUM_LEVELS == 5 - top_levels = 2 - - backbone = BiFPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - num_top_levels=top_levels, - num_repeats=num_repeats, - norm=cfg.MODEL.BIFPN.NORM, - ) - return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dla.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dla.py deleted file mode 100644 index 8b6464153b..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dla.py +++ /dev/null @@ -1,542 +0,0 @@ -import math -from os.path import join - -from detectron2.layers import ( - Conv2d, - DeformConv, - ModulatedDeformConv, - ShapeSpec, - get_norm, -) -from detectron2.modeling.backbone.backbone import Backbone -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from detectron2.modeling.backbone.fpn import FPN -from detectron2.modeling.backbone.resnet import BasicStem, BottleneckBlock, DeformBottleneckBlock -import fvcore.nn.weight_init as weight_init -import numpy as np -import torch -from torch import nn -import torch.nn.functional as F -import torch.utils.model_zoo as model_zoo - -__all__ = [ - "BasicStem", - "BottleneckBlock", - "DeformBottleneckBlock", -] - -DCNV1 = False - -HASH = { - 34: "ba72cf86", - 60: "24839fc4", -} - - -def get_model_url(data, name: str, hash): - return join("http://dl.yf.io/dla/models", data, f"{name}-{hash}.pth") - - -class BasicBlock(nn.Module): - def __init__(self, inplanes, planes, stride: int=1, dilation: int=1, norm: str="BN") -> None: - super().__init__() - self.conv1 = nn.Conv2d( - inplanes, - planes, - kernel_size=3, - stride=stride, - padding=dilation, - bias=False, - dilation=dilation, - ) - self.bn1 = get_norm(norm, planes) - self.relu = nn.ReLU(inplace=True) - self.conv2 = nn.Conv2d( - planes, planes, kernel_size=3, stride=1, padding=dilation, bias=False, dilation=dilation - ) - self.bn2 = get_norm(norm, planes) - self.stride = stride - - def forward(self, x, residual=None): - if residual is None: - residual = x - - out = self.conv1(x) - out = self.bn1(out) - out = self.relu(out) - - out = self.conv2(out) - out = self.bn2(out) - - out += residual - out = self.relu(out) - - return out - - -class Bottleneck(nn.Module): - expansion = 2 - - def __init__(self, inplanes, planes, stride: int=1, dilation: int=1, norm: str="BN") -> None: - super().__init__() - expansion = Bottleneck.expansion - bottle_planes = planes // expansion - self.conv1 = nn.Conv2d(inplanes, bottle_planes, kernel_size=1, bias=False) - self.bn1 = get_norm(norm, bottle_planes) - self.conv2 = nn.Conv2d( - bottle_planes, - bottle_planes, - kernel_size=3, - stride=stride, - padding=dilation, - bias=False, - dilation=dilation, - ) - self.bn2 = get_norm(norm, bottle_planes) - self.conv3 = nn.Conv2d(bottle_planes, planes, kernel_size=1, bias=False) - self.bn3 = get_norm(norm, planes) - self.relu = nn.ReLU(inplace=True) - self.stride = stride - - def forward(self, x, residual=None): - if residual is None: - residual = x - - out = self.conv1(x) - out = self.bn1(out) - out = self.relu(out) - - out = self.conv2(out) - out = self.bn2(out) - out = self.relu(out) - - out = self.conv3(out) - out = self.bn3(out) - - out += residual - out = self.relu(out) - - return out - - -class Root(nn.Module): - def __init__(self, in_channels, out_channels, kernel_size: int, residual, norm: str="BN") -> None: - super().__init__() - self.conv = nn.Conv2d( - in_channels, out_channels, 1, stride=1, bias=False, padding=(kernel_size - 1) // 2 - ) - self.bn = get_norm(norm, out_channels) - self.relu = nn.ReLU(inplace=True) - self.residual = residual - - def forward(self, *x): - children = x - x = self.conv(torch.cat(x, 1)) - x = self.bn(x) - if self.residual: - x += children[0] - x = self.relu(x) - - return x - - -class Tree(nn.Module): - def __init__( - self, - levels, - block, - in_channels, - out_channels, - stride: int=1, - level_root: bool=False, - root_dim: int=0, - root_kernel_size: int=1, - dilation: int=1, - root_residual: bool=False, - norm: str="BN", - ) -> None: - super().__init__() - if root_dim == 0: - root_dim = 2 * out_channels - if level_root: - root_dim += in_channels - if levels == 1: - self.tree1 = block(in_channels, out_channels, stride, dilation=dilation, norm=norm) - self.tree2 = block(out_channels, out_channels, 1, dilation=dilation, norm=norm) - else: - self.tree1 = Tree( - levels - 1, - block, - in_channels, - out_channels, - stride, - root_dim=0, - root_kernel_size=root_kernel_size, - dilation=dilation, - root_residual=root_residual, - norm=norm, - ) - self.tree2 = Tree( - levels - 1, - block, - out_channels, - out_channels, - root_dim=root_dim + out_channels, - root_kernel_size=root_kernel_size, - dilation=dilation, - root_residual=root_residual, - norm=norm, - ) - if levels == 1: - self.root = Root(root_dim, out_channels, root_kernel_size, root_residual, norm=norm) - self.level_root = level_root - self.root_dim = root_dim - self.downsample = None - self.project = None - self.levels = levels - if stride > 1: - self.downsample = nn.MaxPool2d(stride, stride=stride) - if in_channels != out_channels: - self.project = nn.Sequential( - nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, bias=False), - get_norm(norm, out_channels), - ) - - def forward(self, x, residual=None, children=None): - children = [] if children is None else children - bottom = self.downsample(x) if self.downsample else x - residual = self.project(bottom) if self.project else bottom - if self.level_root: - children.append(bottom) - x1 = self.tree1(x, residual) - if self.levels == 1: - x2 = self.tree2(x1) - x = self.root(x2, x1, *children) - else: - children.append(x1) - x = self.tree2(x1, children=children) - return x - - -class DLA(nn.Module): - def __init__( - self, num_layers: int, levels, channels, block=BasicBlock, residual_root: bool=False, norm: str="BN" - ) -> None: - """ - Args: - """ - super().__init__() - self.norm = norm - self.channels = channels - self.base_layer = nn.Sequential( - nn.Conv2d(3, channels[0], kernel_size=7, stride=1, padding=3, bias=False), - get_norm(self.norm, channels[0]), - nn.ReLU(inplace=True), - ) - self.level0 = self._make_conv_level(channels[0], channels[0], levels[0]) - self.level1 = self._make_conv_level(channels[0], channels[1], levels[1], stride=2) - self.level2 = Tree( - levels[2], - block, - channels[1], - channels[2], - 2, - level_root=False, - root_residual=residual_root, - norm=norm, - ) - self.level3 = Tree( - levels[3], - block, - channels[2], - channels[3], - 2, - level_root=True, - root_residual=residual_root, - norm=norm, - ) - self.level4 = Tree( - levels[4], - block, - channels[3], - channels[4], - 2, - level_root=True, - root_residual=residual_root, - norm=norm, - ) - self.level5 = Tree( - levels[5], - block, - channels[4], - channels[5], - 2, - level_root=True, - root_residual=residual_root, - norm=norm, - ) - self.load_pretrained_model( - data="imagenet", name=f"dla{num_layers}", hash=HASH[num_layers] - ) - - def load_pretrained_model(self, data, name: str, hash) -> None: - model_url = get_model_url(data, name, hash) - model_weights = model_zoo.load_url(model_url) - num_classes = len(model_weights[list(model_weights.keys())[-1]]) - self.fc = nn.Conv2d( - self.channels[-1], num_classes, kernel_size=1, stride=1, padding=0, bias=True - ) - print("Loading pretrained") - self.load_state_dict(model_weights, strict=False) - - def _make_conv_level(self, inplanes, planes, convs, stride: int=1, dilation: int=1): - modules = [] - for i in range(convs): - modules.extend( - [ - nn.Conv2d( - inplanes, - planes, - kernel_size=3, - stride=stride if i == 0 else 1, - padding=dilation, - bias=False, - dilation=dilation, - ), - get_norm(self.norm, planes), - nn.ReLU(inplace=True), - ] - ) - inplanes = planes - return nn.Sequential(*modules) - - def forward(self, x): - y = [] - x = self.base_layer(x) - for i in range(6): - x = getattr(self, f"level{i}")(x) - y.append(x) - return y - - -def fill_up_weights(up) -> None: - w = up.weight.data - f = math.ceil(w.size(2) / 2) - c = (2 * f - 1 - f % 2) / (2.0 * f) - for i in range(w.size(2)): - for j in range(w.size(3)): - w[0, 0, i, j] = (1 - math.fabs(i / f - c)) * (1 - math.fabs(j / f - c)) - for c in range(1, w.size(0)): - w[c, 0, :, :] = w[0, 0, :, :] - - -class _DeformConv(nn.Module): - def __init__(self, chi, cho, norm: str="BN") -> None: - super().__init__() - self.actf = nn.Sequential(get_norm(norm, cho), nn.ReLU(inplace=True)) - if DCNV1: - self.offset = Conv2d(chi, 18, kernel_size=3, stride=1, padding=1, dilation=1) - self.conv = DeformConv( - chi, cho, kernel_size=(3, 3), stride=1, padding=1, dilation=1, deformable_groups=1 - ) - else: - self.offset = Conv2d(chi, 27, kernel_size=3, stride=1, padding=1, dilation=1) - self.conv = ModulatedDeformConv( - chi, cho, kernel_size=3, stride=1, padding=1, dilation=1, deformable_groups=1 - ) - nn.init.constant_(self.offset.weight, 0) - nn.init.constant_(self.offset.bias, 0) - - def forward(self, x): - if DCNV1: - offset = self.offset(x) - x = self.conv(x, offset) - else: - offset_mask = self.offset(x) - offset_x, offset_y, mask = torch.chunk(offset_mask, 3, dim=1) - offset = torch.cat((offset_x, offset_y), dim=1) - mask = mask.sigmoid() - x = self.conv(x, offset, mask) - x = self.actf(x) - return x - - -class IDAUp(nn.Module): - def __init__(self, o, channels, up_f, norm: str="BN") -> None: - super().__init__() - for i in range(1, len(channels)): - c = channels[i] - f = int(up_f[i]) - proj = _DeformConv(c, o, norm=norm) - node = _DeformConv(o, o, norm=norm) - - up = nn.ConvTranspose2d( - o, o, f * 2, stride=f, padding=f // 2, output_padding=0, groups=o, bias=False - ) - fill_up_weights(up) - - setattr(self, "proj_" + str(i), proj) - setattr(self, "up_" + str(i), up) - setattr(self, "node_" + str(i), node) - - def forward(self, layers, startp, endp) -> None: - for i in range(startp + 1, endp): - upsample = getattr(self, "up_" + str(i - startp)) - project = getattr(self, "proj_" + str(i - startp)) - layers[i] = upsample(project(layers[i])) - node = getattr(self, "node_" + str(i - startp)) - layers[i] = node(layers[i] + layers[i - 1]) - - -class DLAUp(nn.Module): - def __init__(self, startp, channels, scales, in_channels=None, norm: str="BN") -> None: - super().__init__() - self.startp = startp - if in_channels is None: - in_channels = channels - self.channels = channels - channels = list(channels) - scales = np.array(scales, dtype=int) - for i in range(len(channels) - 1): - j = -i - 2 - setattr( - self, - f"ida_{i}", - IDAUp(channels[j], in_channels[j:], scales[j:] // scales[j], norm=norm), - ) - scales[j + 1 :] = scales[j] - in_channels[j + 1 :] = [channels[j] for _ in channels[j + 1 :]] - - def forward(self, layers): - out = [layers[-1]] # start with 32 - for i in range(len(layers) - self.startp - 1): - ida = getattr(self, f"ida_{i}") - ida(layers, len(layers) - i - 2, len(layers)) - out.insert(0, layers[-1]) - return out - - -DLA_CONFIGS = { - 34: ([1, 1, 1, 2, 2, 1], [16, 32, 64, 128, 256, 512], BasicBlock), - 60: ([1, 1, 1, 2, 3, 1], [16, 32, 128, 256, 512, 1024], Bottleneck), -} - - -class DLASeg(Backbone): - def __init__(self, num_layers: int, out_features, use_dla_up: bool=True, ms_output: bool=False, norm: str="BN") -> None: - super().__init__() - # depth = 34 - levels, channels, Block = DLA_CONFIGS[num_layers] - self.base = DLA( - num_layers=num_layers, levels=levels, channels=channels, block=Block, norm=norm - ) - down_ratio = 4 - self.first_level = int(np.log2(down_ratio)) - self.ms_output = ms_output - self.last_level = 5 if not self.ms_output else 6 - channels = self.base.channels - scales = [2**i for i in range(len(channels[self.first_level :]))] - self.use_dla_up = use_dla_up - if self.use_dla_up: - self.dla_up = DLAUp(self.first_level, channels[self.first_level :], scales, norm=norm) - out_channel = channels[self.first_level] - if not self.ms_output: # stride 4 DLA - self.ida_up = IDAUp( - out_channel, - channels[self.first_level : self.last_level], - [2**i for i in range(self.last_level - self.first_level)], - norm=norm, - ) - self._out_features = out_features - self._out_feature_channels = {f"dla{i}": channels[i] for i in range(6)} - self._out_feature_strides = {f"dla{i}": 2**i for i in range(6)} - self._size_divisibility = 32 - - @property - def size_divisibility(self): - return self._size_divisibility - - def forward(self, x): - x = self.base(x) - if self.use_dla_up: - x = self.dla_up(x) - if not self.ms_output: # stride 4 dla - y = [] - for i in range(self.last_level - self.first_level): - y.append(x[i].clone()) - self.ida_up(y, 0, len(y)) - ret = {} - for i in range(self.last_level - self.first_level): - out_feature = f"dla{i}" - if out_feature in self._out_features: - ret[out_feature] = y[i] - else: - ret = {} - st = self.first_level if self.use_dla_up else 0 - for i in range(self.last_level - st): - out_feature = f"dla{i + st}" - if out_feature in self._out_features: - ret[out_feature] = x[i] - - return ret - - -@BACKBONE_REGISTRY.register() -def build_dla_backbone(cfg, input_shape): - """ - Create a ResNet instance from config. - - Returns: - ResNet: a :class:`ResNet` instance. - """ - return DLASeg( - out_features=cfg.MODEL.DLA.OUT_FEATURES, - num_layers=cfg.MODEL.DLA.NUM_LAYERS, - use_dla_up=cfg.MODEL.DLA.USE_DLA_UP, - ms_output=cfg.MODEL.DLA.MS_OUTPUT, - norm=cfg.MODEL.DLA.NORM, - ) - - -class LastLevelP6P7(nn.Module): - """ - This module is used in RetinaNet to generate extra layers, P6 and P7 from - C5 feature. - """ - - def __init__(self, in_channels, out_channels) -> None: - super().__init__() - self.num_levels = 2 - self.in_feature = "dla5" - self.p6 = nn.Conv2d(in_channels, out_channels, 3, 2, 1) - self.p7 = nn.Conv2d(out_channels, out_channels, 3, 2, 1) - for module in [self.p6, self.p7]: - weight_init.c2_xavier_fill(module) - - def forward(self, c5): - p6 = self.p6(c5) - p7 = self.p7(F.relu(p6)) - return [p6, p7] - - -@BACKBONE_REGISTRY.register() -def build_retinanet_dla_fpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = build_dla_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - in_channels_p6p7 = bottom_up.output_shape()["dla5"].channels - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=LastLevelP6P7(in_channels_p6p7, out_channels), - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dlafpn.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dlafpn.py deleted file mode 100644 index 54f05bf719..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dlafpn.py +++ /dev/null @@ -1,563 +0,0 @@ -#!/usr/bin/env python - -# this file is from https://github.com/ucbdrive/dla/blob/master/dla.py. - -import math -from os.path import join - -from detectron2.layers import Conv2d, ModulatedDeformConv, ShapeSpec -from detectron2.layers.batch_norm import get_norm -from detectron2.modeling.backbone import FPN, Backbone -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -import fvcore.nn.weight_init as weight_init -import numpy as np -import torch -from torch import nn -import torch.nn.functional as F -import torch.utils.model_zoo as model_zoo -from typing import Optional - -WEB_ROOT = "http://dl.yf.io/dla/models" - - -def get_model_url(data, name: str, hash): - return join("http://dl.yf.io/dla/models", data, f"{name}-{hash}.pth") - - -def conv3x3(in_planes, out_planes, stride: int=1): - "3x3 convolution with padding" - return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias=False) - - -class BasicBlock(nn.Module): - def __init__(self, cfg, inplanes, planes, stride: int=1, dilation: int=1) -> None: - super().__init__() - self.conv1 = nn.Conv2d( - inplanes, - planes, - kernel_size=3, - stride=stride, - padding=dilation, - bias=False, - dilation=dilation, - ) - self.bn1 = get_norm(cfg.MODEL.DLA.NORM, planes) - self.relu = nn.ReLU(inplace=True) - self.conv2 = nn.Conv2d( - planes, planes, kernel_size=3, stride=1, padding=dilation, bias=False, dilation=dilation - ) - self.bn2 = get_norm(cfg.MODEL.DLA.NORM, planes) - self.stride = stride - - def forward(self, x, residual=None): - if residual is None: - residual = x - - out = self.conv1(x) - out = self.bn1(out) - out = self.relu(out) - - out = self.conv2(out) - out = self.bn2(out) - - out += residual - out = self.relu(out) - - return out - - -class Bottleneck(nn.Module): - expansion = 2 - - def __init__(self, cfg, inplanes, planes, stride: int=1, dilation: int=1) -> None: - super().__init__() - expansion = Bottleneck.expansion - bottle_planes = planes // expansion - self.conv1 = nn.Conv2d(inplanes, bottle_planes, kernel_size=1, bias=False) - self.bn1 = get_norm(cfg.MODEL.DLA.NORM, bottle_planes) - self.conv2 = nn.Conv2d( - bottle_planes, - bottle_planes, - kernel_size=3, - stride=stride, - padding=dilation, - bias=False, - dilation=dilation, - ) - self.bn2 = get_norm(cfg.MODEL.DLA.NORM, bottle_planes) - self.conv3 = nn.Conv2d(bottle_planes, planes, kernel_size=1, bias=False) - self.bn3 = get_norm(cfg.MODEL.DLA.NORM, planes) - self.relu = nn.ReLU(inplace=True) - self.stride = stride - - def forward(self, x, residual=None): - if residual is None: - residual = x - - out = self.conv1(x) - out = self.bn1(out) - out = self.relu(out) - - out = self.conv2(out) - out = self.bn2(out) - out = self.relu(out) - - out = self.conv3(out) - out = self.bn3(out) - - out += residual - out = self.relu(out) - - return out - - -class Root(nn.Module): - def __init__(self, cfg, in_channels, out_channels, kernel_size: int, residual) -> None: - super().__init__() - self.conv = nn.Conv2d( - in_channels, - out_channels, - kernel_size, - stride=1, - bias=False, - padding=(kernel_size - 1) // 2, - ) - self.bn = get_norm(cfg.MODEL.DLA.NORM, out_channels) - self.relu = nn.ReLU(inplace=True) - self.residual = residual - - def forward(self, *x): - children = x - x = self.conv(torch.cat(x, 1)) - x = self.bn(x) - if self.residual: - x += children[0] - x = self.relu(x) - - return x - - -class Tree(nn.Module): - def __init__( - self, - cfg, - levels, - block, - in_channels, - out_channels, - stride: int=1, - level_root: bool=False, - root_dim: int=0, - root_kernel_size: int=1, - dilation: int=1, - root_residual: bool=False, - ) -> None: - super().__init__() - if root_dim == 0: - root_dim = 2 * out_channels - if level_root: - root_dim += in_channels - if levels == 1: - self.tree1 = block(cfg, in_channels, out_channels, stride, dilation=dilation) - self.tree2 = block(cfg, out_channels, out_channels, 1, dilation=dilation) - else: - self.tree1 = Tree( - cfg, - levels - 1, - block, - in_channels, - out_channels, - stride, - root_dim=0, - root_kernel_size=root_kernel_size, - dilation=dilation, - root_residual=root_residual, - ) - self.tree2 = Tree( - cfg, - levels - 1, - block, - out_channels, - out_channels, - root_dim=root_dim + out_channels, - root_kernel_size=root_kernel_size, - dilation=dilation, - root_residual=root_residual, - ) - if levels == 1: - self.root = Root(cfg, root_dim, out_channels, root_kernel_size, root_residual) - self.level_root = level_root - self.root_dim = root_dim - self.downsample = None - self.project = None - self.levels = levels - if stride > 1: - self.downsample = nn.MaxPool2d(stride, stride=stride) - if in_channels != out_channels: - self.project = nn.Sequential( - nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, bias=False), - get_norm(cfg.MODEL.DLA.NORM, out_channels), - ) - - def forward(self, x, residual=None, children=None): - if self.training and residual is not None: - x = x + residual.sum() * 0.0 - children = [] if children is None else children - bottom = self.downsample(x) if self.downsample else x - residual = self.project(bottom) if self.project else bottom - if self.level_root: - children.append(bottom) - x1 = self.tree1(x, residual) - if self.levels == 1: - x2 = self.tree2(x1) - x = self.root(x2, x1, *children) - else: - children.append(x1) - x = self.tree2(x1, children=children) - return x - - -class DLA(Backbone): - def __init__(self, cfg, levels, channels, block=BasicBlock, residual_root: bool=False) -> None: - super().__init__() - self.cfg = cfg - self.channels = channels - - self._out_features = [f"dla{i}" for i in range(6)] - self._out_feature_channels = {k: channels[i] for i, k in enumerate(self._out_features)} - self._out_feature_strides = {k: 2**i for i, k in enumerate(self._out_features)} - - self.base_layer = nn.Sequential( - nn.Conv2d(3, channels[0], kernel_size=7, stride=1, padding=3, bias=False), - get_norm(cfg.MODEL.DLA.NORM, channels[0]), - nn.ReLU(inplace=True), - ) - self.level0 = self._make_conv_level(channels[0], channels[0], levels[0]) - self.level1 = self._make_conv_level(channels[0], channels[1], levels[1], stride=2) - self.level2 = Tree( - cfg, - levels[2], - block, - channels[1], - channels[2], - 2, - level_root=False, - root_residual=residual_root, - ) - self.level3 = Tree( - cfg, - levels[3], - block, - channels[2], - channels[3], - 2, - level_root=True, - root_residual=residual_root, - ) - self.level4 = Tree( - cfg, - levels[4], - block, - channels[3], - channels[4], - 2, - level_root=True, - root_residual=residual_root, - ) - self.level5 = Tree( - cfg, - levels[5], - block, - channels[4], - channels[5], - 2, - level_root=True, - root_residual=residual_root, - ) - - for m in self.modules(): - if isinstance(m, nn.Conv2d): - n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels - m.weight.data.normal_(0, math.sqrt(2.0 / n)) - - self.load_pretrained_model(data="imagenet", name="dla34", hash="ba72cf86") - - def load_pretrained_model(self, data, name: str, hash) -> None: - model_url = get_model_url(data, name, hash) - model_weights = model_zoo.load_url(model_url) - del model_weights["fc.weight"] - del model_weights["fc.bias"] - print("Loading pretrained DLA!") - self.load_state_dict(model_weights, strict=True) - - def _make_conv_level(self, inplanes, planes, convs, stride: int=1, dilation: int=1): - modules = [] - for i in range(convs): - modules.extend( - [ - nn.Conv2d( - inplanes, - planes, - kernel_size=3, - stride=stride if i == 0 else 1, - padding=dilation, - bias=False, - dilation=dilation, - ), - get_norm(self.cfg.MODEL.DLA.NORM, planes), - nn.ReLU(inplace=True), - ] - ) - inplanes = planes - return nn.Sequential(*modules) - - def forward(self, x): - y = {} - x = self.base_layer(x) - for i in range(6): - name = f"level{i}" - x = getattr(self, name)(x) - y[f"dla{i}"] = x - return y - - -def fill_up_weights(up) -> None: - w = up.weight.data - f = math.ceil(w.size(2) / 2) - c = (2 * f - 1 - f % 2) / (2.0 * f) - for i in range(w.size(2)): - for j in range(w.size(3)): - w[0, 0, i, j] = (1 - math.fabs(i / f - c)) * (1 - math.fabs(j / f - c)) - for c in range(1, w.size(0)): - w[c, 0, :, :] = w[0, 0, :, :] - - -class Conv(nn.Module): - def __init__(self, chi, cho, norm) -> None: - super().__init__() - self.conv = nn.Sequential( - nn.Conv2d(chi, cho, kernel_size=1, stride=1, bias=False), - get_norm(norm, cho), - nn.ReLU(inplace=True), - ) - - def forward(self, x): - return self.conv(x) - - -class DeformConv(nn.Module): - def __init__(self, chi, cho, norm) -> None: - super().__init__() - self.actf = nn.Sequential(get_norm(norm, cho), nn.ReLU(inplace=True)) - self.offset = Conv2d(chi, 27, kernel_size=3, stride=1, padding=1, dilation=1) - self.conv = ModulatedDeformConv( - chi, cho, kernel_size=3, stride=1, padding=1, dilation=1, deformable_groups=1 - ) - nn.init.constant_(self.offset.weight, 0) - nn.init.constant_(self.offset.bias, 0) - - def forward(self, x): - offset_mask = self.offset(x) - offset_x, offset_y, mask = torch.chunk(offset_mask, 3, dim=1) - offset = torch.cat((offset_x, offset_y), dim=1) - mask = mask.sigmoid() - x = self.conv(x, offset, mask) - x = self.actf(x) - return x - - -class IDAUp(nn.Module): - def __init__(self, o, channels, up_f, norm: str="FrozenBN", node_type=Conv) -> None: - super().__init__() - for i in range(1, len(channels)): - c = channels[i] - f = int(up_f[i]) - proj = node_type(c, o, norm) - node = node_type(o, o, norm) - - up = nn.ConvTranspose2d( - o, o, f * 2, stride=f, padding=f // 2, output_padding=0, groups=o, bias=False - ) - fill_up_weights(up) - - setattr(self, "proj_" + str(i), proj) - setattr(self, "up_" + str(i), up) - setattr(self, "node_" + str(i), node) - - def forward(self, layers, startp, endp) -> None: - for i in range(startp + 1, endp): - upsample = getattr(self, "up_" + str(i - startp)) - project = getattr(self, "proj_" + str(i - startp)) - layers[i] = upsample(project(layers[i])) - node = getattr(self, "node_" + str(i - startp)) - layers[i] = node(layers[i] + layers[i - 1]) - - -DLAUP_NODE_MAP = { - "conv": Conv, - "dcn": DeformConv, -} - - -class DLAUP(Backbone): - def __init__(self, bottom_up, in_features, norm, dlaup_node: str="conv") -> None: - super().__init__() - assert isinstance(bottom_up, Backbone) - self.bottom_up = bottom_up - input_shapes = bottom_up.output_shape() - in_strides = [input_shapes[f].stride for f in in_features] - in_channels = [input_shapes[f].channels for f in in_features] - in_levels = [int(math.log2(input_shapes[f].stride)) for f in in_features] - self.in_features = in_features - out_features = [f"dlaup{l}" for l in in_levels] - self._out_features = out_features - self._out_feature_channels = { - f"dlaup{l}": in_channels[i] for i, l in enumerate(in_levels) - } - self._out_feature_strides = {f"dlaup{l}": 2**l for l in in_levels} - - print("self._out_features", self._out_features) - print("self._out_feature_channels", self._out_feature_channels) - print("self._out_feature_strides", self._out_feature_strides) - self._size_divisibility = 32 - - node_type = DLAUP_NODE_MAP[dlaup_node] - - self.startp = int(math.log2(in_strides[0])) - self.channels = in_channels - channels = list(in_channels) - scales = np.array([2**i for i in range(len(out_features))], dtype=int) - for i in range(len(channels) - 1): - j = -i - 2 - setattr( - self, - f"ida_{i}", - IDAUp( - channels[j], - in_channels[j:], - scales[j:] // scales[j], - norm=norm, - node_type=node_type, - ), - ) - scales[j + 1 :] = scales[j] - in_channels[j + 1 :] = [channels[j] for _ in channels[j + 1 :]] - - @property - def size_divisibility(self): - return self._size_divisibility - - def forward(self, x): - bottom_up_features = self.bottom_up(x) - layers = [bottom_up_features[f] for f in self.in_features] - out = [layers[-1]] # start with 32 - for i in range(len(layers) - 1): - ida = getattr(self, f"ida_{i}") - ida(layers, len(layers) - i - 2, len(layers)) - out.insert(0, layers[-1]) - ret = {} - for k, v in zip(self._out_features, out, strict=False): - ret[k] = v - # import pdb; pdb.set_trace() - return ret - - -def dla34(cfg, pretrained: Optional[bool]=None): # DLA-34 - model = DLA(cfg, [1, 1, 1, 2, 2, 1], [16, 32, 64, 128, 256, 512], block=BasicBlock) - return model - - -class LastLevelP6P7(nn.Module): - """ - This module is used in RetinaNet to generate extra layers, P6 and P7 from - C5 feature. - """ - - def __init__(self, in_channels, out_channels) -> None: - super().__init__() - self.num_levels = 2 - self.in_feature = "dla5" - self.p6 = nn.Conv2d(in_channels, out_channels, 3, 2, 1) - self.p7 = nn.Conv2d(out_channels, out_channels, 3, 2, 1) - for module in [self.p6, self.p7]: - weight_init.c2_xavier_fill(module) - - def forward(self, c5): - p6 = self.p6(c5) - p7 = self.p7(F.relu(p6)) - return [p6, p7] - - -@BACKBONE_REGISTRY.register() -def build_dla_fpn3_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - - depth_to_creator = {"dla34": dla34} - bottom_up = depth_to_creator[f"dla{cfg.MODEL.DLA.NUM_LAYERS}"](cfg) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=None, - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - - return backbone - - -@BACKBONE_REGISTRY.register() -def build_dla_fpn5_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - - depth_to_creator = {"dla34": dla34} - bottom_up = depth_to_creator[f"dla{cfg.MODEL.DLA.NUM_LAYERS}"](cfg) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - in_channels_top = bottom_up.output_shape()["dla5"].channels - - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=LastLevelP6P7(in_channels_top, out_channels), - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - - return backbone - - -@BACKBONE_REGISTRY.register() -def build_dlaup_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - - depth_to_creator = {"dla34": dla34} - bottom_up = depth_to_creator[f"dla{cfg.MODEL.DLA.NUM_LAYERS}"](cfg) - - backbone = DLAUP( - bottom_up=bottom_up, - in_features=cfg.MODEL.DLA.DLAUP_IN_FEATURES, - norm=cfg.MODEL.DLA.NORM, - dlaup_node=cfg.MODEL.DLA.DLAUP_NODE, - ) - - return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/fpn_p5.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/fpn_p5.py deleted file mode 100644 index 4ce285b6c6..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/fpn_p5.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -from detectron2.layers import ShapeSpec -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from detectron2.modeling.backbone.fpn import FPN -from detectron2.modeling.backbone.resnet import build_resnet_backbone -import fvcore.nn.weight_init as weight_init -from torch import nn -import torch.nn.functional as F - - -class LastLevelP6P7_P5(nn.Module): - """ - This module is used in RetinaNet to generate extra layers, P6 and P7 from - C5 feature. - """ - - def __init__(self, in_channels, out_channels) -> None: - super().__init__() - self.num_levels = 2 - self.in_feature = "p5" - self.p6 = nn.Conv2d(in_channels, out_channels, 3, 2, 1) - self.p7 = nn.Conv2d(out_channels, out_channels, 3, 2, 1) - for module in [self.p6, self.p7]: - weight_init.c2_xavier_fill(module) - - def forward(self, c5): - p6 = self.p6(c5) - p7 = self.p7(F.relu(p6)) - return [p6, p7] - - -@BACKBONE_REGISTRY.register() -def build_p67_resnet_fpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = build_resnet_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=LastLevelP6P7_P5(out_channels, out_channels), - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - return backbone - - -@BACKBONE_REGISTRY.register() -def build_p35_resnet_fpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = build_resnet_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=None, - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/res2net.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/res2net.py deleted file mode 100644 index e04400032e..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/res2net.py +++ /dev/null @@ -1,810 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# This file is modified from https://github.com/Res2Net/Res2Net-detectron2/blob/master/detectron2/modeling/backbone/resnet.py -# The original file is under Apache-2.0 License -from detectron2.layers import ( - CNNBlockBase, - Conv2d, - DeformConv, - ModulatedDeformConv, - ShapeSpec, - get_norm, -) -from detectron2.modeling.backbone import Backbone -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from detectron2.modeling.backbone.fpn import FPN -import fvcore.nn.weight_init as weight_init -import numpy as np -import torch -from torch import nn -import torch.nn.functional as F - -from .bifpn import BiFPN -from .fpn_p5 import LastLevelP6P7_P5 -from typing import Optional - -__all__ = [ - "BasicBlock", - "BasicStem", - "BottleneckBlock", - "DeformBottleneckBlock", - "ResNet", - "ResNetBlockBase", - "build_res2net_backbone", - "make_stage", -] - - -ResNetBlockBase = CNNBlockBase -""" -Alias for backward compatibiltiy. -""" - - -class BasicBlock(CNNBlockBase): - """ - The basic residual block for ResNet-18 and ResNet-34, with two 3x3 conv layers - and a projection shortcut if needed. - """ - - def __init__(self, in_channels, out_channels, *, stride: int=1, norm: str="BN") -> None: - """ - Args: - in_channels (int): Number of input channels. - out_channels (int): Number of output channels. - stride (int): Stride for the first conv. - norm (str or callable): normalization for all conv layers. - See :func:`layers.get_norm` for supported format. - """ - super().__init__(in_channels, out_channels, stride) - - if in_channels != out_channels: - self.shortcut = Conv2d( - in_channels, - out_channels, - kernel_size=1, - stride=stride, - bias=False, - norm=get_norm(norm, out_channels), - ) - else: - self.shortcut = None - - self.conv1 = Conv2d( - in_channels, - out_channels, - kernel_size=3, - stride=stride, - padding=1, - bias=False, - norm=get_norm(norm, out_channels), - ) - - self.conv2 = Conv2d( - out_channels, - out_channels, - kernel_size=3, - stride=1, - padding=1, - bias=False, - norm=get_norm(norm, out_channels), - ) - - for layer in [self.conv1, self.conv2, self.shortcut]: - if layer is not None: # shortcut can be None - weight_init.c2_msra_fill(layer) - - def forward(self, x): - out = self.conv1(x) - out = F.relu_(out) - out = self.conv2(out) - - if self.shortcut is not None: - shortcut = self.shortcut(x) - else: - shortcut = x - - out += shortcut - out = F.relu_(out) - return out - - -class BottleneckBlock(CNNBlockBase): - """ - The standard bottle2neck residual block used by Res2Net-50, 101 and 152. - """ - - def __init__( - self, - in_channels, - out_channels, - *, - bottleneck_channels, - stride: int=1, - num_groups: int=1, - norm: str="BN", - stride_in_1x1: bool=False, - dilation: int=1, - basewidth: int=26, - scale: int=4, - ) -> None: - """ - Args: - bottleneck_channels (int): number of output channels for the 3x3 - "bottleneck" conv layers. - num_groups (int): number of groups for the 3x3 conv layer. - norm (str or callable): normalization for all conv layers. - See :func:`layers.get_norm` for supported format. - stride_in_1x1 (bool): when stride>1, whether to put stride in the - first 1x1 convolution or the bottleneck 3x3 convolution. - dilation (int): the dilation rate of the 3x3 conv layer. - """ - super().__init__(in_channels, out_channels, stride) - - if in_channels != out_channels: - self.shortcut = nn.Sequential( - nn.AvgPool2d( - kernel_size=stride, stride=stride, ceil_mode=True, count_include_pad=False - ), - Conv2d( - in_channels, - out_channels, - kernel_size=1, - stride=1, - bias=False, - norm=get_norm(norm, out_channels), - ), - ) - else: - self.shortcut = None - - # The original MSRA ResNet models have stride in the first 1x1 conv - # The subsequent fb.torch.resnet and Caffe2 ResNe[X]t implementations have - # stride in the 3x3 conv - stride_1x1, stride_3x3 = (stride, 1) if stride_in_1x1 else (1, stride) - width = bottleneck_channels // scale - - self.conv1 = Conv2d( - in_channels, - bottleneck_channels, - kernel_size=1, - stride=stride_1x1, - bias=False, - norm=get_norm(norm, bottleneck_channels), - ) - if scale == 1: - self.nums = 1 - else: - self.nums = scale - 1 - if self.in_channels != self.out_channels and stride_3x3 != 2: - self.pool = nn.AvgPool2d(kernel_size=3, stride=stride_3x3, padding=1) - - convs = [] - bns = [] - for _i in range(self.nums): - convs.append( - nn.Conv2d( - width, - width, - kernel_size=3, - stride=stride_3x3, - padding=1 * dilation, - bias=False, - groups=num_groups, - dilation=dilation, - ) - ) - bns.append(get_norm(norm, width)) - self.convs = nn.ModuleList(convs) - self.bns = nn.ModuleList(bns) - - self.conv3 = Conv2d( - bottleneck_channels, - out_channels, - kernel_size=1, - bias=False, - norm=get_norm(norm, out_channels), - ) - self.scale = scale - self.width = width - self.in_channels = in_channels - self.out_channels = out_channels - self.stride_3x3 = stride_3x3 - for layer in [self.conv1, self.conv3]: - if layer is not None: # shortcut can be None - weight_init.c2_msra_fill(layer) - if self.shortcut is not None: - for layer in self.shortcut.modules(): - if isinstance(layer, Conv2d): - weight_init.c2_msra_fill(layer) - - for layer in self.convs: - if layer is not None: # shortcut can be None - weight_init.c2_msra_fill(layer) - - # Zero-initialize the last normalization in each residual branch, - # so that at the beginning, the residual branch starts with zeros, - # and each residual block behaves like an identity. - # See Sec 5.1 in "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour": - # "For BN layers, the learnable scaling coefficient γ is initialized - # to be 1, except for each residual block's last BN - # where γ is initialized to be 0." - - # nn.init.constant_(self.conv3.norm.weight, 0) - # TODO this somehow hurts performance when training GN models from scratch. - # Add it as an option when we need to use this code to train a backbone. - - def forward(self, x): - out = self.conv1(x) - out = F.relu_(out) - - spx = torch.split(out, self.width, 1) - for i in range(self.nums): - if i == 0 or self.in_channels != self.out_channels: - sp = spx[i] - else: - sp = sp + spx[i] - sp = self.convs[i](sp) - sp = F.relu_(self.bns[i](sp)) - if i == 0: - out = sp - else: - out = torch.cat((out, sp), 1) - if self.scale != 1 and self.stride_3x3 == 1: - out = torch.cat((out, spx[self.nums]), 1) - elif self.scale != 1 and self.stride_3x3 == 2: - out = torch.cat((out, self.pool(spx[self.nums])), 1) - - out = self.conv3(out) - - if self.shortcut is not None: - shortcut = self.shortcut(x) - else: - shortcut = x - - out += shortcut - out = F.relu_(out) - return out - - -class DeformBottleneckBlock(ResNetBlockBase): - """ - Not implemented for res2net yet. - Similar to :class:`BottleneckBlock`, but with deformable conv in the 3x3 convolution. - """ - - def __init__( - self, - in_channels, - out_channels, - *, - bottleneck_channels, - stride: int=1, - num_groups: int=1, - norm: str="BN", - stride_in_1x1: bool=False, - dilation: int=1, - deform_modulated: bool=False, - deform_num_groups: int=1, - basewidth: int=26, - scale: int=4, - ) -> None: - super().__init__(in_channels, out_channels, stride) - self.deform_modulated = deform_modulated - - if in_channels != out_channels: - # self.shortcut = Conv2d( - # in_channels, - # out_channels, - # kernel_size=1, - # stride=stride, - # bias=False, - # norm=get_norm(norm, out_channels), - # ) - self.shortcut = nn.Sequential( - nn.AvgPool2d( - kernel_size=stride, stride=stride, ceil_mode=True, count_include_pad=False - ), - Conv2d( - in_channels, - out_channels, - kernel_size=1, - stride=1, - bias=False, - norm=get_norm(norm, out_channels), - ), - ) - else: - self.shortcut = None - - stride_1x1, stride_3x3 = (stride, 1) if stride_in_1x1 else (1, stride) - width = bottleneck_channels // scale - - self.conv1 = Conv2d( - in_channels, - bottleneck_channels, - kernel_size=1, - stride=stride_1x1, - bias=False, - norm=get_norm(norm, bottleneck_channels), - ) - - if scale == 1: - self.nums = 1 - else: - self.nums = scale - 1 - if self.in_channels != self.out_channels and stride_3x3 != 2: - self.pool = nn.AvgPool2d(kernel_size=3, stride=stride_3x3, padding=1) - - if deform_modulated: - deform_conv_op = ModulatedDeformConv - # offset channels are 2 or 3 (if with modulated) * kernel_size * kernel_size - offset_channels = 27 - else: - deform_conv_op = DeformConv - offset_channels = 18 - - # self.conv2_offset = Conv2d( - # bottleneck_channels, - # offset_channels * deform_num_groups, - # kernel_size=3, - # stride=stride_3x3, - # padding=1 * dilation, - # dilation=dilation, - # ) - # self.conv2 = deform_conv_op( - # bottleneck_channels, - # bottleneck_channels, - # kernel_size=3, - # stride=stride_3x3, - # padding=1 * dilation, - # bias=False, - # groups=num_groups, - # dilation=dilation, - # deformable_groups=deform_num_groups, - # norm=get_norm(norm, bottleneck_channels), - # ) - - conv2_offsets = [] - convs = [] - bns = [] - for _i in range(self.nums): - conv2_offsets.append( - Conv2d( - width, - offset_channels * deform_num_groups, - kernel_size=3, - stride=stride_3x3, - padding=1 * dilation, - bias=False, - groups=num_groups, - dilation=dilation, - ) - ) - convs.append( - deform_conv_op( - width, - width, - kernel_size=3, - stride=stride_3x3, - padding=1 * dilation, - bias=False, - groups=num_groups, - dilation=dilation, - deformable_groups=deform_num_groups, - ) - ) - bns.append(get_norm(norm, width)) - self.conv2_offsets = nn.ModuleList(conv2_offsets) - self.convs = nn.ModuleList(convs) - self.bns = nn.ModuleList(bns) - - self.conv3 = Conv2d( - bottleneck_channels, - out_channels, - kernel_size=1, - bias=False, - norm=get_norm(norm, out_channels), - ) - self.scale = scale - self.width = width - self.in_channels = in_channels - self.out_channels = out_channels - self.stride_3x3 = stride_3x3 - # for layer in [self.conv1, self.conv2, self.conv3, self.shortcut]: - # if layer is not None: # shortcut can be None - # weight_init.c2_msra_fill(layer) - - # nn.init.constant_(self.conv2_offset.weight, 0) - # nn.init.constant_(self.conv2_offset.bias, 0) - for layer in [self.conv1, self.conv3]: - if layer is not None: # shortcut can be None - weight_init.c2_msra_fill(layer) - if self.shortcut is not None: - for layer in self.shortcut.modules(): - if isinstance(layer, Conv2d): - weight_init.c2_msra_fill(layer) - - for layer in self.convs: - if layer is not None: # shortcut can be None - weight_init.c2_msra_fill(layer) - - for layer in self.conv2_offsets: - if layer.weight is not None: - nn.init.constant_(layer.weight, 0) - if layer.bias is not None: - nn.init.constant_(layer.bias, 0) - - def forward(self, x): - out = self.conv1(x) - out = F.relu_(out) - - # if self.deform_modulated: - # offset_mask = self.conv2_offset(out) - # offset_x, offset_y, mask = torch.chunk(offset_mask, 3, dim=1) - # offset = torch.cat((offset_x, offset_y), dim=1) - # mask = mask.sigmoid() - # out = self.conv2(out, offset, mask) - # else: - # offset = self.conv2_offset(out) - # out = self.conv2(out, offset) - # out = F.relu_(out) - - spx = torch.split(out, self.width, 1) - for i in range(self.nums): - if i == 0 or self.in_channels != self.out_channels: - sp = spx[i].contiguous() - else: - sp = sp + spx[i].contiguous() - - # sp = self.convs[i](sp) - if self.deform_modulated: - offset_mask = self.conv2_offsets[i](sp) - offset_x, offset_y, mask = torch.chunk(offset_mask, 3, dim=1) - offset = torch.cat((offset_x, offset_y), dim=1) - mask = mask.sigmoid() - sp = self.convs[i](sp, offset, mask) - else: - offset = self.conv2_offsets[i](sp) - sp = self.convs[i](sp, offset) - sp = F.relu_(self.bns[i](sp)) - if i == 0: - out = sp - else: - out = torch.cat((out, sp), 1) - if self.scale != 1 and self.stride_3x3 == 1: - out = torch.cat((out, spx[self.nums]), 1) - elif self.scale != 1 and self.stride_3x3 == 2: - out = torch.cat((out, self.pool(spx[self.nums])), 1) - - out = self.conv3(out) - - if self.shortcut is not None: - shortcut = self.shortcut(x) - else: - shortcut = x - - out += shortcut - out = F.relu_(out) - return out - - -def make_stage(block_class, num_blocks: int, first_stride, *, in_channels, out_channels, **kwargs): - """ - Create a list of blocks just like those in a ResNet stage. - Args: - block_class (type): a subclass of ResNetBlockBase - num_blocks (int): - first_stride (int): the stride of the first block. The other blocks will have stride=1. - in_channels (int): input channels of the entire stage. - out_channels (int): output channels of **every block** in the stage. - kwargs: other arguments passed to the constructor of every block. - Returns: - list[nn.Module]: a list of block module. - """ - assert "stride" not in kwargs, "Stride of blocks in make_stage cannot be changed." - blocks = [] - for i in range(num_blocks): - blocks.append( - block_class( - in_channels=in_channels, - out_channels=out_channels, - stride=first_stride if i == 0 else 1, - **kwargs, - ) - ) - in_channels = out_channels - return blocks - - -class BasicStem(CNNBlockBase): - """ - The standard ResNet stem (layers before the first residual block). - """ - - def __init__(self, in_channels: int=3, out_channels: int=64, norm: str="BN") -> None: - """ - Args: - norm (str or callable): norm after the first conv layer. - See :func:`layers.get_norm` for supported format. - """ - super().__init__(in_channels, out_channels, 4) - self.in_channels = in_channels - self.conv1 = nn.Sequential( - Conv2d( - in_channels, - 32, - kernel_size=3, - stride=2, - padding=1, - bias=False, - ), - get_norm(norm, 32), - nn.ReLU(inplace=True), - Conv2d( - 32, - 32, - kernel_size=3, - stride=1, - padding=1, - bias=False, - ), - get_norm(norm, 32), - nn.ReLU(inplace=True), - Conv2d( - 32, - out_channels, - kernel_size=3, - stride=1, - padding=1, - bias=False, - ), - ) - self.bn1 = get_norm(norm, out_channels) - - for layer in self.conv1: - if isinstance(layer, Conv2d): - weight_init.c2_msra_fill(layer) - - def forward(self, x): - x = self.conv1(x) - x = self.bn1(x) - x = F.relu_(x) - x = F.max_pool2d(x, kernel_size=3, stride=2, padding=1) - return x - - -class ResNet(Backbone): - def __init__(self, stem, stages, num_classes: Optional[int]=None, out_features=None) -> None: - """ - Args: - stem (nn.Module): a stem module - stages (list[list[CNNBlockBase]]): several (typically 4) stages, - each contains multiple :class:`CNNBlockBase`. - num_classes (None or int): if None, will not perform classification. - Otherwise, will create a linear layer. - out_features (list[str]): name of the layers whose outputs should - be returned in forward. Can be anything in "stem", "linear", or "res2" ... - If None, will return the output of the last layer. - """ - super().__init__() - self.stem = stem - self.num_classes = num_classes - - current_stride = self.stem.stride - self._out_feature_strides = {"stem": current_stride} - self._out_feature_channels = {"stem": self.stem.out_channels} - - self.stages_and_names = [] - for i, blocks in enumerate(stages): - assert len(blocks) > 0, len(blocks) - for block in blocks: - assert isinstance(block, CNNBlockBase), block - - name = "res" + str(i + 2) - stage = nn.Sequential(*blocks) - - self.add_module(name, stage) - self.stages_and_names.append((stage, name)) - - self._out_feature_strides[name] = current_stride = int( - current_stride * np.prod([k.stride for k in blocks]) - ) - self._out_feature_channels[name] = curr_channels = blocks[-1].out_channels - - if num_classes is not None: - self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) - self.linear = nn.Linear(curr_channels, num_classes) - - # Sec 5.1 in "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour": - # "The 1000-way fully-connected layer is initialized by - # drawing weights from a zero-mean Gaussian with standard deviation of 0.01." - nn.init.normal_(self.linear.weight, std=0.01) - name = "linear" - - if out_features is None: - out_features = [name] - self._out_features = out_features - assert len(self._out_features) - children = [x[0] for x in self.named_children()] - for out_feature in self._out_features: - assert out_feature in children, "Available children: {}".format(", ".join(children)) - - def forward(self, x): - outputs = {} - x = self.stem(x) - if "stem" in self._out_features: - outputs["stem"] = x - for stage, name in self.stages_and_names: - x = stage(x) - if name in self._out_features: - outputs[name] = x - if self.num_classes is not None: - x = self.avgpool(x) - x = torch.flatten(x, 1) - x = self.linear(x) - if "linear" in self._out_features: - outputs["linear"] = x - return outputs - - def output_shape(self): - return { - name: ShapeSpec( - channels=self._out_feature_channels[name], stride=self._out_feature_strides[name] - ) - for name in self._out_features - } - - def freeze(self, freeze_at: int=0): - """ - Freeze the first several stages of the ResNet. Commonly used in - fine-tuning. - Args: - freeze_at (int): number of stem and stages to freeze. - `1` means freezing the stem. `2` means freezing the stem and - the first stage, etc. - Returns: - nn.Module: this ResNet itself - """ - if freeze_at >= 1: - self.stem.freeze() - for idx, (stage, _) in enumerate(self.stages_and_names, start=2): - if freeze_at >= idx: - for block in stage.children(): - block.freeze() - return self - - -@BACKBONE_REGISTRY.register() -def build_res2net_backbone(cfg, input_shape): - """ - Create a Res2Net instance from config. - Returns: - ResNet: a :class:`ResNet` instance. - """ - # need registration of new blocks/stems? - norm = cfg.MODEL.RESNETS.NORM - stem = BasicStem( - in_channels=input_shape.channels, - out_channels=cfg.MODEL.RESNETS.STEM_OUT_CHANNELS, - norm=norm, - ) - - # fmt: off - freeze_at = cfg.MODEL.BACKBONE.FREEZE_AT - out_features = cfg.MODEL.RESNETS.OUT_FEATURES - depth = cfg.MODEL.RESNETS.DEPTH - num_groups = cfg.MODEL.RESNETS.NUM_GROUPS - width_per_group = cfg.MODEL.RESNETS.WIDTH_PER_GROUP - scale = 4 - bottleneck_channels = num_groups * width_per_group * scale - in_channels = cfg.MODEL.RESNETS.STEM_OUT_CHANNELS - out_channels = cfg.MODEL.RESNETS.RES2_OUT_CHANNELS - stride_in_1x1 = cfg.MODEL.RESNETS.STRIDE_IN_1X1 - res5_dilation = cfg.MODEL.RESNETS.RES5_DILATION - deform_on_per_stage = cfg.MODEL.RESNETS.DEFORM_ON_PER_STAGE - deform_modulated = cfg.MODEL.RESNETS.DEFORM_MODULATED - deform_num_groups = cfg.MODEL.RESNETS.DEFORM_NUM_GROUPS - # fmt: on - assert res5_dilation in {1, 2}, f"res5_dilation cannot be {res5_dilation}." - - num_blocks_per_stage = { - 18: [2, 2, 2, 2], - 34: [3, 4, 6, 3], - 50: [3, 4, 6, 3], - 101: [3, 4, 23, 3], - 152: [3, 8, 36, 3], - }[depth] - - if depth in [18, 34]: - assert out_channels == 64, "Must set MODEL.RESNETS.RES2_OUT_CHANNELS = 64 for R18/R34" - assert not any(deform_on_per_stage), ( - "MODEL.RESNETS.DEFORM_ON_PER_STAGE unsupported for R18/R34" - ) - assert res5_dilation == 1, "Must set MODEL.RESNETS.RES5_DILATION = 1 for R18/R34" - assert num_groups == 1, "Must set MODEL.RESNETS.NUM_GROUPS = 1 for R18/R34" - - stages = [] - - # Avoid creating variables without gradients - # It consumes extra memory and may cause allreduce to fail - out_stage_idx = [{"res2": 2, "res3": 3, "res4": 4, "res5": 5}[f] for f in out_features] - max_stage_idx = max(out_stage_idx) - for idx, stage_idx in enumerate(range(2, max_stage_idx + 1)): - dilation = res5_dilation if stage_idx == 5 else 1 - first_stride = 1 if idx == 0 or (stage_idx == 5 and dilation == 2) else 2 - stage_kargs = { - "num_blocks": num_blocks_per_stage[idx], - "first_stride": first_stride, - "in_channels": in_channels, - "out_channels": out_channels, - "norm": norm, - } - # Use BasicBlock for R18 and R34. - if depth in [18, 34]: - stage_kargs["block_class"] = BasicBlock - else: - stage_kargs["bottleneck_channels"] = bottleneck_channels - stage_kargs["stride_in_1x1"] = stride_in_1x1 - stage_kargs["dilation"] = dilation - stage_kargs["num_groups"] = num_groups - stage_kargs["scale"] = scale - - if deform_on_per_stage[idx]: - stage_kargs["block_class"] = DeformBottleneckBlock - stage_kargs["deform_modulated"] = deform_modulated - stage_kargs["deform_num_groups"] = deform_num_groups - else: - stage_kargs["block_class"] = BottleneckBlock - blocks = make_stage(**stage_kargs) - in_channels = out_channels - out_channels *= 2 - bottleneck_channels *= 2 - stages.append(blocks) - return ResNet(stem, stages, out_features=out_features).freeze(freeze_at) - - -@BACKBONE_REGISTRY.register() -def build_p67_res2net_fpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = build_res2net_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - out_channels = cfg.MODEL.FPN.OUT_CHANNELS - backbone = FPN( - bottom_up=bottom_up, - in_features=in_features, - out_channels=out_channels, - norm=cfg.MODEL.FPN.NORM, - top_block=LastLevelP6P7_P5(out_channels, out_channels), - fuse_type=cfg.MODEL.FPN.FUSE_TYPE, - ) - return backbone - - -@BACKBONE_REGISTRY.register() -def build_res2net_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ - Args: - cfg: a detectron2 CfgNode - - Returns: - backbone (Backbone): backbone module, must be a subclass of :class:`Backbone`. - """ - bottom_up = build_res2net_backbone(cfg, input_shape) - in_features = cfg.MODEL.FPN.IN_FEATURES - backbone = BiFPN( - cfg=cfg, - bottom_up=bottom_up, - in_features=in_features, - out_channels=cfg.MODEL.BIFPN.OUT_CHANNELS, - norm=cfg.MODEL.BIFPN.NORM, - num_levels=cfg.MODEL.BIFPN.NUM_LEVELS, - num_bifpn=cfg.MODEL.BIFPN.NUM_BIFPN, - separable_conv=cfg.MODEL.BIFPN.SEPARABLE_CONV, - ) - return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/debug.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/debug.py deleted file mode 100644 index 63186b05c5..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/debug.py +++ /dev/null @@ -1,341 +0,0 @@ -import cv2 -import numpy as np -import torch -import torch.nn.functional as F -from typing import Sequence - -COLORS = ((np.random.rand(1300, 3) * 0.4 + 0.6) * 255).astype(np.uint8).reshape(1300, 1, 1, 3) - - -def _get_color_image(heatmap): - heatmap = heatmap.reshape(heatmap.shape[0], heatmap.shape[1], heatmap.shape[2], 1) - if heatmap.shape[0] == 1: - color_map = ( - (heatmap * np.ones((1, 1, 1, 3), np.uint8) * 255).max(axis=0).astype(np.uint8) - ) # H, W, 3 - else: - color_map = (heatmap * COLORS[: heatmap.shape[0]]).max(axis=0).astype(np.uint8) # H, W, 3 - - return color_map - - -def _blend_image(image, color_map, a: float=0.7): - color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) - ret = np.clip(image * (1 - a) + color_map * a, 0, 255).astype(np.uint8) - return ret - - -def _blend_image_heatmaps(image, color_maps, a: float=0.7): - merges = np.zeros((image.shape[0], image.shape[1], 3), np.float32) - for color_map in color_maps: - color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) - merges = np.maximum(merges, color_map) - ret = np.clip(image * (1 - a) + merges * a, 0, 255).astype(np.uint8) - return ret - - -def _decompose_level(x, shapes_per_level, N): - """ - x: LNHiWi x C - """ - x = x.view(x.shape[0], -1) - ret = [] - st = 0 - for l in range(len(shapes_per_level)): - ret.append([]) - h = shapes_per_level[l][0].int().item() - w = shapes_per_level[l][1].int().item() - for i in range(N): - ret[l].append(x[st + h * w * i : st + h * w * (i + 1)].view(h, w, -1).permute(2, 0, 1)) - st += h * w * N - return ret - - -def _imagelist_to_tensor(images): - images = [x for x in images] - image_sizes = [x.shape[-2:] for x in images] - h = max([size[0] for size in image_sizes]) - w = max([size[1] for size in image_sizes]) - S = 32 - h, w = ((h - 1) // S + 1) * S, ((w - 1) // S + 1) * S - images = [F.pad(x, (0, w - x.shape[2], 0, h - x.shape[1], 0, 0)) for x in images] - images = torch.stack(images) - return images - - -def _ind2il(ind, shapes_per_level, N): - r = ind - l = 0 - S = 0 - while r - S >= N * shapes_per_level[l][0] * shapes_per_level[l][1]: - S += N * shapes_per_level[l][0] * shapes_per_level[l][1] - l += 1 - i = (r - S) // (shapes_per_level[l][0] * shapes_per_level[l][1]) - return i, l - - -def debug_train( - images, - gt_instances, - flattened_hms, - reg_targets, - labels: Sequence[str], - pos_inds, - shapes_per_level, - locations, - strides: Sequence[int], -) -> None: - """ - images: N x 3 x H x W - flattened_hms: LNHiWi x C - shapes_per_level: L x 2 [(H_i, W_i)] - locations: LNHiWi x 2 - """ - reg_inds = torch.nonzero(reg_targets.max(dim=1)[0] > 0).squeeze(1) - N = len(images) - images = _imagelist_to_tensor(images) - repeated_locations = [torch.cat([loc] * N, dim=0) for loc in locations] - locations = torch.cat(repeated_locations, dim=0) - gt_hms = _decompose_level(flattened_hms, shapes_per_level, N) - masks = flattened_hms.new_zeros((flattened_hms.shape[0], 1)) - masks[pos_inds] = 1 - masks = _decompose_level(masks, shapes_per_level, N) - for i in range(len(images)): - image = images[i].detach().cpu().numpy().transpose(1, 2, 0) - color_maps = [] - for l in range(len(gt_hms)): - color_map = _get_color_image(gt_hms[l][i].detach().cpu().numpy()) - color_maps.append(color_map) - cv2.imshow(f"gthm_{l}", color_map) - blend = _blend_image_heatmaps(image.copy(), color_maps) - if gt_instances is not None: - bboxes = gt_instances[i].gt_boxes.tensor - for j in range(len(bboxes)): - bbox = bboxes[j] - cv2.rectangle( - blend, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - (0, 0, 255), - 3, - cv2.LINE_AA, - ) - - for j in range(len(pos_inds)): - image_id, l = _ind2il(pos_inds[j], shapes_per_level, N) - if image_id != i: - continue - loc = locations[pos_inds[j]] - cv2.drawMarker( - blend, (int(loc[0]), int(loc[1])), (0, 255, 255), markerSize=(l + 1) * 16 - ) - - for j in range(len(reg_inds)): - image_id, l = _ind2il(reg_inds[j], shapes_per_level, N) - if image_id != i: - continue - ltrb = reg_targets[reg_inds[j]] - ltrb *= strides[l] - loc = locations[reg_inds[j]] - bbox = [(loc[0] - ltrb[0]), (loc[1] - ltrb[1]), (loc[0] + ltrb[2]), (loc[1] + ltrb[3])] - cv2.rectangle( - blend, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - (255, 0, 0), - 1, - cv2.LINE_AA, - ) - cv2.circle(blend, (int(loc[0]), int(loc[1])), 2, (255, 0, 0), -1) - - cv2.imshow("blend", blend) - cv2.waitKey() - - -def debug_test( - images, - logits_pred, - reg_pred, - agn_hm_pred=None, - preds=None, - vis_thresh: float=0.3, - debug_show_name: bool=False, - mult_agn: bool=False, -) -> None: - """ - images: N x 3 x H x W - class_target: LNHiWi x C - cat_agn_heatmap: LNHiWi - shapes_per_level: L x 2 [(H_i, W_i)] - """ - if preds is None: - preds = [] - if agn_hm_pred is None: - agn_hm_pred = [] - len(images) - for i in range(len(images)): - image = images[i].detach().cpu().numpy().transpose(1, 2, 0) - image.copy().astype(np.uint8) - pred_image = image.copy().astype(np.uint8) - color_maps = [] - L = len(logits_pred) - for l in range(L): - if logits_pred[0] is not None: - stride = min(image.shape[0], image.shape[1]) / min( - logits_pred[l][i].shape[1], logits_pred[l][i].shape[2] - ) - else: - stride = min(image.shape[0], image.shape[1]) / min( - agn_hm_pred[l][i].shape[1], agn_hm_pred[l][i].shape[2] - ) - stride = stride if stride < 60 else 64 if stride < 100 else 128 - if logits_pred[0] is not None: - if mult_agn: - logits_pred[l][i] = logits_pred[l][i] * agn_hm_pred[l][i] - color_map = _get_color_image(logits_pred[l][i].detach().cpu().numpy()) - color_maps.append(color_map) - cv2.imshow(f"predhm_{l}", color_map) - - if debug_show_name: - from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - - cat2name = [x["name"] for x in LVIS_CATEGORIES] - for j in range(len(preds[i].scores) if preds is not None else 0): - if preds[i].scores[j] > vis_thresh: - bbox = ( - preds[i].proposal_boxes[j] - if preds[i].has("proposal_boxes") - else preds[i].pred_boxes[j] - ) - bbox = bbox.tensor[0].detach().cpu().numpy().astype(np.int32) - cat = int(preds[i].pred_classes[j]) if preds[i].has("pred_classes") else 0 - cl = COLORS[cat, 0, 0] - cv2.rectangle( - pred_image, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - (int(cl[0]), int(cl[1]), int(cl[2])), - 2, - cv2.LINE_AA, - ) - if debug_show_name: - txt = "{}{:.1f}".format( - cat2name[cat] if cat > 0 else "", preds[i].scores[j] - ) - font = cv2.FONT_HERSHEY_SIMPLEX - cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] - cv2.rectangle( - pred_image, - (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), - -1, - ) - cv2.putText( - pred_image, - txt, - (int(bbox[0]), int(bbox[1] - 2)), - font, - 0.5, - (0, 0, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - - if agn_hm_pred[l] is not None: - agn_hm_ = agn_hm_pred[l][i, 0, :, :, None].detach().cpu().numpy() - agn_hm_ = (agn_hm_ * np.array([255, 255, 255]).reshape(1, 1, 3)).astype(np.uint8) - cv2.imshow(f"agn_hm_{l}", agn_hm_) - blend = _blend_image_heatmaps(image.copy(), color_maps) - cv2.imshow("blend", blend) - cv2.imshow("preds", pred_image) - cv2.waitKey() - - -global cnt -cnt = 0 - - -def debug_second_stage( - images, instances, proposals=None, vis_thresh: float=0.3, save_debug: bool=False, debug_show_name: bool=False -) -> None: - images = _imagelist_to_tensor(images) - if debug_show_name: - from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - - cat2name = [x["name"] for x in LVIS_CATEGORIES] - for i in range(len(images)): - image = images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() - if instances[i].has("gt_boxes"): - bboxes = instances[i].gt_boxes.tensor.cpu().numpy() - scores = np.ones(bboxes.shape[0]) - cats = instances[i].gt_classes.cpu().numpy() - else: - bboxes = instances[i].pred_boxes.tensor.cpu().numpy() - scores = instances[i].scores.cpu().numpy() - cats = instances[i].pred_classes.cpu().numpy() - for j in range(len(bboxes)): - if scores[j] > vis_thresh: - bbox = bboxes[j] - cl = COLORS[cats[j], 0, 0] - cl = (int(cl[0]), int(cl[1]), int(cl[2])) - cv2.rectangle( - image, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - cl, - 2, - cv2.LINE_AA, - ) - if debug_show_name: - cat = cats[j] - txt = "{}{:.1f}".format(cat2name[cat] if cat > 0 else "", scores[j]) - font = cv2.FONT_HERSHEY_SIMPLEX - cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] - cv2.rectangle( - image, - (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), - -1, - ) - cv2.putText( - image, - txt, - (int(bbox[0]), int(bbox[1] - 2)), - font, - 0.5, - (0, 0, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - if proposals is not None: - proposal_image = ( - images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() - ) - bboxes = proposals[i].proposal_boxes.tensor.cpu().numpy() - if proposals[i].has("scores"): - scores = proposals[i].scores.cpu().numpy() - else: - scores = proposals[i].objectness_logits.sigmoid().cpu().numpy() - for j in range(len(bboxes)): - if scores[j] > vis_thresh: - bbox = bboxes[j] - cl = (209, 159, 83) - cv2.rectangle( - proposal_image, - (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - cl, - 2, - cv2.LINE_AA, - ) - - cv2.imshow("image", image) - if proposals is not None: - cv2.imshow("proposals", proposal_image) - if save_debug: - global cnt - cnt += 1 - cv2.imwrite(f"output/save_debug/{cnt}.jpg", proposal_image) - cv2.waitKey() diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet.py deleted file mode 100644 index cd68ed3f40..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet.py +++ /dev/null @@ -1,912 +0,0 @@ -from detectron2.config import configurable -from detectron2.layers import cat -from detectron2.modeling.proposal_generator.build import PROPOSAL_GENERATOR_REGISTRY -from detectron2.structures import Boxes, Instances -from detectron2.utils.comm import get_world_size -import torch -from torch import nn - -from ..debug import debug_test, debug_train -from ..layers.heatmap_focal_loss import binary_heatmap_focal_loss_jit, heatmap_focal_loss_jit -from ..layers.iou_loss import IOULoss -from ..layers.ml_nms import ml_nms -from .centernet_head import CenterNetHead -from .utils import _transpose, reduce_sum -from typing import Sequence - -__all__ = ["CenterNet"] - -INF = 100000000 - - -@PROPOSAL_GENERATOR_REGISTRY.register() -class CenterNet(nn.Module): - @configurable - def __init__( - self, - # input_shape: Dict[str, ShapeSpec], - in_channels: int=256, - *, - num_classes: int=80, - in_features=("p3", "p4", "p5", "p6", "p7"), - strides: Sequence[int]=(8, 16, 32, 64, 128), - score_thresh: float=0.05, - hm_min_overlap: float=0.8, - loc_loss_type: str="giou", - min_radius: int=4, - hm_focal_alpha: float=0.25, - hm_focal_beta: int=4, - loss_gamma: float=2.0, - reg_weight: float=2.0, - not_norm_reg: bool=True, - with_agn_hm: bool=False, - only_proposal: bool=False, - as_proposal: bool=False, - not_nms: bool=False, - pos_weight: float=1.0, - neg_weight: float=1.0, - sigmoid_clamp: float=1e-4, - ignore_high_fp=-1.0, - center_nms: bool=False, - sizes_of_interest=None, - more_pos: bool=False, - more_pos_thresh: float=0.2, - more_pos_topk: int=9, - pre_nms_topk_train: int=1000, - pre_nms_topk_test: int=1000, - post_nms_topk_train: int=100, - post_nms_topk_test: int=100, - nms_thresh_train: float=0.6, - nms_thresh_test: float=0.6, - no_reduce: bool=False, - not_clamp_box: bool=False, - debug: bool=False, - vis_thresh: float=0.5, - pixel_mean=None, - pixel_std=None, - device: str="cuda", - centernet_head=None, - ) -> None: - if pixel_std is None: - pixel_std = [1.0, 1.0, 1.0] - if pixel_mean is None: - pixel_mean = [103.53, 116.28, 123.675] - if sizes_of_interest is None: - sizes_of_interest = [[0, 80], [64, 160], [128, 320], [256, 640], [512, 10000000]] - super().__init__() - self.num_classes = num_classes - self.in_features = in_features - self.strides = strides - self.score_thresh = score_thresh - self.min_radius = min_radius - self.hm_focal_alpha = hm_focal_alpha - self.hm_focal_beta = hm_focal_beta - self.loss_gamma = loss_gamma - self.reg_weight = reg_weight - self.not_norm_reg = not_norm_reg - self.with_agn_hm = with_agn_hm - self.only_proposal = only_proposal - self.as_proposal = as_proposal - self.not_nms = not_nms - self.pos_weight = pos_weight - self.neg_weight = neg_weight - self.sigmoid_clamp = sigmoid_clamp - self.ignore_high_fp = ignore_high_fp - self.center_nms = center_nms - self.sizes_of_interest = sizes_of_interest - self.more_pos = more_pos - self.more_pos_thresh = more_pos_thresh - self.more_pos_topk = more_pos_topk - self.pre_nms_topk_train = pre_nms_topk_train - self.pre_nms_topk_test = pre_nms_topk_test - self.post_nms_topk_train = post_nms_topk_train - self.post_nms_topk_test = post_nms_topk_test - self.nms_thresh_train = nms_thresh_train - self.nms_thresh_test = nms_thresh_test - self.no_reduce = no_reduce - self.not_clamp_box = not_clamp_box - - self.debug = debug - self.vis_thresh = vis_thresh - if self.center_nms: - self.not_nms = True - self.iou_loss = IOULoss(loc_loss_type) - assert (not self.only_proposal) or self.with_agn_hm - # delta for rendering heatmap - self.delta = (1 - hm_min_overlap) / (1 + hm_min_overlap) - if centernet_head is None: - self.centernet_head = CenterNetHead( - in_channels=in_channels, - num_levels=len(in_features), - with_agn_hm=with_agn_hm, - only_proposal=only_proposal, - ) - else: - self.centernet_head = centernet_head - if self.debug: - pixel_mean = torch.Tensor(pixel_mean).to(torch.device(device)).view(3, 1, 1) - pixel_std = torch.Tensor(pixel_std).to(torch.device(device)).view(3, 1, 1) - self.denormalizer = lambda x: x * pixel_std + pixel_mean - - @classmethod - def from_config(cls, cfg, input_shape): - ret = { - # 'input_shape': input_shape, - "in_channels": input_shape[cfg.MODEL.CENTERNET.IN_FEATURES[0]].channels, - "num_classes": cfg.MODEL.CENTERNET.NUM_CLASSES, - "in_features": cfg.MODEL.CENTERNET.IN_FEATURES, - "strides": cfg.MODEL.CENTERNET.FPN_STRIDES, - "score_thresh": cfg.MODEL.CENTERNET.INFERENCE_TH, - "loc_loss_type": cfg.MODEL.CENTERNET.LOC_LOSS_TYPE, - "hm_min_overlap": cfg.MODEL.CENTERNET.HM_MIN_OVERLAP, - "min_radius": cfg.MODEL.CENTERNET.MIN_RADIUS, - "hm_focal_alpha": cfg.MODEL.CENTERNET.HM_FOCAL_ALPHA, - "hm_focal_beta": cfg.MODEL.CENTERNET.HM_FOCAL_BETA, - "loss_gamma": cfg.MODEL.CENTERNET.LOSS_GAMMA, - "reg_weight": cfg.MODEL.CENTERNET.REG_WEIGHT, - "not_norm_reg": cfg.MODEL.CENTERNET.NOT_NORM_REG, - "with_agn_hm": cfg.MODEL.CENTERNET.WITH_AGN_HM, - "only_proposal": cfg.MODEL.CENTERNET.ONLY_PROPOSAL, - "as_proposal": cfg.MODEL.CENTERNET.AS_PROPOSAL, - "not_nms": cfg.MODEL.CENTERNET.NOT_NMS, - "pos_weight": cfg.MODEL.CENTERNET.POS_WEIGHT, - "neg_weight": cfg.MODEL.CENTERNET.NEG_WEIGHT, - "sigmoid_clamp": cfg.MODEL.CENTERNET.SIGMOID_CLAMP, - "ignore_high_fp": cfg.MODEL.CENTERNET.IGNORE_HIGH_FP, - "center_nms": cfg.MODEL.CENTERNET.CENTER_NMS, - "sizes_of_interest": cfg.MODEL.CENTERNET.SOI, - "more_pos": cfg.MODEL.CENTERNET.MORE_POS, - "more_pos_thresh": cfg.MODEL.CENTERNET.MORE_POS_THRESH, - "more_pos_topk": cfg.MODEL.CENTERNET.MORE_POS_TOPK, - "pre_nms_topk_train": cfg.MODEL.CENTERNET.PRE_NMS_TOPK_TRAIN, - "pre_nms_topk_test": cfg.MODEL.CENTERNET.PRE_NMS_TOPK_TEST, - "post_nms_topk_train": cfg.MODEL.CENTERNET.POST_NMS_TOPK_TRAIN, - "post_nms_topk_test": cfg.MODEL.CENTERNET.POST_NMS_TOPK_TEST, - "nms_thresh_train": cfg.MODEL.CENTERNET.NMS_TH_TRAIN, - "nms_thresh_test": cfg.MODEL.CENTERNET.NMS_TH_TEST, - "no_reduce": cfg.MODEL.CENTERNET.NO_REDUCE, - "not_clamp_box": cfg.INPUT.NOT_CLAMP_BOX, - "debug": cfg.DEBUG, - "vis_thresh": cfg.VIS_THRESH, - "pixel_mean": cfg.MODEL.PIXEL_MEAN, - "pixel_std": cfg.MODEL.PIXEL_STD, - "device": cfg.MODEL.DEVICE, - "centernet_head": CenterNetHead( - cfg, [input_shape[f] for f in cfg.MODEL.CENTERNET.IN_FEATURES] - ), - } - return ret - - def forward(self, images, features_dict, gt_instances): - features = [features_dict[f] for f in self.in_features] - clss_per_level, reg_pred_per_level, agn_hm_pred_per_level = self.centernet_head(features) - grids = self.compute_grids(features) - shapes_per_level = grids[0].new_tensor( - [(x.shape[2], x.shape[3]) for x in reg_pred_per_level] - ) - - if not self.training: - return self.inference( - images, clss_per_level, reg_pred_per_level, agn_hm_pred_per_level, grids - ) - else: - pos_inds, labels, reg_targets, flattened_hms = self._get_ground_truth( - grids, shapes_per_level, gt_instances - ) - # logits_pred: M x F, reg_pred: M x 4, agn_hm_pred: M - logits_pred, reg_pred, agn_hm_pred = self._flatten_outputs( - clss_per_level, reg_pred_per_level, agn_hm_pred_per_level - ) - - if self.more_pos: - # add more pixels as positive if \ - # 1. they are within the center3x3 region of an object - # 2. their regression losses are small (= 0).squeeze(1) - reg_pred = reg_pred[reg_inds] - reg_targets_pos = reg_targets[reg_inds] - reg_weight_map = flattened_hms.max(dim=1)[0] - reg_weight_map = reg_weight_map[reg_inds] - reg_weight_map = reg_weight_map * 0 + 1 if self.not_norm_reg else reg_weight_map - if self.no_reduce: - reg_norm = max(reg_weight_map.sum(), 1) - else: - reg_norm = max(reduce_sum(reg_weight_map.sum()).item() / num_gpus, 1) - - reg_loss = ( - self.reg_weight - * self.iou_loss(reg_pred, reg_targets_pos, reg_weight_map, reduction="sum") - / reg_norm - ) - losses["loss_centernet_loc"] = reg_loss - - if self.with_agn_hm: - cat_agn_heatmap = flattened_hms.max(dim=1)[0] # M - agn_pos_loss, agn_neg_loss = binary_heatmap_focal_loss_jit( - agn_hm_pred.float(), - cat_agn_heatmap.float(), - pos_inds, - alpha=self.hm_focal_alpha, - beta=self.hm_focal_beta, - gamma=self.loss_gamma, - sigmoid_clamp=self.sigmoid_clamp, - ignore_high_fp=self.ignore_high_fp, - ) - agn_pos_loss = self.pos_weight * agn_pos_loss / num_pos_avg - agn_neg_loss = self.neg_weight * agn_neg_loss / num_pos_avg - losses["loss_centernet_agn_pos"] = agn_pos_loss - losses["loss_centernet_agn_neg"] = agn_neg_loss - - if self.debug: - print("losses", losses) - print("total_num_pos", total_num_pos) - return losses - - def compute_grids(self, features): - grids = [] - for level, feature in enumerate(features): - h, w = feature.size()[-2:] - shifts_x = torch.arange( - 0, - w * self.strides[level], - step=self.strides[level], - dtype=torch.float32, - device=feature.device, - ) - shifts_y = torch.arange( - 0, - h * self.strides[level], - step=self.strides[level], - dtype=torch.float32, - device=feature.device, - ) - shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x) - shift_x = shift_x.reshape(-1) - shift_y = shift_y.reshape(-1) - grids_per_level = torch.stack((shift_x, shift_y), dim=1) + self.strides[level] // 2 - grids.append(grids_per_level) - return grids - - def _get_ground_truth(self, grids, shapes_per_level, gt_instances): - """ - Input: - grids: list of tensors [(hl x wl, 2)]_l - shapes_per_level: list of tuples L x 2: - gt_instances: gt instances - Retuen: - pos_inds: N - labels: N - reg_targets: M x 4 - flattened_hms: M x C or M x 1 - N: number of objects in all images - M: number of pixels from all FPN levels - """ - - # get positive pixel index - if not self.more_pos: - pos_inds, labels = self._get_label_inds(gt_instances, shapes_per_level) - else: - pos_inds, labels = None, None - heatmap_channels = self.num_classes - L = len(grids) - num_loc_list = [len(loc) for loc in grids] - strides = torch.cat( - [shapes_per_level.new_ones(num_loc_list[l]) * self.strides[l] for l in range(L)] - ).float() # M - reg_size_ranges = torch.cat( - [ - shapes_per_level.new_tensor(self.sizes_of_interest[l]) - .float() - .view(1, 2) - .expand(num_loc_list[l], 2) - for l in range(L) - ] - ) # M x 2 - grids = torch.cat(grids, dim=0) # M x 2 - M = grids.shape[0] - - reg_targets = [] - flattened_hms = [] - for i in range(len(gt_instances)): # images - boxes = gt_instances[i].gt_boxes.tensor # N x 4 - area = gt_instances[i].gt_boxes.area() # N - gt_classes = gt_instances[i].gt_classes # N in [0, self.num_classes] - - N = boxes.shape[0] - if N == 0: - reg_targets.append(grids.new_zeros((M, 4)) - INF) - flattened_hms.append( - grids.new_zeros((M, 1 if self.only_proposal else heatmap_channels)) - ) - continue - - l = grids[:, 0].view(M, 1) - boxes[:, 0].view(1, N) # M x N - t = grids[:, 1].view(M, 1) - boxes[:, 1].view(1, N) # M x N - r = boxes[:, 2].view(1, N) - grids[:, 0].view(M, 1) # M x N - b = boxes[:, 3].view(1, N) - grids[:, 1].view(M, 1) # M x N - reg_target = torch.stack([l, t, r, b], dim=2) # M x N x 4 - - centers = (boxes[:, [0, 1]] + boxes[:, [2, 3]]) / 2 # N x 2 - centers_expanded = centers.view(1, N, 2).expand(M, N, 2) # M x N x 2 - strides_expanded = strides.view(M, 1, 1).expand(M, N, 2) - centers_discret = ( - (centers_expanded / strides_expanded).int() * strides_expanded - ).float() + strides_expanded / 2 # M x N x 2 - - is_peak = ((grids.view(M, 1, 2).expand(M, N, 2) - centers_discret) ** 2).sum( - dim=2 - ) == 0 # M x N - is_in_boxes = reg_target.min(dim=2)[0] > 0 # M x N - is_center3x3 = self.get_center3x3(grids, centers, strides) & is_in_boxes # M x N - is_cared_in_the_level = self.assign_reg_fpn(reg_target, reg_size_ranges) # M x N - reg_mask = is_center3x3 & is_cared_in_the_level # M x N - - dist2 = ((grids.view(M, 1, 2).expand(M, N, 2) - centers_expanded) ** 2).sum( - dim=2 - ) # M x N - dist2[is_peak] = 0 - radius2 = self.delta**2 * 2 * area # N - radius2 = torch.clamp(radius2, min=self.min_radius**2) - weighted_dist2 = dist2 / radius2.view(1, N).expand(M, N) # M x N - reg_target = self._get_reg_targets( - reg_target, weighted_dist2.clone(), reg_mask, area - ) # M x 4 - - if self.only_proposal: - flattened_hm = self._create_agn_heatmaps_from_dist(weighted_dist2.clone()) # M x 1 - else: - flattened_hm = self._create_heatmaps_from_dist( - weighted_dist2.clone(), gt_classes, channels=heatmap_channels - ) # M x C - - reg_targets.append(reg_target) - flattened_hms.append(flattened_hm) - - # transpose im first training_targets to level first ones - reg_targets = _transpose(reg_targets, num_loc_list) - flattened_hms = _transpose(flattened_hms, num_loc_list) - for l in range(len(reg_targets)): - reg_targets[l] = reg_targets[l] / float(self.strides[l]) - reg_targets = cat([x for x in reg_targets], dim=0) # MB x 4 - flattened_hms = cat([x for x in flattened_hms], dim=0) # MB x C - - return pos_inds, labels, reg_targets, flattened_hms - - def _get_label_inds(self, gt_instances, shapes_per_level): - """ - Inputs: - gt_instances: [n_i], sum n_i = N - shapes_per_level: L x 2 [(h_l, w_l)]_L - Returns: - pos_inds: N' - labels: N' - """ - pos_inds = [] - labels = [] - L = len(self.strides) - B = len(gt_instances) - shapes_per_level = shapes_per_level.long() - loc_per_level = (shapes_per_level[:, 0] * shapes_per_level[:, 1]).long() # L - level_bases = [] - s = 0 - for l in range(L): - level_bases.append(s) - s = s + B * loc_per_level[l] - level_bases = shapes_per_level.new_tensor(level_bases).long() # L - strides_default = shapes_per_level.new_tensor(self.strides).float() # L - for im_i in range(B): - targets_per_im = gt_instances[im_i] - bboxes = targets_per_im.gt_boxes.tensor # n x 4 - n = bboxes.shape[0] - centers = (bboxes[:, [0, 1]] + bboxes[:, [2, 3]]) / 2 # n x 2 - centers = centers.view(n, 1, 2).expand(n, L, 2).contiguous() - if self.not_clamp_box: - h, w = gt_instances[im_i]._image_size - centers[:, :, 0].clamp_(min=0).clamp_(max=w - 1) - centers[:, :, 1].clamp_(min=0).clamp_(max=h - 1) - strides = strides_default.view(1, L, 1).expand(n, L, 2) - centers_inds = (centers / strides).long() # n x L x 2 - Ws = shapes_per_level[:, 1].view(1, L).expand(n, L) - pos_ind = ( - level_bases.view(1, L).expand(n, L) - + im_i * loc_per_level.view(1, L).expand(n, L) - + centers_inds[:, :, 1] * Ws - + centers_inds[:, :, 0] - ) # n x L - is_cared_in_the_level = self.assign_fpn_level(bboxes) - pos_ind = pos_ind[is_cared_in_the_level].view(-1) - label = ( - targets_per_im.gt_classes.view(n, 1).expand(n, L)[is_cared_in_the_level].view(-1) - ) - - pos_inds.append(pos_ind) # n' - labels.append(label) # n' - pos_inds = torch.cat(pos_inds, dim=0).long() - labels = torch.cat(labels, dim=0) - return pos_inds, labels # N, N - - def assign_fpn_level(self, boxes): - """ - Inputs: - boxes: n x 4 - size_ranges: L x 2 - Return: - is_cared_in_the_level: n x L - """ - size_ranges = boxes.new_tensor(self.sizes_of_interest).view( - len(self.sizes_of_interest), 2 - ) # L x 2 - crit = ((boxes[:, 2:] - boxes[:, :2]) ** 2).sum(dim=1) ** 0.5 / 2 # n - n, L = crit.shape[0], size_ranges.shape[0] - crit = crit.view(n, 1).expand(n, L) - size_ranges_expand = size_ranges.view(1, L, 2).expand(n, L, 2) - is_cared_in_the_level = (crit >= size_ranges_expand[:, :, 0]) & ( - crit <= size_ranges_expand[:, :, 1] - ) - return is_cared_in_the_level - - def assign_reg_fpn(self, reg_targets_per_im, size_ranges): - """ - TODO (Xingyi): merge it with assign_fpn_level - Inputs: - reg_targets_per_im: M x N x 4 - size_ranges: M x 2 - """ - crit = ((reg_targets_per_im[:, :, :2] + reg_targets_per_im[:, :, 2:]) ** 2).sum( - dim=2 - ) ** 0.5 / 2 # M x N - is_cared_in_the_level = (crit >= size_ranges[:, [0]]) & (crit <= size_ranges[:, [1]]) - return is_cared_in_the_level - - def _get_reg_targets(self, reg_targets, dist, mask, area): - """ - reg_targets (M x N x 4): long tensor - dist (M x N) - is_*: M x N - """ - dist[mask == 0] = INF * 1.0 - min_dist, min_inds = dist.min(dim=1) # M - reg_targets_per_im = reg_targets[range(len(reg_targets)), min_inds] # M x N x 4 --> M x 4 - reg_targets_per_im[min_dist == INF] = -INF - return reg_targets_per_im - - def _create_heatmaps_from_dist(self, dist, labels: Sequence[str], channels): - """ - dist: M x N - labels: N - return: - heatmaps: M x C - """ - heatmaps = dist.new_zeros((dist.shape[0], channels)) - for c in range(channels): - inds = labels == c # N - if inds.int().sum() == 0: - continue - heatmaps[:, c] = torch.exp(-dist[:, inds].min(dim=1)[0]) - zeros = heatmaps[:, c] < 1e-4 - heatmaps[zeros, c] = 0 - return heatmaps - - def _create_agn_heatmaps_from_dist(self, dist): - """ - TODO (Xingyi): merge it with _create_heatmaps_from_dist - dist: M x N - return: - heatmaps: M x 1 - """ - heatmaps = dist.new_zeros((dist.shape[0], 1)) - heatmaps[:, 0] = torch.exp(-dist.min(dim=1)[0]) - zeros = heatmaps < 1e-4 - heatmaps[zeros] = 0 - return heatmaps - - def _flatten_outputs(self, clss, reg_pred, agn_hm_pred): - # Reshape: (N, F, Hl, Wl) -> (N, Hl, Wl, F) -> (sum_l N*Hl*Wl, F) - clss = ( - cat([x.permute(0, 2, 3, 1).reshape(-1, x.shape[1]) for x in clss], dim=0) - if clss[0] is not None - else None - ) - reg_pred = cat([x.permute(0, 2, 3, 1).reshape(-1, 4) for x in reg_pred], dim=0) - agn_hm_pred = ( - cat([x.permute(0, 2, 3, 1).reshape(-1) for x in agn_hm_pred], dim=0) - if self.with_agn_hm - else None - ) - return clss, reg_pred, agn_hm_pred - - def get_center3x3(self, locations, centers, strides: Sequence[int]): - """ - Inputs: - locations: M x 2 - centers: N x 2 - strides: M - """ - M, N = locations.shape[0], centers.shape[0] - locations_expanded = locations.view(M, 1, 2).expand(M, N, 2) # M x N x 2 - centers_expanded = centers.view(1, N, 2).expand(M, N, 2) # M x N x 2 - strides_expanded = strides.view(M, 1, 1).expand(M, N, 2) # M x N - centers_discret = ( - (centers_expanded / strides_expanded).int() * strides_expanded - ).float() + strides_expanded / 2 # M x N x 2 - dist_x = (locations_expanded[:, :, 0] - centers_discret[:, :, 0]).abs() - dist_y = (locations_expanded[:, :, 1] - centers_discret[:, :, 1]).abs() - return (dist_x <= strides_expanded[:, :, 0]) & (dist_y <= strides_expanded[:, :, 0]) - - @torch.no_grad() - def inference(self, images, clss_per_level, reg_pred_per_level, agn_hm_pred_per_level, grids): - logits_pred = [x.sigmoid() if x is not None else None for x in clss_per_level] - agn_hm_pred_per_level = [ - x.sigmoid() if x is not None else None for x in agn_hm_pred_per_level - ] - - if self.only_proposal: - proposals = self.predict_instances( - grids, - agn_hm_pred_per_level, - reg_pred_per_level, - images.image_sizes, - [None for _ in agn_hm_pred_per_level], - ) - else: - proposals = self.predict_instances( - grids, logits_pred, reg_pred_per_level, images.image_sizes, agn_hm_pred_per_level - ) - if self.as_proposal or self.only_proposal: - for p in range(len(proposals)): - proposals[p].proposal_boxes = proposals[p].get("pred_boxes") - proposals[p].objectness_logits = proposals[p].get("scores") - proposals[p].remove("pred_boxes") - - if self.debug: - debug_test( - [self.denormalizer(x) for x in images], - logits_pred, - reg_pred_per_level, - agn_hm_pred_per_level, - preds=proposals, - vis_thresh=self.vis_thresh, - debug_show_name=False, - ) - return proposals, {} - - @torch.no_grad() - def predict_instances( - self, grids, logits_pred, reg_pred, image_sizes: Sequence[int], agn_hm_pred, is_proposal: bool=False - ): - sampled_boxes = [] - for l in range(len(grids)): - sampled_boxes.append( - self.predict_single_level( - grids[l], - logits_pred[l], - reg_pred[l] * self.strides[l], - image_sizes, - agn_hm_pred[l], - l, - is_proposal=is_proposal, - ) - ) - boxlists = list(zip(*sampled_boxes, strict=False)) - boxlists = [Instances.cat(boxlist) for boxlist in boxlists] - boxlists = self.nms_and_topK(boxlists, nms=not self.not_nms) - return boxlists - - @torch.no_grad() - def predict_single_level( - self, grids, heatmap, reg_pred, image_sizes: Sequence[int], agn_hm, level, is_proposal: bool=False - ): - N, C, H, W = heatmap.shape - # put in the same format as grids - if self.center_nms: - heatmap_nms = nn.functional.max_pool2d(heatmap, (3, 3), stride=1, padding=1) - heatmap = heatmap * (heatmap_nms == heatmap).float() - heatmap = heatmap.permute(0, 2, 3, 1) # N x H x W x C - heatmap = heatmap.reshape(N, -1, C) # N x HW x C - box_regression = reg_pred.view(N, 4, H, W).permute(0, 2, 3, 1) # N x H x W x 4 - box_regression = box_regression.reshape(N, -1, 4) - - candidate_inds = heatmap > self.score_thresh # 0.05 - pre_nms_top_n = candidate_inds.view(N, -1).sum(1) # N - pre_nms_topk = self.pre_nms_topk_train if self.training else self.pre_nms_topk_test - pre_nms_top_n = pre_nms_top_n.clamp(max=pre_nms_topk) # N - - if agn_hm is not None: - agn_hm = agn_hm.view(N, 1, H, W).permute(0, 2, 3, 1) - agn_hm = agn_hm.reshape(N, -1) - heatmap = heatmap * agn_hm[:, :, None] - - results = [] - for i in range(N): - per_box_cls = heatmap[i] # HW x C - per_candidate_inds = candidate_inds[i] # n - per_box_cls = per_box_cls[per_candidate_inds] # n - - per_candidate_nonzeros = per_candidate_inds.nonzero() # n - per_box_loc = per_candidate_nonzeros[:, 0] # n - per_class = per_candidate_nonzeros[:, 1] # n - - per_box_regression = box_regression[i] # HW x 4 - per_box_regression = per_box_regression[per_box_loc] # n x 4 - per_grids = grids[per_box_loc] # n x 2 - - per_pre_nms_top_n = pre_nms_top_n[i] # 1 - - if per_candidate_inds.sum().item() > per_pre_nms_top_n.item(): - per_box_cls, top_k_indices = per_box_cls.topk(per_pre_nms_top_n, sorted=False) - per_class = per_class[top_k_indices] - per_box_regression = per_box_regression[top_k_indices] - per_grids = per_grids[top_k_indices] - - detections = torch.stack( - [ - per_grids[:, 0] - per_box_regression[:, 0], - per_grids[:, 1] - per_box_regression[:, 1], - per_grids[:, 0] + per_box_regression[:, 2], - per_grids[:, 1] + per_box_regression[:, 3], - ], - dim=1, - ) # n x 4 - - # avoid invalid boxes in RoI heads - detections[:, 2] = torch.max(detections[:, 2], detections[:, 0] + 0.01) - detections[:, 3] = torch.max(detections[:, 3], detections[:, 1] + 0.01) - boxlist = Instances(image_sizes[i]) - boxlist.scores = torch.sqrt(per_box_cls) if self.with_agn_hm else per_box_cls # n - # import pdb; pdb.set_trace() - boxlist.pred_boxes = Boxes(detections) - boxlist.pred_classes = per_class - results.append(boxlist) - return results - - @torch.no_grad() - def nms_and_topK(self, boxlists, nms: bool=True): - num_images = len(boxlists) - results = [] - for i in range(num_images): - nms_thresh = self.nms_thresh_train if self.training else self.nms_thresh_test - result = ml_nms(boxlists[i], nms_thresh) if nms else boxlists[i] - if self.debug: - print("#proposals before nms", len(boxlists[i])) - print("#proposals after nms", len(result)) - num_dets = len(result) - post_nms_topk = self.post_nms_topk_train if self.training else self.post_nms_topk_test - if num_dets > post_nms_topk: - cls_scores = result.scores - image_thresh, _ = torch.kthvalue( - cls_scores.float().cpu(), num_dets - post_nms_topk + 1 - ) - keep = cls_scores >= image_thresh.item() - keep = torch.nonzero(keep).squeeze(1) - result = result[keep] - if self.debug: - print("#proposals after filter", len(result)) - results.append(result) - return results - - @torch.no_grad() - def _add_more_pos(self, reg_pred, gt_instances, shapes_per_level): - labels, level_masks, c33_inds, c33_masks, c33_regs = self._get_c33_inds( - gt_instances, shapes_per_level - ) - N, L, K = labels.shape[0], len(self.strides), 9 - c33_inds[c33_masks == 0] = 0 - reg_pred_c33 = reg_pred[c33_inds].detach() # N x L x K - invalid_reg = c33_masks == 0 - c33_regs_expand = c33_regs.view(N * L * K, 4).clamp(min=0) - if N > 0: - with torch.no_grad(): - c33_reg_loss = ( - self.iou_loss( - reg_pred_c33.view(N * L * K, 4), c33_regs_expand, None, reduction="none" - ) - .view(N, L, K) - .detach() - ) # N x L x K - else: - c33_reg_loss = reg_pred_c33.new_zeros((N, L, K)).detach() - c33_reg_loss[invalid_reg] = INF # N x L x K - c33_reg_loss.view(N * L, K)[level_masks.view(N * L), 4] = 0 # real center - c33_reg_loss = c33_reg_loss.view(N, L * K) - if N == 0: - loss_thresh = c33_reg_loss.new_ones(N).float() - else: - loss_thresh = torch.kthvalue(c33_reg_loss, self.more_pos_topk, dim=1)[0] # N - loss_thresh[loss_thresh > self.more_pos_thresh] = self.more_pos_thresh # N - new_pos = c33_reg_loss.view(N, L, K) < loss_thresh.view(N, 1, 1).expand(N, L, K) - pos_inds = c33_inds[new_pos].view(-1) # P - labels = labels.view(N, 1, 1).expand(N, L, K)[new_pos].view(-1) - return pos_inds, labels - - @torch.no_grad() - def _get_c33_inds(self, gt_instances, shapes_per_level): - """ - TODO (Xingyi): The current implementation is ugly. Refactor. - Get the center (and the 3x3 region near center) locations of each objects - Inputs: - gt_instances: [n_i], sum n_i = N - shapes_per_level: L x 2 [(h_l, w_l)]_L - """ - labels = [] - level_masks = [] - c33_inds = [] - c33_masks = [] - c33_regs = [] - L = len(self.strides) - B = len(gt_instances) - shapes_per_level = shapes_per_level.long() - loc_per_level = (shapes_per_level[:, 0] * shapes_per_level[:, 1]).long() # L - level_bases = [] - s = 0 - for l in range(L): - level_bases.append(s) - s = s + B * loc_per_level[l] - level_bases = shapes_per_level.new_tensor(level_bases).long() # L - strides_default = shapes_per_level.new_tensor(self.strides).float() # L - K = 9 - dx = shapes_per_level.new_tensor([-1, 0, 1, -1, 0, 1, -1, 0, 1]).long() - dy = shapes_per_level.new_tensor([-1, -1, -1, 0, 0, 0, 1, 1, 1]).long() - for im_i in range(B): - targets_per_im = gt_instances[im_i] - bboxes = targets_per_im.gt_boxes.tensor # n x 4 - n = bboxes.shape[0] - if n == 0: - continue - centers = (bboxes[:, [0, 1]] + bboxes[:, [2, 3]]) / 2 # n x 2 - centers = centers.view(n, 1, 2).expand(n, L, 2) - - strides = strides_default.view(1, L, 1).expand(n, L, 2) # - centers_inds = (centers / strides).long() # n x L x 2 - center_grids = centers_inds * strides + strides // 2 # n x L x 2 - l = center_grids[:, :, 0] - bboxes[:, 0].view(n, 1).expand(n, L) - t = center_grids[:, :, 1] - bboxes[:, 1].view(n, 1).expand(n, L) - r = bboxes[:, 2].view(n, 1).expand(n, L) - center_grids[:, :, 0] - b = bboxes[:, 3].view(n, 1).expand(n, L) - center_grids[:, :, 1] # n x L - reg = torch.stack([l, t, r, b], dim=2) # n x L x 4 - reg = reg / strides_default.view(1, L, 1).expand(n, L, 4).float() - - Ws = shapes_per_level[:, 1].view(1, L).expand(n, L) - Hs = shapes_per_level[:, 0].view(1, L).expand(n, L) - expand_Ws = Ws.view(n, L, 1).expand(n, L, K) - expand_Hs = Hs.view(n, L, 1).expand(n, L, K) - label = targets_per_im.gt_classes.view(n).clone() - mask = reg.min(dim=2)[0] >= 0 # n x L - mask = mask & self.assign_fpn_level(bboxes) - labels.append(label) # n - level_masks.append(mask) # n x L - - Dy = dy.view(1, 1, K).expand(n, L, K) - Dx = dx.view(1, 1, K).expand(n, L, K) - c33_ind = ( - level_bases.view(1, L, 1).expand(n, L, K) - + im_i * loc_per_level.view(1, L, 1).expand(n, L, K) - + (centers_inds[:, :, 1:2].expand(n, L, K) + Dy) * expand_Ws - + (centers_inds[:, :, 0:1].expand(n, L, K) + Dx) - ) # n x L x K - - c33_mask = ( - ((centers_inds[:, :, 1:2].expand(n, L, K) + dy) < expand_Hs) - & ((centers_inds[:, :, 1:2].expand(n, L, K) + dy) >= 0) - & ((centers_inds[:, :, 0:1].expand(n, L, K) + dx) < expand_Ws) - & ((centers_inds[:, :, 0:1].expand(n, L, K) + dx) >= 0) - ) - # TODO (Xingyi): think about better way to implement this - # Currently it hard codes the 3x3 region - c33_reg = reg.view(n, L, 1, 4).expand(n, L, K, 4).clone() - c33_reg[:, :, [0, 3, 6], 0] -= 1 - c33_reg[:, :, [0, 3, 6], 2] += 1 - c33_reg[:, :, [2, 5, 8], 0] += 1 - c33_reg[:, :, [2, 5, 8], 2] -= 1 - c33_reg[:, :, [0, 1, 2], 1] -= 1 - c33_reg[:, :, [0, 1, 2], 3] += 1 - c33_reg[:, :, [6, 7, 8], 1] += 1 - c33_reg[:, :, [6, 7, 8], 3] -= 1 - c33_mask = c33_mask & (c33_reg.min(dim=3)[0] >= 0) # n x L x K - c33_inds.append(c33_ind) - c33_masks.append(c33_mask) - c33_regs.append(c33_reg) - - if len(level_masks) > 0: - labels = torch.cat(labels, dim=0) - level_masks = torch.cat(level_masks, dim=0) - c33_inds = torch.cat(c33_inds, dim=0).long() - c33_regs = torch.cat(c33_regs, dim=0) - c33_masks = torch.cat(c33_masks, dim=0) - else: - labels = shapes_per_level.new_zeros(0).long() - level_masks = shapes_per_level.new_zeros((0, L)).bool() - c33_inds = shapes_per_level.new_zeros((0, L, K)).long() - c33_regs = shapes_per_level.new_zeros((0, L, K, 4)).float() - c33_masks = shapes_per_level.new_zeros((0, L, K)).bool() - return labels, level_masks, c33_inds, c33_masks, c33_regs # N x L, N x L x K diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet_head.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet_head.py deleted file mode 100644 index e2e1852e27..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet_head.py +++ /dev/null @@ -1,168 +0,0 @@ -import math - -from detectron2.config import configurable -from detectron2.layers import get_norm -import torch -from torch import nn -from torch.nn import functional as F - -from ..layers.deform_conv import DFConv2d - -__all__ = ["CenterNetHead"] - - -class Scale(nn.Module): - def __init__(self, init_value: float=1.0) -> None: - super().__init__() - self.scale = nn.Parameter(torch.FloatTensor([init_value])) - - def forward(self, input): - return input * self.scale - - -class CenterNetHead(nn.Module): - @configurable - def __init__( - self, - # input_shape: List[ShapeSpec], - in_channels, - num_levels: int, - *, - num_classes: int=80, - with_agn_hm: bool=False, - only_proposal: bool=False, - norm: str="GN", - num_cls_convs: int=4, - num_box_convs: int=4, - num_share_convs: int=0, - use_deformable: bool=False, - prior_prob: float=0.01, - ) -> None: - super().__init__() - self.num_classes = num_classes - self.with_agn_hm = with_agn_hm - self.only_proposal = only_proposal - self.out_kernel = 3 - - head_configs = { - "cls": (num_cls_convs if not self.only_proposal else 0, use_deformable), - "bbox": (num_box_convs, use_deformable), - "share": (num_share_convs, use_deformable), - } - - # in_channels = [s.channels for s in input_shape] - # assert len(set(in_channels)) == 1, \ - # "Each level must have the same channel!" - # in_channels = in_channels[0] - channels = { - "cls": in_channels, - "bbox": in_channels, - "share": in_channels, - } - for head in head_configs: - tower = [] - num_convs, use_deformable = head_configs[head] - channel = channels[head] - for i in range(num_convs): - if use_deformable and i == num_convs - 1: - conv_func = DFConv2d - else: - conv_func = nn.Conv2d - tower.append( - conv_func( - in_channels if i == 0 else channel, - channel, - kernel_size=3, - stride=1, - padding=1, - bias=True, - ) - ) - if norm == "GN" and channel % 32 != 0: - tower.append(nn.GroupNorm(25, channel)) - elif norm != "": - tower.append(get_norm(norm, channel)) - tower.append(nn.ReLU()) - self.add_module(f"{head}_tower", nn.Sequential(*tower)) - - self.bbox_pred = nn.Conv2d( - in_channels, 4, kernel_size=self.out_kernel, stride=1, padding=self.out_kernel // 2 - ) - - self.scales = nn.ModuleList([Scale(init_value=1.0) for _ in range(num_levels)]) - - for modules in [ - self.cls_tower, - self.bbox_tower, - self.share_tower, - self.bbox_pred, - ]: - for l in modules.modules(): - if isinstance(l, nn.Conv2d): - torch.nn.init.normal_(l.weight, std=0.01) - torch.nn.init.constant_(l.bias, 0) - - torch.nn.init.constant_(self.bbox_pred.bias, 8.0) - prior_prob = prior_prob - bias_value = -math.log((1 - prior_prob) / prior_prob) - - if self.with_agn_hm: - self.agn_hm = nn.Conv2d( - in_channels, 1, kernel_size=self.out_kernel, stride=1, padding=self.out_kernel // 2 - ) - torch.nn.init.constant_(self.agn_hm.bias, bias_value) - torch.nn.init.normal_(self.agn_hm.weight, std=0.01) - - if not self.only_proposal: - cls_kernel_size = self.out_kernel - self.cls_logits = nn.Conv2d( - in_channels, - self.num_classes, - kernel_size=cls_kernel_size, - stride=1, - padding=cls_kernel_size // 2, - ) - - torch.nn.init.constant_(self.cls_logits.bias, bias_value) - torch.nn.init.normal_(self.cls_logits.weight, std=0.01) - - @classmethod - def from_config(cls, cfg, input_shape): - ret = { - # 'input_shape': input_shape, - "in_channels": next(s.channels for s in input_shape), - "num_levels": len(input_shape), - "num_classes": cfg.MODEL.CENTERNET.NUM_CLASSES, - "with_agn_hm": cfg.MODEL.CENTERNET.WITH_AGN_HM, - "only_proposal": cfg.MODEL.CENTERNET.ONLY_PROPOSAL, - "norm": cfg.MODEL.CENTERNET.NORM, - "num_cls_convs": cfg.MODEL.CENTERNET.NUM_CLS_CONVS, - "num_box_convs": cfg.MODEL.CENTERNET.NUM_BOX_CONVS, - "num_share_convs": cfg.MODEL.CENTERNET.NUM_SHARE_CONVS, - "use_deformable": cfg.MODEL.CENTERNET.USE_DEFORMABLE, - "prior_prob": cfg.MODEL.CENTERNET.PRIOR_PROB, - } - return ret - - def forward(self, x): - clss = [] - bbox_reg = [] - agn_hms = [] - for l, feature in enumerate(x): - feature = self.share_tower(feature) - cls_tower = self.cls_tower(feature) - bbox_tower = self.bbox_tower(feature) - if not self.only_proposal: - clss.append(self.cls_logits(cls_tower)) - else: - clss.append(None) - - if self.with_agn_hm: - agn_hms.append(self.agn_hm(bbox_tower)) - else: - agn_hms.append(None) - reg = self.bbox_pred(bbox_tower) - reg = self.scales[l](reg) - bbox_reg.append(F.relu(reg)) - - return clss, bbox_reg, agn_hms diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/utils.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/utils.py deleted file mode 100644 index ea962943ca..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -from detectron2.utils.comm import get_world_size -import torch - -# from .data import CenterNetCrop - -__all__ = ["_transpose", "reduce_sum"] - -INF = 1000000000 - - -def _transpose(training_targets, num_loc_list): - """ - This function is used to transpose image first training targets to - level first ones - :return: level first training targets - """ - for im_i in range(len(training_targets)): - training_targets[im_i] = torch.split(training_targets[im_i], num_loc_list, dim=0) - - targets_level_first = [] - for targets_per_level in zip(*training_targets, strict=False): - targets_level_first.append(torch.cat(targets_per_level, dim=0)) - return targets_level_first - - -def reduce_sum(tensor): - world_size = get_world_size() - if world_size < 2: - return tensor - tensor = tensor.clone() - torch.distributed.all_reduce(tensor, op=torch.distributed.ReduceOp.SUM) - return tensor diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/deform_conv.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/deform_conv.py deleted file mode 100644 index 643660c6bc..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/deform_conv.py +++ /dev/null @@ -1,114 +0,0 @@ -from detectron2.layers import Conv2d -import torch -from torch import nn - - -class _NewEmptyTensorOp(torch.autograd.Function): - @staticmethod - def forward(ctx, x, new_shape): - ctx.shape = x.shape - return x.new_empty(new_shape) - - @staticmethod - def backward(ctx, grad): - shape = ctx.shape - return _NewEmptyTensorOp.apply(grad, shape), None - - -class DFConv2d(nn.Module): - """Deformable convolutional layer""" - - def __init__( - self, - in_channels, - out_channels, - with_modulated_dcn: bool=True, - kernel_size: int=3, - stride: int=1, - groups: int=1, - dilation: int=1, - deformable_groups: int=1, - bias: bool=False, - padding=None, - ) -> None: - super().__init__() - if isinstance(kernel_size, list | tuple): - assert isinstance(stride, list | tuple) - assert isinstance(dilation, list | tuple) - assert len(kernel_size) == 2 - assert len(stride) == 2 - assert len(dilation) == 2 - padding = ( - dilation[0] * (kernel_size[0] - 1) // 2, - dilation[1] * (kernel_size[1] - 1) // 2, - ) - offset_base_channels = kernel_size[0] * kernel_size[1] - else: - padding = dilation * (kernel_size - 1) // 2 - offset_base_channels = kernel_size * kernel_size - if with_modulated_dcn: - from detectron2.layers.deform_conv import ModulatedDeformConv - - offset_channels = offset_base_channels * 3 # default: 27 - conv_block = ModulatedDeformConv - else: - from detectron2.layers.deform_conv import DeformConv - - offset_channels = offset_base_channels * 2 # default: 18 - conv_block = DeformConv - self.offset = Conv2d( - in_channels, - deformable_groups * offset_channels, - kernel_size=kernel_size, - stride=stride, - padding=padding, - groups=1, - dilation=dilation, - ) - nn.init.constant_(self.offset.weight, 0) - nn.init.constant_(self.offset.bias, 0) - """ - for l in [self.offset, ]: - nn.init.kaiming_uniform_(l.weight, a=1) - torch.nn.init.constant_(l.bias, 0.) - """ - self.conv = conv_block( - in_channels, - out_channels, - kernel_size=kernel_size, - stride=stride, - padding=padding, - dilation=dilation, - groups=groups, - deformable_groups=deformable_groups, - bias=bias, - ) - self.with_modulated_dcn = with_modulated_dcn - self.kernel_size = kernel_size - self.stride = stride - self.padding = padding - self.dilation = dilation - self.offset_split = offset_base_channels * deformable_groups * 2 - - def forward(self, x, return_offset: bool=False): - if x.numel() > 0: - if not self.with_modulated_dcn: - offset_mask = self.offset(x) - x = self.conv(x, offset_mask) - else: - offset_mask = self.offset(x) - offset = offset_mask[:, : self.offset_split, :, :] - mask = offset_mask[:, self.offset_split :, :, :].sigmoid() - x = self.conv(x, offset, mask) - if return_offset: - return x, offset_mask - return x - # get output shape - output_shape = [ - (i + 2 * p - (di * (k - 1) + 1)) // d + 1 - for i, p, di, k, d in zip( - x.shape[-2:], self.padding, self.dilation, self.kernel_size, self.stride, strict=False - ) - ] - output_shape = [x.shape[0], self.conv.weight.shape[0], *output_shape] - return _NewEmptyTensorOp.apply(x, output_shape) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/heatmap_focal_loss.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/heatmap_focal_loss.py deleted file mode 100644 index 50ccf371c9..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/heatmap_focal_loss.py +++ /dev/null @@ -1,91 +0,0 @@ -import torch -from typing import Sequence - - -# TODO: merge these two function -def heatmap_focal_loss( - inputs, - targets, - pos_inds, - labels: Sequence[str], - alpha: float = -1, - beta: float = 4, - gamma: float = 2, - reduction: str = "sum", - sigmoid_clamp: float = 1e-4, - ignore_high_fp: float = -1.0, -): - """ - Loss used in RetinaNet for dense detection: https://arxiv.org/abs/1708.02002. - Args: - inputs: (sum_l N*Hl*Wl, C) - targets: (sum_l N*Hl*Wl, C) - pos_inds: N - labels: N - Returns: - Loss tensor with the reduction option applied. - """ - pred = torch.clamp(inputs.sigmoid_(), min=sigmoid_clamp, max=1 - sigmoid_clamp) - neg_weights = torch.pow(1 - targets, beta) - pos_pred_pix = pred[pos_inds] # N x C - pos_pred = pos_pred_pix.gather(1, labels.unsqueeze(1)) - pos_loss = torch.log(pos_pred) * torch.pow(1 - pos_pred, gamma) - neg_loss = torch.log(1 - pred) * torch.pow(pred, gamma) * neg_weights - - if ignore_high_fp > 0: - not_high_fp = (pred < ignore_high_fp).float() - neg_loss = not_high_fp * neg_loss - - if reduction == "sum": - pos_loss = pos_loss.sum() - neg_loss = neg_loss.sum() - - if alpha >= 0: - pos_loss = alpha * pos_loss - neg_loss = (1 - alpha) * neg_loss - - return -pos_loss, -neg_loss - - -heatmap_focal_loss_jit = torch.jit.script(heatmap_focal_loss) -# heatmap_focal_loss_jit = heatmap_focal_loss - - -def binary_heatmap_focal_loss( - inputs, - targets, - pos_inds, - alpha: float = -1, - beta: float = 4, - gamma: float = 2, - sigmoid_clamp: float = 1e-4, - ignore_high_fp: float = -1.0, -): - """ - Args: - inputs: (sum_l N*Hl*Wl,) - targets: (sum_l N*Hl*Wl,) - pos_inds: N - Returns: - Loss tensor with the reduction option applied. - """ - pred = torch.clamp(inputs.sigmoid_(), min=sigmoid_clamp, max=1 - sigmoid_clamp) - neg_weights = torch.pow(1 - targets, beta) - pos_pred = pred[pos_inds] # N - pos_loss = torch.log(pos_pred) * torch.pow(1 - pos_pred, gamma) - neg_loss = torch.log(1 - pred) * torch.pow(pred, gamma) * neg_weights - if ignore_high_fp > 0: - not_high_fp = (pred < ignore_high_fp).float() - neg_loss = not_high_fp * neg_loss - - pos_loss = -pos_loss.sum() - neg_loss = -neg_loss.sum() - - if alpha >= 0: - pos_loss = alpha * pos_loss - neg_loss = (1 - alpha) * neg_loss - - return pos_loss, neg_loss - - -binary_heatmap_focal_loss_jit = torch.jit.script(binary_heatmap_focal_loss) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/iou_loss.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/iou_loss.py deleted file mode 100644 index 55fa2a186d..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/iou_loss.py +++ /dev/null @@ -1,115 +0,0 @@ -import torch -from torch import nn - - -class IOULoss(nn.Module): - def __init__(self, loc_loss_type: str="iou") -> None: - super().__init__() - self.loc_loss_type = loc_loss_type - - def forward(self, pred, target, weight=None, reduction: str="sum"): - pred_left = pred[:, 0] - pred_top = pred[:, 1] - pred_right = pred[:, 2] - pred_bottom = pred[:, 3] - - target_left = target[:, 0] - target_top = target[:, 1] - target_right = target[:, 2] - target_bottom = target[:, 3] - - target_aera = (target_left + target_right) * (target_top + target_bottom) - pred_aera = (pred_left + pred_right) * (pred_top + pred_bottom) - - w_intersect = torch.min(pred_left, target_left) + torch.min(pred_right, target_right) - h_intersect = torch.min(pred_bottom, target_bottom) + torch.min(pred_top, target_top) - - g_w_intersect = torch.max(pred_left, target_left) + torch.max(pred_right, target_right) - g_h_intersect = torch.max(pred_bottom, target_bottom) + torch.max(pred_top, target_top) - ac_uion = g_w_intersect * g_h_intersect - - area_intersect = w_intersect * h_intersect - area_union = target_aera + pred_aera - area_intersect - - ious = (area_intersect + 1.0) / (area_union + 1.0) - gious = ious - (ac_uion - area_union) / ac_uion - if self.loc_loss_type == "iou": - losses = -torch.log(ious) - elif self.loc_loss_type == "linear_iou": - losses = 1 - ious - elif self.loc_loss_type == "giou": - losses = 1 - gious - else: - raise NotImplementedError - - if weight is not None: - losses = losses * weight - else: - losses = losses - - if reduction == "sum": - return losses.sum() - elif reduction == "batch": - return losses.sum(dim=[1]) - elif reduction == "none": - return losses - else: - raise NotImplementedError - - -def giou_loss( - boxes1: torch.Tensor, - boxes2: torch.Tensor, - reduction: str = "none", - eps: float = 1e-7, -) -> torch.Tensor: - """ - Generalized Intersection over Union Loss (Hamid Rezatofighi et. al) - https://arxiv.org/abs/1902.09630 - Gradient-friendly IoU loss with an additional penalty that is non-zero when the - boxes do not overlap and scales with the size of their smallest enclosing box. - This loss is symmetric, so the boxes1 and boxes2 arguments are interchangeable. - Args: - boxes1, boxes2 (Tensor): box locations in XYXY format, shape (N, 4) or (4,). - reduction: 'none' | 'mean' | 'sum' - 'none': No reduction will be applied to the output. - 'mean': The output will be averaged. - 'sum': The output will be summed. - eps (float): small number to prevent division by zero - """ - - x1, y1, x2, y2 = boxes1.unbind(dim=-1) - x1g, y1g, x2g, y2g = boxes2.unbind(dim=-1) - - assert (x2 >= x1).all(), "bad box: x1 larger than x2" - assert (y2 >= y1).all(), "bad box: y1 larger than y2" - - # Intersection keypoints - xkis1 = torch.max(x1, x1g) - ykis1 = torch.max(y1, y1g) - xkis2 = torch.min(x2, x2g) - ykis2 = torch.min(y2, y2g) - - intsctk = torch.zeros_like(x1) - mask = (ykis2 > ykis1) & (xkis2 > xkis1) - intsctk[mask] = (xkis2[mask] - xkis1[mask]) * (ykis2[mask] - ykis1[mask]) - unionk = (x2 - x1) * (y2 - y1) + (x2g - x1g) * (y2g - y1g) - intsctk - iouk = intsctk / (unionk + eps) - - # smallest enclosing box - xc1 = torch.min(x1, x1g) - yc1 = torch.min(y1, y1g) - xc2 = torch.max(x2, x2g) - yc2 = torch.max(y2, y2g) - - area_c = (xc2 - xc1) * (yc2 - yc1) - miouk = iouk - ((area_c - unionk) / (area_c + eps)) - - loss = 1 - miouk - - if reduction == "mean": - loss = loss.mean() - elif reduction == "sum": - loss = loss.sum() - - return loss diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/ml_nms.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/ml_nms.py deleted file mode 100644 index 429c986cfe..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/ml_nms.py +++ /dev/null @@ -1,29 +0,0 @@ -from detectron2.layers import batched_nms - - -def ml_nms(boxlist, nms_thresh, max_proposals=-1, score_field: str="scores", label_field: str="labels"): - """ - Performs non-maximum suppression on a boxlist, with scores specified - in a boxlist field via score_field. - Arguments: - boxlist(BoxList) - nms_thresh (float) - max_proposals (int): if > 0, then only the top max_proposals are kept - after non-maximum suppression - score_field (str) - """ - if nms_thresh <= 0: - return boxlist - if boxlist.has("pred_boxes"): - boxes = boxlist.pred_boxes.tensor - labels = boxlist.pred_classes - else: - boxes = boxlist.proposal_boxes.tensor - labels = boxlist.proposal_boxes.tensor.new_zeros(len(boxlist.proposal_boxes.tensor)) - scores = boxlist.scores - - keep = batched_nms(boxes, scores, labels, nms_thresh) - if max_proposals > 0: - keep = keep[:max_proposals] - boxlist = boxlist[keep] - return boxlist diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/meta_arch/centernet_detector.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/meta_arch/centernet_detector.py deleted file mode 100644 index 02cd3da416..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/meta_arch/centernet_detector.py +++ /dev/null @@ -1,63 +0,0 @@ -from detectron2.modeling import build_backbone, build_proposal_generator, detector_postprocess -from detectron2.modeling.meta_arch.build import META_ARCH_REGISTRY -from detectron2.structures import ImageList -import torch -from torch import nn - - -@META_ARCH_REGISTRY.register() -class CenterNetDetector(nn.Module): - def __init__(self, cfg) -> None: - super().__init__() - self.mean, self.std = cfg.MODEL.PIXEL_MEAN, cfg.MODEL.PIXEL_STD - self.register_buffer("pixel_mean", torch.Tensor(cfg.MODEL.PIXEL_MEAN).view(-1, 1, 1)) - self.register_buffer("pixel_std", torch.Tensor(cfg.MODEL.PIXEL_STD).view(-1, 1, 1)) - - self.backbone = build_backbone(cfg) - self.proposal_generator = build_proposal_generator( - cfg, self.backbone.output_shape() - ) # TODO: change to a more precise name - - def forward(self, batched_inputs): - if not self.training: - return self.inference(batched_inputs) - images = self.preprocess_image(batched_inputs) - features = self.backbone(images.tensor) - gt_instances = [x["instances"].to(self.device) for x in batched_inputs] - - _, proposal_losses = self.proposal_generator(images, features, gt_instances) - return proposal_losses - - @property - def device(self): - return self.pixel_mean.device - - @torch.no_grad() - def inference(self, batched_inputs, do_postprocess: bool=True): - images = self.preprocess_image(batched_inputs) - inp = images.tensor - features = self.backbone(inp) - proposals, _ = self.proposal_generator(images, features, None) - - processed_results = [] - for results_per_image, input_per_image, image_size in zip( - proposals, batched_inputs, images.image_sizes, strict=False - ): - if do_postprocess: - height = input_per_image.get("height", image_size[0]) - width = input_per_image.get("width", image_size[1]) - r = detector_postprocess(results_per_image, height, width) - processed_results.append({"instances": r}) - else: - r = results_per_image - processed_results.append(r) - return processed_results - - def preprocess_image(self, batched_inputs): - """ - Normalize, pad and batch the input images. - """ - images = [x["image"].to(self.device) for x in batched_inputs] - images = [(x - self.pixel_mean) / self.pixel_std for x in images] - images = ImageList.from_tensors(images, self.backbone.size_divisibility) - return images diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_fast_rcnn.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_fast_rcnn.py deleted file mode 100644 index b48b5447ac..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_fast_rcnn.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# Part of the code is from https://github.com/tztztztztz/eql.detectron2/blob/master/projects/EQL/eql/fast_rcnn.py -import math - -from detectron2.layers import ShapeSpec, cat -from detectron2.modeling.roi_heads.fast_rcnn import ( - FastRCNNOutputLayers, - _log_classification_stats, - fast_rcnn_inference, -) -import torch -from torch import nn -from torch.nn import functional as F - -from .fed_loss import get_fed_loss_inds, load_class_freq - -__all__ = ["CustomFastRCNNOutputLayers"] - - -class CustomFastRCNNOutputLayers(FastRCNNOutputLayers): - def __init__(self, cfg, input_shape: ShapeSpec, **kwargs) -> None: - super().__init__(cfg, input_shape, **kwargs) - self.use_sigmoid_ce = cfg.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE - if self.use_sigmoid_ce: - prior_prob = cfg.MODEL.ROI_BOX_HEAD.PRIOR_PROB - bias_value = -math.log((1 - prior_prob) / prior_prob) - nn.init.constant_(self.cls_score.bias, bias_value) - - self.cfg = cfg - self.use_fed_loss = cfg.MODEL.ROI_BOX_HEAD.USE_FED_LOSS - if self.use_fed_loss: - self.fed_loss_num_cat = cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT - self.register_buffer( - "freq_weight", - load_class_freq( - cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, - cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT, - ), - ) - - def losses(self, predictions, proposals): - """ - enable advanced loss - """ - scores, proposal_deltas = predictions - gt_classes = ( - cat([p.gt_classes for p in proposals], dim=0) if len(proposals) else torch.empty(0) - ) - _log_classification_stats(scores, gt_classes) - - if len(proposals): - proposal_boxes = cat([p.proposal_boxes.tensor for p in proposals], dim=0) # Nx4 - assert not proposal_boxes.requires_grad, "Proposals should not require gradients!" - gt_boxes = cat( - [(p.gt_boxes if p.has("gt_boxes") else p.proposal_boxes).tensor for p in proposals], - dim=0, - ) - else: - proposal_boxes = gt_boxes = torch.empty((0, 4), device=proposal_deltas.device) - - if self.use_sigmoid_ce: - loss_cls = self.sigmoid_cross_entropy_loss(scores, gt_classes) - else: - loss_cls = self.softmax_cross_entropy_loss(scores, gt_classes) - return { - "loss_cls": loss_cls, - "loss_box_reg": self.box_reg_loss( - proposal_boxes, gt_boxes, proposal_deltas, gt_classes - ), - } - - def sigmoid_cross_entropy_loss(self, pred_class_logits, gt_classes): - if pred_class_logits.numel() == 0: - return pred_class_logits.new_zeros([1])[0] # This is more robust than .sum() * 0. - - B = pred_class_logits.shape[0] - C = pred_class_logits.shape[1] - 1 - - target = pred_class_logits.new_zeros(B, C + 1) - target[range(len(gt_classes)), gt_classes] = 1 # B x (C + 1) - target = target[:, :C] # B x C - - weight = 1 - if self.use_fed_loss and (self.freq_weight is not None): # fedloss - appeared = get_fed_loss_inds( - gt_classes, num_sample_cats=self.fed_loss_num_cat, C=C, weight=self.freq_weight - ) - appeared_mask = appeared.new_zeros(C + 1) - appeared_mask[appeared] = 1 # C + 1 - appeared_mask = appeared_mask[:C] - fed_w = appeared_mask.view(1, C).expand(B, C) - weight = weight * fed_w.float() - - cls_loss = F.binary_cross_entropy_with_logits( - pred_class_logits[:, :-1], target, reduction="none" - ) # B x C - loss = torch.sum(cls_loss * weight) / B - return loss - - def softmax_cross_entropy_loss(self, pred_class_logits, gt_classes): - """ - change _no_instance handling - """ - if pred_class_logits.numel() == 0: - return pred_class_logits.new_zeros([1])[0] - - if self.use_fed_loss and (self.freq_weight is not None): - C = pred_class_logits.shape[1] - 1 - appeared = get_fed_loss_inds( - gt_classes, num_sample_cats=self.fed_loss_num_cat, C=C, weight=self.freq_weight - ) - appeared_mask = appeared.new_zeros(C + 1).float() - appeared_mask[appeared] = 1.0 # C + 1 - appeared_mask[C] = 1.0 - loss = F.cross_entropy( - pred_class_logits, gt_classes, weight=appeared_mask, reduction="mean" - ) - else: - loss = F.cross_entropy(pred_class_logits, gt_classes, reduction="mean") - return loss - - def inference(self, predictions, proposals): - """ - enable use proposal boxes - """ - boxes = self.predict_boxes(predictions, proposals) - scores = self.predict_probs(predictions, proposals) - if self.cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE: - proposal_scores = [p.get("objectness_logits") for p in proposals] - scores = [(s * ps[:, None]) ** 0.5 for s, ps in zip(scores, proposal_scores, strict=False)] - image_shapes = [x.image_size for x in proposals] - return fast_rcnn_inference( - boxes, - scores, - image_shapes, - self.test_score_thresh, - self.test_nms_thresh, - self.test_topk_per_image, - ) - - def predict_probs(self, predictions, proposals): - """ - support sigmoid - """ - scores, _ = predictions - num_inst_per_image = [len(p) for p in proposals] - if self.use_sigmoid_ce: - probs = scores.sigmoid() - else: - probs = F.softmax(scores, dim=-1) - return probs.split(num_inst_per_image, dim=0) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_roi_heads.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_roi_heads.py deleted file mode 100644 index d0478de2f3..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_roi_heads.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.modeling.roi_heads.cascade_rcnn import CascadeROIHeads -from detectron2.modeling.roi_heads.fast_rcnn import fast_rcnn_inference -from detectron2.modeling.roi_heads.roi_heads import ROI_HEADS_REGISTRY, StandardROIHeads -from detectron2.utils.events import get_event_storage -import torch - -from .custom_fast_rcnn import CustomFastRCNNOutputLayers - - -@ROI_HEADS_REGISTRY.register() -class CustomROIHeads(StandardROIHeads): - @classmethod - def _init_box_head(cls, cfg, input_shape): - ret = super()._init_box_head(cfg, input_shape) - del ret["box_predictor"] - ret["box_predictor"] = CustomFastRCNNOutputLayers(cfg, ret["box_head"].output_shape) - cls.debug = cfg.DEBUG - if cls.debug: - cls.debug_show_name = cfg.DEBUG_SHOW_NAME - cls.save_debug = cfg.SAVE_DEBUG - cls.vis_thresh = cfg.VIS_THRESH - cls.pixel_mean = ( - torch.Tensor(cfg.MODEL.PIXEL_MEAN).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - ) - cls.pixel_std = ( - torch.Tensor(cfg.MODEL.PIXEL_STD).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - ) - return ret - - def forward(self, images, features, proposals, targets=None): - """ - enable debug - """ - if not self.debug: - del images - if self.training: - assert targets - proposals = self.label_and_sample_proposals(proposals, targets) - del targets - - if self.training: - losses = self._forward_box(features, proposals) - losses.update(self._forward_mask(features, proposals)) - losses.update(self._forward_keypoint(features, proposals)) - return proposals, losses - else: - pred_instances = self._forward_box(features, proposals) - pred_instances = self.forward_with_given_boxes(features, pred_instances) - if self.debug: - from ..debug import debug_second_stage - - def denormalizer(x): - return x * self.pixel_std + self.pixel_mean - debug_second_stage( - [denormalizer(images[0].clone())], - pred_instances, - proposals=proposals, - debug_show_name=self.debug_show_name, - ) - return pred_instances, {} - - -@ROI_HEADS_REGISTRY.register() -class CustomCascadeROIHeads(CascadeROIHeads): - @classmethod - def _init_box_head(cls, cfg, input_shape): - cls.mult_proposal_score = cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE - ret = super()._init_box_head(cfg, input_shape) - del ret["box_predictors"] - cascade_bbox_reg_weights = cfg.MODEL.ROI_BOX_CASCADE_HEAD.BBOX_REG_WEIGHTS - box_predictors = [] - for box_head, bbox_reg_weights in zip(ret["box_heads"], cascade_bbox_reg_weights, strict=False): - box_predictors.append( - CustomFastRCNNOutputLayers( - cfg, - box_head.output_shape, - box2box_transform=Box2BoxTransform(weights=bbox_reg_weights), - ) - ) - ret["box_predictors"] = box_predictors - cls.debug = cfg.DEBUG - if cls.debug: - cls.debug_show_name = cfg.DEBUG_SHOW_NAME - cls.save_debug = cfg.SAVE_DEBUG - cls.vis_thresh = cfg.VIS_THRESH - cls.pixel_mean = ( - torch.Tensor(cfg.MODEL.PIXEL_MEAN).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - ) - cls.pixel_std = ( - torch.Tensor(cfg.MODEL.PIXEL_STD).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - ) - return ret - - def _forward_box(self, features, proposals, targets=None): - """ - Add mult proposal scores at testing - """ - if (not self.training) and self.mult_proposal_score: - if len(proposals) > 0 and proposals[0].has("scores"): - proposal_scores = [p.get("scores") for p in proposals] - else: - proposal_scores = [p.get("objectness_logits") for p in proposals] - - features = [features[f] for f in self.box_in_features] - head_outputs = [] # (predictor, predictions, proposals) - prev_pred_boxes = None - image_sizes = [x.image_size for x in proposals] - for k in range(self.num_cascade_stages): - if k > 0: - proposals = self._create_proposals_from_boxes(prev_pred_boxes, image_sizes) - if self.training: - proposals = self._match_and_label_boxes(proposals, k, targets) - predictions = self._run_stage(features, proposals, k) - prev_pred_boxes = self.box_predictor[k].predict_boxes(predictions, proposals) - head_outputs.append((self.box_predictor[k], predictions, proposals)) - - if self.training: - losses = {} - storage = get_event_storage() - for stage, (predictor, predictions, proposals) in enumerate(head_outputs): - with storage.name_scope(f"stage{stage}"): - stage_losses = predictor.losses(predictions, proposals) - losses.update({k + f"_stage{stage}": v for k, v in stage_losses.items()}) - return losses - else: - # Each is a list[Tensor] of length #image. Each tensor is Ri x (K+1) - scores_per_stage = [h[0].predict_probs(h[1], h[2]) for h in head_outputs] - scores = [ - sum(list(scores_per_image)) * (1.0 / self.num_cascade_stages) - for scores_per_image in zip(*scores_per_stage, strict=False) - ] - - if self.mult_proposal_score: - scores = [(s * ps[:, None]) ** 0.5 for s, ps in zip(scores, proposal_scores, strict=False)] - - predictor, predictions, proposals = head_outputs[-1] - boxes = predictor.predict_boxes(predictions, proposals) - pred_instances, _ = fast_rcnn_inference( - boxes, - scores, - image_sizes, - predictor.test_score_thresh, - predictor.test_nms_thresh, - predictor.test_topk_per_image, - ) - - return pred_instances - - def forward(self, images, features, proposals, targets=None): - """ - enable debug - """ - if not self.debug: - del images - if self.training: - proposals = self.label_and_sample_proposals(proposals, targets) - - if self.training: - losses = self._forward_box(features, proposals, targets) - losses.update(self._forward_mask(features, proposals)) - losses.update(self._forward_keypoint(features, proposals)) - return proposals, losses - else: - # import pdb; pdb.set_trace() - pred_instances = self._forward_box(features, proposals) - pred_instances = self.forward_with_given_boxes(features, pred_instances) - if self.debug: - from ..debug import debug_second_stage - - def denormalizer(x): - return x * self.pixel_std + self.pixel_mean - debug_second_stage( - [denormalizer(x.clone()) for x in images], - pred_instances, - proposals=proposals, - save_debug=self.save_debug, - debug_show_name=self.debug_show_name, - vis_thresh=self.vis_thresh, - ) - return pred_instances, {} diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/fed_loss.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/fed_loss.py deleted file mode 100644 index 8a41607ea9..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/fed_loss.py +++ /dev/null @@ -1,25 +0,0 @@ -import json - -import torch - - -def load_class_freq(path: str="datasets/lvis/lvis_v1_train_cat_info.json", freq_weight: float=0.5): - cat_info = json.load(open(path)) - cat_info = torch.tensor([c["image_count"] for c in sorted(cat_info, key=lambda x: x["id"])]) - freq_weight = cat_info.float() ** freq_weight - return freq_weight - - -def get_fed_loss_inds(gt_classes, num_sample_cats: int=50, C: int=1203, weight=None, fed_cls_inds=-1): - appeared = torch.unique(gt_classes) # C' - prob = appeared.new_ones(C + 1).float() - prob[-1] = 0 - if len(appeared) < num_sample_cats: - if weight is not None: - prob[:C] = weight.float().clone() - prob[appeared] = 0 - if fed_cls_inds > 0: - prob[fed_cls_inds:] = 0 - more_appeared = torch.multinomial(prob, num_sample_cats - len(appeared), replacement=False) - appeared = torch.cat([appeared, more_appeared]) - return appeared diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/Base-CenterNet-FPN.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/Base-CenterNet-FPN.yaml deleted file mode 100644 index bef3dc10de..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/Base-CenterNet-FPN.yaml +++ /dev/null @@ -1,28 +0,0 @@ -MODEL: - META_ARCHITECTURE: "CenterNetDetector" - PROPOSAL_GENERATOR: - NAME: "CenterNet" - BACKBONE: - NAME: "build_p67_resnet_fpn_backbone" - WEIGHTS: "detectron2://ImageNetPretrained/MSRA/R-50.pkl" - RESNETS: - DEPTH: 50 - OUT_FEATURES: ["res3", "res4", "res5"] - FPN: - IN_FEATURES: ["res3", "res4", "res5"] -DATASETS: - TRAIN: ("coco_2017_train",) - TEST: ("coco_2017_val",) -SOLVER: - IMS_PER_BATCH: 16 - BASE_LR: 0.01 - STEPS: (60000, 80000) - MAX_ITER: 90000 - CHECKPOINT_PERIOD: 1000000000 - WARMUP_ITERS: 4000 - WARMUP_FACTOR: 0.00025 - CLIP_GRADIENTS: - ENABLED: True -INPUT: - MIN_SIZE_TRAIN: (640, 672, 704, 736, 768, 800) -OUTPUT_DIR: "./output/CenterNet2/auto" diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/Base-CenterNet2.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/Base-CenterNet2.yaml deleted file mode 100644 index 6893723101..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/Base-CenterNet2.yaml +++ /dev/null @@ -1,56 +0,0 @@ -MODEL: - META_ARCHITECTURE: "GeneralizedRCNN" - PROPOSAL_GENERATOR: - NAME: "CenterNet" - BACKBONE: - NAME: "build_p67_resnet_fpn_backbone" - WEIGHTS: "detectron2://ImageNetPretrained/MSRA/R-50.pkl" - RESNETS: - DEPTH: 50 - OUT_FEATURES: ["res3", "res4", "res5"] - FPN: - IN_FEATURES: ["res3", "res4", "res5"] - ROI_HEADS: - NAME: CustomCascadeROIHeads - IN_FEATURES: ["p3", "p4", "p5", "p6", "p7"] - IOU_THRESHOLDS: [0.6] - NMS_THRESH_TEST: 0.7 - ROI_BOX_CASCADE_HEAD: - IOUS: [0.6, 0.7, 0.8] - ROI_BOX_HEAD: - NAME: "FastRCNNConvFCHead" - NUM_FC: 2 - POOLER_RESOLUTION: 7 - CLS_AGNOSTIC_BBOX_REG: True - MULT_PROPOSAL_SCORE: True - CENTERNET: - REG_WEIGHT: 1. - NOT_NORM_REG: True - ONLY_PROPOSAL: True - WITH_AGN_HM: True - INFERENCE_TH: 0.0001 - PRE_NMS_TOPK_TRAIN: 4000 - POST_NMS_TOPK_TRAIN: 2000 - PRE_NMS_TOPK_TEST: 1000 - POST_NMS_TOPK_TEST: 256 - NMS_TH_TRAIN: 0.9 - NMS_TH_TEST: 0.9 - POS_WEIGHT: 0.5 - NEG_WEIGHT: 0.5 - IGNORE_HIGH_FP: 0.85 -DATASETS: - TRAIN: ("coco_2017_train",) - TEST: ("coco_2017_val",) -SOLVER: - IMS_PER_BATCH: 16 - BASE_LR: 0.02 - STEPS: (60000, 80000) - MAX_ITER: 90000 - CHECKPOINT_PERIOD: 1000000000 - WARMUP_ITERS: 4000 - WARMUP_FACTOR: 0.00025 - CLIP_GRADIENTS: - ENABLED: True -INPUT: - MIN_SIZE_TRAIN: (640, 672, 704, 736, 768, 800) -OUTPUT_DIR: "./output/CenterNet2/auto" diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/Base_S4_DLA.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/Base_S4_DLA.yaml deleted file mode 100644 index 7e01be7e55..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/Base_S4_DLA.yaml +++ /dev/null @@ -1,40 +0,0 @@ -MODEL: - META_ARCHITECTURE: "CenterNetDetector" - PROPOSAL_GENERATOR: - NAME: "CenterNet" - PIXEL_STD: [57.375, 57.120, 58.395] - BACKBONE: - NAME: "build_dla_backbone" - DLA: - NORM: "BN" - CENTERNET: - IN_FEATURES: ["dla2"] - FPN_STRIDES: [4] - SOI: [[0, 1000000]] - NUM_CLS_CONVS: 1 - NUM_BOX_CONVS: 1 - REG_WEIGHT: 1. - MORE_POS: True - HM_FOCAL_ALPHA: 0.25 -DATASETS: - TRAIN: ("coco_2017_train",) - TEST: ("coco_2017_val",) -SOLVER: - LR_SCHEDULER_NAME: "WarmupCosineLR" - MAX_ITER: 90000 - BASE_LR: 0.04 - IMS_PER_BATCH: 64 - WEIGHT_DECAY: 0.0001 - CHECKPOINT_PERIOD: 1000000 - CLIP_GRADIENTS: - ENABLED: True -INPUT: - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 640 - MIN_SIZE_TEST: 608 - MAX_SIZE_TEST: 900 -TEST: - EVAL_PERIOD: 7500 -DATALOADER: - NUM_WORKERS: 8 -OUTPUT_DIR: "output/CenterNet2/auto" diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet-FPN_R50_1x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet-FPN_R50_1x.yaml deleted file mode 100644 index 6ea7d9b703..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet-FPN_R50_1x.yaml +++ /dev/null @@ -1,4 +0,0 @@ -_BASE_: "Base-CenterNet-FPN.yaml" -MODEL: - CENTERNET: - MORE_POS: True \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet-S4_DLA_8x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet-S4_DLA_8x.yaml deleted file mode 100644 index b3d88be9f5..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet-S4_DLA_8x.yaml +++ /dev/null @@ -1,5 +0,0 @@ -_BASE_: "Base_S4_DLA.yaml" -SOLVER: - MAX_ITER: 90000 - BASE_LR: 0.08 - IMS_PER_BATCH: 128 \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2-F_R50_1x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2-F_R50_1x.yaml deleted file mode 100644 index c40eecc13a..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2-F_R50_1x.yaml +++ /dev/null @@ -1,4 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - ROI_HEADS: - NAME: CustomROIHeads \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P3_24x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P3_24x.yaml deleted file mode 100644 index d7491447eb..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P3_24x.yaml +++ /dev/null @@ -1,36 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - BACKBONE: - NAME: "build_p35_fcos_dla_bifpn_backbone" - BIFPN: - OUT_CHANNELS: 160 - NUM_LEVELS: 3 - NUM_BIFPN: 4 - DLA: - NUM_LAYERS: 34 - NORM: "SyncBN" - FPN: - IN_FEATURES: ["dla3", "dla4", "dla5"] - ROI_HEADS: - IN_FEATURES: ["p3", "p4", "p5"] - CENTERNET: - POST_NMS_TOPK_TEST: 128 - FPN_STRIDES: [8, 16, 32] - IN_FEATURES: ['p3', 'p4', 'p5'] - SOI: [[0, 64], [48, 192], [128, 1000000]] -DATASETS: - TRAIN: ("coco_2017_train",) - TEST: ("coco_2017_val",) -SOLVER: - IMS_PER_BATCH: 16 - BASE_LR: 0.02 - STEPS: (300000, 340000) - MAX_ITER: 360000 - CHECKPOINT_PERIOD: 100000 - WARMUP_ITERS: 4000 - WARMUP_FACTOR: 0.00025 -INPUT: - MIN_SIZE_TRAIN: (256, 288, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608) - MAX_SIZE_TRAIN: 900 - MAX_SIZE_TEST: 736 - MIN_SIZE_TEST: 512 \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P3_4x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P3_4x.yaml deleted file mode 100644 index d7491447eb..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P3_4x.yaml +++ /dev/null @@ -1,36 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - BACKBONE: - NAME: "build_p35_fcos_dla_bifpn_backbone" - BIFPN: - OUT_CHANNELS: 160 - NUM_LEVELS: 3 - NUM_BIFPN: 4 - DLA: - NUM_LAYERS: 34 - NORM: "SyncBN" - FPN: - IN_FEATURES: ["dla3", "dla4", "dla5"] - ROI_HEADS: - IN_FEATURES: ["p3", "p4", "p5"] - CENTERNET: - POST_NMS_TOPK_TEST: 128 - FPN_STRIDES: [8, 16, 32] - IN_FEATURES: ['p3', 'p4', 'p5'] - SOI: [[0, 64], [48, 192], [128, 1000000]] -DATASETS: - TRAIN: ("coco_2017_train",) - TEST: ("coco_2017_val",) -SOLVER: - IMS_PER_BATCH: 16 - BASE_LR: 0.02 - STEPS: (300000, 340000) - MAX_ITER: 360000 - CHECKPOINT_PERIOD: 100000 - WARMUP_ITERS: 4000 - WARMUP_FACTOR: 0.00025 -INPUT: - MIN_SIZE_TRAIN: (256, 288, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608) - MAX_SIZE_TRAIN: 900 - MAX_SIZE_TEST: 736 - MIN_SIZE_TEST: 512 \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P5_640_16x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P5_640_16x.yaml deleted file mode 100644 index 80413a62d6..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P5_640_16x.yaml +++ /dev/null @@ -1,29 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - BACKBONE: - NAME: "build_p37_dla_bifpn_backbone" - BIFPN: - OUT_CHANNELS: 160 - NUM_LEVELS: 5 - NUM_BIFPN: 3 - CENTERNET: - POST_NMS_TOPK_TEST: 128 - WEIGHTS: '' - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.12, 57.375] - FPN: - IN_FEATURES: ["dla3", "dla4", "dla5"] -SOLVER: - LR_SCHEDULER_NAME: "WarmupCosineLR" - MAX_ITER: 360000 - BASE_LR: 0.08 - IMS_PER_BATCH: 64 - CHECKPOINT_PERIOD: 90000 -TEST: - EVAL_PERIOD: 7500 -INPUT: - FORMAT: RGB - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 640 - MIN_SIZE_TEST: 608 - MAX_SIZE_TEST: 900 diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P5_640_16x_ST.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P5_640_16x_ST.yaml deleted file mode 100644 index 8813b39c1c..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-BiFPN-P5_640_16x_ST.yaml +++ /dev/null @@ -1,30 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - BACKBONE: - NAME: "build_p37_dla_bifpn_backbone" - BIFPN: - OUT_CHANNELS: 160 - NUM_LEVELS: 5 - NUM_BIFPN: 3 - CENTERNET: - POST_NMS_TOPK_TEST: 128 - WEIGHTS: '' - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.12, 57.375] - FPN: - IN_FEATURES: ["dla3", "dla4", "dla5"] -SOLVER: - LR_SCHEDULER_NAME: "WarmupCosineLR" - MAX_ITER: 360000 - BASE_LR: 0.08 - IMS_PER_BATCH: 64 -TEST: - EVAL_PERIOD: 7500 -INPUT: - FORMAT: RGB - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 640 - MIN_SIZE_TEST: 608 - MAX_SIZE_TEST: 900 -DATASETS: - TRAIN: ("coco_2017_train","coco_un_yolov4_55_0.5",) diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-fcosBiFPN-P5_640_16x_ST.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-fcosBiFPN-P5_640_16x_ST.yaml deleted file mode 100644 index f94f1358ce..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_DLA-fcosBiFPN-P5_640_16x_ST.yaml +++ /dev/null @@ -1,30 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - BACKBONE: - NAME: "build_p37_fcos_dla_bifpn_backbone" - BIFPN: - OUT_CHANNELS: 160 - NUM_LEVELS: 5 - NUM_BIFPN: 3 - CENTERNET: - POST_NMS_TOPK_TEST: 128 - WEIGHTS: '' - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.12, 57.375] - FPN: - IN_FEATURES: ["dla3", "dla4", "dla5"] -TEST: - EVAL_PERIOD: 7500 -SOLVER: - LR_SCHEDULER_NAME: "WarmupCosineLR" - MAX_ITER: 360000 - BASE_LR: 0.08 - IMS_PER_BATCH: 64 -INPUT: - FORMAT: RGB - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 640 - MIN_SIZE_TEST: 608 - MAX_SIZE_TEST: 900 -DATASETS: - TRAIN: ("coco_2017_train","coco_un_yolov4_55_0.5",) diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN-BiFPN_1280_4x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN-BiFPN_1280_4x.yaml deleted file mode 100644 index e07574b351..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN-BiFPN_1280_4x.yaml +++ /dev/null @@ -1,32 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - BACKBONE: - NAME: "build_res2net_bifpn_backbone" - BIFPN: - NUM_BIFPN: 7 - OUT_CHANNELS: 288 - WEIGHTS: "output/r2_101.pkl" - RESNETS: - DEPTH: 101 - WIDTH_PER_GROUP: 26 - DEFORM_ON_PER_STAGE: [False, False, True, True] # on Res4, Res5 - DEFORM_MODULATED: True - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.12, 57.375] - CENTERNET: - USE_DEFORMABLE: True - ROI_HEADS: - IN_FEATURES: ["p3", "p4"] -INPUT: - FORMAT: RGB -TEST: - EVAL_PERIOD: 7500 -SOLVER: - MAX_ITER: 180000 - CHECKPOINT_PERIOD: 60000 - LR_SCHEDULER_NAME: "WarmupCosineLR" - BASE_LR: 0.04 - IMS_PER_BATCH: 32 -INPUT: - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 1280 diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN-BiFPN_4x+4x_1560_ST.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN-BiFPN_4x+4x_1560_ST.yaml deleted file mode 100644 index 81fcab0972..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN-BiFPN_4x+4x_1560_ST.yaml +++ /dev/null @@ -1,36 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - BACKBONE: - NAME: "build_res2net_bifpn_backbone" - BIFPN: - NUM_BIFPN: 7 - OUT_CHANNELS: 288 - WEIGHTS: "output/r2_101.pkl" - RESNETS: - DEPTH: 101 - WIDTH_PER_GROUP: 26 - DEFORM_ON_PER_STAGE: [False, False, True, True] # on Res4, Res5 - DEFORM_MODULATED: True - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.12, 57.375] - CENTERNET: - USE_DEFORMABLE: True - ROI_HEADS: - IN_FEATURES: ["p3", "p4"] -TEST: - EVAL_PERIOD: 7500 -SOLVER: - MAX_ITER: 180000 - CHECKPOINT_PERIOD: 7500 - LR_SCHEDULER_NAME: "WarmupCosineLR" - BASE_LR: 0.04 - IMS_PER_BATCH: 32 -DATASETS: - TRAIN: "('coco_2017_train', 'coco_un_yolov4_55_0.5')" -INPUT: - FORMAT: RGB - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 1280 - TEST_SIZE: 1560 - TEST_INPUT_TYPE: 'square' - \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN_896_4x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN_896_4x.yaml deleted file mode 100644 index fd6c49ee40..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R2-101-DCN_896_4x.yaml +++ /dev/null @@ -1,29 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - BACKBONE: - NAME: "build_p67_res2net_fpn_backbone" - WEIGHTS: "output/r2_101.pkl" - RESNETS: - DEPTH: 101 - WIDTH_PER_GROUP: 26 - DEFORM_ON_PER_STAGE: [False, False, True, True] # on Res4, Res5 - DEFORM_MODULATED: True - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.12, 57.375] - CENTERNET: - USE_DEFORMABLE: True - ROI_HEADS: - IN_FEATURES: ["p3", "p4"] -INPUT: - FORMAT: RGB -TEST: - EVAL_PERIOD: 7500 -SOLVER: - MAX_ITER: 180000 - CHECKPOINT_PERIOD: 600000 - LR_SCHEDULER_NAME: "WarmupCosineLR" - BASE_LR: 0.04 - IMS_PER_BATCH: 32 -INPUT: - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 896 \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R50_1x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R50_1x.yaml deleted file mode 100644 index 9dcdf5b8b6..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_R50_1x.yaml +++ /dev/null @@ -1 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_X101-DCN_2x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_X101-DCN_2x.yaml deleted file mode 100644 index 009c68085b..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/CenterNet2_X101-DCN_2x.yaml +++ /dev/null @@ -1,22 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - CENTERNET: - USE_DEFORMABLE: True - WEIGHTS: "detectron2://ImageNetPretrained/FAIR/X-101-32x8d.pkl" - PIXEL_STD: [57.375, 57.120, 58.395] - RESNETS: - STRIDE_IN_1X1: False - NUM_GROUPS: 32 - WIDTH_PER_GROUP: 8 - DEPTH: 101 - DEFORM_ON_PER_STAGE: [False, False, True, True] # on Res4, Res5 - DEFORM_MODULATED: True - ROI_HEADS: - IN_FEATURES: ["p3", "p4"] -SOLVER: - STEPS: (120000, 160000) - MAX_ITER: 180000 - CHECKPOINT_PERIOD: 40000 -INPUT: - MIN_SIZE_TRAIN: (480, 960) - MIN_SIZE_TRAIN_SAMPLING: "range" diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/LVIS_CenterNet2_R50_1x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/LVIS_CenterNet2_R50_1x.yaml deleted file mode 100644 index 912e8925dc..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/LVIS_CenterNet2_R50_1x.yaml +++ /dev/null @@ -1,17 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - ROI_HEADS: - NUM_CLASSES: 1203 - SCORE_THRESH_TEST: 0.02 - NMS_THRESH_TEST: 0.5 - CENTERNET: - NUM_CLASSES: 1203 - -DATASETS: - TRAIN: ("lvis_v1_train",) - TEST: ("lvis_v1_val",) -DATALOADER: - SAMPLER_TRAIN: "RepeatFactorTrainingSampler" - REPEAT_THRESHOLD: 0.001 -TEST: - DETECTIONS_PER_IMAGE: 300 diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/LVIS_CenterNet2_R50_Fed_1x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/LVIS_CenterNet2_R50_Fed_1x.yaml deleted file mode 100644 index d6b6c823f2..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/LVIS_CenterNet2_R50_Fed_1x.yaml +++ /dev/null @@ -1,19 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - ROI_HEADS: - NUM_CLASSES: 1203 - SCORE_THRESH_TEST: 0.02 - NMS_THRESH_TEST: 0.5 - CENTERNET: - NUM_CLASSES: 1203 - ROI_BOX_HEAD: - USE_SIGMOID_CE: True - USE_FED_LOSS: True -DATASETS: - TRAIN: ("lvis_v1_train",) - TEST: ("lvis_v1_val",) -DATALOADER: - SAMPLER_TRAIN: "RepeatFactorTrainingSampler" - REPEAT_THRESHOLD: 0.001 -TEST: - DETECTIONS_PER_IMAGE: 300 diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/O365_CenterNet2_R50_1x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/O365_CenterNet2_R50_1x.yaml deleted file mode 100644 index 514e52cddc..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/O365_CenterNet2_R50_1x.yaml +++ /dev/null @@ -1,13 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - ROI_HEADS: - NUM_CLASSES: 365 - CENTERNET: - NUM_CLASSES: 365 -DATASETS: - TRAIN: ("objects365_train",) - TEST: ("objects365_val",) -DATALOADER: - SAMPLER_TRAIN: "ClassAwareSampler" -TEST: - DETECTIONS_PER_IMAGE: 300 \ No newline at end of file diff --git a/dimos/models/Detic/third_party/CenterNet2/configs/nuImages_CenterNet2_DLA_640_8x.yaml b/dimos/models/Detic/third_party/CenterNet2/configs/nuImages_CenterNet2_DLA_640_8x.yaml deleted file mode 100644 index c400e92ce7..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/configs/nuImages_CenterNet2_DLA_640_8x.yaml +++ /dev/null @@ -1,42 +0,0 @@ -_BASE_: "Base-CenterNet2.yaml" -MODEL: - MASK_ON: True - ROI_MASK_HEAD: - NAME: "MaskRCNNConvUpsampleHead" - NUM_CONV: 4 - POOLER_RESOLUTION: 14 - ROI_HEADS: - NUM_CLASSES: 10 - IN_FEATURES: ["dla2"] - BACKBONE: - NAME: "build_dla_backbone" - DLA: - NORM: "BN" - CENTERNET: - IN_FEATURES: ["dla2"] - FPN_STRIDES: [4] - SOI: [[0, 1000000]] - NUM_CLS_CONVS: 1 - NUM_BOX_CONVS: 1 - REG_WEIGHT: 1. - MORE_POS: True - HM_FOCAL_ALPHA: 0.25 - POST_NMS_TOPK_TEST: 128 - WEIGHTS: '' - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.12, 57.375] -SOLVER: - MAX_ITER: 180000 - STEPS: (120000, 160000) - BASE_LR: 0.08 - IMS_PER_BATCH: 64 -INPUT: - FORMAT: RGB - CUSTOM_AUG: EfficientDetResizeCrop - TRAIN_SIZE: 640 - MIN_SIZE_TEST: 608 - MAX_SIZE_TEST: 900 - MASK_FORMAT: bitmask -DATASETS: - TRAIN: ("nuimages_train",) - TEST: ("nuimages_val",) diff --git a/dimos/models/Detic/third_party/CenterNet2/datasets/README.md b/dimos/models/Detic/third_party/CenterNet2/datasets/README.md deleted file mode 100644 index 0eb44cc3b2..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/datasets/README.md +++ /dev/null @@ -1,140 +0,0 @@ -# Use Builtin Datasets - -A dataset can be used by accessing [DatasetCatalog](https://detectron2.readthedocs.io/modules/data.html#detectron2.data.DatasetCatalog) -for its data, or [MetadataCatalog](https://detectron2.readthedocs.io/modules/data.html#detectron2.data.MetadataCatalog) for its metadata (class names, etc). -This document explains how to setup the builtin datasets so they can be used by the above APIs. -[Use Custom Datasets](https://detectron2.readthedocs.io/tutorials/datasets.html) gives a deeper dive on how to use `DatasetCatalog` and `MetadataCatalog`, -and how to add new datasets to them. - -Detectron2 has builtin support for a few datasets. -The datasets are assumed to exist in a directory specified by the environment variable -`DETECTRON2_DATASETS`. -Under this directory, detectron2 will look for datasets in the structure described below, if needed. -``` -$DETECTRON2_DATASETS/ - coco/ - lvis/ - cityscapes/ - VOC20{07,12}/ -``` - -You can set the location for builtin datasets by `export DETECTRON2_DATASETS=/path/to/datasets`. -If left unset, the default is `./datasets` relative to your current working directory. - -The [model zoo](https://github.com/facebookresearch/detectron2/blob/master/MODEL_ZOO.md) -contains configs and models that use these builtin datasets. - -## Expected dataset structure for [COCO instance/keypoint detection](https://cocodataset.org/#download): - -``` -coco/ - annotations/ - instances_{train,val}2017.json - person_keypoints_{train,val}2017.json - {train,val}2017/ - # image files that are mentioned in the corresponding json -``` - -You can use the 2014 version of the dataset as well. - -Some of the builtin tests (`dev/run_*_tests.sh`) uses a tiny version of the COCO dataset, -which you can download with `./datasets/prepare_for_tests.sh`. - -## Expected dataset structure for PanopticFPN: - -Extract panoptic annotations from [COCO website](https://cocodataset.org/#download) -into the following structure: -``` -coco/ - annotations/ - panoptic_{train,val}2017.json - panoptic_{train,val}2017/ # png annotations - panoptic_stuff_{train,val}2017/ # generated by the script mentioned below -``` - -Install panopticapi by: -``` -pip install git+https://github.com/cocodataset/panopticapi.git -``` -Then, run `python datasets/prepare_panoptic_fpn.py`, to extract semantic annotations from panoptic annotations. - -## Expected dataset structure for [LVIS instance segmentation](https://www.lvisdataset.org/dataset): -``` -coco/ - {train,val,test}2017/ -lvis/ - lvis_v0.5_{train,val}.json - lvis_v0.5_image_info_test.json - lvis_v1_{train,val}.json - lvis_v1_image_info_test{,_challenge}.json -``` - -Install lvis-api by: -``` -pip install git+https://github.com/lvis-dataset/lvis-api.git -``` - -To evaluate models trained on the COCO dataset using LVIS annotations, -run `python datasets/prepare_cocofied_lvis.py` to prepare "cocofied" LVIS annotations. - -## Expected dataset structure for [cityscapes](https://www.cityscapes-dataset.com/downloads/): -``` -cityscapes/ - gtFine/ - train/ - aachen/ - color.png, instanceIds.png, labelIds.png, polygons.json, - labelTrainIds.png - ... - val/ - test/ - # below are generated Cityscapes panoptic annotation - cityscapes_panoptic_train.json - cityscapes_panoptic_train/ - cityscapes_panoptic_val.json - cityscapes_panoptic_val/ - cityscapes_panoptic_test.json - cityscapes_panoptic_test/ - leftImg8bit/ - train/ - val/ - test/ -``` -Install cityscapes scripts by: -``` -pip install git+https://github.com/mcordts/cityscapesScripts.git -``` - -Note: to create labelTrainIds.png, first prepare the above structure, then run cityscapesescript with: -``` -CITYSCAPES_DATASET=/path/to/abovementioned/cityscapes python cityscapesscripts/preparation/createTrainIdLabelImgs.py -``` -These files are not needed for instance segmentation. - -Note: to generate Cityscapes panoptic dataset, run cityscapesescript with: -``` -CITYSCAPES_DATASET=/path/to/abovementioned/cityscapes python cityscapesscripts/preparation/createPanopticImgs.py -``` -These files are not needed for semantic and instance segmentation. - -## Expected dataset structure for [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/index.html): -``` -VOC20{07,12}/ - Annotations/ - ImageSets/ - Main/ - trainval.txt - test.txt - # train.txt or val.txt, if you use these splits - JPEGImages/ -``` - -## Expected dataset structure for [ADE20k Scene Parsing](http://sceneparsing.csail.mit.edu/): -``` -ADEChallengeData2016/ - annotations/ - annotations_detectron2/ - images/ - objectInfo150.txt -``` -The directory `annotations_detectron2` is generated by running `python datasets/prepare_ade20k_sem_seg.py`. diff --git a/dimos/models/Detic/third_party/CenterNet2/demo.py b/dimos/models/Detic/third_party/CenterNet2/demo.py deleted file mode 100644 index 3177d838ac..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/demo.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -import argparse -import glob -import multiprocessing as mp -import os -import time - -from centernet.config import add_centernet_config -import cv2 -from detectron2.config import get_cfg -from detectron2.data.detection_utils import read_image -from detectron2.utils.logger import setup_logger -from predictor import VisualizationDemo -import tqdm - -# constants -WINDOW_NAME = "CenterNet2 detections" - -from detectron2.data import MetadataCatalog -from detectron2.utils.video_visualizer import VideoVisualizer -from detectron2.utils.visualizer import ColorMode - - -def setup_cfg(args): - # load config from file and command-line arguments - cfg = get_cfg() - add_centernet_config(cfg) - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - # Set score_threshold for builtin models - cfg.MODEL.RETINANET.SCORE_THRESH_TEST = args.confidence_threshold - cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = args.confidence_threshold - if cfg.MODEL.META_ARCHITECTURE in ["ProposalNetwork", "CenterNetDetector"]: - cfg.MODEL.CENTERNET.INFERENCE_TH = args.confidence_threshold - cfg.MODEL.CENTERNET.NMS_TH = cfg.MODEL.ROI_HEADS.NMS_THRESH_TEST - cfg.MODEL.PANOPTIC_FPN.COMBINE.INSTANCES_CONFIDENCE_THRESH = args.confidence_threshold - cfg.freeze() - return cfg - - -def get_parser(): - parser = argparse.ArgumentParser(description="Detectron2 demo for builtin models") - parser.add_argument( - "--config-file", - default="configs/quick_schedules/mask_rcnn_R_50_FPN_inference_acc_test.yaml", - metavar="FILE", - help="path to config file", - ) - parser.add_argument("--webcam", action="store_true", help="Take inputs from webcam.") - parser.add_argument("--video-input", help="Path to video file.") - parser.add_argument("--input", nargs="+", help="A list of space separated input images") - parser.add_argument( - "--output", - help="A file or directory to save output visualizations. If not given, will show output in an OpenCV window.", - ) - - parser.add_argument( - "--confidence-threshold", - type=float, - default=0.3, - help="Minimum score for instance predictions to be shown", - ) - parser.add_argument( - "--opts", - help="Modify config options using the command-line 'KEY VALUE' pairs", - default=[], - nargs=argparse.REMAINDER, - ) - return parser - - -if __name__ == "__main__": - mp.set_start_method("spawn", force=True) - args = get_parser().parse_args() - logger = setup_logger() - logger.info("Arguments: " + str(args)) - - cfg = setup_cfg(args) - - demo = VisualizationDemo(cfg) - output_file = None - if args.input: - if len(args.input) == 1: - args.input = glob.glob(os.path.expanduser(args.input[0])) - files = os.listdir(args.input[0]) - args.input = [args.input[0] + x for x in files] - assert args.input, "The input path(s) was not found" - visualizer = VideoVisualizer( - MetadataCatalog.get(cfg.DATASETS.TEST[0] if len(cfg.DATASETS.TEST) else "__unused"), - instance_mode=ColorMode.IMAGE, - ) - for path in tqdm.tqdm(args.input, disable=not args.output): - # use PIL, to be consistent with evaluation - img = read_image(path, format="BGR") - start_time = time.time() - predictions, visualized_output = demo.run_on_image(img, visualizer=visualizer) - if "instances" in predictions: - logger.info( - "{}: detected {} instances in {:.2f}s".format( - path, len(predictions["instances"]), time.time() - start_time - ) - ) - else: - logger.info( - "{}: detected {} instances in {:.2f}s".format( - path, len(predictions["proposals"]), time.time() - start_time - ) - ) - - if args.output: - if os.path.isdir(args.output): - assert os.path.isdir(args.output), args.output - out_filename = os.path.join(args.output, os.path.basename(path)) - visualized_output.save(out_filename) - else: - # assert len(args.input) == 1, "Please specify a directory with args.output" - # out_filename = args.output - if output_file is None: - width = visualized_output.get_image().shape[1] - height = visualized_output.get_image().shape[0] - frames_per_second = 15 - output_file = cv2.VideoWriter( - filename=args.output, - # some installation of opencv may not support x264 (due to its license), - # you can try other format (e.g. MPEG) - fourcc=cv2.VideoWriter_fourcc(*"x264"), - fps=float(frames_per_second), - frameSize=(width, height), - isColor=True, - ) - output_file.write(visualized_output.get_image()[:, :, ::-1]) - else: - # cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) - cv2.imshow(WINDOW_NAME, visualized_output.get_image()[:, :, ::-1]) - if cv2.waitKey(1) == 27: - break # esc to quit - elif args.webcam: - assert args.input is None, "Cannot have both --input and --webcam!" - cam = cv2.VideoCapture(0) - for vis in tqdm.tqdm(demo.run_on_video(cam)): - cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) - cv2.imshow(WINDOW_NAME, vis) - if cv2.waitKey(1) == 27: - break # esc to quit - cv2.destroyAllWindows() - elif args.video_input: - video = cv2.VideoCapture(args.video_input) - width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)) - frames_per_second = 15 # video.get(cv2.CAP_PROP_FPS) - num_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - basename = os.path.basename(args.video_input) - - if args.output: - if os.path.isdir(args.output): - output_fname = os.path.join(args.output, basename) - output_fname = os.path.splitext(output_fname)[0] + ".mkv" - else: - output_fname = args.output - # assert not os.path.isfile(output_fname), output_fname - output_file = cv2.VideoWriter( - filename=output_fname, - # some installation of opencv may not support x264 (due to its license), - # you can try other format (e.g. MPEG) - fourcc=cv2.VideoWriter_fourcc(*"x264"), - fps=float(frames_per_second), - frameSize=(width, height), - isColor=True, - ) - assert os.path.isfile(args.video_input) - for vis_frame in tqdm.tqdm(demo.run_on_video(video), total=num_frames): - if args.output: - output_file.write(vis_frame) - - cv2.namedWindow(basename, cv2.WINDOW_NORMAL) - cv2.imshow(basename, vis_frame) - if cv2.waitKey(1) == 27: - break # esc to quit - video.release() - if args.output: - output_file.release() - else: - cv2.destroyAllWindows() diff --git a/dimos/models/Detic/third_party/CenterNet2/docs/MODEL_ZOO.md b/dimos/models/Detic/third_party/CenterNet2/docs/MODEL_ZOO.md deleted file mode 100644 index 97063b95c8..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/docs/MODEL_ZOO.md +++ /dev/null @@ -1,73 +0,0 @@ -# MODEL_ZOO - -### Common settings and notes - -- Multiscale training is used by default in all models. The results are all reported using single-scale testing. -- We report runtime on our local workstation with a TitanXp GPU and a Titan RTX GPU. -- All models are trained on 8-GPU servers by default. The 1280 models are trained on 24G GPUs. Reducing the batchsize with the linear learning rate rule should be fine. -- All models can be downloaded directly from [Google drive](https://drive.google.com/drive/folders/1meZIsz8E3Ia9CRxLOAULDLeYrKMhhjJE). - - -## COCO - -### CenterNet - -| Model | val mAP | FPS (Titan Xp/ Titan RTX) | links | -|-------------------------------------------|---------|---------|-----------| -| CenterNet-S4_DLA_8x | 42.5 | 50 / 71 |[config](../configs/CenterNet-S4_DLA_8x.yaml)/[model](https://drive.google.com/file/d/1AVfs9OoLePk_sqTPvqdRi1cXmO2cD0W_)| -| CenterNet-FPN_R50_1x | 40.2 | 20 / 24 |[config](../configs/CenterNet-FPN_R50_1x.yaml)/[model](https://drive.google.com/file/d/1iYlmjsBt9YIcaI8NzEwiMoaDDMHRmcR9)| - -#### Note - -- `CenterNet-S4_DLA_8x` is a re-implemented version of the original CenterNet (stride 4), with several changes, including - - Using top-left-right-bottom box encoding and GIoU Loss; adding regression loss to the center 3x3 region. - - Adding more positive pixels for the heatmap loss whose regression loss is small and is within the center3x3 region. - - Using more heavy crop augmentation (EfficientDet-style crop ratio 0.1-2), and removing color augmentations. - - Using standard NMS instead of max pooling. - - Using RetinaNet-style optimizer (SGD), learning rate rule (0.01 for each batch size 16), and schedule (8x12 epochs). -- `CenterNet-FPN_R50_1x` is a (new) FPN version of CenterNet. It includes the changes above, and assigns objects to FPN levels based on a fixed size range. The model is trained with standard short edge 640-800 multi-scale training with 12 epochs (1x). - - -### CenterNet2 - -| Model | val mAP | FPS (Titan Xp/ Titan RTX) | links | -|-------------------------------------------|---------|---------|-----------| -| CenterNet2-F_R50_1x | 41.7 | 22 / 27 |[config](../configs/CenterNet2-F_R50_1x.yaml)/[model](X)| -| CenterNet2_R50_1x | 42.9 | 18 / 24 |[config](../configs/CenterNet2_R50_1x.yaml)/[model](https://drive.google.com/file/d/1Qn0E_F1cmXtKPEdyZ_lSt-bnM9NueQpq)| -| CenterNet2_X101-DCN_2x | 49.9 | 6 / 8 |[config](../configs/CenterNet2_X101-DCN_2x.yaml)/[model](https://drive.google.com/file/d/1yuJbIlUgMiXdaDWRWArcsRsSoHti9e1y)| -| CenterNet2_DLA-BiFPN-P3_4x | 43.8 | 40 / 50|[config](../configs/CenterNet2_DLA-BiFPN-P3_4x.yaml)/[model](https://drive.google.com/file/d/1UGrnOE0W8Tgu6ffcCOQEbeUgThtDkbuQ)| -| CenterNet2_DLA-BiFPN-P3_24x | 45.6 | 40 / 50 |[config](../configs/CenterNet2_DLA-BiFPN-P3_24x.yaml)/[model](https://drive.google.com/file/d/17osgvr_Zhp9SS2uMa_YLiKwkKJIDtwPZ)| -| CenterNet2_R2-101-DCN_896_4x | 51.2 | 9 / 13 |[config](../configs/CenterNet2_R2-101-DCN_896_4x.yaml)/[model](https://drive.google.com/file/d/1YiJm7UtMstl63E8I4qQ8owteYC5zRFuQ)| -| CenterNet2_R2-101-DCN-BiFPN_1280_4x | 52.9 | 6 / 8 |[config](../configs/CenterNet2_R2-101-DCN-BiFPN_1280_4x.yaml)/[model](https://drive.google.com/file/d/1BIfEH04Lm3EvW9ov76yEPntUOJxaVoKd)| -| CenterNet2_R2-101-DCN-BiFPN_4x+4x_1560_ST | 56.1 | 3 / 5 |[config](../configs/CenterNet2_R2-101-DCN-BiFPN_4x+4x_1560_ST.yaml)/[model](https://drive.google.com/file/d/1GZyzJLB3FTcs8C7MpZRQWw44liYPyOMD)| -| CenterNet2_DLA-BiFPN-P5_640_24x_ST | 49.2 | 33 / 38 |[config](../configs/CenterNet2_DLA-BiFPN-P5_640_24x_ST.yaml)/[model](https://drive.google.com/file/d/1pGXpnHhvi66my_p5dASTnTjvaaj0FEvE)| - -#### Note - -- `CenterNet2-F_R50_1x` uses Faster RCNN as the second stage. All other CenterNet2 models use Cascade RCNN as the second stage. -- `CenterNet2_DLA-BiFPN-P3_4x` follows the same training setting as [realtime-FCOS](https://github.com/aim-uofa/AdelaiDet/blob/master/configs/FCOS-Detection/README.md). -- `CenterNet2_DLA-BiFPN-P3_24x` is trained by repeating the `4x` schedule (starting from learning rate 0.01) 6 times. -- R2 means [Res2Net](https://github.com/Res2Net/Res2Net-detectron2) backbone. To train Res2Net models, you need to download the ImageNet pre-trained weight [here](https://github.com/Res2Net/Res2Net-detectron2) and place it in `output/r2_101.pkl`. -- The last 4 models in the table are trained with the EfficientDet-style resize-and-crop augmentation, instead of the default random resizing short edge in detectron2. We found this trains faster (per-iteration) and gives better performance under a long schedule. -- `_ST` means using [self-training](https://arxiv.org/abs/2006.06882) using pseudo-labels produced by [Scaled-YOLOv4](https://github.com/WongKinYiu/ScaledYOLOv4) on COCO unlabeled images, with a hard score threshold 0.5. Our processed pseudo-labels can be downloaded [here](https://drive.google.com/file/d/1R9tHlUaIrujmK6T08yJ0T77b2XzekisC). -- `CenterNet2_R2-101-DCN-BiFPN_4x+4x_1560_ST` finetunes from `CenterNet2_R2-101-DCN-BiFPN_1280_4x` for an additional `4x` schedule with the self-training data. It is trained under `1280x1280` but tested under `1560x1560`. - -## LVIS v1 - -| Model | val mAP box | links | -|-------------------------------------------|--------------|-----------| -| LVIS_CenterNet2_R50_1x | 26.5 |[config](../configs/LVIS_CenterNet2_R50_1x.yaml)/[model](https://drive.google.com/file/d/1oOOKEDQIWW19AHhfnTb7HYZ3Z9gkZn_K)| -| LVIS_CenterNet2_R50_Fed_1x | 28.3 |[config](../configs/LVIS_CenterNet2_R50_Fed_1x.yaml)/[model](https://drive.google.com/file/d/1ETurGA7KIC5XMkMBI8MOIMDD_iJyMTif)| - -- The models are trained with repeat-factor sampling. -- `LVIS_CenterNet2_R50_Fed_1x` is CenterNet2 with our federated loss. Check our Appendix D of our [paper](https://arxiv.org/abs/2103.07461) or our [technical report at LVIS challenge](https://www.lvisdataset.org/assets/challenge_reports/2020/CenterNet2.pdf) for references. - -## Objects365 - -| Model | val mAP| links | -|-------------------------------------------|---------|-----------| -| O365_CenterNet2_R50_1x | 22.6 |[config](../configs/O365_CenterNet2_R50_1x.yaml)/[model](https://drive.google.com/file/d/11d1Qx75otBAQQL2raxMTVJb17Qr56M3O)| - -#### Note -- Objects365 dataset can be downloaded [here](https://www.objects365.org/overview.html). -- The model is trained with class-aware sampling. diff --git a/dimos/models/Detic/third_party/CenterNet2/predictor.py b/dimos/models/Detic/third_party/CenterNet2/predictor.py deleted file mode 100644 index 0bdee56264..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/predictor.py +++ /dev/null @@ -1,241 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -import atexit -import bisect -from collections import deque -import multiprocessing as mp - -import cv2 -from detectron2.data import MetadataCatalog -from detectron2.engine.defaults import DefaultPredictor -from detectron2.utils.video_visualizer import VideoVisualizer -from detectron2.utils.visualizer import ColorMode, Visualizer -import torch - - -class VisualizationDemo: - def __init__(self, cfg, instance_mode=ColorMode.IMAGE, parallel: bool=False) -> None: - """ - Args: - cfg (CfgNode): - instance_mode (ColorMode): - parallel (bool): whether to run the model in different processes from visualization. - Useful since the visualization logic can be slow. - """ - self.metadata = MetadataCatalog.get( - cfg.DATASETS.TRAIN[0] if len(cfg.DATASETS.TRAIN) else "__unused" - ) - self.cpu_device = torch.device("cpu") - self.instance_mode = instance_mode - - self.parallel = parallel - if parallel: - num_gpu = torch.cuda.device_count() - self.predictor = AsyncPredictor(cfg, num_gpus=num_gpu) - else: - self.predictor = DefaultPredictor(cfg) - - def run_on_image(self, image, visualizer=None): - """ - Args: - image (np.ndarray): an image of shape (H, W, C) (in BGR order). - This is the format used by OpenCV. - - Returns: - predictions (dict): the output of the model. - vis_output (VisImage): the visualized image output. - """ - vis_output = None - predictions = self.predictor(image) - # Convert image from OpenCV BGR format to Matplotlib RGB format. - image = image[:, :, ::-1] - use_video_vis = True - if visualizer is None: - use_video_vis = False - visualizer = Visualizer(image, self.metadata, instance_mode=self.instance_mode) - if "panoptic_seg" in predictions: - panoptic_seg, segments_info = predictions["panoptic_seg"] - vis_output = visualizer.draw_panoptic_seg_predictions( - panoptic_seg.to(self.cpu_device), segments_info - ) - else: - if "sem_seg" in predictions: - vis_output = visualizer.draw_sem_seg( - predictions["sem_seg"].argmax(dim=0).to(self.cpu_device) - ) - if "instances" in predictions: - instances = predictions["instances"].to(self.cpu_device) - if use_video_vis: - vis_output = visualizer.draw_instance_predictions(image, predictions=instances) - else: - vis_output = visualizer.draw_instance_predictions(predictions=instances) - elif "proposals" in predictions: - instances = predictions["proposals"].to(self.cpu_device) - instances.pred_boxes = instances.proposal_boxes - instances.scores = instances.objectness_logits - instances.pred_classes[:] = -1 - if use_video_vis: - vis_output = visualizer.draw_instance_predictions(image, predictions=instances) - else: - vis_output = visualizer.draw_instance_predictions(predictions=instances) - - return predictions, vis_output - - def _frame_from_video(self, video): - while video.isOpened(): - success, frame = video.read() - if success: - yield frame - else: - break - - def run_on_video(self, video): - """ - Visualizes predictions on frames of the input video. - - Args: - video (cv2.VideoCapture): a :class:`VideoCapture` object, whose source can be - either a webcam or a video file. - - Yields: - ndarray: BGR visualizations of each video frame. - """ - video_visualizer = VideoVisualizer(self.metadata, self.instance_mode) - - def process_predictions(frame, predictions): - frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - if "panoptic_seg" in predictions: - panoptic_seg, segments_info = predictions["panoptic_seg"] - vis_frame = video_visualizer.draw_panoptic_seg_predictions( - frame, panoptic_seg.to(self.cpu_device), segments_info - ) - elif "instances" in predictions: - predictions = predictions["instances"].to(self.cpu_device) - vis_frame = video_visualizer.draw_instance_predictions(frame, predictions) - elif "sem_seg" in predictions: - vis_frame = video_visualizer.draw_sem_seg( - frame, predictions["sem_seg"].argmax(dim=0).to(self.cpu_device) - ) - elif "proposals" in predictions: - predictions = predictions["proposals"].to(self.cpu_device) - predictions.pred_boxes = predictions.proposal_boxes - predictions.scores = predictions.objectness_logits - predictions.pred_classes[:] = -1 - vis_frame = video_visualizer.draw_instance_predictions(frame, predictions) - - # Converts Matplotlib RGB format to OpenCV BGR format - vis_frame = cv2.cvtColor(vis_frame.get_image(), cv2.COLOR_RGB2BGR) - return vis_frame - - frame_gen = self._frame_from_video(video) - if self.parallel: - buffer_size = self.predictor.default_buffer_size - - frame_data = deque() - - for cnt, frame in enumerate(frame_gen): - frame_data.append(frame) - self.predictor.put(frame) - - if cnt >= buffer_size: - frame = frame_data.popleft() - predictions = self.predictor.get() - yield process_predictions(frame, predictions) - - while len(frame_data): - frame = frame_data.popleft() - predictions = self.predictor.get() - yield process_predictions(frame, predictions) - else: - for frame in frame_gen: - yield process_predictions(frame, self.predictor(frame)) - - -class AsyncPredictor: - """ - A predictor that runs the model asynchronously, possibly on >1 GPUs. - Because rendering the visualization takes considerably amount of time, - this helps improve throughput when rendering videos. - """ - - class _StopToken: - pass - - class _PredictWorker(mp.Process): - def __init__(self, cfg, task_queue, result_queue) -> None: - self.cfg = cfg - self.task_queue = task_queue - self.result_queue = result_queue - super().__init__() - - def run(self) -> None: - predictor = DefaultPredictor(self.cfg) - - while True: - task = self.task_queue.get() - if isinstance(task, AsyncPredictor._StopToken): - break - idx, data = task - result = predictor(data) - self.result_queue.put((idx, result)) - - def __init__(self, cfg, num_gpus: int = 1) -> None: - """ - Args: - cfg (CfgNode): - num_gpus (int): if 0, will run on CPU - """ - num_workers = max(num_gpus, 1) - self.task_queue = mp.Queue(maxsize=num_workers * 3) - self.result_queue = mp.Queue(maxsize=num_workers * 3) - self.procs = [] - for gpuid in range(max(num_gpus, 1)): - cfg = cfg.clone() - cfg.defrost() - cfg.MODEL.DEVICE = f"cuda:{gpuid}" if num_gpus > 0 else "cpu" - self.procs.append( - AsyncPredictor._PredictWorker(cfg, self.task_queue, self.result_queue) - ) - - self.put_idx = 0 - self.get_idx = 0 - self.result_rank = [] - self.result_data = [] - - for p in self.procs: - p.start() - atexit.register(self.shutdown) - - def put(self, image) -> None: - self.put_idx += 1 - self.task_queue.put((self.put_idx, image)) - - def get(self): - self.get_idx += 1 # the index needed for this request - if len(self.result_rank) and self.result_rank[0] == self.get_idx: - res = self.result_data[0] - del self.result_data[0], self.result_rank[0] - return res - - while True: - # make sure the results are returned in the correct order - idx, res = self.result_queue.get() - if idx == self.get_idx: - return res - insert = bisect.bisect(self.result_rank, idx) - self.result_rank.insert(insert, idx) - self.result_data.insert(insert, res) - - def __len__(self) -> int: - return self.put_idx - self.get_idx - - def __call__(self, image): - self.put(image) - return self.get() - - def shutdown(self) -> None: - for _ in self.procs: - self.task_queue.put(AsyncPredictor._StopToken()) - - @property - def default_buffer_size(self): - return len(self.procs) * 5 diff --git a/dimos/models/Detic/third_party/CenterNet2/requirements.txt b/dimos/models/Detic/third_party/CenterNet2/requirements.txt deleted file mode 100644 index 0dd006bbc3..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -opencv-python diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/README.md b/dimos/models/Detic/third_party/CenterNet2/tools/README.md deleted file mode 100644 index 0b40d5319c..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/README.md +++ /dev/null @@ -1,49 +0,0 @@ - -This directory contains a few example scripts that demonstrate features of detectron2. - - -* `train_net.py` - -An example training script that's made to train builtin models of detectron2. - -For usage, see [GETTING_STARTED.md](../GETTING_STARTED.md). - -* `plain_train_net.py` - -Similar to `train_net.py`, but implements a training loop instead of using `Trainer`. -This script includes fewer features but it may be more friendly to hackers. - -* `benchmark.py` - -Benchmark the training speed, inference speed or data loading speed of a given config. - -Usage: -``` -python benchmark.py --config-file config.yaml --task train/eval/data [optional DDP flags] -``` - -* `analyze_model.py` - -Analyze FLOPs, parameters, activations of a detectron2 model. See its `--help` for usage. - -* `visualize_json_results.py` - -Visualize the json instance detection/segmentation results dumped by `COCOEvalutor` or `LVISEvaluator` - -Usage: -``` -python visualize_json_results.py --input x.json --output dir/ --dataset coco_2017_val -``` -If not using a builtin dataset, you'll need your own script or modify this script. - -* `visualize_data.py` - -Visualize ground truth raw annotations or training data (after preprocessing/augmentations). - -Usage: -``` -python visualize_data.py --config-file config.yaml --source annotation/dataloader --output-dir dir/ [--show] -``` - -NOTE: the script does not stop by itself when using `--source dataloader` because a training -dataloader is usually infinite. diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/analyze_model.py b/dimos/models/Detic/third_party/CenterNet2/tools/analyze_model.py deleted file mode 100755 index 7b7b9e3432..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/analyze_model.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. - -from collections import Counter -import logging - -from detectron2.checkpoint import DetectionCheckpointer -from detectron2.config import CfgNode, LazyConfig, get_cfg, instantiate -from detectron2.data import build_detection_test_loader -from detectron2.engine import default_argument_parser -from detectron2.modeling import build_model -from detectron2.utils.analysis import ( - FlopCountAnalysis, - activation_count_operators, - parameter_count_table, -) -from detectron2.utils.logger import setup_logger -from fvcore.nn import flop_count_table # can also try flop_count_str -import numpy as np -import tqdm - -logger = logging.getLogger("detectron2") - - -def setup(args): - if args.config_file.endswith(".yaml"): - cfg = get_cfg() - cfg.merge_from_file(args.config_file) - cfg.DATALOADER.NUM_WORKERS = 0 - cfg.merge_from_list(args.opts) - cfg.freeze() - else: - cfg = LazyConfig.load(args.config_file) - cfg = LazyConfig.apply_overrides(cfg, args.opts) - setup_logger(name="fvcore") - setup_logger() - return cfg - - -def do_flop(cfg) -> None: - if isinstance(cfg, CfgNode): - data_loader = build_detection_test_loader(cfg, cfg.DATASETS.TEST[0]) - model = build_model(cfg) - DetectionCheckpointer(model).load(cfg.MODEL.WEIGHTS) - else: - data_loader = instantiate(cfg.dataloader.test) - model = instantiate(cfg.model) - model.to(cfg.train.device) - DetectionCheckpointer(model).load(cfg.train.init_checkpoint) - model.eval() - - counts = Counter() - total_flops = [] - for idx, data in zip(tqdm.trange(args.num_inputs), data_loader): # noqa - flops = FlopCountAnalysis(model, data) - if idx > 0: - flops.unsupported_ops_warnings(False).uncalled_modules_warnings(False) - counts += flops.by_operator() - total_flops.append(flops.total()) - - logger.info("Flops table computed from only one input sample:\n" + flop_count_table(flops)) - logger.info( - "Average GFlops for each type of operators:\n" - + str([(k, v / (idx + 1) / 1e9) for k, v in counts.items()]) - ) - logger.info( - f"Total GFlops: {np.mean(total_flops) / 1e9:.1f}±{np.std(total_flops) / 1e9:.1f}" - ) - - -def do_activation(cfg) -> None: - if isinstance(cfg, CfgNode): - data_loader = build_detection_test_loader(cfg, cfg.DATASETS.TEST[0]) - model = build_model(cfg) - DetectionCheckpointer(model).load(cfg.MODEL.WEIGHTS) - else: - data_loader = instantiate(cfg.dataloader.test) - model = instantiate(cfg.model) - model.to(cfg.train.device) - DetectionCheckpointer(model).load(cfg.train.init_checkpoint) - model.eval() - - counts = Counter() - total_activations = [] - for idx, data in zip(tqdm.trange(args.num_inputs), data_loader): # noqa - count = activation_count_operators(model, data) - counts += count - total_activations.append(sum(count.values())) - logger.info( - "(Million) Activations for Each Type of Operators:\n" - + str([(k, v / idx) for k, v in counts.items()]) - ) - logger.info( - f"Total (Million) Activations: {np.mean(total_activations)}±{np.std(total_activations)}" - ) - - -def do_parameter(cfg) -> None: - if isinstance(cfg, CfgNode): - model = build_model(cfg) - else: - model = instantiate(cfg.model) - logger.info("Parameter Count:\n" + parameter_count_table(model, max_depth=5)) - - -def do_structure(cfg) -> None: - if isinstance(cfg, CfgNode): - model = build_model(cfg) - else: - model = instantiate(cfg.model) - logger.info("Model Structure:\n" + str(model)) - - -if __name__ == "__main__": - parser = default_argument_parser( - epilog=""" -Examples: - -To show parameters of a model: -$ ./analyze_model.py --tasks parameter \\ - --config-file ../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml - -Flops and activations are data-dependent, therefore inputs and model weights -are needed to count them: - -$ ./analyze_model.py --num-inputs 100 --tasks flop \\ - --config-file ../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml \\ - MODEL.WEIGHTS /path/to/model.pkl -""" - ) - parser.add_argument( - "--tasks", - choices=["flop", "activation", "parameter", "structure"], - required=True, - nargs="+", - ) - parser.add_argument( - "-n", - "--num-inputs", - default=100, - type=int, - help="number of inputs used to compute statistics for flops/activations, both are data dependent.", - ) - args = parser.parse_args() - assert not args.eval_only - assert args.num_gpus == 1 - - cfg = setup(args) - - for task in args.tasks: - { - "flop": do_flop, - "activation": do_activation, - "parameter": do_parameter, - "structure": do_structure, - }[task](cfg) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/benchmark.py b/dimos/models/Detic/third_party/CenterNet2/tools/benchmark.py deleted file mode 100755 index 48f398d83d..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/benchmark.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. -""" -A script to benchmark builtin models. - -Note: this script has an extra dependency of psutil. -""" - -import itertools -import logging - -from detectron2.checkpoint import DetectionCheckpointer -from detectron2.config import LazyConfig, get_cfg, instantiate -from detectron2.data import ( - DatasetFromList, - build_detection_test_loader, - build_detection_train_loader, -) -from detectron2.data.benchmark import DataLoaderBenchmark -from detectron2.engine import AMPTrainer, SimpleTrainer, default_argument_parser, hooks, launch -from detectron2.modeling import build_model -from detectron2.solver import build_optimizer -from detectron2.utils import comm -from detectron2.utils.collect_env import collect_env_info -from detectron2.utils.events import CommonMetricPrinter -from detectron2.utils.logger import setup_logger -from fvcore.common.timer import Timer -import psutil -import torch -from torch.nn.parallel import DistributedDataParallel -import tqdm - -logger = logging.getLogger("detectron2") - - -def setup(args): - if args.config_file.endswith(".yaml"): - cfg = get_cfg() - cfg.merge_from_file(args.config_file) - cfg.SOLVER.BASE_LR = 0.001 # Avoid NaNs. Not useful in this script anyway. - cfg.merge_from_list(args.opts) - cfg.freeze() - else: - cfg = LazyConfig.load(args.config_file) - cfg = LazyConfig.apply_overrides(cfg, args.opts) - setup_logger(distributed_rank=comm.get_rank()) - return cfg - - -def create_data_benchmark(cfg, args): - if args.config_file.endswith(".py"): - dl_cfg = cfg.dataloader.train - dl_cfg._target_ = DataLoaderBenchmark - return instantiate(dl_cfg) - else: - kwargs = build_detection_train_loader.from_config(cfg) - kwargs.pop("aspect_ratio_grouping", None) - kwargs["_target_"] = DataLoaderBenchmark - return instantiate(kwargs) - - -def RAM_msg() -> str: - vram = psutil.virtual_memory() - return f"RAM Usage: {(vram.total - vram.available) / 1024**3:.2f}/{vram.total / 1024**3:.2f} GB" - - -def benchmark_data(args) -> None: - cfg = setup(args) - logger.info("After spawning " + RAM_msg()) - - benchmark = create_data_benchmark(cfg, args) - benchmark.benchmark_distributed(250, 10) - # test for a few more rounds - for k in range(10): - logger.info(f"Iteration {k} " + RAM_msg()) - benchmark.benchmark_distributed(250, 1) - - -def benchmark_data_advanced(args) -> None: - # benchmark dataloader with more details to help analyze performance bottleneck - cfg = setup(args) - benchmark = create_data_benchmark(cfg, args) - - if comm.get_rank() == 0: - benchmark.benchmark_dataset(100) - benchmark.benchmark_mapper(100) - benchmark.benchmark_workers(100, warmup=10) - benchmark.benchmark_IPC(100, warmup=10) - if comm.get_world_size() > 1: - benchmark.benchmark_distributed(100) - logger.info("Rerun ...") - benchmark.benchmark_distributed(100) - - -def benchmark_train(args) -> None: - cfg = setup(args) - model = build_model(cfg) - logger.info(f"Model:\n{model}") - if comm.get_world_size() > 1: - model = DistributedDataParallel( - model, device_ids=[comm.get_local_rank()], broadcast_buffers=False - ) - optimizer = build_optimizer(cfg, model) - checkpointer = DetectionCheckpointer(model, optimizer=optimizer) - checkpointer.load(cfg.MODEL.WEIGHTS) - - cfg.defrost() - cfg.DATALOADER.NUM_WORKERS = 2 - data_loader = build_detection_train_loader(cfg) - dummy_data = list(itertools.islice(data_loader, 100)) - - def f(): - data = DatasetFromList(dummy_data, copy=False, serialize=False) - while True: - yield from data - - max_iter = 400 - trainer = (AMPTrainer if cfg.SOLVER.AMP.ENABLED else SimpleTrainer)(model, f(), optimizer) - trainer.register_hooks( - [ - hooks.IterationTimer(), - hooks.PeriodicWriter([CommonMetricPrinter(max_iter)]), - hooks.TorchProfiler( - lambda trainer: trainer.iter == max_iter - 1, cfg.OUTPUT_DIR, save_tensorboard=True - ), - ] - ) - trainer.train(1, max_iter) - - -@torch.no_grad() -def benchmark_eval(args) -> None: - cfg = setup(args) - if args.config_file.endswith(".yaml"): - model = build_model(cfg) - DetectionCheckpointer(model).load(cfg.MODEL.WEIGHTS) - - cfg.defrost() - cfg.DATALOADER.NUM_WORKERS = 0 - data_loader = build_detection_test_loader(cfg, cfg.DATASETS.TEST[0]) - else: - model = instantiate(cfg.model) - model.to(cfg.train.device) - DetectionCheckpointer(model).load(cfg.train.init_checkpoint) - - cfg.dataloader.num_workers = 0 - data_loader = instantiate(cfg.dataloader.test) - - model.eval() - logger.info(f"Model:\n{model}") - dummy_data = DatasetFromList(list(itertools.islice(data_loader, 100)), copy=False) - - def f(): - while True: - yield from dummy_data - - for k in range(5): # warmup - model(dummy_data[k]) - - max_iter = 300 - timer = Timer() - with tqdm.tqdm(total=max_iter) as pbar: - for idx, d in enumerate(f()): - if idx == max_iter: - break - model(d) - pbar.update() - logger.info(f"{max_iter} iters in {timer.seconds()} seconds.") - - -if __name__ == "__main__": - parser = default_argument_parser() - parser.add_argument("--task", choices=["train", "eval", "data", "data_advanced"], required=True) - args = parser.parse_args() - assert not args.eval_only - - logger.info("Environment info:\n" + collect_env_info()) - if "data" in args.task: - print("Initial " + RAM_msg()) - if args.task == "data": - f = benchmark_data - if args.task == "data_advanced": - f = benchmark_data_advanced - elif args.task == "train": - """ - Note: training speed may not be representative. - The training cost of a R-CNN model varies with the content of the data - and the quality of the model. - """ - f = benchmark_train - elif args.task == "eval": - f = benchmark_eval - # only benchmark single-GPU inference. - assert args.num_gpus == 1 and args.num_machines == 1 - launch(f, args.num_gpus, args.num_machines, args.machine_rank, args.dist_url, args=(args,)) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/convert-torchvision-to-d2.py b/dimos/models/Detic/third_party/CenterNet2/tools/convert-torchvision-to-d2.py deleted file mode 100755 index 8bf0565d5e..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/convert-torchvision-to-d2.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. - -import pickle as pkl -import sys - -import torch - -""" -Usage: - # download one of the ResNet{18,34,50,101,152} models from torchvision: - wget https://download.pytorch.org/models/resnet50-19c8e357.pth -O r50.pth - # run the conversion - ./convert-torchvision-to-d2.py r50.pth r50.pkl - - # Then, use r50.pkl with the following changes in config: - -MODEL: - WEIGHTS: "/path/to/r50.pkl" - PIXEL_MEAN: [123.675, 116.280, 103.530] - PIXEL_STD: [58.395, 57.120, 57.375] - RESNETS: - DEPTH: 50 - STRIDE_IN_1X1: False -INPUT: - FORMAT: "RGB" - - These models typically produce slightly worse results than the - pre-trained ResNets we use in official configs, which are the - original ResNet models released by MSRA. -""" - -if __name__ == "__main__": - input = sys.argv[1] - - obj = torch.load(input, map_location="cpu") - - newmodel = {} - for k in list(obj.keys()): - old_k = k - if "layer" not in k: - k = "stem." + k - for t in [1, 2, 3, 4]: - k = k.replace(f"layer{t}", f"res{t + 1}") - for t in [1, 2, 3]: - k = k.replace(f"bn{t}", f"conv{t}.norm") - k = k.replace("downsample.0", "shortcut") - k = k.replace("downsample.1", "shortcut.norm") - print(old_k, "->", k) - newmodel[k] = obj.pop(old_k).detach().numpy() - - res = {"model": newmodel, "__author__": "torchvision", "matching_heuristics": True} - - with open(sys.argv[2], "wb") as f: - pkl.dump(res, f) - if obj: - print("Unconverted keys:", obj.keys()) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/CMakeLists.txt b/dimos/models/Detic/third_party/CenterNet2/tools/deploy/CMakeLists.txt deleted file mode 100644 index 80dae12500..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -# See https://pytorch.org/tutorials/advanced/cpp_frontend.html -cmake_minimum_required(VERSION 3.12 FATAL_ERROR) -project(torchscript_mask_rcnn) - -find_package(Torch REQUIRED) -find_package(OpenCV REQUIRED) -find_package(TorchVision REQUIRED) # needed by export-method=tracing/scripting - -add_executable(torchscript_mask_rcnn torchscript_mask_rcnn.cpp) -target_link_libraries( - torchscript_mask_rcnn - -Wl,--no-as-needed TorchVision::TorchVision -Wl,--as-needed - "${TORCH_LIBRARIES}" ${OpenCV_LIBS}) -set_property(TARGET torchscript_mask_rcnn PROPERTY CXX_STANDARD 14) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/README.md b/dimos/models/Detic/third_party/CenterNet2/tools/deploy/README.md deleted file mode 100644 index e33cbeb54c..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/README.md +++ /dev/null @@ -1,66 +0,0 @@ -See [deployment tutorial](https://detectron2.readthedocs.io/tutorials/deployment.html) -for some high-level background about deployment. - -This directory contains the following examples: - -1. An example script `export_model.py` - that exports a detectron2 model for deployment using different methods and formats. - -2. A C++ example that runs inference with Mask R-CNN model in TorchScript format. - -## Build -Deployment depends on libtorch and OpenCV. Some require more dependencies: - -* Running TorchScript-format models produced by `--export-method=caffe2_tracing` requires libtorch - to be built with caffe2 enabled. -* Running TorchScript-format models produced by `--export-method=tracing/scripting` requires libtorchvision (C++ library of torchvision). - -All methods are supported in one C++ file that requires all the above dependencies. -Adjust it and remove code you don't need. -As a reference, we provide a [Dockerfile](../../docker/deploy.Dockerfile) that installs all the above dependencies and builds the C++ example. - -## Use - -We show a few example commands to export and execute a Mask R-CNN model in C++. - -* `export-method=tracing, format=torchscript`: -``` -./export_model.py --config-file ../../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml \ - --output ./output --export-method tracing --format torchscript \ - MODEL.WEIGHTS detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl \ - MODEL.DEVICE cuda - -./build/torchscript_mask_rcnn output/model.ts input.jpg tracing -``` - -* `export-method=scripting, format=torchscript`: -``` -./export_model.py --config-file ../../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml \ - --output ./output --export-method scripting --format torchscript \ - MODEL.WEIGHTS detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl \ - -./build/torchscript_mask_rcnn output/model.ts input.jpg scripting -``` - -* `export-method=caffe2_tracing, format=torchscript`: - -``` -./export_model.py --config-file ../../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml \ - --output ./output --export-method caffe2_tracing --format torchscript \ - MODEL.WEIGHTS detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl \ - -./build/torchscript_mask_rcnn output/model.ts input.jpg caffe2_tracing -``` - - -## Notes: - -1. Tracing/Caffe2-tracing requires valid weights & sample inputs. - Therefore the above commands require pre-trained models and [COCO dataset](https://detectron2.readthedocs.io/tutorials/builtin_datasets.html). - You can modify the script to obtain sample inputs in other ways instead of from COCO. - -2. `--run-eval` is implemented only for tracing mode - to evaluate the exported model using the dataset in the config. - It's recommended to always verify the accuracy in case the conversion is not successful. - Evaluation can be slow if model is exported to CPU or dataset is too large ("coco_2017_val_100" is a small subset of COCO useful for evaluation). - `caffe2_tracing` accuracy may be slightly different (within 0.1 AP) from original model due to numerical precisions between different runtime. diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/export_model.py b/dimos/models/Detic/third_party/CenterNet2/tools/deploy/export_model.py deleted file mode 100755 index 6b9d2d60be..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/export_model.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import os -from typing import Dict, List, Tuple - -from detectron2.checkpoint import DetectionCheckpointer -from detectron2.config import get_cfg -from detectron2.data import build_detection_test_loader, detection_utils -import detectron2.data.transforms as T -from detectron2.evaluation import COCOEvaluator, inference_on_dataset, print_csv_format -from detectron2.export import TracingAdapter, dump_torchscript_IR, scripting_with_instances -from detectron2.modeling import GeneralizedRCNN, RetinaNet, build_model -from detectron2.modeling.postprocessing import detector_postprocess -from detectron2.projects.point_rend import add_pointrend_config -from detectron2.structures import Boxes -from detectron2.utils.env import TORCH_VERSION -from detectron2.utils.file_io import PathManager -from detectron2.utils.logger import setup_logger -import torch -from torch import Tensor, nn - - -def setup_cfg(args): - cfg = get_cfg() - # cuda context is initialized before creating dataloader, so we don't fork anymore - cfg.DATALOADER.NUM_WORKERS = 0 - add_pointrend_config(cfg) - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - cfg.freeze() - return cfg - - -def export_caffe2_tracing(cfg, torch_model, inputs): - from detectron2.export import Caffe2Tracer - - tracer = Caffe2Tracer(cfg, torch_model, inputs) - if args.format == "caffe2": - caffe2_model = tracer.export_caffe2() - caffe2_model.save_protobuf(args.output) - # draw the caffe2 graph - caffe2_model.save_graph(os.path.join(args.output, "model.svg"), inputs=inputs) - return caffe2_model - elif args.format == "onnx": - import onnx - - onnx_model = tracer.export_onnx() - onnx.save(onnx_model, os.path.join(args.output, "model.onnx")) - elif args.format == "torchscript": - ts_model = tracer.export_torchscript() - with PathManager.open(os.path.join(args.output, "model.ts"), "wb") as f: - torch.jit.save(ts_model, f) - dump_torchscript_IR(ts_model, args.output) - - -# experimental. API not yet final -def export_scripting(torch_model): - assert TORCH_VERSION >= (1, 8) - fields = { - "proposal_boxes": Boxes, - "objectness_logits": Tensor, - "pred_boxes": Boxes, - "scores": Tensor, - "pred_classes": Tensor, - "pred_masks": Tensor, - "pred_keypoints": torch.Tensor, - "pred_keypoint_heatmaps": torch.Tensor, - } - assert args.format == "torchscript", "Scripting only supports torchscript format." - - class ScriptableAdapterBase(nn.Module): - # Use this adapter to workaround https://github.com/pytorch/pytorch/issues/46944 - # by not retuning instances but dicts. Otherwise the exported model is not deployable - def __init__(self) -> None: - super().__init__() - self.model = torch_model - self.eval() - - if isinstance(torch_model, GeneralizedRCNN): - - class ScriptableAdapter(ScriptableAdapterBase): - def forward(self, inputs: tuple[dict[str, torch.Tensor]]) -> list[dict[str, Tensor]]: - instances = self.model.inference(inputs, do_postprocess=False) - return [i.get_fields() for i in instances] - - else: - - class ScriptableAdapter(ScriptableAdapterBase): - def forward(self, inputs: tuple[dict[str, torch.Tensor]]) -> list[dict[str, Tensor]]: - instances = self.model(inputs) - return [i.get_fields() for i in instances] - - ts_model = scripting_with_instances(ScriptableAdapter(), fields) - with PathManager.open(os.path.join(args.output, "model.ts"), "wb") as f: - torch.jit.save(ts_model, f) - dump_torchscript_IR(ts_model, args.output) - # TODO inference in Python now missing postprocessing glue code - return None - - -# experimental. API not yet final -def export_tracing(torch_model, inputs): - assert TORCH_VERSION >= (1, 8) - image = inputs[0]["image"] - inputs = [{"image": image}] # remove other unused keys - - if isinstance(torch_model, GeneralizedRCNN): - - def inference(model, inputs): - # use do_postprocess=False so it returns ROI mask - inst = model.inference(inputs, do_postprocess=False)[0] - return [{"instances": inst}] - - else: - inference = None # assume that we just call the model directly - - traceable_model = TracingAdapter(torch_model, inputs, inference) - - if args.format == "torchscript": - ts_model = torch.jit.trace(traceable_model, (image,)) - with PathManager.open(os.path.join(args.output, "model.ts"), "wb") as f: - torch.jit.save(ts_model, f) - dump_torchscript_IR(ts_model, args.output) - elif args.format == "onnx": - with PathManager.open(os.path.join(args.output, "model.onnx"), "wb") as f: - torch.onnx.export(traceable_model, (image,), f, opset_version=11) - logger.info("Inputs schema: " + str(traceable_model.inputs_schema)) - logger.info("Outputs schema: " + str(traceable_model.outputs_schema)) - - if args.format != "torchscript": - return None - if not isinstance(torch_model, GeneralizedRCNN | RetinaNet): - return None - - def eval_wrapper(inputs): - """ - The exported model does not contain the final resize step, which is typically - unused in deployment but needed for evaluation. We add it manually here. - """ - input = inputs[0] - instances = traceable_model.outputs_schema(ts_model(input["image"]))[0]["instances"] - postprocessed = detector_postprocess(instances, input["height"], input["width"]) - return [{"instances": postprocessed}] - - return eval_wrapper - - -def get_sample_inputs(args): - if args.sample_image is None: - # get a first batch from dataset - data_loader = build_detection_test_loader(cfg, cfg.DATASETS.TEST[0]) - first_batch = next(iter(data_loader)) - return first_batch - else: - # get a sample data - original_image = detection_utils.read_image(args.sample_image, format=cfg.INPUT.FORMAT) - # Do same preprocessing as DefaultPredictor - aug = T.ResizeShortestEdge( - [cfg.INPUT.MIN_SIZE_TEST, cfg.INPUT.MIN_SIZE_TEST], cfg.INPUT.MAX_SIZE_TEST - ) - height, width = original_image.shape[:2] - image = aug.get_transform(original_image).apply_image(original_image) - image = torch.as_tensor(image.astype("float32").transpose(2, 0, 1)) - - inputs = {"image": image, "height": height, "width": width} - - # Sample ready - sample_inputs = [inputs] - return sample_inputs - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Export a model for deployment.") - parser.add_argument( - "--format", - choices=["caffe2", "onnx", "torchscript"], - help="output format", - default="torchscript", - ) - parser.add_argument( - "--export-method", - choices=["caffe2_tracing", "tracing", "scripting"], - help="Method to export models", - default="tracing", - ) - parser.add_argument("--config-file", default="", metavar="FILE", help="path to config file") - parser.add_argument("--sample-image", default=None, type=str, help="sample image for input") - parser.add_argument("--run-eval", action="store_true") - parser.add_argument("--output", help="output directory for the converted model") - parser.add_argument( - "opts", - help="Modify config options using the command-line", - default=None, - nargs=argparse.REMAINDER, - ) - args = parser.parse_args() - logger = setup_logger() - logger.info("Command line arguments: " + str(args)) - PathManager.mkdirs(args.output) - # Disable respecialization on new shapes. Otherwise --run-eval will be slow - torch._C._jit_set_bailout_depth(1) - - cfg = setup_cfg(args) - - # create a torch model - torch_model = build_model(cfg) - DetectionCheckpointer(torch_model).resume_or_load(cfg.MODEL.WEIGHTS) - torch_model.eval() - - # get sample data - sample_inputs = get_sample_inputs(args) - - # convert and save model - if args.export_method == "caffe2_tracing": - exported_model = export_caffe2_tracing(cfg, torch_model, sample_inputs) - elif args.export_method == "scripting": - exported_model = export_scripting(torch_model) - elif args.export_method == "tracing": - exported_model = export_tracing(torch_model, sample_inputs) - - # run evaluation with the converted model - if args.run_eval: - assert exported_model is not None, ( - f"Python inference is not yet implemented for export_method={args.export_method}, format={args.format}." - ) - logger.info("Running evaluation ... this takes a long time if you export to CPU.") - dataset = cfg.DATASETS.TEST[0] - data_loader = build_detection_test_loader(cfg, dataset) - # NOTE: hard-coded evaluator. change to the evaluator for your dataset - evaluator = COCOEvaluator(dataset, output_dir=args.output) - metrics = inference_on_dataset(exported_model, data_loader, evaluator) - print_csv_format(metrics) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/torchscript_mask_rcnn.cpp b/dimos/models/Detic/third_party/CenterNet2/tools/deploy/torchscript_mask_rcnn.cpp deleted file mode 100644 index b40f13b81f..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/torchscript_mask_rcnn.cpp +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) Facebook, Inc. and its affiliates. -// @lint-ignore-every CLANGTIDY -// This is an example code that demonstrates how to run inference -// with a torchscript format Mask R-CNN model exported by ./export_model.py -// using export method=tracing, caffe2_tracing & scripting. - -#include -#include -#include - -#include -#include -#include -#include - -// only needed for export_method=tracing -#include // @oss-only -// @fb-only: #include - -using namespace std; - -c10::IValue get_caffe2_tracing_inputs(cv::Mat& img, c10::Device device) { - const int height = img.rows; - const int width = img.cols; - // FPN models require divisibility of 32. - // Tracing mode does padding inside the graph, but caffe2_tracing does not. - assert(height % 32 == 0 && width % 32 == 0); - const int channels = 3; - - auto input = - torch::from_blob(img.data, {1, height, width, channels}, torch::kUInt8); - // NHWC to NCHW - input = input.to(device, torch::kFloat).permute({0, 3, 1, 2}).contiguous(); - - std::array im_info_data{height * 1.0f, width * 1.0f, 1.0f}; - auto im_info = - torch::from_blob(im_info_data.data(), {1, 3}).clone().to(device); - return std::make_tuple(input, im_info); -} - -c10::IValue get_tracing_inputs(cv::Mat& img, c10::Device device) { - const int height = img.rows; - const int width = img.cols; - const int channels = 3; - - auto input = - torch::from_blob(img.data, {height, width, channels}, torch::kUInt8); - // HWC to CHW - input = input.to(device, torch::kFloat).permute({2, 0, 1}).contiguous(); - return input; -} - -// create a Tuple[Dict[str, Tensor]] which is the input type of scripted model -c10::IValue get_scripting_inputs(cv::Mat& img, c10::Device device) { - const int height = img.rows; - const int width = img.cols; - const int channels = 3; - - auto img_tensor = - torch::from_blob(img.data, {height, width, channels}, torch::kUInt8); - // HWC to CHW - img_tensor = - img_tensor.to(device, torch::kFloat).permute({2, 0, 1}).contiguous(); - auto dic = c10::Dict(); - dic.insert("image", img_tensor); - return std::make_tuple(dic); -} - -c10::IValue -get_inputs(std::string export_method, cv::Mat& img, c10::Device device) { - // Given an image, create inputs in the format required by the model. - if (export_method == "tracing") - return get_tracing_inputs(img, device); - if (export_method == "caffe2_tracing") - return get_caffe2_tracing_inputs(img, device); - if (export_method == "scripting") - return get_scripting_inputs(img, device); - abort(); -} - -struct MaskRCNNOutputs { - at::Tensor pred_boxes, pred_classes, pred_masks, scores; - int num_instances() const { - return pred_boxes.sizes()[0]; - } -}; - -MaskRCNNOutputs get_outputs(std::string export_method, c10::IValue outputs) { - // Given outputs of the model, extract tensors from it to turn into a - // common MaskRCNNOutputs format. - if (export_method == "tracing") { - auto out_tuple = outputs.toTuple()->elements(); - // They are ordered alphabetically by their field name in Instances - return MaskRCNNOutputs{ - out_tuple[0].toTensor(), - out_tuple[1].toTensor(), - out_tuple[2].toTensor(), - out_tuple[3].toTensor()}; - } - if (export_method == "caffe2_tracing") { - auto out_tuple = outputs.toTuple()->elements(); - // A legacy order used by caffe2 models - return MaskRCNNOutputs{ - out_tuple[0].toTensor(), - out_tuple[2].toTensor(), - out_tuple[3].toTensor(), - out_tuple[1].toTensor()}; - } - if (export_method == "scripting") { - // With the ScriptableAdapter defined in export_model.py, the output is - // List[Dict[str, Any]]. - auto out_dict = outputs.toList().get(0).toGenericDict(); - return MaskRCNNOutputs{ - out_dict.at("pred_boxes").toTensor(), - out_dict.at("pred_classes").toTensor(), - out_dict.at("pred_masks").toTensor(), - out_dict.at("scores").toTensor()}; - } - abort(); -} - -int main(int argc, const char* argv[]) { - if (argc != 4) { - cerr << R"xx( -Usage: - ./torchscript_mask_rcnn model.ts input.jpg EXPORT_METHOD - - EXPORT_METHOD can be "tracing", "caffe2_tracing" or "scripting". -)xx"; - return 1; - } - std::string image_file = argv[2]; - std::string export_method = argv[3]; - assert( - export_method == "caffe2_tracing" || export_method == "tracing" || - export_method == "scripting"); - - torch::jit::getBailoutDepth() = 1; - torch::autograd::AutoGradMode guard(false); - auto module = torch::jit::load(argv[1]); - - assert(module.buffers().size() > 0); - // Assume that the entire model is on the same device. - // We just put input to this device. - auto device = (*begin(module.buffers())).device(); - - cv::Mat input_img = cv::imread(image_file, cv::IMREAD_COLOR); - auto inputs = get_inputs(export_method, input_img, device); - - // Run the network - auto output = module.forward({inputs}); - if (device.is_cuda()) - c10::cuda::getCurrentCUDAStream().synchronize(); - - // run 3 more times to benchmark - int N_benchmark = 3, N_warmup = 1; - auto start_time = chrono::high_resolution_clock::now(); - for (int i = 0; i < N_benchmark + N_warmup; ++i) { - if (i == N_warmup) - start_time = chrono::high_resolution_clock::now(); - output = module.forward({inputs}); - if (device.is_cuda()) - c10::cuda::getCurrentCUDAStream().synchronize(); - } - auto end_time = chrono::high_resolution_clock::now(); - auto ms = chrono::duration_cast(end_time - start_time) - .count(); - cout << "Latency (should vary with different inputs): " - << ms * 1.0 / 1e6 / N_benchmark << " seconds" << endl; - - // Parse Mask R-CNN outputs - auto rcnn_outputs = get_outputs(export_method, output); - cout << "Number of detected objects: " << rcnn_outputs.num_instances() - << endl; - - cout << "pred_boxes: " << rcnn_outputs.pred_boxes.toString() << " " - << rcnn_outputs.pred_boxes.sizes() << endl; - cout << "scores: " << rcnn_outputs.scores.toString() << " " - << rcnn_outputs.scores.sizes() << endl; - cout << "pred_classes: " << rcnn_outputs.pred_classes.toString() << " " - << rcnn_outputs.pred_classes.sizes() << endl; - cout << "pred_masks: " << rcnn_outputs.pred_masks.toString() << " " - << rcnn_outputs.pred_masks.sizes() << endl; - - cout << rcnn_outputs.pred_boxes << endl; - return 0; -} diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/lazyconfig_train_net.py b/dimos/models/Detic/third_party/CenterNet2/tools/lazyconfig_train_net.py deleted file mode 100755 index 8f40a40c39..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/lazyconfig_train_net.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. -""" -Training script using the new "LazyConfig" python config files. - -This scripts reads a given python config file and runs the training or evaluation. -It can be used to train any models or dataset as long as they can be -instantiated by the recursive construction defined in the given config file. - -Besides lazy construction of models, dataloader, etc., this scripts expects a -few common configuration parameters currently defined in "configs/common/train.py". -To add more complicated training logic, you can easily add other configs -in the config file and implement a new train_net.py to handle them. -""" - -import logging - -from detectron2.checkpoint import DetectionCheckpointer -from detectron2.config import LazyConfig, instantiate -from detectron2.engine import ( - AMPTrainer, - SimpleTrainer, - default_argument_parser, - default_setup, - default_writers, - hooks, - launch, -) -from detectron2.engine.defaults import create_ddp_model -from detectron2.evaluation import inference_on_dataset, print_csv_format -from detectron2.utils import comm - -logger = logging.getLogger("detectron2") - - -def do_test(cfg, model): - if "evaluator" in cfg.dataloader: - ret = inference_on_dataset( - model, instantiate(cfg.dataloader.test), instantiate(cfg.dataloader.evaluator) - ) - print_csv_format(ret) - return ret - - -def do_train(args, cfg) -> None: - """ - Args: - cfg: an object with the following attributes: - model: instantiate to a module - dataloader.{train,test}: instantiate to dataloaders - dataloader.evaluator: instantiate to evaluator for test set - optimizer: instantaite to an optimizer - lr_multiplier: instantiate to a fvcore scheduler - train: other misc config defined in `configs/common/train.py`, including: - output_dir (str) - init_checkpoint (str) - amp.enabled (bool) - max_iter (int) - eval_period, log_period (int) - device (str) - checkpointer (dict) - ddp (dict) - """ - model = instantiate(cfg.model) - logger = logging.getLogger("detectron2") - logger.info(f"Model:\n{model}") - model.to(cfg.train.device) - - cfg.optimizer.params.model = model - optim = instantiate(cfg.optimizer) - - train_loader = instantiate(cfg.dataloader.train) - - model = create_ddp_model(model, **cfg.train.ddp) - trainer = (AMPTrainer if cfg.train.amp.enabled else SimpleTrainer)(model, train_loader, optim) - checkpointer = DetectionCheckpointer( - model, - cfg.train.output_dir, - trainer=trainer, - ) - trainer.register_hooks( - [ - hooks.IterationTimer(), - hooks.LRScheduler(scheduler=instantiate(cfg.lr_multiplier)), - hooks.PeriodicCheckpointer(checkpointer, **cfg.train.checkpointer) - if comm.is_main_process() - else None, - hooks.EvalHook(cfg.train.eval_period, lambda: do_test(cfg, model)), - hooks.PeriodicWriter( - default_writers(cfg.train.output_dir, cfg.train.max_iter), - period=cfg.train.log_period, - ) - if comm.is_main_process() - else None, - ] - ) - - checkpointer.resume_or_load(cfg.train.init_checkpoint, resume=args.resume) - if args.resume and checkpointer.has_checkpoint(): - # The checkpoint stores the training iteration that just finished, thus we start - # at the next iteration - start_iter = trainer.iter + 1 - else: - start_iter = 0 - trainer.train(start_iter, cfg.train.max_iter) - - -def main(args) -> None: - cfg = LazyConfig.load(args.config_file) - cfg = LazyConfig.apply_overrides(cfg, args.opts) - default_setup(cfg, args) - - if args.eval_only: - model = instantiate(cfg.model) - model.to(cfg.train.device) - model = create_ddp_model(model) - DetectionCheckpointer(model).load(cfg.train.init_checkpoint) - print(do_test(cfg, model)) - else: - do_train(args, cfg) - - -if __name__ == "__main__": - args = default_argument_parser().parse_args() - launch( - main, - args.num_gpus, - num_machines=args.num_machines, - machine_rank=args.machine_rank, - dist_url=args.dist_url, - args=(args,), - ) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/lightning_train_net.py b/dimos/models/Detic/third_party/CenterNet2/tools/lightning_train_net.py deleted file mode 100644 index dbb6cb6e43..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/lightning_train_net.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Facebook, Inc. and its affiliates. -# Lightning Trainer should be considered beta at this point -# We have confirmed that training and validation run correctly and produce correct results -# Depending on how you launch the trainer, there are issues with processes terminating correctly -# This module is still dependent on D2 logging, but could be transferred to use Lightning logging - -from collections import OrderedDict -import logging -import os -import time -from typing import Any, Dict, List -import weakref - -from detectron2.checkpoint import DetectionCheckpointer -from detectron2.config import get_cfg -from detectron2.data import build_detection_test_loader, build_detection_train_loader -from detectron2.engine import ( - DefaultTrainer, - SimpleTrainer, - default_argument_parser, - default_setup, - default_writers, - hooks, -) -from detectron2.evaluation import print_csv_format -from detectron2.evaluation.testing import flatten_results_dict -from detectron2.modeling import build_model -from detectron2.solver import build_lr_scheduler, build_optimizer -import detectron2.utils.comm as comm -from detectron2.utils.events import EventStorage -from detectron2.utils.logger import setup_logger -import pytorch_lightning as pl # type: ignore -from pytorch_lightning import LightningDataModule, LightningModule -from train_net import build_evaluator - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("detectron2") - - -class TrainingModule(LightningModule): - def __init__(self, cfg) -> None: - super().__init__() - if not logger.isEnabledFor(logging.INFO): # setup_logger is not called for d2 - setup_logger() - self.cfg = DefaultTrainer.auto_scale_workers(cfg, comm.get_world_size()) - self.storage: EventStorage = None - self.model = build_model(self.cfg) - - self.start_iter = 0 - self.max_iter = cfg.SOLVER.MAX_ITER - - def on_save_checkpoint(self, checkpoint: dict[str, Any]) -> None: - checkpoint["iteration"] = self.storage.iter - - def on_load_checkpoint(self, checkpointed_state: dict[str, Any]) -> None: - self.start_iter = checkpointed_state["iteration"] - self.storage.iter = self.start_iter - - def setup(self, stage: str) -> None: - if self.cfg.MODEL.WEIGHTS: - self.checkpointer = DetectionCheckpointer( - # Assume you want to save checkpoints together with logs/statistics - self.model, - self.cfg.OUTPUT_DIR, - ) - logger.info(f"Load model weights from checkpoint: {self.cfg.MODEL.WEIGHTS}.") - # Only load weights, use lightning checkpointing if you want to resume - self.checkpointer.load(self.cfg.MODEL.WEIGHTS) - - self.iteration_timer = hooks.IterationTimer() - self.iteration_timer.before_train() - self.data_start = time.perf_counter() - self.writers = None - - def training_step(self, batch, batch_idx): - data_time = time.perf_counter() - self.data_start - # Need to manually enter/exit since trainer may launch processes - # This ideally belongs in setup, but setup seems to run before processes are spawned - if self.storage is None: - self.storage = EventStorage(0) - self.storage.__enter__() - self.iteration_timer.trainer = weakref.proxy(self) - self.iteration_timer.before_step() - self.writers = ( - default_writers(self.cfg.OUTPUT_DIR, self.max_iter) - if comm.is_main_process() - else {} - ) - - loss_dict = self.model(batch) - SimpleTrainer.write_metrics(loss_dict, data_time) - - opt = self.optimizers() - self.storage.put_scalar( - "lr", opt.param_groups[self._best_param_group_id]["lr"], smoothing_hint=False - ) - self.iteration_timer.after_step() - self.storage.step() - # A little odd to put before step here, but it's the best way to get a proper timing - self.iteration_timer.before_step() - - if self.storage.iter % 20 == 0: - for writer in self.writers: - writer.write() - return sum(loss_dict.values()) - - def training_step_end(self, training_step_outpus): - self.data_start = time.perf_counter() - return training_step_outpus - - def training_epoch_end(self, training_step_outputs) -> None: - self.iteration_timer.after_train() - if comm.is_main_process(): - self.checkpointer.save("model_final") - for writer in self.writers: - writer.write() - writer.close() - self.storage.__exit__(None, None, None) - - def _process_dataset_evaluation_results(self) -> OrderedDict: - results = OrderedDict() - for idx, dataset_name in enumerate(self.cfg.DATASETS.TEST): - results[dataset_name] = self._evaluators[idx].evaluate() - if comm.is_main_process(): - print_csv_format(results[dataset_name]) - - if len(results) == 1: - results = next(iter(results.values())) - return results - - def _reset_dataset_evaluators(self) -> None: - self._evaluators = [] - for dataset_name in self.cfg.DATASETS.TEST: - evaluator = build_evaluator(self.cfg, dataset_name) - evaluator.reset() - self._evaluators.append(evaluator) - - def on_validation_epoch_start(self, _outputs) -> None: - self._reset_dataset_evaluators() - - def validation_epoch_end(self, _outputs): - results = self._process_dataset_evaluation_results(_outputs) - - flattened_results = flatten_results_dict(results) - for k, v in flattened_results.items(): - try: - v = float(v) - except Exception as e: - raise ValueError( - f"[EvalHook] eval_function should return a nested dict of float. Got '{k}: {v}' instead." - ) from e - self.storage.put_scalars(**flattened_results, smoothing_hint=False) - - def validation_step(self, batch, batch_idx: int, dataloader_idx: int = 0) -> None: - if not isinstance(batch, list): - batch = [batch] - outputs = self.model(batch) - self._evaluators[dataloader_idx].process(batch, outputs) - - def configure_optimizers(self): - optimizer = build_optimizer(self.cfg, self.model) - self._best_param_group_id = hooks.LRScheduler.get_best_param_group_id(optimizer) - scheduler = build_lr_scheduler(self.cfg, optimizer) - return [optimizer], [{"scheduler": scheduler, "interval": "step"}] - - -class DataModule(LightningDataModule): - def __init__(self, cfg) -> None: - super().__init__() - self.cfg = DefaultTrainer.auto_scale_workers(cfg, comm.get_world_size()) - - def train_dataloader(self): - return build_detection_train_loader(self.cfg) - - def val_dataloader(self): - dataloaders = [] - for dataset_name in self.cfg.DATASETS.TEST: - dataloaders.append(build_detection_test_loader(self.cfg, dataset_name)) - return dataloaders - - -def main(args) -> None: - cfg = setup(args) - train(cfg, args) - - -def train(cfg, args) -> None: - trainer_params = { - # training loop is bounded by max steps, use a large max_epochs to make - # sure max_steps is met first - "max_epochs": 10**8, - "max_steps": cfg.SOLVER.MAX_ITER, - "val_check_interval": cfg.TEST.EVAL_PERIOD if cfg.TEST.EVAL_PERIOD > 0 else 10**8, - "num_nodes": args.num_machines, - "gpus": args.num_gpus, - "num_sanity_val_steps": 0, - } - if cfg.SOLVER.AMP.ENABLED: - trainer_params["precision"] = 16 - - last_checkpoint = os.path.join(cfg.OUTPUT_DIR, "last.ckpt") - if args.resume: - # resume training from checkpoint - trainer_params["resume_from_checkpoint"] = last_checkpoint - logger.info(f"Resuming training from checkpoint: {last_checkpoint}.") - - trainer = pl.Trainer(**trainer_params) - logger.info(f"start to train with {args.num_machines} nodes and {args.num_gpus} GPUs") - - module = TrainingModule(cfg) - data_module = DataModule(cfg) - if args.eval_only: - logger.info("Running inference") - trainer.validate(module, data_module) - else: - logger.info("Running training") - trainer.fit(module, data_module) - - -def setup(args): - """ - Create configs and perform basic setups. - """ - cfg = get_cfg() - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - cfg.freeze() - default_setup(cfg, args) - return cfg - - -if __name__ == "__main__": - parser = default_argument_parser() - args = parser.parse_args() - logger.info("Command Line Args:", args) - main(args) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/plain_train_net.py b/dimos/models/Detic/third_party/CenterNet2/tools/plain_train_net.py deleted file mode 100755 index a06d19aff2..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/plain_train_net.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. -""" -Detectron2 training script with a plain training loop. - -This script reads a given config file and runs the training or evaluation. -It is an entry point that is able to train standard models in detectron2. - -In order to let one script support training of many models, -this script contains logic that are specific to these built-in models and therefore -may not be suitable for your own project. -For example, your research project perhaps only needs a single "evaluator". - -Therefore, we recommend you to use detectron2 as a library and take -this file as an example of how to use the library. -You may want to write your own script with your datasets and other customizations. - -Compared to "train_net.py", this script supports fewer default features. -It also includes fewer abstraction, therefore is easier to add custom logic. -""" - -from collections import OrderedDict -import logging -import os - -from detectron2.checkpoint import DetectionCheckpointer, PeriodicCheckpointer -from detectron2.config import get_cfg -from detectron2.data import ( - MetadataCatalog, - build_detection_test_loader, - build_detection_train_loader, -) -from detectron2.engine import default_argument_parser, default_setup, default_writers, launch -from detectron2.evaluation import ( - CityscapesInstanceEvaluator, - CityscapesSemSegEvaluator, - COCOEvaluator, - COCOPanopticEvaluator, - DatasetEvaluators, - LVISEvaluator, - PascalVOCDetectionEvaluator, - SemSegEvaluator, - inference_on_dataset, - print_csv_format, -) -from detectron2.modeling import build_model -from detectron2.solver import build_lr_scheduler, build_optimizer -import detectron2.utils.comm as comm -from detectron2.utils.events import EventStorage -import torch -from torch.nn.parallel import DistributedDataParallel - -logger = logging.getLogger("detectron2") - - -def get_evaluator(cfg, dataset_name: str, output_folder=None): - """ - Create evaluator(s) for a given dataset. - This uses the special metadata "evaluator_type" associated with each builtin dataset. - For your own dataset, you can simply create an evaluator manually in your - script and do not have to worry about the hacky if-else logic here. - """ - if output_folder is None: - output_folder = os.path.join(cfg.OUTPUT_DIR, "inference") - evaluator_list = [] - evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type - if evaluator_type in ["sem_seg", "coco_panoptic_seg"]: - evaluator_list.append( - SemSegEvaluator( - dataset_name, - distributed=True, - output_dir=output_folder, - ) - ) - if evaluator_type in ["coco", "coco_panoptic_seg"]: - evaluator_list.append(COCOEvaluator(dataset_name, output_dir=output_folder)) - if evaluator_type == "coco_panoptic_seg": - evaluator_list.append(COCOPanopticEvaluator(dataset_name, output_folder)) - if evaluator_type == "cityscapes_instance": - assert torch.cuda.device_count() > comm.get_rank(), ( - "CityscapesEvaluator currently do not work with multiple machines." - ) - return CityscapesInstanceEvaluator(dataset_name) - if evaluator_type == "cityscapes_sem_seg": - assert torch.cuda.device_count() > comm.get_rank(), ( - "CityscapesEvaluator currently do not work with multiple machines." - ) - return CityscapesSemSegEvaluator(dataset_name) - if evaluator_type == "pascal_voc": - return PascalVOCDetectionEvaluator(dataset_name) - if evaluator_type == "lvis": - return LVISEvaluator(dataset_name, cfg, True, output_folder) - if len(evaluator_list) == 0: - raise NotImplementedError( - f"no Evaluator for the dataset {dataset_name} with the type {evaluator_type}" - ) - if len(evaluator_list) == 1: - return evaluator_list[0] - return DatasetEvaluators(evaluator_list) - - -def do_test(cfg, model): - results = OrderedDict() - for dataset_name in cfg.DATASETS.TEST: - data_loader = build_detection_test_loader(cfg, dataset_name) - evaluator = get_evaluator( - cfg, dataset_name, os.path.join(cfg.OUTPUT_DIR, "inference", dataset_name) - ) - results_i = inference_on_dataset(model, data_loader, evaluator) - results[dataset_name] = results_i - if comm.is_main_process(): - logger.info(f"Evaluation results for {dataset_name} in csv format:") - print_csv_format(results_i) - if len(results) == 1: - results = next(iter(results.values())) - return results - - -def do_train(cfg, model, resume: bool=False) -> None: - model.train() - optimizer = build_optimizer(cfg, model) - scheduler = build_lr_scheduler(cfg, optimizer) - - checkpointer = DetectionCheckpointer( - model, cfg.OUTPUT_DIR, optimizer=optimizer, scheduler=scheduler - ) - start_iter = ( - checkpointer.resume_or_load(cfg.MODEL.WEIGHTS, resume=resume).get("iteration", -1) + 1 - ) - max_iter = cfg.SOLVER.MAX_ITER - - periodic_checkpointer = PeriodicCheckpointer( - checkpointer, cfg.SOLVER.CHECKPOINT_PERIOD, max_iter=max_iter - ) - - writers = default_writers(cfg.OUTPUT_DIR, max_iter) if comm.is_main_process() else [] - - # compared to "train_net.py", we do not support accurate timing and - # precise BN here, because they are not trivial to implement in a small training loop - data_loader = build_detection_train_loader(cfg) - logger.info(f"Starting training from iteration {start_iter}") - with EventStorage(start_iter) as storage: - for data, iteration in zip(data_loader, range(start_iter, max_iter), strict=False): - storage.iter = iteration - - loss_dict = model(data) - losses = sum(loss_dict.values()) - assert torch.isfinite(losses).all(), loss_dict - - loss_dict_reduced = {k: v.item() for k, v in comm.reduce_dict(loss_dict).items()} - losses_reduced = sum(loss for loss in loss_dict_reduced.values()) - if comm.is_main_process(): - storage.put_scalars(total_loss=losses_reduced, **loss_dict_reduced) - - optimizer.zero_grad() - losses.backward() - optimizer.step() - storage.put_scalar("lr", optimizer.param_groups[0]["lr"], smoothing_hint=False) - scheduler.step() - - if ( - cfg.TEST.EVAL_PERIOD > 0 - and (iteration + 1) % cfg.TEST.EVAL_PERIOD == 0 - and iteration != max_iter - 1 - ): - do_test(cfg, model) - # Compared to "train_net.py", the test results are not dumped to EventStorage - comm.synchronize() - - if iteration - start_iter > 5 and ( - (iteration + 1) % 20 == 0 or iteration == max_iter - 1 - ): - for writer in writers: - writer.write() - periodic_checkpointer.step(iteration) - - -def setup(args): - """ - Create configs and perform basic setups. - """ - cfg = get_cfg() - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - cfg.freeze() - default_setup( - cfg, args - ) # if you don't like any of the default setup, write your own setup code - return cfg - - -def main(args): - cfg = setup(args) - - model = build_model(cfg) - logger.info(f"Model:\n{model}") - if args.eval_only: - DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load( - cfg.MODEL.WEIGHTS, resume=args.resume - ) - return do_test(cfg, model) - - distributed = comm.get_world_size() > 1 - if distributed: - model = DistributedDataParallel( - model, device_ids=[comm.get_local_rank()], broadcast_buffers=False - ) - - do_train(cfg, model, resume=args.resume) - return do_test(cfg, model) - - -if __name__ == "__main__": - args = default_argument_parser().parse_args() - print("Command Line Args:", args) - launch( - main, - args.num_gpus, - num_machines=args.num_machines, - machine_rank=args.machine_rank, - dist_url=args.dist_url, - args=(args,), - ) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/train_net.py b/dimos/models/Detic/third_party/CenterNet2/tools/train_net.py deleted file mode 100755 index deb2ca6db8..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/train_net.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. -""" -A main training script. - -This scripts reads a given config file and runs the training or evaluation. -It is an entry point that is made to train standard models in detectron2. - -In order to let one script support training of many models, -this script contains logic that are specific to these built-in models and therefore -may not be suitable for your own project. -For example, your research project perhaps only needs a single "evaluator". - -Therefore, we recommend you to use detectron2 as an library and take -this file as an example of how to use the library. -You may want to write your own script with your datasets and other customizations. -""" - -from collections import OrderedDict -import logging -import os - -from detectron2.checkpoint import DetectionCheckpointer -from detectron2.config import get_cfg -from detectron2.data import MetadataCatalog -from detectron2.engine import DefaultTrainer, default_argument_parser, default_setup, hooks, launch -from detectron2.evaluation import ( - CityscapesInstanceEvaluator, - CityscapesSemSegEvaluator, - COCOEvaluator, - COCOPanopticEvaluator, - DatasetEvaluators, - LVISEvaluator, - PascalVOCDetectionEvaluator, - SemSegEvaluator, - verify_results, -) -from detectron2.modeling import GeneralizedRCNNWithTTA -import detectron2.utils.comm as comm -import torch - - -def build_evaluator(cfg, dataset_name: str, output_folder=None): - """ - Create evaluator(s) for a given dataset. - This uses the special metadata "evaluator_type" associated with each builtin dataset. - For your own dataset, you can simply create an evaluator manually in your - script and do not have to worry about the hacky if-else logic here. - """ - if output_folder is None: - output_folder = os.path.join(cfg.OUTPUT_DIR, "inference") - evaluator_list = [] - evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type - if evaluator_type in ["sem_seg", "coco_panoptic_seg"]: - evaluator_list.append( - SemSegEvaluator( - dataset_name, - distributed=True, - output_dir=output_folder, - ) - ) - if evaluator_type in ["coco", "coco_panoptic_seg"]: - evaluator_list.append(COCOEvaluator(dataset_name, output_dir=output_folder)) - if evaluator_type == "coco_panoptic_seg": - evaluator_list.append(COCOPanopticEvaluator(dataset_name, output_folder)) - if evaluator_type == "cityscapes_instance": - assert torch.cuda.device_count() > comm.get_rank(), ( - "CityscapesEvaluator currently do not work with multiple machines." - ) - return CityscapesInstanceEvaluator(dataset_name) - if evaluator_type == "cityscapes_sem_seg": - assert torch.cuda.device_count() > comm.get_rank(), ( - "CityscapesEvaluator currently do not work with multiple machines." - ) - return CityscapesSemSegEvaluator(dataset_name) - elif evaluator_type == "pascal_voc": - return PascalVOCDetectionEvaluator(dataset_name) - elif evaluator_type == "lvis": - return LVISEvaluator(dataset_name, output_dir=output_folder) - if len(evaluator_list) == 0: - raise NotImplementedError( - f"no Evaluator for the dataset {dataset_name} with the type {evaluator_type}" - ) - elif len(evaluator_list) == 1: - return evaluator_list[0] - return DatasetEvaluators(evaluator_list) - - -class Trainer(DefaultTrainer): - """ - We use the "DefaultTrainer" which contains pre-defined default logic for - standard training workflow. They may not work for you, especially if you - are working on a new research project. In that case you can write your - own training loop. You can use "tools/plain_train_net.py" as an example. - """ - - @classmethod - def build_evaluator(cls, cfg, dataset_name: str, output_folder=None): - return build_evaluator(cfg, dataset_name, output_folder) - - @classmethod - def test_with_TTA(cls, cfg, model): - logger = logging.getLogger("detectron2.trainer") - # In the end of training, run an evaluation with TTA - # Only support some R-CNN models. - logger.info("Running inference with test-time augmentation ...") - model = GeneralizedRCNNWithTTA(cfg, model) - evaluators = [ - cls.build_evaluator( - cfg, name, output_folder=os.path.join(cfg.OUTPUT_DIR, "inference_TTA") - ) - for name in cfg.DATASETS.TEST - ] - res = cls.test(cfg, model, evaluators) - res = OrderedDict({k + "_TTA": v for k, v in res.items()}) - return res - - -def setup(args): - """ - Create configs and perform basic setups. - """ - cfg = get_cfg() - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - cfg.freeze() - default_setup(cfg, args) - return cfg - - -def main(args): - cfg = setup(args) - - if args.eval_only: - model = Trainer.build_model(cfg) - DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load( - cfg.MODEL.WEIGHTS, resume=args.resume - ) - res = Trainer.test(cfg, model) - if cfg.TEST.AUG.ENABLED: - res.update(Trainer.test_with_TTA(cfg, model)) - if comm.is_main_process(): - verify_results(cfg, res) - return res - - """ - If you'd like to do anything fancier than the standard training logic, - consider writing your own training loop (see plain_train_net.py) or - subclassing the trainer. - """ - trainer = Trainer(cfg) - trainer.resume_or_load(resume=args.resume) - if cfg.TEST.AUG.ENABLED: - trainer.register_hooks( - [hooks.EvalHook(0, lambda: trainer.test_with_TTA(cfg, trainer.model))] - ) - return trainer.train() - - -if __name__ == "__main__": - args = default_argument_parser().parse_args() - print("Command Line Args:", args) - launch( - main, - args.num_gpus, - num_machines=args.num_machines, - machine_rank=args.machine_rank, - dist_url=args.dist_url, - args=(args,), - ) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/visualize_data.py b/dimos/models/Detic/third_party/CenterNet2/tools/visualize_data.py deleted file mode 100755 index 99abfdff4e..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/visualize_data.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -from itertools import chain -import os - -import cv2 -from detectron2.config import get_cfg -from detectron2.data import ( - DatasetCatalog, - MetadataCatalog, - build_detection_train_loader, - detection_utils as utils, -) -from detectron2.data.build import filter_images_with_few_keypoints -from detectron2.utils.logger import setup_logger -from detectron2.utils.visualizer import Visualizer -import tqdm - - -def setup(args): - cfg = get_cfg() - if args.config_file: - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - cfg.DATALOADER.NUM_WORKERS = 0 - cfg.freeze() - return cfg - - -def parse_args(in_args=None): - parser = argparse.ArgumentParser(description="Visualize ground-truth data") - parser.add_argument( - "--source", - choices=["annotation", "dataloader"], - required=True, - help="visualize the annotations or the data loader (with pre-processing)", - ) - parser.add_argument("--config-file", metavar="FILE", help="path to config file") - parser.add_argument("--output-dir", default="./", help="path to output directory") - parser.add_argument("--show", action="store_true", help="show output in a window") - parser.add_argument( - "opts", - help="Modify config options using the command-line", - default=None, - nargs=argparse.REMAINDER, - ) - return parser.parse_args(in_args) - - -if __name__ == "__main__": - args = parse_args() - logger = setup_logger() - logger.info("Arguments: " + str(args)) - cfg = setup(args) - - dirname = args.output_dir - os.makedirs(dirname, exist_ok=True) - metadata = MetadataCatalog.get(cfg.DATASETS.TRAIN[0]) - - def output(vis, fname) -> None: - if args.show: - print(fname) - cv2.imshow("window", vis.get_image()[:, :, ::-1]) - cv2.waitKey() - else: - filepath = os.path.join(dirname, fname) - print(f"Saving to {filepath} ...") - vis.save(filepath) - - scale = 1.0 - if args.source == "dataloader": - train_data_loader = build_detection_train_loader(cfg) - for batch in train_data_loader: - for per_image in batch: - # Pytorch tensor is in (C, H, W) format - img = per_image["image"].permute(1, 2, 0).cpu().detach().numpy() - img = utils.convert_image_to_rgb(img, cfg.INPUT.FORMAT) - - visualizer = Visualizer(img, metadata=metadata, scale=scale) - target_fields = per_image["instances"].get_fields() - labels = [metadata.thing_classes[i] for i in target_fields["gt_classes"]] - vis = visualizer.overlay_instances( - labels=labels, - boxes=target_fields.get("gt_boxes", None), - masks=target_fields.get("gt_masks", None), - keypoints=target_fields.get("gt_keypoints", None), - ) - output(vis, str(per_image["image_id"]) + ".jpg") - else: - dicts = list(chain.from_iterable([DatasetCatalog.get(k) for k in cfg.DATASETS.TRAIN])) - if cfg.MODEL.KEYPOINT_ON: - dicts = filter_images_with_few_keypoints(dicts, 1) - for dic in tqdm.tqdm(dicts): - img = utils.read_image(dic["file_name"], "RGB") - visualizer = Visualizer(img, metadata=metadata, scale=scale) - vis = visualizer.draw_dataset_dict(dic) - output(vis, os.path.basename(dic["file_name"])) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/visualize_json_results.py b/dimos/models/Detic/third_party/CenterNet2/tools/visualize_json_results.py deleted file mode 100755 index 04dea72446..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/tools/visualize_json_results.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. - -import argparse -from collections import defaultdict -import json -import os - -import cv2 -from detectron2.data import DatasetCatalog, MetadataCatalog -from detectron2.structures import Boxes, BoxMode, Instances -from detectron2.utils.file_io import PathManager -from detectron2.utils.logger import setup_logger -from detectron2.utils.visualizer import Visualizer -import numpy as np -import tqdm - - -def create_instances(predictions, image_size: int): - ret = Instances(image_size) - - score = np.asarray([x["score"] for x in predictions]) - chosen = (score > args.conf_threshold).nonzero()[0] - score = score[chosen] - bbox = np.asarray([predictions[i]["bbox"] for i in chosen]).reshape(-1, 4) - bbox = BoxMode.convert(bbox, BoxMode.XYWH_ABS, BoxMode.XYXY_ABS) - - labels = np.asarray([dataset_id_map(predictions[i]["category_id"]) for i in chosen]) - - ret.scores = score - ret.pred_boxes = Boxes(bbox) - ret.pred_classes = labels - - try: - ret.pred_masks = [predictions[i]["segmentation"] for i in chosen] - except KeyError: - pass - return ret - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="A script that visualizes the json predictions from COCO or LVIS dataset." - ) - parser.add_argument("--input", required=True, help="JSON file produced by the model") - parser.add_argument("--output", required=True, help="output directory") - parser.add_argument("--dataset", help="name of the dataset", default="coco_2017_val") - parser.add_argument("--conf-threshold", default=0.5, type=float, help="confidence threshold") - args = parser.parse_args() - - logger = setup_logger() - - with PathManager.open(args.input, "r") as f: - predictions = json.load(f) - - pred_by_image = defaultdict(list) - for p in predictions: - pred_by_image[p["image_id"]].append(p) - - dicts = list(DatasetCatalog.get(args.dataset)) - metadata = MetadataCatalog.get(args.dataset) - if hasattr(metadata, "thing_dataset_id_to_contiguous_id"): - - def dataset_id_map(ds_id): - return metadata.thing_dataset_id_to_contiguous_id[ds_id] - - elif "lvis" in args.dataset: - # LVIS results are in the same format as COCO results, but have a different - # mapping from dataset category id to contiguous category id in [0, #categories - 1] - def dataset_id_map(ds_id): - return ds_id - 1 - - else: - raise ValueError(f"Unsupported dataset: {args.dataset}") - - os.makedirs(args.output, exist_ok=True) - - for dic in tqdm.tqdm(dicts): - img = cv2.imread(dic["file_name"], cv2.IMREAD_COLOR)[:, :, ::-1] - basename = os.path.basename(dic["file_name"]) - - predictions = create_instances(pred_by_image[dic["image_id"]], img.shape[:2]) - vis = Visualizer(img, metadata) - vis_pred = vis.draw_instance_predictions(predictions).get_image() - - vis = Visualizer(img, metadata) - vis_gt = vis.draw_dataset_dict(dic).get_image() - - concat = np.concatenate((vis_pred, vis_gt), axis=1) - cv2.imwrite(os.path.join(args.output, basename), concat[:, :, ::-1]) diff --git a/dimos/models/Detic/third_party/CenterNet2/train_net.py b/dimos/models/Detic/third_party/CenterNet2/train_net.py deleted file mode 100644 index 92859d7586..0000000000 --- a/dimos/models/Detic/third_party/CenterNet2/train_net.py +++ /dev/null @@ -1,227 +0,0 @@ -from collections import OrderedDict -import datetime -import logging -import os -import time - -from centernet.config import add_centernet_config -from centernet.data.custom_build_augmentation import build_custom_augmentation -from detectron2.checkpoint import DetectionCheckpointer, PeriodicCheckpointer -from detectron2.config import get_cfg -from detectron2.data import ( - MetadataCatalog, - build_detection_test_loader, -) -from detectron2.data.build import build_detection_train_loader -from detectron2.data.dataset_mapper import DatasetMapper -from detectron2.engine import default_argument_parser, default_setup, launch -from detectron2.evaluation import ( - COCOEvaluator, - LVISEvaluator, - inference_on_dataset, - print_csv_format, -) -from detectron2.modeling import build_model -from detectron2.modeling.test_time_augmentation import GeneralizedRCNNWithTTA -from detectron2.solver import build_lr_scheduler, build_optimizer -import detectron2.utils.comm as comm -from detectron2.utils.events import ( - CommonMetricPrinter, - EventStorage, - JSONWriter, - TensorboardXWriter, -) -from fvcore.common.timer import Timer -import torch -from torch.nn.parallel import DistributedDataParallel - -logger = logging.getLogger("detectron2") - - -def do_test(cfg, model): - results = OrderedDict() - for dataset_name in cfg.DATASETS.TEST: - mapper = ( - None - if cfg.INPUT.TEST_INPUT_TYPE == "default" - else DatasetMapper(cfg, False, augmentations=build_custom_augmentation(cfg, False)) - ) - data_loader = build_detection_test_loader(cfg, dataset_name, mapper=mapper) - output_folder = os.path.join(cfg.OUTPUT_DIR, f"inference_{dataset_name}") - evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type - - if evaluator_type == "lvis": - evaluator = LVISEvaluator(dataset_name, cfg, True, output_folder) - elif evaluator_type == "coco": - evaluator = COCOEvaluator(dataset_name, cfg, True, output_folder) - else: - assert 0, evaluator_type - - results[dataset_name] = inference_on_dataset(model, data_loader, evaluator) - if comm.is_main_process(): - logger.info(f"Evaluation results for {dataset_name} in csv format:") - print_csv_format(results[dataset_name]) - if len(results) == 1: - results = next(iter(results.values())) - return results - - -def do_train(cfg, model, resume: bool=False) -> None: - model.train() - optimizer = build_optimizer(cfg, model) - scheduler = build_lr_scheduler(cfg, optimizer) - - checkpointer = DetectionCheckpointer( - model, cfg.OUTPUT_DIR, optimizer=optimizer, scheduler=scheduler - ) - - start_iter = ( - checkpointer.resume_or_load( - cfg.MODEL.WEIGHTS, - resume=resume, - ).get("iteration", -1) - + 1 - ) - if cfg.SOLVER.RESET_ITER: - logger.info("Reset loaded iteration. Start training from iteration 0.") - start_iter = 0 - max_iter = cfg.SOLVER.MAX_ITER if cfg.SOLVER.TRAIN_ITER < 0 else cfg.SOLVER.TRAIN_ITER - - periodic_checkpointer = PeriodicCheckpointer( - checkpointer, cfg.SOLVER.CHECKPOINT_PERIOD, max_iter=max_iter - ) - - writers = ( - [ - CommonMetricPrinter(max_iter), - JSONWriter(os.path.join(cfg.OUTPUT_DIR, "metrics.json")), - TensorboardXWriter(cfg.OUTPUT_DIR), - ] - if comm.is_main_process() - else [] - ) - - mapper = ( - DatasetMapper(cfg, True) - if cfg.INPUT.CUSTOM_AUG == "" - else DatasetMapper(cfg, True, augmentations=build_custom_augmentation(cfg, True)) - ) - if cfg.DATALOADER.SAMPLER_TRAIN in ["TrainingSampler", "RepeatFactorTrainingSampler"]: - data_loader = build_detection_train_loader(cfg, mapper=mapper) - else: - from centernet.data.custom_dataset_dataloader import build_custom_train_loader - - data_loader = build_custom_train_loader(cfg, mapper=mapper) - - logger.info(f"Starting training from iteration {start_iter}") - with EventStorage(start_iter) as storage: - step_timer = Timer() - data_timer = Timer() - start_time = time.perf_counter() - for data, iteration in zip(data_loader, range(start_iter, max_iter), strict=False): - data_time = data_timer.seconds() - storage.put_scalars(data_time=data_time) - step_timer.reset() - iteration = iteration + 1 - storage.step() - loss_dict = model(data) - - losses = sum(loss for k, loss in loss_dict.items()) - assert torch.isfinite(losses).all(), loss_dict - - loss_dict_reduced = {k: v.item() for k, v in comm.reduce_dict(loss_dict).items()} - losses_reduced = sum(loss for loss in loss_dict_reduced.values()) - if comm.is_main_process(): - storage.put_scalars(total_loss=losses_reduced, **loss_dict_reduced) - - optimizer.zero_grad() - losses.backward() - optimizer.step() - - storage.put_scalar("lr", optimizer.param_groups[0]["lr"], smoothing_hint=False) - - step_time = step_timer.seconds() - storage.put_scalars(time=step_time) - data_timer.reset() - scheduler.step() - - if ( - cfg.TEST.EVAL_PERIOD > 0 - and iteration % cfg.TEST.EVAL_PERIOD == 0 - and iteration != max_iter - ): - do_test(cfg, model) - comm.synchronize() - - if iteration - start_iter > 5 and (iteration % 20 == 0 or iteration == max_iter): - for writer in writers: - writer.write() - periodic_checkpointer.step(iteration) - - total_time = time.perf_counter() - start_time - logger.info( - f"Total training time: {datetime.timedelta(seconds=int(total_time))!s}" - ) - - -def setup(args): - """ - Create configs and perform basic setups. - """ - cfg = get_cfg() - add_centernet_config(cfg) - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - if "/auto" in cfg.OUTPUT_DIR: - file_name = os.path.basename(args.config_file)[:-5] - cfg.OUTPUT_DIR = cfg.OUTPUT_DIR.replace("/auto", f"/{file_name}") - logger.info(f"OUTPUT_DIR: {cfg.OUTPUT_DIR}") - cfg.freeze() - default_setup(cfg, args) - return cfg - - -def main(args): - cfg = setup(args) - - model = build_model(cfg) - logger.info(f"Model:\n{model}") - if args.eval_only: - DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load( - cfg.MODEL.WEIGHTS, resume=args.resume - ) - if cfg.TEST.AUG.ENABLED: - logger.info("Running inference with test-time augmentation ...") - model = GeneralizedRCNNWithTTA(cfg, model, batch_size=1) - - return do_test(cfg, model) - - distributed = comm.get_world_size() > 1 - if distributed: - model = DistributedDataParallel( - model, - device_ids=[comm.get_local_rank()], - broadcast_buffers=False, - find_unused_parameters=True, - ) - - do_train(cfg, model, resume=args.resume) - return do_test(cfg, model) - - -if __name__ == "__main__": - args = default_argument_parser() - args.add_argument("--manual_device", default="") - args = args.parse_args() - if args.manual_device != "": - os.environ["CUDA_VISIBLE_DEVICES"] = args.manual_device - args.dist_url = f"tcp://127.0.0.1:{torch.randint(11111, 60000, (1,))[0].item()}" - print("Command Line Args:", args) - launch( - main, - args.num_gpus, - num_machines=args.num_machines, - machine_rank=args.machine_rank, - dist_url=args.dist_url, - args=(args,), - ) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/LICENSE b/dimos/models/Detic/third_party/Deformable-DETR/LICENSE deleted file mode 100644 index 522e5bd3b6..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/LICENSE +++ /dev/null @@ -1,220 +0,0 @@ -Copyright (c) 2020 SenseTime. All Rights Reserved. - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2020 SenseTime - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -DETR - -Copyright 2020 - present, Facebook, Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/dimos/models/Detic/third_party/Deformable-DETR/README.md b/dimos/models/Detic/third_party/Deformable-DETR/README.md deleted file mode 100644 index c9db563511..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# Deformable DETR - -By [Xizhou Zhu](https://scholar.google.com/citations?user=02RXI00AAAAJ), [Weijie Su](https://www.weijiesu.com/), [Lewei Lu](https://www.linkedin.com/in/lewei-lu-94015977/), [Bin Li](http://staff.ustc.edu.cn/~binli/), [Xiaogang Wang](http://www.ee.cuhk.edu.hk/~xgwang/), [Jifeng Dai](https://jifengdai.org/). - -This repository is an official implementation of the paper [Deformable DETR: Deformable Transformers for End-to-End Object Detection](https://arxiv.org/abs/2010.04159). - - -## Introduction - -**TL; DR.** Deformable DETR is an efficient and fast-converging end-to-end object detector. It mitigates the high complexity and slow convergence issues of DETR via a novel sampling-based efficient attention mechanism. - -![deformable_detr](./figs/illustration.png) - -![deformable_detr](./figs/convergence.png) - -**Abstract.** DETR has been recently proposed to eliminate the need for many hand-designed components in object detection while demonstrating good performance. However, it suffers from slow convergence and limited feature spatial resolution, due to the limitation of Transformer attention modules in processing image feature maps. To mitigate these issues, we proposed Deformable DETR, whose attention modules only attend to a small set of key sampling points around a reference. Deformable DETR can achieve better performance than DETR (especially on small objects) with 10Ɨ less training epochs. Extensive experiments on the COCO benchmark demonstrate the effectiveness of our approach. - -## License - -This project is released under the [Apache 2.0 license](./LICENSE). - -## Changelog - -See [changelog.md](./docs/changelog.md) for detailed logs of major changes. - - -## Citing Deformable DETR -If you find Deformable DETR useful in your research, please consider citing: -```bibtex -@article{zhu2020deformable, - title={Deformable DETR: Deformable Transformers for End-to-End Object Detection}, - author={Zhu, Xizhou and Su, Weijie and Lu, Lewei and Li, Bin and Wang, Xiaogang and Dai, Jifeng}, - journal={arXiv preprint arXiv:2010.04159}, - year={2020} -} -``` - -## Main Results - -| Method | Epochs | AP | APS | APM | APL | params
(M)
| FLOPs
(G)
| Total
Train
Time
(GPU
hours)
| Train
Speed
(GPU
hours
/epoch)
| Infer
Speed
(FPS)
| Batch
Infer
Speed
(FPS)
| URL | -| ----------------------------------- | :----: | :--: | :----: | :---: | :------------------------------: | :--------------------:| :----------------------------------------------------------: | :--: | :---: | :---: | ----- | ----- | -| Faster R-CNN + FPN | 109 | 42.0 | 26.6 | 45.4 | 53.4 | 42 | 180 | 380 | 3.5 | 25.6 | 28.0 | - | -| DETR | 500 | 42.0 | 20.5 | 45.8 | 61.1 | 41 | 86 | 2000 | 4.0 | 27.0 | 38.3 | - | -| DETR-DC5 | 500 | 43.3 | 22.5 | 47.3 | 61.1 | 41 |187|7000|14.0|11.4|12.4| - | -| DETR-DC5 | 50 | 35.3 | 15.2 | 37.5 | 53.6 | 41 |187|700|14.0|11.4|12.4| - | -| DETR-DC5+ | 50 | 36.2 | 16.3 | 39.2 | 53.9 | 41 |187|700|14.0|11.4|12.4| - | -| **Deformable DETR
(single scale)
** | 50 | 39.4 | 20.6 | 43.0 | 55.5 | 34 |78|160|3.2|27.0|42.4| [config](./configs/r50_deformable_detr_single_scale.sh)
[log](https://drive.google.com/file/d/1n3ZnZ-UAqmTUR4AZoM4qQntIDn6qCZx4/view?usp=sharing)
[model](https://drive.google.com/file/d/1WEjQ9_FgfI5sw5OZZ4ix-OKk-IJ_-SDU/view?usp=sharing)
| -| **Deformable DETR
(single scale, DC5)
** | 50 | 41.5 | 24.1 | 45.3 | 56.0 | 34 |128|215|4.3|22.1|29.4| [config](./configs/r50_deformable_detr_single_scale_dc5.sh)
[log](https://drive.google.com/file/d/1-UfTp2q4GIkJjsaMRIkQxa5k5vn8_n-B/view?usp=sharing)
[model](https://drive.google.com/file/d/1m_TgMjzH7D44fbA-c_jiBZ-xf-odxGdk/view?usp=sharing)
| -| **Deformable DETR** | 50 | 44.5 | 27.1 | 47.6 | 59.6 | 40 |173|325|6.5|15.0|19.4|[config](./configs/r50_deformable_detr.sh)
[log](https://drive.google.com/file/d/18YSLshFjc_erOLfFC-hHu4MX4iyz1Dqr/view?usp=sharing)
[model](https://drive.google.com/file/d/1nDWZWHuRwtwGden77NLM9JoWe-YisJnA/view?usp=sharing)
| -| **+ iterative bounding box refinement** | 50 | 46.2 | 28.3 | 49.2 | 61.5 | 41 |173|325|6.5|15.0|19.4|[config](./configs/r50_deformable_detr_plus_iterative_bbox_refinement.sh)
[log](https://drive.google.com/file/d/1DFNloITi1SFBWjYzvVEAI75ndwmGM1Uj/view?usp=sharing)
[model](https://drive.google.com/file/d/1JYKyRYzUH7uo9eVfDaVCiaIGZb5YTCuI/view?usp=sharing)
| -| **++ two-stage Deformable DETR** | 50 | 46.9 | 29.6 | 50.1 | 61.6 | 41 |173|340|6.8|14.5|18.8|[config](./configs/r50_deformable_detr_plus_iterative_bbox_refinement_plus_plus_two_stage.sh)
[log](https://drive.google.com/file/d/1ozi0wbv5-Sc5TbWt1jAuXco72vEfEtbY/view?usp=sharing)
[model](https://drive.google.com/file/d/15I03A7hNTpwuLNdfuEmW9_taZMNVssEp/view?usp=sharing)
| - -*Note:* - -1. All models of Deformable DETR are trained with total batch size of 32. -2. Training and inference speed are measured on NVIDIA Tesla V100 GPU. -3. "Deformable DETR (single scale)" means only using res5 feature map (of stride 32) as input feature maps for Deformable Transformer Encoder. -4. "DC5" means removing the stride in C5 stage of ResNet and add a dilation of 2 instead. -5. "DETR-DC5+" indicates DETR-DC5 with some modifications, including using Focal Loss for bounding box classification and increasing number of object queries to 300. -6. "Batch Infer Speed" refer to inference with batch size = 4 to maximize GPU utilization. -7. The original implementation is based on our internal codebase. There are slight differences in the final accuracy and running time due to the plenty details in platform switch. - - -## Installation - -### Requirements - -* Linux, CUDA>=9.2, GCC>=5.4 - -* Python>=3.7 - - We recommend you to use Anaconda to create a conda environment: - ```bash - conda create -n deformable_detr python=3.7 pip - ``` - Then, activate the environment: - ```bash - conda activate deformable_detr - ``` - -* PyTorch>=1.5.1, torchvision>=0.6.1 (following instructions [here](https://pytorch.org/)) - - For example, if your CUDA version is 9.2, you could install pytorch and torchvision as following: - ```bash - conda install pytorch=1.5.1 torchvision=0.6.1 cudatoolkit=9.2 -c pytorch - ``` - -* Other requirements - ```bash - pip install -r requirements.txt - ``` - -### Compiling CUDA operators -```bash -cd ./models/ops -sh ./make.sh -# unit test (should see all checking is True) -python test.py -``` - -## Usage - -### Dataset preparation - -Please download [COCO 2017 dataset](https://cocodataset.org/) and organize them as following: - -``` -code_root/ -└── data/ - └── coco/ - ā”œā”€ā”€ train2017/ - ā”œā”€ā”€ val2017/ - └── annotations/ - ā”œā”€ā”€ instances_train2017.json - └── instances_val2017.json -``` - -### Training - -#### Training on single node - -For example, the command for training Deformable DETR on 8 GPUs is as following: - -```bash -GPUS_PER_NODE=8 ./tools/run_dist_launch.sh 8 ./configs/r50_deformable_detr.sh -``` - -#### Training on multiple nodes - -For example, the command for training Deformable DETR on 2 nodes of each with 8 GPUs is as following: - -On node 1: - -```bash -MASTER_ADDR= NODE_RANK=0 GPUS_PER_NODE=8 ./tools/run_dist_launch.sh 16 ./configs/r50_deformable_detr.sh -``` - -On node 2: - -```bash -MASTER_ADDR= NODE_RANK=1 GPUS_PER_NODE=8 ./tools/run_dist_launch.sh 16 ./configs/r50_deformable_detr.sh -``` - -#### Training on slurm cluster - -If you are using slurm cluster, you can simply run the following command to train on 1 node with 8 GPUs: - -```bash -GPUS_PER_NODE=8 ./tools/run_dist_slurm.sh deformable_detr 8 configs/r50_deformable_detr.sh -``` - -Or 2 nodes of each with 8 GPUs: - -```bash -GPUS_PER_NODE=8 ./tools/run_dist_slurm.sh deformable_detr 16 configs/r50_deformable_detr.sh -``` -#### Some tips to speed-up training -* If your file system is slow to read images, you may consider enabling '--cache_mode' option to load whole dataset into memory at the beginning of training. -* You may increase the batch size to maximize the GPU utilization, according to GPU memory of yours, e.g., set '--batch_size 3' or '--batch_size 4'. - -### Evaluation - -You can get the config file and pretrained model of Deformable DETR (the link is in "Main Results" session), then run following command to evaluate it on COCO 2017 validation set: - -```bash - --resume --eval -``` - -You can also run distributed evaluation by using ```./tools/run_dist_launch.sh``` or ```./tools/run_dist_slurm.sh```. diff --git a/dimos/models/Detic/third_party/Deformable-DETR/benchmark.py b/dimos/models/Detic/third_party/Deformable-DETR/benchmark.py deleted file mode 100644 index 3a4fcbd4e6..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/benchmark.py +++ /dev/null @@ -1,70 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ - -""" -Benchmark inference speed of Deformable DETR. -""" - -import argparse -import os -import time - -from datasets import build_dataset -from main import get_args_parser as get_main_args_parser -from models import build_model -import torch -from util.misc import nested_tensor_from_tensor_list - - -def get_benckmark_arg_parser(): - parser = argparse.ArgumentParser("Benchmark inference speed of Deformable DETR.") - parser.add_argument("--num_iters", type=int, default=300, help="total iters to benchmark speed") - parser.add_argument( - "--warm_iters", type=int, default=5, help="ignore first several iters that are very slow" - ) - parser.add_argument("--batch_size", type=int, default=1, help="batch size in inference") - parser.add_argument("--resume", type=str, help="load the pre-trained checkpoint") - return parser - - -@torch.no_grad() -def measure_average_inference_time(model, inputs, num_iters: int=100, warm_iters: int=5): - ts = [] - for iter_ in range(num_iters): - torch.cuda.synchronize() - t_ = time.perf_counter() - model(inputs) - torch.cuda.synchronize() - t = time.perf_counter() - t_ - if iter_ >= warm_iters: - ts.append(t) - print(ts) - return sum(ts) / len(ts) - - -def benchmark(): - args, _ = get_benckmark_arg_parser().parse_known_args() - main_args = get_main_args_parser().parse_args(_) - assert args.warm_iters < args.num_iters and args.num_iters > 0 and args.warm_iters >= 0 - assert args.batch_size > 0 - assert args.resume is None or os.path.exists(args.resume) - dataset = build_dataset("val", main_args) - model, _, _ = build_model(main_args) - model.cuda() - model.eval() - if args.resume is not None: - ckpt = torch.load(args.resume, map_location=lambda storage, loc: storage) - model.load_state_dict(ckpt["model"]) - inputs = nested_tensor_from_tensor_list( - [dataset.__getitem__(0)[0].cuda() for _ in range(args.batch_size)] - ) - t = measure_average_inference_time(model, inputs, args.num_iters, args.warm_iters) - return 1.0 / t * args.batch_size - - -if __name__ == "__main__": - fps = benchmark() - print(f"Inference Speed: {fps:.1f} FPS") diff --git a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr.sh b/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr.sh deleted file mode 100755 index a42953f266..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -x - -EXP_DIR=exps/r50_deformable_detr -PY_ARGS=${@:1} - -python -u main.py \ - --output_dir ${EXP_DIR} \ - ${PY_ARGS} diff --git a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_plus_iterative_bbox_refinement.sh b/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_plus_iterative_bbox_refinement.sh deleted file mode 100755 index 8ea20006b1..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_plus_iterative_bbox_refinement.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -x - -EXP_DIR=exps/r50_deformable_detr_plus_iterative_bbox_refinement -PY_ARGS=${@:1} - -python -u main.py \ - --output_dir ${EXP_DIR} \ - --with_box_refine \ - ${PY_ARGS} diff --git a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_plus_iterative_bbox_refinement_plus_plus_two_stage.sh b/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_plus_iterative_bbox_refinement_plus_plus_two_stage.sh deleted file mode 100755 index 722c658e45..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_plus_iterative_bbox_refinement_plus_plus_two_stage.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -x - -EXP_DIR=exps/r50_deformable_detr_plus_iterative_bbox_refinement_plus_plus_two_stage -PY_ARGS=${@:1} - -python -u main.py \ - --output_dir ${EXP_DIR} \ - --with_box_refine \ - --two_stage \ - ${PY_ARGS} diff --git a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_single_scale.sh b/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_single_scale.sh deleted file mode 100755 index a24e54718d..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_single_scale.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -x - -EXP_DIR=exps/r50_deformable_detr_single_scale -PY_ARGS=${@:1} - -python -u main.py \ - --num_feature_levels 1 \ - --output_dir ${EXP_DIR} \ - ${PY_ARGS} diff --git a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_single_scale_dc5.sh b/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_single_scale_dc5.sh deleted file mode 100755 index 26d35d6a49..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/configs/r50_deformable_detr_single_scale_dc5.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -x - -EXP_DIR=exps/r50_deformable_detr_single_scale_dc5 -PY_ARGS=${@:1} - -python -u main.py \ - --num_feature_levels 1 \ - --dilation \ - --output_dir ${EXP_DIR} \ - ${PY_ARGS} diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/__init__.py deleted file mode 100644 index 870166e145..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -import torch.utils.data - -from .coco import build as build_coco -from .torchvision_datasets import CocoDetection - - -def get_coco_api_from_dataset(dataset): - for _ in range(10): - # if isinstance(dataset, torchvision.datasets.CocoDetection): - # break - if isinstance(dataset, torch.utils.data.Subset): - dataset = dataset.dataset - if isinstance(dataset, CocoDetection): - return dataset.coco - - -def build_dataset(image_set, args): - if args.dataset_file == "coco": - return build_coco(image_set, args) - if args.dataset_file == "coco_panoptic": - # to avoid making panopticapi required for coco - from .coco_panoptic import build as build_coco_panoptic - - return build_coco_panoptic(image_set, args) - raise ValueError(f"dataset {args.dataset_file} not supported") diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco.py deleted file mode 100644 index aa00ce49e3..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco.py +++ /dev/null @@ -1,194 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -COCO dataset which returns image_id for evaluation. - -Mostly copy-paste from https://github.com/pytorch/vision/blob/13b35ff/references/detection/coco_utils.py -""" - -from pathlib import Path - -from pycocotools import mask as coco_mask -import torch -import torch.utils.data -from util.misc import get_local_rank, get_local_size - -import datasets.transforms as T - -from .torchvision_datasets import CocoDetection as TvCocoDetection - - -class CocoDetection(TvCocoDetection): - def __init__( - self, - img_folder, - ann_file, - transforms, - return_masks, - cache_mode: bool=False, - local_rank: int=0, - local_size: int=1, - ) -> None: - super().__init__( - img_folder, - ann_file, - cache_mode=cache_mode, - local_rank=local_rank, - local_size=local_size, - ) - self._transforms = transforms - self.prepare = ConvertCocoPolysToMask(return_masks) - - def __getitem__(self, idx: int): - img, target = super().__getitem__(idx) - image_id = self.ids[idx] - target = {"image_id": image_id, "annotations": target} - img, target = self.prepare(img, target) - if self._transforms is not None: - img, target = self._transforms(img, target) - return img, target - - -def convert_coco_poly_to_mask(segmentations, height, width: int): - masks = [] - for polygons in segmentations: - rles = coco_mask.frPyObjects(polygons, height, width) - mask = coco_mask.decode(rles) - if len(mask.shape) < 3: - mask = mask[..., None] - mask = torch.as_tensor(mask, dtype=torch.uint8) - mask = mask.any(dim=2) - masks.append(mask) - if masks: - masks = torch.stack(masks, dim=0) - else: - masks = torch.zeros((0, height, width), dtype=torch.uint8) - return masks - - -class ConvertCocoPolysToMask: - def __init__(self, return_masks: bool=False) -> None: - self.return_masks = return_masks - - def __call__(self, image, target): - w, h = image.size - - image_id = target["image_id"] - image_id = torch.tensor([image_id]) - - anno = target["annotations"] - - anno = [obj for obj in anno if "iscrowd" not in obj or obj["iscrowd"] == 0] - - boxes = [obj["bbox"] for obj in anno] - # guard against no boxes via resizing - boxes = torch.as_tensor(boxes, dtype=torch.float32).reshape(-1, 4) - boxes[:, 2:] += boxes[:, :2] - boxes[:, 0::2].clamp_(min=0, max=w) - boxes[:, 1::2].clamp_(min=0, max=h) - - classes = [obj["category_id"] for obj in anno] - classes = torch.tensor(classes, dtype=torch.int64) - - if self.return_masks: - segmentations = [obj["segmentation"] for obj in anno] - masks = convert_coco_poly_to_mask(segmentations, h, w) - - keypoints = None - if anno and "keypoints" in anno[0]: - keypoints = [obj["keypoints"] for obj in anno] - keypoints = torch.as_tensor(keypoints, dtype=torch.float32) - num_keypoints = keypoints.shape[0] - if num_keypoints: - keypoints = keypoints.view(num_keypoints, -1, 3) - - keep = (boxes[:, 3] > boxes[:, 1]) & (boxes[:, 2] > boxes[:, 0]) - boxes = boxes[keep] - classes = classes[keep] - if self.return_masks: - masks = masks[keep] - if keypoints is not None: - keypoints = keypoints[keep] - - target = {} - target["boxes"] = boxes - target["labels"] = classes - if self.return_masks: - target["masks"] = masks - target["image_id"] = image_id - if keypoints is not None: - target["keypoints"] = keypoints - - # for conversion to coco api - area = torch.tensor([obj["area"] for obj in anno]) - iscrowd = torch.tensor([obj["iscrowd"] if "iscrowd" in obj else 0 for obj in anno]) - target["area"] = area[keep] - target["iscrowd"] = iscrowd[keep] - - target["orig_size"] = torch.as_tensor([int(h), int(w)]) - target["size"] = torch.as_tensor([int(h), int(w)]) - - return image, target - - -def make_coco_transforms(image_set): - normalize = T.Compose([T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) - - scales = [480, 512, 544, 576, 608, 640, 672, 704, 736, 768, 800] - - if image_set == "train": - return T.Compose( - [ - T.RandomHorizontalFlip(), - T.RandomSelect( - T.RandomResize(scales, max_size=1333), - T.Compose( - [ - T.RandomResize([400, 500, 600]), - T.RandomSizeCrop(384, 600), - T.RandomResize(scales, max_size=1333), - ] - ), - ), - normalize, - ] - ) - - if image_set == "val": - return T.Compose( - [ - T.RandomResize([800], max_size=1333), - normalize, - ] - ) - - raise ValueError(f"unknown {image_set}") - - -def build(image_set, args): - root = Path(args.coco_path) - assert root.exists(), f"provided COCO path {root} does not exist" - mode = "instances" - PATHS = { - "train": (root / "train2017", root / "annotations" / f"{mode}_train2017.json"), - "val": (root / "val2017", root / "annotations" / f"{mode}_val2017.json"), - } - - img_folder, ann_file = PATHS[image_set] - dataset = CocoDetection( - img_folder, - ann_file, - transforms=make_coco_transforms(image_set), - return_masks=args.masks, - cache_mode=args.cache_mode, - local_rank=get_local_rank(), - local_size=get_local_size(), - ) - return dataset diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_eval.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_eval.py deleted file mode 100644 index 1a0e7962bd..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_eval.py +++ /dev/null @@ -1,265 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -COCO evaluator that works in distributed mode. - -Mostly copy-paste from https://github.com/pytorch/vision/blob/edfd5a7/references/detection/coco_eval.py -The difference is that there is less copy-pasting from pycocotools -in the end of the file, as python3 can suppress prints with contextlib -""" - -import contextlib -import copy -import os - -import numpy as np -from pycocotools.coco import COCO -from pycocotools.cocoeval import COCOeval -import pycocotools.mask as mask_util -import torch -from util.misc import all_gather - - -class CocoEvaluator: - def __init__(self, coco_gt, iou_types) -> None: - assert isinstance(iou_types, list | tuple) - coco_gt = copy.deepcopy(coco_gt) - self.coco_gt = coco_gt - - self.iou_types = iou_types - self.coco_eval = {} - for iou_type in iou_types: - self.coco_eval[iou_type] = COCOeval(coco_gt, iouType=iou_type) - - self.img_ids = [] - self.eval_imgs = {k: [] for k in iou_types} - - def update(self, predictions) -> None: - img_ids = list(np.unique(list(predictions.keys()))) - self.img_ids.extend(img_ids) - - for iou_type in self.iou_types: - results = self.prepare(predictions, iou_type) - - # suppress pycocotools prints - with open(os.devnull, "w") as devnull: - with contextlib.redirect_stdout(devnull): - coco_dt = COCO.loadRes(self.coco_gt, results) if results else COCO() - coco_eval = self.coco_eval[iou_type] - - coco_eval.cocoDt = coco_dt - coco_eval.params.imgIds = list(img_ids) - img_ids, eval_imgs = evaluate(coco_eval) - - self.eval_imgs[iou_type].append(eval_imgs) - - def synchronize_between_processes(self) -> None: - for iou_type in self.iou_types: - self.eval_imgs[iou_type] = np.concatenate(self.eval_imgs[iou_type], 2) - create_common_coco_eval( - self.coco_eval[iou_type], self.img_ids, self.eval_imgs[iou_type] - ) - - def accumulate(self) -> None: - for coco_eval in self.coco_eval.values(): - coco_eval.accumulate() - - def summarize(self) -> None: - for iou_type, coco_eval in self.coco_eval.items(): - print(f"IoU metric: {iou_type}") - coco_eval.summarize() - - def prepare(self, predictions, iou_type): - if iou_type == "bbox": - return self.prepare_for_coco_detection(predictions) - elif iou_type == "segm": - return self.prepare_for_coco_segmentation(predictions) - elif iou_type == "keypoints": - return self.prepare_for_coco_keypoint(predictions) - else: - raise ValueError(f"Unknown iou type {iou_type}") - - def prepare_for_coco_detection(self, predictions): - coco_results = [] - for original_id, prediction in predictions.items(): - if len(prediction) == 0: - continue - - boxes = prediction["boxes"] - boxes = convert_to_xywh(boxes).tolist() - scores = prediction["scores"].tolist() - labels = prediction["labels"].tolist() - - coco_results.extend( - [ - { - "image_id": original_id, - "category_id": labels[k], - "bbox": box, - "score": scores[k], - } - for k, box in enumerate(boxes) - ] - ) - return coco_results - - def prepare_for_coco_segmentation(self, predictions): - coco_results = [] - for original_id, prediction in predictions.items(): - if len(prediction) == 0: - continue - - scores = prediction["scores"] - labels = prediction["labels"] - masks = prediction["masks"] - - masks = masks > 0.5 - - scores = prediction["scores"].tolist() - labels = prediction["labels"].tolist() - - rles = [ - mask_util.encode(np.array(mask[0, :, :, np.newaxis], dtype=np.uint8, order="F"))[0] - for mask in masks - ] - for rle in rles: - rle["counts"] = rle["counts"].decode("utf-8") - - coco_results.extend( - [ - { - "image_id": original_id, - "category_id": labels[k], - "segmentation": rle, - "score": scores[k], - } - for k, rle in enumerate(rles) - ] - ) - return coco_results - - def prepare_for_coco_keypoint(self, predictions): - coco_results = [] - for original_id, prediction in predictions.items(): - if len(prediction) == 0: - continue - - boxes = prediction["boxes"] - boxes = convert_to_xywh(boxes).tolist() - scores = prediction["scores"].tolist() - labels = prediction["labels"].tolist() - keypoints = prediction["keypoints"] - keypoints = keypoints.flatten(start_dim=1).tolist() - - coco_results.extend( - [ - { - "image_id": original_id, - "category_id": labels[k], - "keypoints": keypoint, - "score": scores[k], - } - for k, keypoint in enumerate(keypoints) - ] - ) - return coco_results - - -def convert_to_xywh(boxes): - xmin, ymin, xmax, ymax = boxes.unbind(1) - return torch.stack((xmin, ymin, xmax - xmin, ymax - ymin), dim=1) - - -def merge(img_ids, eval_imgs): - all_img_ids = all_gather(img_ids) - all_eval_imgs = all_gather(eval_imgs) - - merged_img_ids = [] - for p in all_img_ids: - merged_img_ids.extend(p) - - merged_eval_imgs = [] - for p in all_eval_imgs: - merged_eval_imgs.append(p) - - merged_img_ids = np.array(merged_img_ids) - merged_eval_imgs = np.concatenate(merged_eval_imgs, 2) - - # keep only unique (and in sorted order) images - merged_img_ids, idx = np.unique(merged_img_ids, return_index=True) - merged_eval_imgs = merged_eval_imgs[..., idx] - - return merged_img_ids, merged_eval_imgs - - -def create_common_coco_eval(coco_eval, img_ids, eval_imgs) -> None: - img_ids, eval_imgs = merge(img_ids, eval_imgs) - img_ids = list(img_ids) - eval_imgs = list(eval_imgs.flatten()) - - coco_eval.evalImgs = eval_imgs - coco_eval.params.imgIds = img_ids - coco_eval._paramsEval = copy.deepcopy(coco_eval.params) - - -################################################################# -# From pycocotools, just removed the prints and fixed -# a Python3 bug about unicode not defined -################################################################# - - -def evaluate(self): - """ - Run per image evaluation on given images and store results (a list of dict) in self.evalImgs - :return: None - """ - # tic = time.time() - # print('Running per image evaluation...') - p = self.params - # add backward compatibility if useSegm is specified in params - if p.useSegm is not None: - p.iouType = "segm" if p.useSegm == 1 else "bbox" - print(f"useSegm (deprecated) is not None. Running {p.iouType} evaluation") - # print('Evaluate annotation type *{}*'.format(p.iouType)) - p.imgIds = list(np.unique(p.imgIds)) - if p.useCats: - p.catIds = list(np.unique(p.catIds)) - p.maxDets = sorted(p.maxDets) - self.params = p - - self._prepare() - # loop through images, area range, max detection number - catIds = p.catIds if p.useCats else [-1] - - if p.iouType == "segm" or p.iouType == "bbox": - computeIoU = self.computeIoU - elif p.iouType == "keypoints": - computeIoU = self.computeOks - self.ious = {(imgId, catId): computeIoU(imgId, catId) for imgId in p.imgIds for catId in catIds} - - evaluateImg = self.evaluateImg - maxDet = p.maxDets[-1] - evalImgs = [ - evaluateImg(imgId, catId, areaRng, maxDet) - for catId in catIds - for areaRng in p.areaRng - for imgId in p.imgIds - ] - # this is NOT in the pycocotools code, but could be done outside - evalImgs = np.asarray(evalImgs).reshape(len(catIds), len(p.areaRng), len(p.imgIds)) - self._paramsEval = copy.deepcopy(self.params) - # toc = time.time() - # print('DONE (t={:0.2f}s).'.format(toc-tic)) - return p.imgIds, evalImgs - - -################################################################# -# end of straight copy from pycocotools, just removing the prints -################################################################# diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_panoptic.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_panoptic.py deleted file mode 100644 index d1dd9bda59..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_panoptic.py +++ /dev/null @@ -1,119 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -import json -from pathlib import Path - -import numpy as np -from panopticapi.utils import rgb2id -from PIL import Image -import torch -from util.box_ops import masks_to_boxes - -from .coco import make_coco_transforms - - -class CocoPanoptic: - def __init__(self, img_folder, ann_folder, ann_file, transforms=None, return_masks: bool=True) -> None: - with open(ann_file) as f: - self.coco = json.load(f) - - # sort 'images' field so that they are aligned with 'annotations' - # i.e., in alphabetical order - self.coco["images"] = sorted(self.coco["images"], key=lambda x: x["id"]) - # sanity check - if "annotations" in self.coco: - for img, ann in zip(self.coco["images"], self.coco["annotations"], strict=False): - assert img["file_name"][:-4] == ann["file_name"][:-4] - - self.img_folder = img_folder - self.ann_folder = ann_folder - self.ann_file = ann_file - self.transforms = transforms - self.return_masks = return_masks - - def __getitem__(self, idx: int): - ann_info = ( - self.coco["annotations"][idx] - if "annotations" in self.coco - else self.coco["images"][idx] - ) - img_path = Path(self.img_folder) / ann_info["file_name"].replace(".png", ".jpg") - ann_path = Path(self.ann_folder) / ann_info["file_name"] - - img = Image.open(img_path).convert("RGB") - w, h = img.size - if "segments_info" in ann_info: - masks = np.asarray(Image.open(ann_path), dtype=np.uint32) - masks = rgb2id(masks) - - ids = np.array([ann["id"] for ann in ann_info["segments_info"]]) - masks = masks == ids[:, None, None] - - masks = torch.as_tensor(masks, dtype=torch.uint8) - labels = torch.tensor( - [ann["category_id"] for ann in ann_info["segments_info"]], dtype=torch.int64 - ) - - target = {} - target["image_id"] = torch.tensor( - [ann_info["image_id"] if "image_id" in ann_info else ann_info["id"]] - ) - if self.return_masks: - target["masks"] = masks - target["labels"] = labels - - target["boxes"] = masks_to_boxes(masks) - - target["size"] = torch.as_tensor([int(h), int(w)]) - target["orig_size"] = torch.as_tensor([int(h), int(w)]) - if "segments_info" in ann_info: - for name in ["iscrowd", "area"]: - target[name] = torch.tensor([ann[name] for ann in ann_info["segments_info"]]) - - if self.transforms is not None: - img, target = self.transforms(img, target) - - return img, target - - def __len__(self) -> int: - return len(self.coco["images"]) - - def get_height_and_width(self, idx: int): - img_info = self.coco["images"][idx] - height = img_info["height"] - width = img_info["width"] - return height, width - - -def build(image_set, args): - img_folder_root = Path(args.coco_path) - ann_folder_root = Path(args.coco_panoptic_path) - assert img_folder_root.exists(), f"provided COCO path {img_folder_root} does not exist" - assert ann_folder_root.exists(), f"provided COCO path {ann_folder_root} does not exist" - mode = "panoptic" - PATHS = { - "train": ("train2017", Path("annotations") / f"{mode}_train2017.json"), - "val": ("val2017", Path("annotations") / f"{mode}_val2017.json"), - } - - img_folder, ann_file = PATHS[image_set] - img_folder_path = img_folder_root / img_folder - ann_folder = ann_folder_root / f"{mode}_{img_folder}" - ann_file = ann_folder_root / ann_file - - dataset = CocoPanoptic( - img_folder_path, - ann_folder, - ann_file, - transforms=make_coco_transforms(image_set), - return_masks=args.masks, - ) - - return dataset diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/data_prefetcher.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/data_prefetcher.py deleted file mode 100644 index 4942500801..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/data_prefetcher.py +++ /dev/null @@ -1,74 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ - -import torch - - -def to_cuda(samples, targets, device): - samples = samples.to(device, non_blocking=True) - targets = [{k: v.to(device, non_blocking=True) for k, v in t.items()} for t in targets] - return samples, targets - - -class data_prefetcher: - def __init__(self, loader, device, prefetch: bool=True) -> None: - self.loader = iter(loader) - self.prefetch = prefetch - self.device = device - if prefetch: - self.stream = torch.cuda.Stream() - self.preload() - - def preload(self) -> None: - try: - self.next_samples, self.next_targets = next(self.loader) - except StopIteration: - self.next_samples = None - self.next_targets = None - return - # if record_stream() doesn't work, another option is to make sure device inputs are created - # on the main stream. - # self.next_input_gpu = torch.empty_like(self.next_input, device='cuda') - # self.next_target_gpu = torch.empty_like(self.next_target, device='cuda') - # Need to make sure the memory allocated for next_* is not still in use by the main stream - # at the time we start copying to next_*: - # self.stream.wait_stream(torch.cuda.current_stream()) - with torch.cuda.stream(self.stream): - self.next_samples, self.next_targets = to_cuda( - self.next_samples, self.next_targets, self.device - ) - # more code for the alternative if record_stream() doesn't work: - # copy_ will record the use of the pinned source tensor in this side stream. - # self.next_input_gpu.copy_(self.next_input, non_blocking=True) - # self.next_target_gpu.copy_(self.next_target, non_blocking=True) - # self.next_input = self.next_input_gpu - # self.next_target = self.next_target_gpu - - # With Amp, it isn't necessary to manually convert data to half. - # if args.fp16: - # self.next_input = self.next_input.half() - # else: - - def next(self): - if self.prefetch: - torch.cuda.current_stream().wait_stream(self.stream) - samples = self.next_samples - targets = self.next_targets - if samples is not None: - samples.record_stream(torch.cuda.current_stream()) - if targets is not None: - for t in targets: - for _k, v in t.items(): - v.record_stream(torch.cuda.current_stream()) - self.preload() - else: - try: - samples, targets = next(self.loader) - samples, targets = to_cuda(samples, targets, self.device) - except StopIteration: - samples = None - targets = None - return samples, targets diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/panoptic_eval.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/panoptic_eval.py deleted file mode 100644 index 1a8ed7a82f..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/panoptic_eval.py +++ /dev/null @@ -1,57 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -import json -import os - -import util.misc as utils - -try: - from panopticapi.evaluation import pq_compute -except ImportError: - pass - - -class PanopticEvaluator: - def __init__(self, ann_file, ann_folder, output_dir: str="panoptic_eval") -> None: - self.gt_json = ann_file - self.gt_folder = ann_folder - if utils.is_main_process(): - if not os.path.exists(output_dir): - os.mkdir(output_dir) - self.output_dir = output_dir - self.predictions = [] - - def update(self, predictions) -> None: - for p in predictions: - with open(os.path.join(self.output_dir, p["file_name"]), "wb") as f: - f.write(p.pop("png_string")) - - self.predictions += predictions - - def synchronize_between_processes(self) -> None: - all_predictions = utils.all_gather(self.predictions) - merged_predictions = [] - for p in all_predictions: - merged_predictions += p - self.predictions = merged_predictions - - def summarize(self): - if utils.is_main_process(): - json_data = {"annotations": self.predictions} - predictions_json = os.path.join(self.output_dir, "predictions.json") - with open(predictions_json, "w") as f: - f.write(json.dumps(json_data)) - return pq_compute( - self.gt_json, - predictions_json, - gt_folder=self.gt_folder, - pred_folder=self.output_dir, - ) - return None diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/samplers.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/samplers.py deleted file mode 100644 index 5c2fff2d46..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/samplers.py +++ /dev/null @@ -1,148 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from codes in torch.utils.data.distributed -# ------------------------------------------------------------------------ - -import math -import os - -import torch -import torch.distributed as dist -from torch.utils.data.sampler import Sampler -from typing import Iterator, Optional - - -class DistributedSampler(Sampler): - """Sampler that restricts data loading to a subset of the dataset. - It is especially useful in conjunction with - :class:`torch.nn.parallel.DistributedDataParallel`. In such case, each - process can pass a DistributedSampler instance as a DataLoader sampler, - and load a subset of the original dataset that is exclusive to it. - .. note:: - Dataset is assumed to be of constant size. - Arguments: - dataset: Dataset used for sampling. - num_replicas (optional): Number of processes participating in - distributed training. - rank (optional): Rank of the current process within num_replicas. - """ - - def __init__( - self, dataset, num_replicas: Optional[int]=None, rank=None, local_rank=None, local_size: Optional[int]=None, shuffle: bool=True - ) -> None: - if num_replicas is None: - if not dist.is_available(): - raise RuntimeError("Requires distributed package to be available") - num_replicas = dist.get_world_size() - if rank is None: - if not dist.is_available(): - raise RuntimeError("Requires distributed package to be available") - rank = dist.get_rank() - self.dataset = dataset - self.num_replicas = num_replicas - self.rank = rank - self.epoch = 0 - self.num_samples = math.ceil(len(self.dataset) * 1.0 / self.num_replicas) - self.total_size = self.num_samples * self.num_replicas - self.shuffle = shuffle - - def __iter__(self) -> Iterator: - if self.shuffle: - # deterministically shuffle based on epoch - g = torch.Generator() - g.manual_seed(self.epoch) - indices = torch.randperm(len(self.dataset), generator=g).tolist() - else: - indices = torch.arange(len(self.dataset)).tolist() - - # add extra samples to make it evenly divisible - indices += indices[: (self.total_size - len(indices))] - assert len(indices) == self.total_size - - # subsample - offset = self.num_samples * self.rank - indices = indices[offset : offset + self.num_samples] - assert len(indices) == self.num_samples - - return iter(indices) - - def __len__(self) -> int: - return self.num_samples - - def set_epoch(self, epoch: int) -> None: - self.epoch = epoch - - -class NodeDistributedSampler(Sampler): - """Sampler that restricts data loading to a subset of the dataset. - It is especially useful in conjunction with - :class:`torch.nn.parallel.DistributedDataParallel`. In such case, each - process can pass a DistributedSampler instance as a DataLoader sampler, - and load a subset of the original dataset that is exclusive to it. - .. note:: - Dataset is assumed to be of constant size. - Arguments: - dataset: Dataset used for sampling. - num_replicas (optional): Number of processes participating in - distributed training. - rank (optional): Rank of the current process within num_replicas. - """ - - def __init__( - self, dataset, num_replicas: Optional[int]=None, rank=None, local_rank=None, local_size: Optional[int]=None, shuffle: bool=True - ) -> None: - if num_replicas is None: - if not dist.is_available(): - raise RuntimeError("Requires distributed package to be available") - num_replicas = dist.get_world_size() - if rank is None: - if not dist.is_available(): - raise RuntimeError("Requires distributed package to be available") - rank = dist.get_rank() - if local_rank is None: - local_rank = int(os.environ.get("LOCAL_RANK", 0)) - if local_size is None: - local_size = int(os.environ.get("LOCAL_SIZE", 1)) - self.dataset = dataset - self.shuffle = shuffle - self.num_replicas = num_replicas - self.num_parts = local_size - self.rank = rank - self.local_rank = local_rank - self.epoch = 0 - self.num_samples = math.ceil(len(self.dataset) * 1.0 / self.num_replicas) - self.total_size = self.num_samples * self.num_replicas - - self.total_size_parts = self.num_samples * self.num_replicas // self.num_parts - - def __iter__(self) -> Iterator: - if self.shuffle: - # deterministically shuffle based on epoch - g = torch.Generator() - g.manual_seed(self.epoch) - indices = torch.randperm(len(self.dataset), generator=g).tolist() - else: - indices = torch.arange(len(self.dataset)).tolist() - indices = [i for i in indices if i % self.num_parts == self.local_rank] - - # add extra samples to make it evenly divisible - indices += indices[: (self.total_size_parts - len(indices))] - assert len(indices) == self.total_size_parts - - # subsample - indices = indices[ - self.rank // self.num_parts : self.total_size_parts : self.num_replicas - // self.num_parts - ] - assert len(indices) == self.num_samples - - return iter(indices) - - def __len__(self) -> int: - return self.num_samples - - def set_epoch(self, epoch: int) -> None: - self.epoch = epoch diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/__init__.py deleted file mode 100644 index 162303c4ce..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ - -from .coco import CocoDetection diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/coco.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/coco.py deleted file mode 100644 index 65eb674294..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/coco.py +++ /dev/null @@ -1,96 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from torchvision -# ------------------------------------------------------------------------ - -""" -Copy-Paste from torchvision, but add utility of caching images on memory -""" - -from io import BytesIO -import os -import os.path - -from PIL import Image -from torchvision.datasets.vision import VisionDataset -import tqdm - - -class CocoDetection(VisionDataset): - """`MS Coco Detection `_ Dataset. - Args: - root (string): Root directory where images are downloaded to. - annFile (string): Path to json annotation file. - transform (callable, optional): A function/transform that takes in an PIL image - and returns a transformed version. E.g, ``transforms.ToTensor`` - target_transform (callable, optional): A function/transform that takes in the - target and transforms it. - transforms (callable, optional): A function/transform that takes input sample and its target as entry - and returns a transformed version. - """ - - def __init__( - self, - root, - annFile, - transform=None, - target_transform=None, - transforms=None, - cache_mode: bool=False, - local_rank: int=0, - local_size: int=1, - ) -> None: - super().__init__(root, transforms, transform, target_transform) - from pycocotools.coco import COCO - - self.coco = COCO(annFile) - self.ids = list(sorted(self.coco.imgs.keys())) - self.cache_mode = cache_mode - self.local_rank = local_rank - self.local_size = local_size - if cache_mode: - self.cache = {} - self.cache_images() - - def cache_images(self) -> None: - self.cache = {} - for index, img_id in zip(tqdm.trange(len(self.ids)), self.ids, strict=False): - if index % self.local_size != self.local_rank: - continue - path = self.coco.loadImgs(img_id)[0]["file_name"] - with open(os.path.join(self.root, path), "rb") as f: - self.cache[path] = f.read() - - def get_image(self, path): - if self.cache_mode: - if path not in self.cache.keys(): - with open(os.path.join(self.root, path), "rb") as f: - self.cache[path] = f.read() - return Image.open(BytesIO(self.cache[path])).convert("RGB") - return Image.open(os.path.join(self.root, path)).convert("RGB") - - def __getitem__(self, index): - """ - Args: - index (int): Index - Returns: - tuple: Tuple (image, target). target is the object returned by ``coco.loadAnns``. - """ - coco = self.coco - img_id = self.ids[index] - ann_ids = coco.getAnnIds(imgIds=img_id) - target = coco.loadAnns(ann_ids) - - path = coco.loadImgs(img_id)[0]["file_name"] - - img = self.get_image(path) - if self.transforms is not None: - img, target = self.transforms(img, target) - - return img, target - - def __len__(self) -> int: - return len(self.ids) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/transforms.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/transforms.py deleted file mode 100644 index 3c2947ee36..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/transforms.py +++ /dev/null @@ -1,290 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Transforms and data augmentation for both image + bbox. -""" - -import random - -import PIL -import torch -import torchvision.transforms as T -import torchvision.transforms.functional as F -from util.box_ops import box_xyxy_to_cxcywh -from util.misc import interpolate -from typing import Optional, Sequence - - -def crop(image, target, region): - cropped_image = F.crop(image, *region) - - target = target.copy() - i, j, h, w = region - - # should we do something wrt the original size? - target["size"] = torch.tensor([h, w]) - - fields = ["labels", "area", "iscrowd"] - - if "boxes" in target: - boxes = target["boxes"] - max_size = torch.as_tensor([w, h], dtype=torch.float32) - cropped_boxes = boxes - torch.as_tensor([j, i, j, i]) - cropped_boxes = torch.min(cropped_boxes.reshape(-1, 2, 2), max_size) - cropped_boxes = cropped_boxes.clamp(min=0) - area = (cropped_boxes[:, 1, :] - cropped_boxes[:, 0, :]).prod(dim=1) - target["boxes"] = cropped_boxes.reshape(-1, 4) - target["area"] = area - fields.append("boxes") - - if "masks" in target: - # FIXME should we update the area here if there are no boxes? - target["masks"] = target["masks"][:, i : i + h, j : j + w] - fields.append("masks") - - # remove elements for which the boxes or masks that have zero area - if "boxes" in target or "masks" in target: - # favor boxes selection when defining which elements to keep - # this is compatible with previous implementation - if "boxes" in target: - cropped_boxes = target["boxes"].reshape(-1, 2, 2) - keep = torch.all(cropped_boxes[:, 1, :] > cropped_boxes[:, 0, :], dim=1) - else: - keep = target["masks"].flatten(1).any(1) - - for field in fields: - target[field] = target[field][keep] - - return cropped_image, target - - -def hflip(image, target): - flipped_image = F.hflip(image) - - w, h = image.size - - target = target.copy() - if "boxes" in target: - boxes = target["boxes"] - boxes = boxes[:, [2, 1, 0, 3]] * torch.as_tensor([-1, 1, -1, 1]) + torch.as_tensor( - [w, 0, w, 0] - ) - target["boxes"] = boxes - - if "masks" in target: - target["masks"] = target["masks"].flip(-1) - - return flipped_image, target - - -def resize(image, target, size: int, max_size: Optional[int]=None): - # size can be min_size (scalar) or (w, h) tuple - - def get_size_with_aspect_ratio(image_size: int, size: int, max_size: Optional[int]=None): - w, h = image_size - if max_size is not None: - min_original_size = float(min((w, h))) - max_original_size = float(max((w, h))) - if max_original_size / min_original_size * size > max_size: - size = round(max_size * min_original_size / max_original_size) - - if (w <= h and w == size) or (h <= w and h == size): - return (h, w) - - if w < h: - ow = size - oh = int(size * h / w) - else: - oh = size - ow = int(size * w / h) - - return (oh, ow) - - def get_size(image_size: int, size: int, max_size: Optional[int]=None): - if isinstance(size, list | tuple): - return size[::-1] - else: - return get_size_with_aspect_ratio(image_size, size, max_size) - - size = get_size(image.size, size, max_size) - rescaled_image = F.resize(image, size) - - if target is None: - return rescaled_image, None - - ratios = tuple(float(s) / float(s_orig) for s, s_orig in zip(rescaled_image.size, image.size, strict=False)) - ratio_width, ratio_height = ratios - - target = target.copy() - if "boxes" in target: - boxes = target["boxes"] - scaled_boxes = boxes * torch.as_tensor( - [ratio_width, ratio_height, ratio_width, ratio_height] - ) - target["boxes"] = scaled_boxes - - if "area" in target: - area = target["area"] - scaled_area = area * (ratio_width * ratio_height) - target["area"] = scaled_area - - h, w = size - target["size"] = torch.tensor([h, w]) - - if "masks" in target: - target["masks"] = ( - interpolate(target["masks"][:, None].float(), size, mode="nearest")[:, 0] > 0.5 - ) - - return rescaled_image, target - - -def pad(image, target, padding): - # assumes that we only pad on the bottom right corners - padded_image = F.pad(image, (0, 0, padding[0], padding[1])) - if target is None: - return padded_image, None - target = target.copy() - # should we do something wrt the original size? - target["size"] = torch.tensor(padded_image[::-1]) - if "masks" in target: - target["masks"] = torch.nn.functional.pad(target["masks"], (0, padding[0], 0, padding[1])) - return padded_image, target - - -class RandomCrop: - def __init__(self, size: int) -> None: - self.size = size - - def __call__(self, img, target): - region = T.RandomCrop.get_params(img, self.size) - return crop(img, target, region) - - -class RandomSizeCrop: - def __init__(self, min_size: int, max_size: int) -> None: - self.min_size = min_size - self.max_size = max_size - - def __call__(self, img: PIL.Image.Image, target: dict): - w = random.randint(self.min_size, min(img.width, self.max_size)) - h = random.randint(self.min_size, min(img.height, self.max_size)) - region = T.RandomCrop.get_params(img, [h, w]) - return crop(img, target, region) - - -class CenterCrop: - def __init__(self, size: int) -> None: - self.size = size - - def __call__(self, img, target): - image_width, image_height = img.size - crop_height, crop_width = self.size - crop_top = round((image_height - crop_height) / 2.0) - crop_left = round((image_width - crop_width) / 2.0) - return crop(img, target, (crop_top, crop_left, crop_height, crop_width)) - - -class RandomHorizontalFlip: - def __init__(self, p: float=0.5) -> None: - self.p = p - - def __call__(self, img, target): - if random.random() < self.p: - return hflip(img, target) - return img, target - - -class RandomResize: - def __init__(self, sizes: Sequence[int], max_size: Optional[int]=None) -> None: - assert isinstance(sizes, list | tuple) - self.sizes = sizes - self.max_size = max_size - - def __call__(self, img, target=None): - size = random.choice(self.sizes) - return resize(img, target, size, self.max_size) - - -class RandomPad: - def __init__(self, max_pad) -> None: - self.max_pad = max_pad - - def __call__(self, img, target): - pad_x = random.randint(0, self.max_pad) - pad_y = random.randint(0, self.max_pad) - return pad(img, target, (pad_x, pad_y)) - - -class RandomSelect: - """ - Randomly selects between transforms1 and transforms2, - with probability p for transforms1 and (1 - p) for transforms2 - """ - - def __init__(self, transforms1, transforms2, p: float=0.5) -> None: - self.transforms1 = transforms1 - self.transforms2 = transforms2 - self.p = p - - def __call__(self, img, target): - if random.random() < self.p: - return self.transforms1(img, target) - return self.transforms2(img, target) - - -class ToTensor: - def __call__(self, img, target): - return F.to_tensor(img), target - - -class RandomErasing: - def __init__(self, *args, **kwargs) -> None: - self.eraser = T.RandomErasing(*args, **kwargs) - - def __call__(self, img, target): - return self.eraser(img), target - - -class Normalize: - def __init__(self, mean, std) -> None: - self.mean = mean - self.std = std - - def __call__(self, image, target=None): - image = F.normalize(image, mean=self.mean, std=self.std) - if target is None: - return image, None - target = target.copy() - h, w = image.shape[-2:] - if "boxes" in target: - boxes = target["boxes"] - boxes = box_xyxy_to_cxcywh(boxes) - boxes = boxes / torch.tensor([w, h, w, h], dtype=torch.float32) - target["boxes"] = boxes - return image, target - - -class Compose: - def __init__(self, transforms) -> None: - self.transforms = transforms - - def __call__(self, image, target): - for t in self.transforms: - image, target = t(image, target) - return image, target - - def __repr__(self) -> str: - format_string = self.__class__.__name__ + "(" - for t in self.transforms: - format_string += "\n" - format_string += f" {t}" - format_string += "\n)" - return format_string diff --git a/dimos/models/Detic/third_party/Deformable-DETR/docs/changelog.md b/dimos/models/Detic/third_party/Deformable-DETR/docs/changelog.md deleted file mode 100644 index 1ed5e79a4d..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/docs/changelog.md +++ /dev/null @@ -1,3 +0,0 @@ -## Changelog - -**[2020.12.07]** Fix a bug of sampling offset normalization (see [this issue](https://github.com/fundamentalvision/Deformable-DETR/issues/6)) in the MSDeformAttn module. The final accuracy on COCO is slightly improved. Code and pre-trained models have been updated. This bug only occurs in this released version but not in the original implementation used in our paper. \ No newline at end of file diff --git a/dimos/models/Detic/third_party/Deformable-DETR/engine.py b/dimos/models/Detic/third_party/Deformable-DETR/engine.py deleted file mode 100644 index 7e6e7c2c20..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/engine.py +++ /dev/null @@ -1,177 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Train and eval functions used in main.py -""" - -import math -import os -import sys -from typing import Iterable - -from datasets.coco_eval import CocoEvaluator -from datasets.data_prefetcher import data_prefetcher -from datasets.panoptic_eval import PanopticEvaluator -import torch -import util.misc as utils - - -def train_one_epoch( - model: torch.nn.Module, - criterion: torch.nn.Module, - data_loader: Iterable, - optimizer: torch.optim.Optimizer, - device: torch.device, - epoch: int, - max_norm: float = 0, -): - model.train() - criterion.train() - metric_logger = utils.MetricLogger(delimiter=" ") - metric_logger.add_meter("lr", utils.SmoothedValue(window_size=1, fmt="{value:.6f}")) - metric_logger.add_meter("class_error", utils.SmoothedValue(window_size=1, fmt="{value:.2f}")) - metric_logger.add_meter("grad_norm", utils.SmoothedValue(window_size=1, fmt="{value:.2f}")) - header = f"Epoch: [{epoch}]" - print_freq = 10 - - prefetcher = data_prefetcher(data_loader, device, prefetch=True) - samples, targets = prefetcher.next() - - # for samples, targets in metric_logger.log_every(data_loader, print_freq, header): - for _ in metric_logger.log_every(range(len(data_loader)), print_freq, header): - outputs = model(samples) - loss_dict = criterion(outputs, targets) - weight_dict = criterion.weight_dict - losses = sum(loss_dict[k] * weight_dict[k] for k in loss_dict.keys() if k in weight_dict) - - # reduce losses over all GPUs for logging purposes - loss_dict_reduced = utils.reduce_dict(loss_dict) - loss_dict_reduced_unscaled = {f"{k}_unscaled": v for k, v in loss_dict_reduced.items()} - loss_dict_reduced_scaled = { - k: v * weight_dict[k] for k, v in loss_dict_reduced.items() if k in weight_dict - } - losses_reduced_scaled = sum(loss_dict_reduced_scaled.values()) - - loss_value = losses_reduced_scaled.item() - - if not math.isfinite(loss_value): - print(f"Loss is {loss_value}, stopping training") - print(loss_dict_reduced) - sys.exit(1) - - optimizer.zero_grad() - losses.backward() - if max_norm > 0: - grad_total_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm) - else: - grad_total_norm = utils.get_total_grad_norm(model.parameters(), max_norm) - optimizer.step() - - metric_logger.update( - loss=loss_value, **loss_dict_reduced_scaled, **loss_dict_reduced_unscaled - ) - metric_logger.update(class_error=loss_dict_reduced["class_error"]) - metric_logger.update(lr=optimizer.param_groups[0]["lr"]) - metric_logger.update(grad_norm=grad_total_norm) - - samples, targets = prefetcher.next() - # gather the stats from all processes - metric_logger.synchronize_between_processes() - print("Averaged stats:", metric_logger) - return {k: meter.global_avg for k, meter in metric_logger.meters.items()} - - -@torch.no_grad() -def evaluate(model, criterion, postprocessors, data_loader, base_ds, device, output_dir): - model.eval() - criterion.eval() - - metric_logger = utils.MetricLogger(delimiter=" ") - metric_logger.add_meter("class_error", utils.SmoothedValue(window_size=1, fmt="{value:.2f}")) - header = "Test:" - - iou_types = tuple(k for k in ("segm", "bbox") if k in postprocessors.keys()) - coco_evaluator = CocoEvaluator(base_ds, iou_types) - # coco_evaluator.coco_eval[iou_types[0]].params.iouThrs = [0, 0.1, 0.5, 0.75] - - panoptic_evaluator = None - if "panoptic" in postprocessors.keys(): - panoptic_evaluator = PanopticEvaluator( - data_loader.dataset.ann_file, - data_loader.dataset.ann_folder, - output_dir=os.path.join(output_dir, "panoptic_eval"), - ) - - for samples, targets in metric_logger.log_every(data_loader, 10, header): - samples = samples.to(device) - targets = [{k: v.to(device) for k, v in t.items()} for t in targets] - - outputs = model(samples) - loss_dict = criterion(outputs, targets) - weight_dict = criterion.weight_dict - - # reduce losses over all GPUs for logging purposes - loss_dict_reduced = utils.reduce_dict(loss_dict) - loss_dict_reduced_scaled = { - k: v * weight_dict[k] for k, v in loss_dict_reduced.items() if k in weight_dict - } - loss_dict_reduced_unscaled = {f"{k}_unscaled": v for k, v in loss_dict_reduced.items()} - metric_logger.update( - loss=sum(loss_dict_reduced_scaled.values()), - **loss_dict_reduced_scaled, - **loss_dict_reduced_unscaled, - ) - metric_logger.update(class_error=loss_dict_reduced["class_error"]) - - orig_target_sizes = torch.stack([t["orig_size"] for t in targets], dim=0) - results = postprocessors["bbox"](outputs, orig_target_sizes) - if "segm" in postprocessors.keys(): - target_sizes = torch.stack([t["size"] for t in targets], dim=0) - results = postprocessors["segm"](results, outputs, orig_target_sizes, target_sizes) - res = {target["image_id"].item(): output for target, output in zip(targets, results, strict=False)} - if coco_evaluator is not None: - coco_evaluator.update(res) - - if panoptic_evaluator is not None: - res_pano = postprocessors["panoptic"](outputs, target_sizes, orig_target_sizes) - for i, target in enumerate(targets): - image_id = target["image_id"].item() - file_name = f"{image_id:012d}.png" - res_pano[i]["image_id"] = image_id - res_pano[i]["file_name"] = file_name - - panoptic_evaluator.update(res_pano) - - # gather the stats from all processes - metric_logger.synchronize_between_processes() - print("Averaged stats:", metric_logger) - if coco_evaluator is not None: - coco_evaluator.synchronize_between_processes() - if panoptic_evaluator is not None: - panoptic_evaluator.synchronize_between_processes() - - # accumulate predictions from all images - if coco_evaluator is not None: - coco_evaluator.accumulate() - coco_evaluator.summarize() - panoptic_res = None - if panoptic_evaluator is not None: - panoptic_res = panoptic_evaluator.summarize() - stats = {k: meter.global_avg for k, meter in metric_logger.meters.items()} - if coco_evaluator is not None: - if "bbox" in postprocessors.keys(): - stats["coco_eval_bbox"] = coco_evaluator.coco_eval["bbox"].stats.tolist() - if "segm" in postprocessors.keys(): - stats["coco_eval_masks"] = coco_evaluator.coco_eval["segm"].stats.tolist() - if panoptic_res is not None: - stats["PQ_all"] = panoptic_res["All"] - stats["PQ_th"] = panoptic_res["Things"] - stats["PQ_st"] = panoptic_res["Stuff"] - return stats, coco_evaluator diff --git a/dimos/models/Detic/third_party/Deformable-DETR/main.py b/dimos/models/Detic/third_party/Deformable-DETR/main.py deleted file mode 100644 index 187b93a868..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/main.py +++ /dev/null @@ -1,418 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - - -import argparse -import datetime -import json -from pathlib import Path -import random -import time - -import datasets -from datasets import build_dataset, get_coco_api_from_dataset -import datasets.samplers as samplers -from engine import evaluate, train_one_epoch -from models import build_model -import numpy as np -import torch -from torch.utils.data import DataLoader -import util.misc as utils - - -def get_args_parser(): - parser = argparse.ArgumentParser("Deformable DETR Detector", add_help=False) - parser.add_argument("--lr", default=2e-4, type=float) - parser.add_argument("--lr_backbone_names", default=["backbone.0"], type=str, nargs="+") - parser.add_argument("--lr_backbone", default=2e-5, type=float) - parser.add_argument( - "--lr_linear_proj_names", - default=["reference_points", "sampling_offsets"], - type=str, - nargs="+", - ) - parser.add_argument("--lr_linear_proj_mult", default=0.1, type=float) - parser.add_argument("--batch_size", default=2, type=int) - parser.add_argument("--weight_decay", default=1e-4, type=float) - parser.add_argument("--epochs", default=50, type=int) - parser.add_argument("--lr_drop", default=40, type=int) - parser.add_argument("--lr_drop_epochs", default=None, type=int, nargs="+") - parser.add_argument( - "--clip_max_norm", default=0.1, type=float, help="gradient clipping max norm" - ) - - parser.add_argument("--sgd", action="store_true") - - # Variants of Deformable DETR - parser.add_argument("--with_box_refine", default=False, action="store_true") - parser.add_argument("--two_stage", default=False, action="store_true") - - # Model parameters - parser.add_argument( - "--frozen_weights", - type=str, - default=None, - help="Path to the pretrained model. If set, only the mask head will be trained", - ) - - # * Backbone - parser.add_argument( - "--backbone", default="resnet50", type=str, help="Name of the convolutional backbone to use" - ) - parser.add_argument( - "--dilation", - action="store_true", - help="If true, we replace stride with dilation in the last convolutional block (DC5)", - ) - parser.add_argument( - "--position_embedding", - default="sine", - type=str, - choices=("sine", "learned"), - help="Type of positional embedding to use on top of the image features", - ) - parser.add_argument( - "--position_embedding_scale", default=2 * np.pi, type=float, help="position / size * scale" - ) - parser.add_argument( - "--num_feature_levels", default=4, type=int, help="number of feature levels" - ) - - # * Transformer - parser.add_argument( - "--enc_layers", default=6, type=int, help="Number of encoding layers in the transformer" - ) - parser.add_argument( - "--dec_layers", default=6, type=int, help="Number of decoding layers in the transformer" - ) - parser.add_argument( - "--dim_feedforward", - default=1024, - type=int, - help="Intermediate size of the feedforward layers in the transformer blocks", - ) - parser.add_argument( - "--hidden_dim", - default=256, - type=int, - help="Size of the embeddings (dimension of the transformer)", - ) - parser.add_argument( - "--dropout", default=0.1, type=float, help="Dropout applied in the transformer" - ) - parser.add_argument( - "--nheads", - default=8, - type=int, - help="Number of attention heads inside the transformer's attentions", - ) - parser.add_argument("--num_queries", default=300, type=int, help="Number of query slots") - parser.add_argument("--dec_n_points", default=4, type=int) - parser.add_argument("--enc_n_points", default=4, type=int) - - # * Segmentation - parser.add_argument( - "--masks", action="store_true", help="Train segmentation head if the flag is provided" - ) - - # Loss - parser.add_argument( - "--no_aux_loss", - dest="aux_loss", - action="store_false", - help="Disables auxiliary decoding losses (loss at each layer)", - ) - - # * Matcher - parser.add_argument( - "--set_cost_class", default=2, type=float, help="Class coefficient in the matching cost" - ) - parser.add_argument( - "--set_cost_bbox", default=5, type=float, help="L1 box coefficient in the matching cost" - ) - parser.add_argument( - "--set_cost_giou", default=2, type=float, help="giou box coefficient in the matching cost" - ) - - # * Loss coefficients - parser.add_argument("--mask_loss_coef", default=1, type=float) - parser.add_argument("--dice_loss_coef", default=1, type=float) - parser.add_argument("--cls_loss_coef", default=2, type=float) - parser.add_argument("--bbox_loss_coef", default=5, type=float) - parser.add_argument("--giou_loss_coef", default=2, type=float) - parser.add_argument("--focal_alpha", default=0.25, type=float) - - # dataset parameters - parser.add_argument("--dataset_file", default="coco") - parser.add_argument("--coco_path", default="./data/coco", type=str) - parser.add_argument("--coco_panoptic_path", type=str) - parser.add_argument("--remove_difficult", action="store_true") - - parser.add_argument("--output_dir", default="", help="path where to save, empty for no saving") - parser.add_argument("--device", default="cuda", help="device to use for training / testing") - parser.add_argument("--seed", default=42, type=int) - parser.add_argument("--resume", default="", help="resume from checkpoint") - parser.add_argument("--start_epoch", default=0, type=int, metavar="N", help="start epoch") - parser.add_argument("--eval", action="store_true") - parser.add_argument("--num_workers", default=2, type=int) - parser.add_argument( - "--cache_mode", default=False, action="store_true", help="whether to cache images on memory" - ) - - return parser - - -def main(args) -> None: - utils.init_distributed_mode(args) - print(f"git:\n {utils.get_sha()}\n") - - if args.frozen_weights is not None: - assert args.masks, "Frozen training is meant for segmentation only" - print(args) - - device = torch.device(args.device) - - # fix the seed for reproducibility - seed = args.seed + utils.get_rank() - torch.manual_seed(seed) - np.random.seed(seed) - random.seed(seed) - - model, criterion, postprocessors = build_model(args) - model.to(device) - - model_without_ddp = model - n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad) - print("number of params:", n_parameters) - - dataset_train = build_dataset(image_set="train", args=args) - dataset_val = build_dataset(image_set="val", args=args) - - if args.distributed: - if args.cache_mode: - sampler_train = samplers.NodeDistributedSampler(dataset_train) - sampler_val = samplers.NodeDistributedSampler(dataset_val, shuffle=False) - else: - sampler_train = samplers.DistributedSampler(dataset_train) - sampler_val = samplers.DistributedSampler(dataset_val, shuffle=False) - else: - sampler_train = torch.utils.data.RandomSampler(dataset_train) - sampler_val = torch.utils.data.SequentialSampler(dataset_val) - - batch_sampler_train = torch.utils.data.BatchSampler( - sampler_train, args.batch_size, drop_last=True - ) - - data_loader_train = DataLoader( - dataset_train, - batch_sampler=batch_sampler_train, - collate_fn=utils.collate_fn, - num_workers=args.num_workers, - pin_memory=True, - ) - data_loader_val = DataLoader( - dataset_val, - args.batch_size, - sampler=sampler_val, - drop_last=False, - collate_fn=utils.collate_fn, - num_workers=args.num_workers, - pin_memory=True, - ) - - # lr_backbone_names = ["backbone.0", "backbone.neck", "input_proj", "transformer.encoder"] - def match_name_keywords(n, name_keywords): - out = False - for b in name_keywords: - if b in n: - out = True - break - return out - - for n, _p in model_without_ddp.named_parameters(): - print(n) - - param_dicts = [ - { - "params": [ - p - for n, p in model_without_ddp.named_parameters() - if not match_name_keywords(n, args.lr_backbone_names) - and not match_name_keywords(n, args.lr_linear_proj_names) - and p.requires_grad - ], - "lr": args.lr, - }, - { - "params": [ - p - for n, p in model_without_ddp.named_parameters() - if match_name_keywords(n, args.lr_backbone_names) and p.requires_grad - ], - "lr": args.lr_backbone, - }, - { - "params": [ - p - for n, p in model_without_ddp.named_parameters() - if match_name_keywords(n, args.lr_linear_proj_names) and p.requires_grad - ], - "lr": args.lr * args.lr_linear_proj_mult, - }, - ] - if args.sgd: - optimizer = torch.optim.SGD( - param_dicts, lr=args.lr, momentum=0.9, weight_decay=args.weight_decay - ) - else: - optimizer = torch.optim.AdamW(param_dicts, lr=args.lr, weight_decay=args.weight_decay) - lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, args.lr_drop) - - if args.distributed: - model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu]) - model_without_ddp = model.module - - if args.dataset_file == "coco_panoptic": - # We also evaluate AP during panoptic training, on original coco DS - coco_val = datasets.coco.build("val", args) - base_ds = get_coco_api_from_dataset(coco_val) - else: - base_ds = get_coco_api_from_dataset(dataset_val) - - if args.frozen_weights is not None: - checkpoint = torch.load(args.frozen_weights, map_location="cpu") - model_without_ddp.detr.load_state_dict(checkpoint["model"]) - - output_dir = Path(args.output_dir) - if args.resume: - if args.resume.startswith("https"): - checkpoint = torch.hub.load_state_dict_from_url( - args.resume, map_location="cpu", check_hash=True - ) - else: - checkpoint = torch.load(args.resume, map_location="cpu") - missing_keys, unexpected_keys = model_without_ddp.load_state_dict( - checkpoint["model"], strict=False - ) - unexpected_keys = [ - k - for k in unexpected_keys - if not (k.endswith("total_params") or k.endswith("total_ops")) - ] - if len(missing_keys) > 0: - print(f"Missing Keys: {missing_keys}") - if len(unexpected_keys) > 0: - print(f"Unexpected Keys: {unexpected_keys}") - if ( - not args.eval - and "optimizer" in checkpoint - and "lr_scheduler" in checkpoint - and "epoch" in checkpoint - ): - import copy - - p_groups = copy.deepcopy(optimizer.param_groups) - optimizer.load_state_dict(checkpoint["optimizer"]) - for pg, pg_old in zip(optimizer.param_groups, p_groups, strict=False): - pg["lr"] = pg_old["lr"] - pg["initial_lr"] = pg_old["initial_lr"] - print(optimizer.param_groups) - lr_scheduler.load_state_dict(checkpoint["lr_scheduler"]) - # todo: this is a hack for doing experiment that resume from checkpoint and also modify lr scheduler (e.g., decrease lr in advance). - args.override_resumed_lr_drop = True - if args.override_resumed_lr_drop: - print( - "Warning: (hack) args.override_resumed_lr_drop is set to True, so args.lr_drop would override lr_drop in resumed lr_scheduler." - ) - lr_scheduler.step_size = args.lr_drop - lr_scheduler.base_lrs = list( - map(lambda group: group["initial_lr"], optimizer.param_groups) - ) - lr_scheduler.step(lr_scheduler.last_epoch) - args.start_epoch = checkpoint["epoch"] + 1 - # check the resumed model - if not args.eval: - test_stats, coco_evaluator = evaluate( - model, criterion, postprocessors, data_loader_val, base_ds, device, args.output_dir - ) - - if args.eval: - test_stats, coco_evaluator = evaluate( - model, criterion, postprocessors, data_loader_val, base_ds, device, args.output_dir - ) - if args.output_dir: - utils.save_on_master(coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval.pth") - return - - print("Start training") - start_time = time.time() - for epoch in range(args.start_epoch, args.epochs): - if args.distributed: - sampler_train.set_epoch(epoch) - train_stats = train_one_epoch( - model, criterion, data_loader_train, optimizer, device, epoch, args.clip_max_norm - ) - lr_scheduler.step() - if args.output_dir: - checkpoint_paths = [output_dir / "checkpoint.pth"] - # extra checkpoint before LR drop and every 5 epochs - if (epoch + 1) % args.lr_drop == 0 or (epoch + 1) % 5 == 0: - checkpoint_paths.append(output_dir / f"checkpoint{epoch:04}.pth") - for checkpoint_path in checkpoint_paths: - utils.save_on_master( - { - "model": model_without_ddp.state_dict(), - "optimizer": optimizer.state_dict(), - "lr_scheduler": lr_scheduler.state_dict(), - "epoch": epoch, - "args": args, - }, - checkpoint_path, - ) - - test_stats, coco_evaluator = evaluate( - model, criterion, postprocessors, data_loader_val, base_ds, device, args.output_dir - ) - - log_stats = { - **{f"train_{k}": v for k, v in train_stats.items()}, - **{f"test_{k}": v for k, v in test_stats.items()}, - "epoch": epoch, - "n_parameters": n_parameters, - } - - if args.output_dir and utils.is_main_process(): - with (output_dir / "log.txt").open("a") as f: - f.write(json.dumps(log_stats) + "\n") - - # for evaluation logs - if coco_evaluator is not None: - (output_dir / "eval").mkdir(exist_ok=True) - if "bbox" in coco_evaluator.coco_eval: - filenames = ["latest.pth"] - if epoch % 50 == 0: - filenames.append(f"{epoch:03}.pth") - for name in filenames: - torch.save( - coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval" / name - ) - - total_time = time.time() - start_time - total_time_str = str(datetime.timedelta(seconds=int(total_time))) - print(f"Training time {total_time_str}") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - "Deformable DETR training and evaluation script", parents=[get_args_parser()] - ) - args = parser.parse_args() - if args.output_dir: - Path(args.output_dir).mkdir(parents=True, exist_ok=True) - main(args) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/models/__init__.py deleted file mode 100644 index 46b898b988..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -from .deformable_detr import build - - -def build_model(args): - return build(args) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/backbone.py b/dimos/models/Detic/third_party/Deformable-DETR/models/backbone.py deleted file mode 100644 index cd973fa891..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/backbone.py +++ /dev/null @@ -1,142 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Backbone modules. -""" - -from typing import Dict, List - -import torch -from torch import nn -import torch.nn.functional as F -import torchvision -from torchvision.models._utils import IntermediateLayerGetter -from util.misc import NestedTensor, is_main_process - -from .position_encoding import build_position_encoding - - -class FrozenBatchNorm2d(torch.nn.Module): - """ - BatchNorm2d where the batch statistics and the affine parameters are fixed. - - Copy-paste from torchvision.misc.ops with added eps before rqsrt, - without which any other models than torchvision.models.resnet[18,34,50,101] - produce nans. - """ - - def __init__(self, n, eps: float=1e-5) -> None: - super().__init__() - self.register_buffer("weight", torch.ones(n)) - self.register_buffer("bias", torch.zeros(n)) - self.register_buffer("running_mean", torch.zeros(n)) - self.register_buffer("running_var", torch.ones(n)) - self.eps = eps - - def _load_from_state_dict( - self, state_dict, prefix: str, local_metadata, strict: bool, missing_keys, unexpected_keys, error_msgs - ) -> None: - num_batches_tracked_key = prefix + "num_batches_tracked" - if num_batches_tracked_key in state_dict: - del state_dict[num_batches_tracked_key] - - super()._load_from_state_dict( - state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs - ) - - def forward(self, x): - # move reshapes to the beginning - # to make it fuser-friendly - w = self.weight.reshape(1, -1, 1, 1) - b = self.bias.reshape(1, -1, 1, 1) - rv = self.running_var.reshape(1, -1, 1, 1) - rm = self.running_mean.reshape(1, -1, 1, 1) - eps = self.eps - scale = w * (rv + eps).rsqrt() - bias = b - rm * scale - return x * scale + bias - - -class BackboneBase(nn.Module): - def __init__(self, backbone: nn.Module, train_backbone: bool, return_interm_layers: bool) -> None: - super().__init__() - for name, parameter in backbone.named_parameters(): - if ( - not train_backbone - or ("layer2" not in name - and "layer3" not in name - and "layer4" not in name) - ): - parameter.requires_grad_(False) - if return_interm_layers: - # return_layers = {"layer1": "0", "layer2": "1", "layer3": "2", "layer4": "3"} - return_layers = {"layer2": "0", "layer3": "1", "layer4": "2"} - self.strides = [8, 16, 32] - self.num_channels = [512, 1024, 2048] - else: - return_layers = {"layer4": "0"} - self.strides = [32] - self.num_channels = [2048] - self.body = IntermediateLayerGetter(backbone, return_layers=return_layers) - - def forward(self, tensor_list: NestedTensor): - xs = self.body(tensor_list.tensors) - out: dict[str, NestedTensor] = {} - for name, x in xs.items(): - m = tensor_list.mask - assert m is not None - mask = F.interpolate(m[None].float(), size=x.shape[-2:]).to(torch.bool)[0] - out[name] = NestedTensor(x, mask) - return out - - -class Backbone(BackboneBase): - """ResNet backbone with frozen BatchNorm.""" - - def __init__(self, name: str, train_backbone: bool, return_interm_layers: bool, dilation: bool) -> None: - norm_layer = FrozenBatchNorm2d - backbone = getattr(torchvision.models, name)( - replace_stride_with_dilation=[False, False, dilation], - pretrained=is_main_process(), - norm_layer=norm_layer, - ) - assert name not in ("resnet18", "resnet34"), "number of channels are hard coded" - super().__init__(backbone, train_backbone, return_interm_layers) - if dilation: - self.strides[-1] = self.strides[-1] // 2 - - -class Joiner(nn.Sequential): - def __init__(self, backbone, position_embedding) -> None: - super().__init__(backbone, position_embedding) - self.strides = backbone.strides - self.num_channels = backbone.num_channels - - def forward(self, tensor_list: NestedTensor): - xs = self[0](tensor_list) - out: list[NestedTensor] = [] - pos = [] - for _name, x in sorted(xs.items()): - out.append(x) - - # position encoding - for x in out: - pos.append(self[1](x).to(x.tensors.dtype)) - - return out, pos - - -def build_backbone(args): - position_embedding = build_position_encoding(args) - train_backbone = args.lr_backbone > 0 - return_interm_layers = args.masks or (args.num_feature_levels > 1) - backbone = Backbone(args.backbone, train_backbone, return_interm_layers, args.dilation) - model = Joiner(backbone, position_embedding) - return model diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_detr.py b/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_detr.py deleted file mode 100644 index 661c6b3d98..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_detr.py +++ /dev/null @@ -1,552 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Deformable DETR model and criterion classes. -""" - -import copy -import math - -import torch -from torch import nn -import torch.nn.functional as F -from util import box_ops -from util.misc import ( - NestedTensor, - accuracy, - get_world_size, - interpolate, - inverse_sigmoid, - is_dist_avail_and_initialized, - nested_tensor_from_tensor_list, -) - -from .backbone import build_backbone -from .deformable_transformer import build_deforamble_transformer -from .matcher import build_matcher -from .segmentation import ( - DETRsegm, - PostProcessPanoptic, - PostProcessSegm, - dice_loss, - sigmoid_focal_loss, -) -from typing import Sequence - - -def _get_clones(module, N): - return nn.ModuleList([copy.deepcopy(module) for i in range(N)]) - - -class DeformableDETR(nn.Module): - """This is the Deformable DETR module that performs object detection""" - - def __init__( - self, - backbone, - transformer, - num_classes: int, - num_queries: int, - num_feature_levels: int, - aux_loss: bool=True, - with_box_refine: bool=False, - two_stage: bool=False, - ) -> None: - """Initializes the model. - Parameters: - backbone: torch module of the backbone to be used. See backbone.py - transformer: torch module of the transformer architecture. See transformer.py - num_classes: number of object classes - num_queries: number of object queries, ie detection slot. This is the maximal number of objects - DETR can detect in a single image. For COCO, we recommend 100 queries. - aux_loss: True if auxiliary decoding losses (loss at each decoder layer) are to be used. - with_box_refine: iterative bounding box refinement - two_stage: two-stage Deformable DETR - """ - super().__init__() - self.num_queries = num_queries - self.transformer = transformer - hidden_dim = transformer.d_model - self.class_embed = nn.Linear(hidden_dim, num_classes) - self.bbox_embed = MLP(hidden_dim, hidden_dim, 4, 3) - self.num_feature_levels = num_feature_levels - if not two_stage: - self.query_embed = nn.Embedding(num_queries, hidden_dim * 2) - if num_feature_levels > 1: - num_backbone_outs = len(backbone.strides) - input_proj_list = [] - for _ in range(num_backbone_outs): - in_channels = backbone.num_channels[_] - input_proj_list.append( - nn.Sequential( - nn.Conv2d(in_channels, hidden_dim, kernel_size=1), - nn.GroupNorm(32, hidden_dim), - ) - ) - for _ in range(num_feature_levels - num_backbone_outs): - input_proj_list.append( - nn.Sequential( - nn.Conv2d(in_channels, hidden_dim, kernel_size=3, stride=2, padding=1), - nn.GroupNorm(32, hidden_dim), - ) - ) - in_channels = hidden_dim - self.input_proj = nn.ModuleList(input_proj_list) - else: - self.input_proj = nn.ModuleList( - [ - nn.Sequential( - nn.Conv2d(backbone.num_channels[0], hidden_dim, kernel_size=1), - nn.GroupNorm(32, hidden_dim), - ) - ] - ) - self.backbone = backbone - self.aux_loss = aux_loss - self.with_box_refine = with_box_refine - self.two_stage = two_stage - - prior_prob = 0.01 - bias_value = -math.log((1 - prior_prob) / prior_prob) - self.class_embed.bias.data = torch.ones(num_classes) * bias_value - nn.init.constant_(self.bbox_embed.layers[-1].weight.data, 0) - nn.init.constant_(self.bbox_embed.layers[-1].bias.data, 0) - for proj in self.input_proj: - nn.init.xavier_uniform_(proj[0].weight, gain=1) - nn.init.constant_(proj[0].bias, 0) - - # if two-stage, the last class_embed and bbox_embed is for region proposal generation - num_pred = ( - (transformer.decoder.num_layers + 1) if two_stage else transformer.decoder.num_layers - ) - if with_box_refine: - self.class_embed = _get_clones(self.class_embed, num_pred) - self.bbox_embed = _get_clones(self.bbox_embed, num_pred) - nn.init.constant_(self.bbox_embed[0].layers[-1].bias.data[2:], -2.0) - # hack implementation for iterative bounding box refinement - self.transformer.decoder.bbox_embed = self.bbox_embed - else: - nn.init.constant_(self.bbox_embed.layers[-1].bias.data[2:], -2.0) - self.class_embed = nn.ModuleList([self.class_embed for _ in range(num_pred)]) - self.bbox_embed = nn.ModuleList([self.bbox_embed for _ in range(num_pred)]) - self.transformer.decoder.bbox_embed = None - if two_stage: - # hack implementation for two-stage - self.transformer.decoder.class_embed = self.class_embed - for box_embed in self.bbox_embed: - nn.init.constant_(box_embed.layers[-1].bias.data[2:], 0.0) - - def forward(self, samples: NestedTensor): - """The forward expects a NestedTensor, which consists of: - - samples.tensor: batched images, of shape [batch_size x 3 x H x W] - - samples.mask: a binary mask of shape [batch_size x H x W], containing 1 on padded pixels - - It returns a dict with the following elements: - - "pred_logits": the classification logits (including no-object) for all queries. - Shape= [batch_size x num_queries x (num_classes + 1)] - - "pred_boxes": The normalized boxes coordinates for all queries, represented as - (center_x, center_y, height, width). These values are normalized in [0, 1], - relative to the size of each individual image (disregarding possible padding). - See PostProcess for information on how to retrieve the unnormalized bounding box. - - "aux_outputs": Optional, only returned when auxilary losses are activated. It is a list of - dictionnaries containing the two above keys for each decoder layer. - """ - if not isinstance(samples, NestedTensor): - samples = nested_tensor_from_tensor_list(samples) - features, pos = self.backbone(samples) - - srcs = [] - masks = [] - for l, feat in enumerate(features): - src, mask = feat.decompose() - srcs.append(self.input_proj[l](src)) - masks.append(mask) - assert mask is not None - if self.num_feature_levels > len(srcs): - _len_srcs = len(srcs) - for l in range(_len_srcs, self.num_feature_levels): - if l == _len_srcs: - src = self.input_proj[l](features[-1].tensors) - else: - src = self.input_proj[l](srcs[-1]) - m = samples.mask - mask = F.interpolate(m[None].float(), size=src.shape[-2:]).to(torch.bool)[0] - pos_l = self.backbone[1](NestedTensor(src, mask)).to(src.dtype) - srcs.append(src) - masks.append(mask) - pos.append(pos_l) - - query_embeds = None - if not self.two_stage: - query_embeds = self.query_embed.weight - hs, init_reference, inter_references, enc_outputs_class, enc_outputs_coord_unact = ( - self.transformer(srcs, masks, pos, query_embeds) - ) - - outputs_classes = [] - outputs_coords = [] - for lvl in range(hs.shape[0]): - if lvl == 0: - reference = init_reference - else: - reference = inter_references[lvl - 1] - reference = inverse_sigmoid(reference) - outputs_class = self.class_embed[lvl](hs[lvl]) - tmp = self.bbox_embed[lvl](hs[lvl]) - if reference.shape[-1] == 4: - tmp += reference - else: - assert reference.shape[-1] == 2 - tmp[..., :2] += reference - outputs_coord = tmp.sigmoid() - outputs_classes.append(outputs_class) - outputs_coords.append(outputs_coord) - outputs_class = torch.stack(outputs_classes) - outputs_coord = torch.stack(outputs_coords) - - out = {"pred_logits": outputs_class[-1], "pred_boxes": outputs_coord[-1]} - if self.aux_loss: - out["aux_outputs"] = self._set_aux_loss(outputs_class, outputs_coord) - - if self.two_stage: - enc_outputs_coord = enc_outputs_coord_unact.sigmoid() - out["enc_outputs"] = {"pred_logits": enc_outputs_class, "pred_boxes": enc_outputs_coord} - return out - - @torch.jit.unused - def _set_aux_loss(self, outputs_class, outputs_coord): - # this is a workaround to make torchscript happy, as torchscript - # doesn't support dictionary with non-homogeneous values, such - # as a dict having both a Tensor and a list. - return [ - {"pred_logits": a, "pred_boxes": b} - for a, b in zip(outputs_class[:-1], outputs_coord[:-1], strict=False) - ] - - -class SetCriterion(nn.Module): - """This class computes the loss for DETR. - The process happens in two steps: - 1) we compute hungarian assignment between ground truth boxes and the outputs of the model - 2) we supervise each pair of matched ground-truth / prediction (supervise class and box) - """ - - def __init__(self, num_classes: int, matcher, weight_dict, losses, focal_alpha: float=0.25) -> None: - """Create the criterion. - Parameters: - num_classes: number of object categories, omitting the special no-object category - matcher: module able to compute a matching between targets and proposals - weight_dict: dict containing as key the names of the losses and as values their relative weight. - losses: list of all the losses to be applied. See get_loss for list of available losses. - focal_alpha: alpha in Focal Loss - """ - super().__init__() - self.num_classes = num_classes - self.matcher = matcher - self.weight_dict = weight_dict - self.losses = losses - self.focal_alpha = focal_alpha - - def loss_labels(self, outputs, targets, indices, num_boxes: int, log: bool=True): - """Classification loss (NLL) - targets dicts must contain the key "labels" containing a tensor of dim [nb_target_boxes] - """ - assert "pred_logits" in outputs - src_logits = outputs["pred_logits"] - - idx = self._get_src_permutation_idx(indices) - target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices, strict=False)]) - target_classes = torch.full( - src_logits.shape[:2], self.num_classes, dtype=torch.int64, device=src_logits.device - ) - target_classes[idx] = target_classes_o - - target_classes_onehot = torch.zeros( - [src_logits.shape[0], src_logits.shape[1], src_logits.shape[2] + 1], - dtype=src_logits.dtype, - layout=src_logits.layout, - device=src_logits.device, - ) - target_classes_onehot.scatter_(2, target_classes.unsqueeze(-1), 1) - - target_classes_onehot = target_classes_onehot[:, :, :-1] - loss_ce = ( - sigmoid_focal_loss( - src_logits, target_classes_onehot, num_boxes, alpha=self.focal_alpha, gamma=2 - ) - * src_logits.shape[1] - ) - losses = {"loss_ce": loss_ce} - - if log: - # TODO this should probably be a separate loss, not hacked in this one here - losses["class_error"] = 100 - accuracy(src_logits[idx], target_classes_o)[0] - return losses - - @torch.no_grad() - def loss_cardinality(self, outputs, targets, indices, num_boxes: int): - """Compute the cardinality error, ie the absolute error in the number of predicted non-empty boxes - This is not really a loss, it is intended for logging purposes only. It doesn't propagate gradients - """ - pred_logits = outputs["pred_logits"] - device = pred_logits.device - tgt_lengths = torch.as_tensor([len(v["labels"]) for v in targets], device=device) - # Count the number of predictions that are NOT "no-object" (which is the last class) - card_pred = (pred_logits.argmax(-1) != pred_logits.shape[-1] - 1).sum(1) - card_err = F.l1_loss(card_pred.float(), tgt_lengths.float()) - losses = {"cardinality_error": card_err} - return losses - - def loss_boxes(self, outputs, targets, indices, num_boxes: int): - """Compute the losses related to the bounding boxes, the L1 regression loss and the GIoU loss - targets dicts must contain the key "boxes" containing a tensor of dim [nb_target_boxes, 4] - The target boxes are expected in format (center_x, center_y, h, w), normalized by the image size. - """ - assert "pred_boxes" in outputs - idx = self._get_src_permutation_idx(indices) - src_boxes = outputs["pred_boxes"][idx] - target_boxes = torch.cat([t["boxes"][i] for t, (_, i) in zip(targets, indices, strict=False)], dim=0) - - loss_bbox = F.l1_loss(src_boxes, target_boxes, reduction="none") - - losses = {} - losses["loss_bbox"] = loss_bbox.sum() / num_boxes - - loss_giou = 1 - torch.diag( - box_ops.generalized_box_iou( - box_ops.box_cxcywh_to_xyxy(src_boxes), box_ops.box_cxcywh_to_xyxy(target_boxes) - ) - ) - losses["loss_giou"] = loss_giou.sum() / num_boxes - return losses - - def loss_masks(self, outputs, targets, indices, num_boxes: int): - """Compute the losses related to the masks: the focal loss and the dice loss. - targets dicts must contain the key "masks" containing a tensor of dim [nb_target_boxes, h, w] - """ - assert "pred_masks" in outputs - - src_idx = self._get_src_permutation_idx(indices) - tgt_idx = self._get_tgt_permutation_idx(indices) - - src_masks = outputs["pred_masks"] - - # TODO use valid to mask invalid areas due to padding in loss - target_masks, valid = nested_tensor_from_tensor_list( - [t["masks"] for t in targets] - ).decompose() - target_masks = target_masks.to(src_masks) - - src_masks = src_masks[src_idx] - # upsample predictions to the target size - src_masks = interpolate( - src_masks[:, None], size=target_masks.shape[-2:], mode="bilinear", align_corners=False - ) - src_masks = src_masks[:, 0].flatten(1) - - target_masks = target_masks[tgt_idx].flatten(1) - - losses = { - "loss_mask": sigmoid_focal_loss(src_masks, target_masks, num_boxes), - "loss_dice": dice_loss(src_masks, target_masks, num_boxes), - } - return losses - - def _get_src_permutation_idx(self, indices): - # permute predictions following indices - batch_idx = torch.cat([torch.full_like(src, i) for i, (src, _) in enumerate(indices)]) - src_idx = torch.cat([src for (src, _) in indices]) - return batch_idx, src_idx - - def _get_tgt_permutation_idx(self, indices): - # permute targets following indices - batch_idx = torch.cat([torch.full_like(tgt, i) for i, (_, tgt) in enumerate(indices)]) - tgt_idx = torch.cat([tgt for (_, tgt) in indices]) - return batch_idx, tgt_idx - - def get_loss(self, loss, outputs, targets, indices, num_boxes: int, **kwargs): - loss_map = { - "labels": self.loss_labels, - "cardinality": self.loss_cardinality, - "boxes": self.loss_boxes, - "masks": self.loss_masks, - } - assert loss in loss_map, f"do you really want to compute {loss} loss?" - return loss_map[loss](outputs, targets, indices, num_boxes, **kwargs) - - def forward(self, outputs, targets): - """This performs the loss computation. - Parameters: - outputs: dict of tensors, see the output specification of the model for the format - targets: list of dicts, such that len(targets) == batch_size. - The expected keys in each dict depends on the losses applied, see each loss' doc - """ - outputs_without_aux = { - k: v for k, v in outputs.items() if k != "aux_outputs" and k != "enc_outputs" - } - - # Retrieve the matching between the outputs of the last layer and the targets - indices = self.matcher(outputs_without_aux, targets) - - # Compute the average number of target boxes accross all nodes, for normalization purposes - num_boxes = sum(len(t["labels"]) for t in targets) - num_boxes = torch.as_tensor( - [num_boxes], dtype=torch.float, device=next(iter(outputs.values())).device - ) - if is_dist_avail_and_initialized(): - torch.distributed.all_reduce(num_boxes) - num_boxes = torch.clamp(num_boxes / get_world_size(), min=1).item() - - # Compute all the requested losses - losses = {} - for loss in self.losses: - kwargs = {} - losses.update(self.get_loss(loss, outputs, targets, indices, num_boxes, **kwargs)) - - # In case of auxiliary losses, we repeat this process with the output of each intermediate layer. - if "aux_outputs" in outputs: - for i, aux_outputs in enumerate(outputs["aux_outputs"]): - indices = self.matcher(aux_outputs, targets) - for loss in self.losses: - if loss == "masks": - # Intermediate masks losses are too costly to compute, we ignore them. - continue - kwargs = {} - if loss == "labels": - # Logging is enabled only for the last layer - kwargs["log"] = False - l_dict = self.get_loss(loss, aux_outputs, targets, indices, num_boxes, **kwargs) - l_dict = {k + f"_{i}": v for k, v in l_dict.items()} - losses.update(l_dict) - - if "enc_outputs" in outputs: - enc_outputs = outputs["enc_outputs"] - bin_targets = copy.deepcopy(targets) - for bt in bin_targets: - bt["labels"] = torch.zeros_like(bt["labels"]) - indices = self.matcher(enc_outputs, bin_targets) - for loss in self.losses: - if loss == "masks": - # Intermediate masks losses are too costly to compute, we ignore them. - continue - kwargs = {} - if loss == "labels": - # Logging is enabled only for the last layer - kwargs["log"] = False - l_dict = self.get_loss(loss, enc_outputs, bin_targets, indices, num_boxes, **kwargs) - l_dict = {k + "_enc": v for k, v in l_dict.items()} - losses.update(l_dict) - - return losses - - -class PostProcess(nn.Module): - """This module converts the model's output into the format expected by the coco api""" - - @torch.no_grad() - def forward(self, outputs, target_sizes: Sequence[int]): - """Perform the computation - Parameters: - outputs: raw outputs of the model - target_sizes: tensor of dimension [batch_size x 2] containing the size of each images of the batch - For evaluation, this must be the original image size (before any data augmentation) - For visualization, this should be the image size after data augment, but before padding - """ - out_logits, out_bbox = outputs["pred_logits"], outputs["pred_boxes"] - - assert len(out_logits) == len(target_sizes) - assert target_sizes.shape[1] == 2 - - prob = out_logits.sigmoid() - topk_values, topk_indexes = torch.topk(prob.view(out_logits.shape[0], -1), 100, dim=1) - scores = topk_values - topk_boxes = topk_indexes // out_logits.shape[2] - labels = topk_indexes % out_logits.shape[2] - boxes = box_ops.box_cxcywh_to_xyxy(out_bbox) - boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1, 1, 4)) - - # and from relative [0, 1] to absolute [0, height] coordinates - img_h, img_w = target_sizes.unbind(1) - scale_fct = torch.stack([img_w, img_h, img_w, img_h], dim=1) - boxes = boxes * scale_fct[:, None, :] - - results = [{"scores": s, "labels": l, "boxes": b} for s, l, b in zip(scores, labels, boxes, strict=False)] - - return results - - -class MLP(nn.Module): - """Very simple multi-layer perceptron (also called FFN)""" - - def __init__(self, input_dim, hidden_dim, output_dim, num_layers: int) -> None: - super().__init__() - self.num_layers = num_layers - h = [hidden_dim] * (num_layers - 1) - self.layers = nn.ModuleList( - nn.Linear(n, k) for n, k in zip([input_dim, *h], [*h, output_dim], strict=False) - ) - - def forward(self, x): - for i, layer in enumerate(self.layers): - x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x) - return x - - -def build(args): - num_classes = 20 if args.dataset_file != "coco" else 91 - if args.dataset_file == "coco_panoptic": - num_classes = 250 - device = torch.device(args.device) - - backbone = build_backbone(args) - - transformer = build_deforamble_transformer(args) - model = DeformableDETR( - backbone, - transformer, - num_classes=num_classes, - num_queries=args.num_queries, - num_feature_levels=args.num_feature_levels, - aux_loss=args.aux_loss, - with_box_refine=args.with_box_refine, - two_stage=args.two_stage, - ) - if args.masks: - model = DETRsegm(model, freeze_detr=(args.frozen_weights is not None)) - matcher = build_matcher(args) - weight_dict = {"loss_ce": args.cls_loss_coef, "loss_bbox": args.bbox_loss_coef} - weight_dict["loss_giou"] = args.giou_loss_coef - if args.masks: - weight_dict["loss_mask"] = args.mask_loss_coef - weight_dict["loss_dice"] = args.dice_loss_coef - # TODO this is a hack - if args.aux_loss: - aux_weight_dict = {} - for i in range(args.dec_layers - 1): - aux_weight_dict.update({k + f"_{i}": v for k, v in weight_dict.items()}) - aux_weight_dict.update({k + "_enc": v for k, v in weight_dict.items()}) - weight_dict.update(aux_weight_dict) - - losses = ["labels", "boxes", "cardinality"] - if args.masks: - losses += ["masks"] - # num_classes, matcher, weight_dict, losses, focal_alpha=0.25 - criterion = SetCriterion( - num_classes, matcher, weight_dict, losses, focal_alpha=args.focal_alpha - ) - criterion.to(device) - postprocessors = {"bbox": PostProcess()} - if args.masks: - postprocessors["segm"] = PostProcessSegm() - if args.dataset_file == "coco_panoptic": - is_thing_map = {i: i <= 90 for i in range(201)} - postprocessors["panoptic"] = PostProcessPanoptic(is_thing_map, threshold=0.85) - - return model, criterion, postprocessors diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_transformer.py b/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_transformer.py deleted file mode 100644 index f3cde19e1b..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_transformer.py +++ /dev/null @@ -1,507 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -import copy -import math - -import torch -from torch import nn -import torch.nn.functional as F -from torch.nn.init import constant_, normal_, xavier_uniform_ -from util.misc import inverse_sigmoid - -from models.ops.modules import MSDeformAttn - - -class DeformableTransformer(nn.Module): - def __init__( - self, - d_model: int=256, - nhead: int=8, - num_encoder_layers: int=6, - num_decoder_layers: int=6, - dim_feedforward: int=1024, - dropout: float=0.1, - activation: str="relu", - return_intermediate_dec: bool=False, - num_feature_levels: int=4, - dec_n_points: int=4, - enc_n_points: int=4, - two_stage: bool=False, - two_stage_num_proposals: int=300, - ) -> None: - super().__init__() - - self.d_model = d_model - self.nhead = nhead - self.two_stage = two_stage - self.two_stage_num_proposals = two_stage_num_proposals - - encoder_layer = DeformableTransformerEncoderLayer( - d_model, dim_feedforward, dropout, activation, num_feature_levels, nhead, enc_n_points - ) - self.encoder = DeformableTransformerEncoder(encoder_layer, num_encoder_layers) - - decoder_layer = DeformableTransformerDecoderLayer( - d_model, dim_feedforward, dropout, activation, num_feature_levels, nhead, dec_n_points - ) - self.decoder = DeformableTransformerDecoder( - decoder_layer, num_decoder_layers, return_intermediate_dec - ) - - self.level_embed = nn.Parameter(torch.Tensor(num_feature_levels, d_model)) - - if two_stage: - self.enc_output = nn.Linear(d_model, d_model) - self.enc_output_norm = nn.LayerNorm(d_model) - self.pos_trans = nn.Linear(d_model * 2, d_model * 2) - self.pos_trans_norm = nn.LayerNorm(d_model * 2) - else: - self.reference_points = nn.Linear(d_model, 2) - - self._reset_parameters() - - def _reset_parameters(self) -> None: - for p in self.parameters(): - if p.dim() > 1: - nn.init.xavier_uniform_(p) - for m in self.modules(): - if isinstance(m, MSDeformAttn): - m._reset_parameters() - if not self.two_stage: - xavier_uniform_(self.reference_points.weight.data, gain=1.0) - constant_(self.reference_points.bias.data, 0.0) - normal_(self.level_embed) - - def get_proposal_pos_embed(self, proposals): - num_pos_feats = 128 - temperature = 10000 - scale = 2 * math.pi - - dim_t = torch.arange(num_pos_feats, dtype=torch.float32, device=proposals.device) - dim_t = temperature ** (2 * (dim_t // 2) / num_pos_feats) - # N, L, 4 - proposals = proposals.sigmoid() * scale - # N, L, 4, 128 - pos = proposals[:, :, :, None] / dim_t - # N, L, 4, 64, 2 - pos = torch.stack((pos[:, :, :, 0::2].sin(), pos[:, :, :, 1::2].cos()), dim=4).flatten(2) - return pos - - def gen_encoder_output_proposals(self, memory, memory_padding_mask, spatial_shapes): - N_, S_, C_ = memory.shape - proposals = [] - _cur = 0 - for lvl, (H_, W_) in enumerate(spatial_shapes): - mask_flatten_ = memory_padding_mask[:, _cur : (_cur + H_ * W_)].view(N_, H_, W_, 1) - valid_H = torch.sum(~mask_flatten_[:, :, 0, 0], 1) - valid_W = torch.sum(~mask_flatten_[:, 0, :, 0], 1) - - grid_y, grid_x = torch.meshgrid( - torch.linspace(0, H_ - 1, H_, dtype=torch.float32, device=memory.device), - torch.linspace(0, W_ - 1, W_, dtype=torch.float32, device=memory.device), - ) - grid = torch.cat([grid_x.unsqueeze(-1), grid_y.unsqueeze(-1)], -1) - - scale = torch.cat([valid_W.unsqueeze(-1), valid_H.unsqueeze(-1)], 1).view(N_, 1, 1, 2) - grid = (grid.unsqueeze(0).expand(N_, -1, -1, -1) + 0.5) / scale - wh = torch.ones_like(grid) * 0.05 * (2.0**lvl) - proposal = torch.cat((grid, wh), -1).view(N_, -1, 4) - proposals.append(proposal) - _cur += H_ * W_ - output_proposals = torch.cat(proposals, 1) - output_proposals_valid = ((output_proposals > 0.01) & (output_proposals < 0.99)).all( - -1, keepdim=True - ) - output_proposals = torch.log(output_proposals / (1 - output_proposals)) - output_proposals = output_proposals.masked_fill( - memory_padding_mask.unsqueeze(-1), float("inf") - ) - output_proposals = output_proposals.masked_fill(~output_proposals_valid, float("inf")) - - output_memory = memory - output_memory = output_memory.masked_fill(memory_padding_mask.unsqueeze(-1), float(0)) - output_memory = output_memory.masked_fill(~output_proposals_valid, float(0)) - output_memory = self.enc_output_norm(self.enc_output(output_memory)) - return output_memory, output_proposals - - def get_valid_ratio(self, mask): - _, H, W = mask.shape - valid_H = torch.sum(~mask[:, :, 0], 1) - valid_W = torch.sum(~mask[:, 0, :], 1) - valid_ratio_h = valid_H.float() / H - valid_ratio_w = valid_W.float() / W - valid_ratio = torch.stack([valid_ratio_w, valid_ratio_h], -1) - return valid_ratio - - def forward(self, srcs, masks, pos_embeds, query_embed=None): - assert self.two_stage or query_embed is not None - - # prepare input for encoder - src_flatten = [] - mask_flatten = [] - lvl_pos_embed_flatten = [] - spatial_shapes = [] - for lvl, (src, mask, pos_embed) in enumerate(zip(srcs, masks, pos_embeds, strict=False)): - bs, c, h, w = src.shape - spatial_shape = (h, w) - spatial_shapes.append(spatial_shape) - src = src.flatten(2).transpose(1, 2) - mask = mask.flatten(1) - pos_embed = pos_embed.flatten(2).transpose(1, 2) - lvl_pos_embed = pos_embed + self.level_embed[lvl].view(1, 1, -1) - lvl_pos_embed_flatten.append(lvl_pos_embed) - src_flatten.append(src) - mask_flatten.append(mask) - src_flatten = torch.cat(src_flatten, 1) - mask_flatten = torch.cat(mask_flatten, 1) - lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1) - spatial_shapes = torch.as_tensor( - spatial_shapes, dtype=torch.long, device=src_flatten.device - ) - level_start_index = torch.cat( - (spatial_shapes.new_zeros((1,)), spatial_shapes.prod(1).cumsum(0)[:-1]) - ) - valid_ratios = torch.stack([self.get_valid_ratio(m) for m in masks], 1) - - # encoder - memory = self.encoder( - src_flatten, - spatial_shapes, - level_start_index, - valid_ratios, - lvl_pos_embed_flatten, - mask_flatten, - ) - - # prepare input for decoder - bs, _, c = memory.shape - if self.two_stage: - output_memory, output_proposals = self.gen_encoder_output_proposals( - memory, mask_flatten, spatial_shapes - ) - - # hack implementation for two-stage Deformable DETR - enc_outputs_class = self.decoder.class_embed[self.decoder.num_layers](output_memory) - enc_outputs_coord_unact = ( - self.decoder.bbox_embed[self.decoder.num_layers](output_memory) + output_proposals - ) - - topk = self.two_stage_num_proposals - topk_proposals = torch.topk(enc_outputs_class[..., 0], topk, dim=1)[1] - topk_coords_unact = torch.gather( - enc_outputs_coord_unact, 1, topk_proposals.unsqueeze(-1).repeat(1, 1, 4) - ) - topk_coords_unact = topk_coords_unact.detach() - reference_points = topk_coords_unact.sigmoid() - init_reference_out = reference_points - pos_trans_out = self.pos_trans_norm( - self.pos_trans(self.get_proposal_pos_embed(topk_coords_unact)) - ) - query_embed, tgt = torch.split(pos_trans_out, c, dim=2) - else: - query_embed, tgt = torch.split(query_embed, c, dim=1) - query_embed = query_embed.unsqueeze(0).expand(bs, -1, -1) - tgt = tgt.unsqueeze(0).expand(bs, -1, -1) - reference_points = self.reference_points(query_embed).sigmoid() - init_reference_out = reference_points - - # decoder - hs, inter_references = self.decoder( - tgt, - reference_points, - memory, - spatial_shapes, - level_start_index, - valid_ratios, - query_embed, - mask_flatten, - ) - - inter_references_out = inter_references - if self.two_stage: - return ( - hs, - init_reference_out, - inter_references_out, - enc_outputs_class, - enc_outputs_coord_unact, - ) - return hs, init_reference_out, inter_references_out, None, None - - -class DeformableTransformerEncoderLayer(nn.Module): - def __init__( - self, - d_model: int=256, - d_ffn: int=1024, - dropout: float=0.1, - activation: str="relu", - n_levels: int=4, - n_heads: int=8, - n_points: int=4, - ) -> None: - super().__init__() - - # self attention - self.self_attn = MSDeformAttn(d_model, n_levels, n_heads, n_points) - self.dropout1 = nn.Dropout(dropout) - self.norm1 = nn.LayerNorm(d_model) - - # ffn - self.linear1 = nn.Linear(d_model, d_ffn) - self.activation = _get_activation_fn(activation) - self.dropout2 = nn.Dropout(dropout) - self.linear2 = nn.Linear(d_ffn, d_model) - self.dropout3 = nn.Dropout(dropout) - self.norm2 = nn.LayerNorm(d_model) - - @staticmethod - def with_pos_embed(tensor, pos): - return tensor if pos is None else tensor + pos - - def forward_ffn(self, src): - src2 = self.linear2(self.dropout2(self.activation(self.linear1(src)))) - src = src + self.dropout3(src2) - src = self.norm2(src) - return src - - def forward( - self, src, pos, reference_points, spatial_shapes, level_start_index, padding_mask=None - ): - # self attention - src2 = self.self_attn( - self.with_pos_embed(src, pos), - reference_points, - src, - spatial_shapes, - level_start_index, - padding_mask, - ) - src = src + self.dropout1(src2) - src = self.norm1(src) - - # ffn - src = self.forward_ffn(src) - - return src - - -class DeformableTransformerEncoder(nn.Module): - def __init__(self, encoder_layer, num_layers: int) -> None: - super().__init__() - self.layers = _get_clones(encoder_layer, num_layers) - self.num_layers = num_layers - - @staticmethod - def get_reference_points(spatial_shapes, valid_ratios, device): - reference_points_list = [] - for lvl, (H_, W_) in enumerate(spatial_shapes): - ref_y, ref_x = torch.meshgrid( - torch.linspace(0.5, H_ - 0.5, H_, dtype=torch.float32, device=device), - torch.linspace(0.5, W_ - 0.5, W_, dtype=torch.float32, device=device), - ) - ref_y = ref_y.reshape(-1)[None] / (valid_ratios[:, None, lvl, 1] * H_) - ref_x = ref_x.reshape(-1)[None] / (valid_ratios[:, None, lvl, 0] * W_) - ref = torch.stack((ref_x, ref_y), -1) - reference_points_list.append(ref) - reference_points = torch.cat(reference_points_list, 1) - reference_points = reference_points[:, :, None] * valid_ratios[:, None] - return reference_points - - def forward( - self, src, spatial_shapes, level_start_index, valid_ratios, pos=None, padding_mask=None - ): - output = src - reference_points = self.get_reference_points( - spatial_shapes, valid_ratios, device=src.device - ) - for _, layer in enumerate(self.layers): - output = layer( - output, pos, reference_points, spatial_shapes, level_start_index, padding_mask - ) - - return output - - -class DeformableTransformerDecoderLayer(nn.Module): - def __init__( - self, - d_model: int=256, - d_ffn: int=1024, - dropout: float=0.1, - activation: str="relu", - n_levels: int=4, - n_heads: int=8, - n_points: int=4, - ) -> None: - super().__init__() - - # cross attention - self.cross_attn = MSDeformAttn(d_model, n_levels, n_heads, n_points) - self.dropout1 = nn.Dropout(dropout) - self.norm1 = nn.LayerNorm(d_model) - - # self attention - self.self_attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout) - self.dropout2 = nn.Dropout(dropout) - self.norm2 = nn.LayerNorm(d_model) - - # ffn - self.linear1 = nn.Linear(d_model, d_ffn) - self.activation = _get_activation_fn(activation) - self.dropout3 = nn.Dropout(dropout) - self.linear2 = nn.Linear(d_ffn, d_model) - self.dropout4 = nn.Dropout(dropout) - self.norm3 = nn.LayerNorm(d_model) - - @staticmethod - def with_pos_embed(tensor, pos): - return tensor if pos is None else tensor + pos - - def forward_ffn(self, tgt): - tgt2 = self.linear2(self.dropout3(self.activation(self.linear1(tgt)))) - tgt = tgt + self.dropout4(tgt2) - tgt = self.norm3(tgt) - return tgt - - def forward( - self, - tgt, - query_pos, - reference_points, - src, - src_spatial_shapes, - level_start_index, - src_padding_mask=None, - ): - # self attention - q = k = self.with_pos_embed(tgt, query_pos) - tgt2 = self.self_attn(q.transpose(0, 1), k.transpose(0, 1), tgt.transpose(0, 1))[ - 0 - ].transpose(0, 1) - tgt = tgt + self.dropout2(tgt2) - tgt = self.norm2(tgt) - - # cross attention - tgt2 = self.cross_attn( - self.with_pos_embed(tgt, query_pos), - reference_points, - src, - src_spatial_shapes, - level_start_index, - src_padding_mask, - ) - tgt = tgt + self.dropout1(tgt2) - tgt = self.norm1(tgt) - - # ffn - tgt = self.forward_ffn(tgt) - - return tgt - - -class DeformableTransformerDecoder(nn.Module): - def __init__(self, decoder_layer, num_layers: int, return_intermediate: bool=False) -> None: - super().__init__() - self.layers = _get_clones(decoder_layer, num_layers) - self.num_layers = num_layers - self.return_intermediate = return_intermediate - # hack implementation for iterative bounding box refinement and two-stage Deformable DETR - self.bbox_embed = None - self.class_embed = None - - def forward( - self, - tgt, - reference_points, - src, - src_spatial_shapes, - src_level_start_index, - src_valid_ratios, - query_pos=None, - src_padding_mask=None, - ): - output = tgt - - intermediate = [] - intermediate_reference_points = [] - for lid, layer in enumerate(self.layers): - if reference_points.shape[-1] == 4: - reference_points_input = ( - reference_points[:, :, None] - * torch.cat([src_valid_ratios, src_valid_ratios], -1)[:, None] - ) - else: - assert reference_points.shape[-1] == 2 - reference_points_input = reference_points[:, :, None] * src_valid_ratios[:, None] - output = layer( - output, - query_pos, - reference_points_input, - src, - src_spatial_shapes, - src_level_start_index, - src_padding_mask, - ) - - # hack implementation for iterative bounding box refinement - if self.bbox_embed is not None: - tmp = self.bbox_embed[lid](output) - if reference_points.shape[-1] == 4: - new_reference_points = tmp + inverse_sigmoid(reference_points) - new_reference_points = new_reference_points.sigmoid() - else: - assert reference_points.shape[-1] == 2 - new_reference_points = tmp - new_reference_points[..., :2] = tmp[..., :2] + inverse_sigmoid(reference_points) - new_reference_points = new_reference_points.sigmoid() - reference_points = new_reference_points.detach() - - if self.return_intermediate: - intermediate.append(output) - intermediate_reference_points.append(reference_points) - - if self.return_intermediate: - return torch.stack(intermediate), torch.stack(intermediate_reference_points) - - return output, reference_points - - -def _get_clones(module, N): - return nn.ModuleList([copy.deepcopy(module) for i in range(N)]) - - -def _get_activation_fn(activation): - """Return an activation function given a string""" - if activation == "relu": - return F.relu - if activation == "gelu": - return F.gelu - if activation == "glu": - return F.glu - raise RuntimeError(f"activation should be relu/gelu, not {activation}.") - - -def build_deforamble_transformer(args): - return DeformableTransformer( - d_model=args.hidden_dim, - nhead=args.nheads, - num_encoder_layers=args.enc_layers, - num_decoder_layers=args.dec_layers, - dim_feedforward=args.dim_feedforward, - dropout=args.dropout, - activation="relu", - return_intermediate_dec=True, - num_feature_levels=args.num_feature_levels, - dec_n_points=args.dec_n_points, - enc_n_points=args.enc_n_points, - two_stage=args.two_stage, - two_stage_num_proposals=args.num_queries, - ) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/matcher.py b/dimos/models/Detic/third_party/Deformable-DETR/models/matcher.py deleted file mode 100644 index 7cbcf4a82e..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/matcher.py +++ /dev/null @@ -1,107 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Modules to compute the matching cost and solve the corresponding LSAP. -""" - -from scipy.optimize import linear_sum_assignment -import torch -from torch import nn -from util.box_ops import box_cxcywh_to_xyxy, generalized_box_iou - - -class HungarianMatcher(nn.Module): - """This class computes an assignment between the targets and the predictions of the network - - For efficiency reasons, the targets don't include the no_object. Because of this, in general, - there are more predictions than targets. In this case, we do a 1-to-1 matching of the best predictions, - while the others are un-matched (and thus treated as non-objects). - """ - - def __init__(self, cost_class: float = 1, cost_bbox: float = 1, cost_giou: float = 1) -> None: - """Creates the matcher - - Params: - cost_class: This is the relative weight of the classification error in the matching cost - cost_bbox: This is the relative weight of the L1 error of the bounding box coordinates in the matching cost - cost_giou: This is the relative weight of the giou loss of the bounding box in the matching cost - """ - super().__init__() - self.cost_class = cost_class - self.cost_bbox = cost_bbox - self.cost_giou = cost_giou - assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, "all costs cant be 0" - - def forward(self, outputs, targets): - """Performs the matching - - Params: - outputs: This is a dict that contains at least these entries: - "pred_logits": Tensor of dim [batch_size, num_queries, num_classes] with the classification logits - "pred_boxes": Tensor of dim [batch_size, num_queries, 4] with the predicted box coordinates - - targets: This is a list of targets (len(targets) = batch_size), where each target is a dict containing: - "labels": Tensor of dim [num_target_boxes] (where num_target_boxes is the number of ground-truth - objects in the target) containing the class labels - "boxes": Tensor of dim [num_target_boxes, 4] containing the target box coordinates - - Returns: - A list of size batch_size, containing tuples of (index_i, index_j) where: - - index_i is the indices of the selected predictions (in order) - - index_j is the indices of the corresponding selected targets (in order) - For each batch element, it holds: - len(index_i) = len(index_j) = min(num_queries, num_target_boxes) - """ - with torch.no_grad(): - bs, num_queries = outputs["pred_logits"].shape[:2] - - # We flatten to compute the cost matrices in a batch - out_prob = outputs["pred_logits"].flatten(0, 1).sigmoid() - out_bbox = outputs["pred_boxes"].flatten(0, 1) # [batch_size * num_queries, 4] - - # Also concat the target labels and boxes - tgt_ids = torch.cat([v["labels"] for v in targets]) - tgt_bbox = torch.cat([v["boxes"] for v in targets]) - - # Compute the classification cost. - alpha = 0.25 - gamma = 2.0 - neg_cost_class = (1 - alpha) * (out_prob**gamma) * (-(1 - out_prob + 1e-8).log()) - pos_cost_class = alpha * ((1 - out_prob) ** gamma) * (-(out_prob + 1e-8).log()) - cost_class = pos_cost_class[:, tgt_ids] - neg_cost_class[:, tgt_ids] - - # Compute the L1 cost between boxes - cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1) - - # Compute the giou cost betwen boxes - cost_giou = -generalized_box_iou( - box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox) - ) - - # Final cost matrix - C = ( - self.cost_bbox * cost_bbox - + self.cost_class * cost_class - + self.cost_giou * cost_giou - ) - C = C.view(bs, num_queries, -1).cpu() - - sizes = [len(v["boxes"]) for v in targets] - indices = [linear_sum_assignment(c[i]) for i, c in enumerate(C.split(sizes, -1))] - return [ - (torch.as_tensor(i, dtype=torch.int64), torch.as_tensor(j, dtype=torch.int64)) - for i, j in indices - ] - - -def build_matcher(args): - return HungarianMatcher( - cost_class=args.set_cost_class, cost_bbox=args.set_cost_bbox, cost_giou=args.set_cost_giou - ) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/__init__.py deleted file mode 100644 index c528f3c6cf..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# ------------------------------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------------------------------ -# Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -# ------------------------------------------------------------------------------------------------ - -from .ms_deform_attn_func import MSDeformAttnFunction diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/ms_deform_attn_func.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/ms_deform_attn_func.py deleted file mode 100644 index 965811ed7f..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/ms_deform_attn_func.py +++ /dev/null @@ -1,94 +0,0 @@ -# ------------------------------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------------------------------ -# Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -# ------------------------------------------------------------------------------------------------ - - -import MultiScaleDeformableAttention as MSDA -import torch -from torch.autograd import Function -from torch.autograd.function import once_differentiable -import torch.nn.functional as F - - -class MSDeformAttnFunction(Function): - @staticmethod - def forward( - ctx, - value, - value_spatial_shapes, - value_level_start_index, - sampling_locations, - attention_weights, - im2col_step, - ): - ctx.im2col_step = im2col_step - output = MSDA.ms_deform_attn_forward( - value, - value_spatial_shapes, - value_level_start_index, - sampling_locations, - attention_weights, - ctx.im2col_step, - ) - ctx.save_for_backward( - value, - value_spatial_shapes, - value_level_start_index, - sampling_locations, - attention_weights, - ) - return output - - @staticmethod - @once_differentiable - def backward(ctx, grad_output): - ( - value, - value_spatial_shapes, - value_level_start_index, - sampling_locations, - attention_weights, - ) = ctx.saved_tensors - grad_value, grad_sampling_loc, grad_attn_weight = MSDA.ms_deform_attn_backward( - value, - value_spatial_shapes, - value_level_start_index, - sampling_locations, - attention_weights, - grad_output, - ctx.im2col_step, - ) - - return grad_value, None, None, grad_sampling_loc, grad_attn_weight, None - - -def ms_deform_attn_core_pytorch(value, value_spatial_shapes, sampling_locations, attention_weights): - # for debug and test only, - # need to use cuda version instead - N_, S_, M_, D_ = value.shape - _, Lq_, M_, L_, P_, _ = sampling_locations.shape - value_list = value.split([H_ * W_ for H_, W_ in value_spatial_shapes], dim=1) - sampling_grids = 2 * sampling_locations - 1 - sampling_value_list = [] - for lid_, (H_, W_) in enumerate(value_spatial_shapes): - # N_, H_*W_, M_, D_ -> N_, H_*W_, M_*D_ -> N_, M_*D_, H_*W_ -> N_*M_, D_, H_, W_ - value_l_ = value_list[lid_].flatten(2).transpose(1, 2).reshape(N_ * M_, D_, H_, W_) - # N_, Lq_, M_, P_, 2 -> N_, M_, Lq_, P_, 2 -> N_*M_, Lq_, P_, 2 - sampling_grid_l_ = sampling_grids[:, :, :, lid_].transpose(1, 2).flatten(0, 1) - # N_*M_, D_, Lq_, P_ - sampling_value_l_ = F.grid_sample( - value_l_, sampling_grid_l_, mode="bilinear", padding_mode="zeros", align_corners=False - ) - sampling_value_list.append(sampling_value_l_) - # (N_, Lq_, M_, L_, P_) -> (N_, M_, Lq_, L_, P_) -> (N_, M_, 1, Lq_, L_*P_) - attention_weights = attention_weights.transpose(1, 2).reshape(N_ * M_, 1, Lq_, L_ * P_) - output = ( - (torch.stack(sampling_value_list, dim=-2).flatten(-2) * attention_weights) - .sum(-1) - .view(N_, M_ * D_, Lq_) - ) - return output.transpose(1, 2).contiguous() diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/make.sh b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/make.sh deleted file mode 100755 index 106b685722..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/make.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -# ------------------------------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------------------------------ -# Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -# ------------------------------------------------------------------------------------------------ - -python setup.py build install diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/__init__.py deleted file mode 100644 index f82cb1ad9d..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# ------------------------------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------------------------------ -# Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -# ------------------------------------------------------------------------------------------------ - -from .ms_deform_attn import MSDeformAttn diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/ms_deform_attn.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/ms_deform_attn.py deleted file mode 100644 index 1d70af7cc4..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/ms_deform_attn.py +++ /dev/null @@ -1,147 +0,0 @@ -# ------------------------------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------------------------------ -# Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -# ------------------------------------------------------------------------------------------------ - - -import math -import warnings - -import torch -from torch import nn -import torch.nn.functional as F -from torch.nn.init import constant_, xavier_uniform_ - -from ..functions import MSDeformAttnFunction - - -def _is_power_of_2(n): - if (not isinstance(n, int)) or (n < 0): - raise ValueError(f"invalid input for _is_power_of_2: {n} (type: {type(n)})") - return (n & (n - 1) == 0) and n != 0 - - -class MSDeformAttn(nn.Module): - def __init__(self, d_model: int=256, n_levels: int=4, n_heads: int=8, n_points: int=4) -> None: - """ - Multi-Scale Deformable Attention Module - :param d_model hidden dimension - :param n_levels number of feature levels - :param n_heads number of attention heads - :param n_points number of sampling points per attention head per feature level - """ - super().__init__() - if d_model % n_heads != 0: - raise ValueError( - f"d_model must be divisible by n_heads, but got {d_model} and {n_heads}" - ) - _d_per_head = d_model // n_heads - # you'd better set _d_per_head to a power of 2 which is more efficient in our CUDA implementation - if not _is_power_of_2(_d_per_head): - warnings.warn( - "You'd better set d_model in MSDeformAttn to make the dimension of each attention head a power of 2 " - "which is more efficient in our CUDA implementation.", stacklevel=2 - ) - - self.im2col_step = 64 - - self.d_model = d_model - self.n_levels = n_levels - self.n_heads = n_heads - self.n_points = n_points - - self.sampling_offsets = nn.Linear(d_model, n_heads * n_levels * n_points * 2) - self.attention_weights = nn.Linear(d_model, n_heads * n_levels * n_points) - self.value_proj = nn.Linear(d_model, d_model) - self.output_proj = nn.Linear(d_model, d_model) - - self._reset_parameters() - - def _reset_parameters(self) -> None: - constant_(self.sampling_offsets.weight.data, 0.0) - thetas = torch.arange(self.n_heads, dtype=torch.float32) * (2.0 * math.pi / self.n_heads) - grid_init = torch.stack([thetas.cos(), thetas.sin()], -1) - grid_init = ( - (grid_init / grid_init.abs().max(-1, keepdim=True)[0]) - .view(self.n_heads, 1, 1, 2) - .repeat(1, self.n_levels, self.n_points, 1) - ) - for i in range(self.n_points): - grid_init[:, :, i, :] *= i + 1 - with torch.no_grad(): - self.sampling_offsets.bias = nn.Parameter(grid_init.view(-1)) - constant_(self.attention_weights.weight.data, 0.0) - constant_(self.attention_weights.bias.data, 0.0) - xavier_uniform_(self.value_proj.weight.data) - constant_(self.value_proj.bias.data, 0.0) - xavier_uniform_(self.output_proj.weight.data) - constant_(self.output_proj.bias.data, 0.0) - - def forward( - self, - query, - reference_points, - input_flatten, - input_spatial_shapes, - input_level_start_index, - input_padding_mask=None, - ): - r""" - :param query (N, Length_{query}, C) - :param reference_points (N, Length_{query}, n_levels, 2), range in [0, 1], top-left (0,0), bottom-right (1, 1), including padding area - or (N, Length_{query}, n_levels, 4), add additional (w, h) to form reference boxes - :param input_flatten (N, \sum_{l=0}^{L-1} H_l \cdot W_l, C) - :param input_spatial_shapes (n_levels, 2), [(H_0, W_0), (H_1, W_1), ..., (H_{L-1}, W_{L-1})] - :param input_level_start_index (n_levels, ), [0, H_0*W_0, H_0*W_0+H_1*W_1, H_0*W_0+H_1*W_1+H_2*W_2, ..., H_0*W_0+H_1*W_1+...+H_{L-1}*W_{L-1}] - :param input_padding_mask (N, \sum_{l=0}^{L-1} H_l \cdot W_l), True for padding elements, False for non-padding elements - - :return output (N, Length_{query}, C) - """ - N, Len_q, _ = query.shape - N, Len_in, _ = input_flatten.shape - assert (input_spatial_shapes[:, 0] * input_spatial_shapes[:, 1]).sum() == Len_in - - value = self.value_proj(input_flatten) - if input_padding_mask is not None: - value = value.masked_fill(input_padding_mask[..., None], float(0)) - value = value.view(N, Len_in, self.n_heads, self.d_model // self.n_heads) - sampling_offsets = self.sampling_offsets(query).view( - N, Len_q, self.n_heads, self.n_levels, self.n_points, 2 - ) - attention_weights = self.attention_weights(query).view( - N, Len_q, self.n_heads, self.n_levels * self.n_points - ) - attention_weights = F.softmax(attention_weights, -1).view( - N, Len_q, self.n_heads, self.n_levels, self.n_points - ) - # N, Len_q, n_heads, n_levels, n_points, 2 - if reference_points.shape[-1] == 2: - offset_normalizer = torch.stack( - [input_spatial_shapes[..., 1], input_spatial_shapes[..., 0]], -1 - ) - sampling_locations = ( - reference_points[:, :, None, :, None, :] - + sampling_offsets / offset_normalizer[None, None, None, :, None, :] - ) - elif reference_points.shape[-1] == 4: - sampling_locations = ( - reference_points[:, :, None, :, None, :2] - + sampling_offsets / self.n_points * reference_points[:, :, None, :, None, 2:] * 0.5 - ) - else: - raise ValueError( - f"Last dim of reference_points must be 2 or 4, but get {reference_points.shape[-1]} instead." - ) - output = MSDeformAttnFunction.apply( - value, - input_spatial_shapes, - input_level_start_index, - sampling_locations, - attention_weights, - self.im2col_step, - ) - output = self.output_proj(output) - return output diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/setup.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/setup.py deleted file mode 100644 index 7a5560a83f..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/setup.py +++ /dev/null @@ -1,73 +0,0 @@ -# ------------------------------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------------------------------ -# Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -# ------------------------------------------------------------------------------------------------ - -import glob -import os - -from setuptools import find_packages, setup -import torch -from torch.utils.cpp_extension import CUDA_HOME, CppExtension, CUDAExtension - -requirements = ["torch", "torchvision"] - - -def get_extensions(): - this_dir = os.path.dirname(os.path.abspath(__file__)) - extensions_dir = os.path.join(this_dir, "src") - - main_file = glob.glob(os.path.join(extensions_dir, "*.cpp")) - source_cpu = glob.glob(os.path.join(extensions_dir, "cpu", "*.cpp")) - source_cuda = glob.glob(os.path.join(extensions_dir, "cuda", "*.cu")) - - sources = main_file + source_cpu - extension = CppExtension - extra_compile_args = {"cxx": []} - define_macros = [] - - if torch.cuda.is_available() and CUDA_HOME is not None: - extension = CUDAExtension - sources += source_cuda - define_macros += [("WITH_CUDA", None)] - extra_compile_args["nvcc"] = [ - "-DCUDA_HAS_FP16=1", - "-D__CUDA_NO_HALF_OPERATORS__", - "-D__CUDA_NO_HALF_CONVERSIONS__", - "-D__CUDA_NO_HALF2_OPERATORS__", - ] - else: - raise NotImplementedError("Cuda is not availabel") - - sources = [os.path.join(extensions_dir, s) for s in sources] - include_dirs = [extensions_dir] - ext_modules = [ - extension( - "MultiScaleDeformableAttention", - sources, - include_dirs=include_dirs, - define_macros=define_macros, - extra_compile_args=extra_compile_args, - ) - ] - return ext_modules - - -setup( - name="MultiScaleDeformableAttention", - version="1.0", - author="Weijie Su", - url="https://github.com/fundamentalvision/Deformable-DETR", - description="PyTorch Wrapper for CUDA Functions of Multi-Scale Deformable Attention", - packages=find_packages( - exclude=( - "configs", - "tests", - ) - ), - ext_modules=get_extensions(), - cmdclass={"build_ext": torch.utils.cpp_extension.BuildExtension}, -) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cpu/ms_deform_attn_cpu.cpp b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cpu/ms_deform_attn_cpu.cpp deleted file mode 100644 index e1bf854de1..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cpu/ms_deform_attn_cpu.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/*! -************************************************************************************************** -* Deformable DETR -* Copyright (c) 2020 SenseTime. All Rights Reserved. -* Licensed under the Apache License, Version 2.0 [see LICENSE for details] -************************************************************************************************** -* Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -************************************************************************************************** -*/ - -#include - -#include -#include - - -at::Tensor -ms_deform_attn_cpu_forward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const int im2col_step) -{ - AT_ERROR("Not implement on cpu"); -} - -std::vector -ms_deform_attn_cpu_backward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const at::Tensor &grad_output, - const int im2col_step) -{ - AT_ERROR("Not implement on cpu"); -} - diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cpu/ms_deform_attn_cpu.h b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cpu/ms_deform_attn_cpu.h deleted file mode 100644 index 81b7b58a3d..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cpu/ms_deform_attn_cpu.h +++ /dev/null @@ -1,33 +0,0 @@ -/*! -************************************************************************************************** -* Deformable DETR -* Copyright (c) 2020 SenseTime. All Rights Reserved. -* Licensed under the Apache License, Version 2.0 [see LICENSE for details] -************************************************************************************************** -* Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -************************************************************************************************** -*/ - -#pragma once -#include - -at::Tensor -ms_deform_attn_cpu_forward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const int im2col_step); - -std::vector -ms_deform_attn_cpu_backward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const at::Tensor &grad_output, - const int im2col_step); - - diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_attn_cuda.cu b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_attn_cuda.cu deleted file mode 100644 index d6d583647c..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_attn_cuda.cu +++ /dev/null @@ -1,153 +0,0 @@ -/*! -************************************************************************************************** -* Deformable DETR -* Copyright (c) 2020 SenseTime. All Rights Reserved. -* Licensed under the Apache License, Version 2.0 [see LICENSE for details] -************************************************************************************************** -* Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -************************************************************************************************** -*/ - -#include -#include "cuda/ms_deform_im2col_cuda.cuh" - -#include -#include -#include -#include - - -at::Tensor ms_deform_attn_cuda_forward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const int im2col_step) -{ - AT_ASSERTM(value.is_contiguous(), "value tensor has to be contiguous"); - AT_ASSERTM(spatial_shapes.is_contiguous(), "spatial_shapes tensor has to be contiguous"); - AT_ASSERTM(level_start_index.is_contiguous(), "level_start_index tensor has to be contiguous"); - AT_ASSERTM(sampling_loc.is_contiguous(), "sampling_loc tensor has to be contiguous"); - AT_ASSERTM(attn_weight.is_contiguous(), "attn_weight tensor has to be contiguous"); - - AT_ASSERTM(value.type().is_cuda(), "value must be a CUDA tensor"); - AT_ASSERTM(spatial_shapes.type().is_cuda(), "spatial_shapes must be a CUDA tensor"); - AT_ASSERTM(level_start_index.type().is_cuda(), "level_start_index must be a CUDA tensor"); - AT_ASSERTM(sampling_loc.type().is_cuda(), "sampling_loc must be a CUDA tensor"); - AT_ASSERTM(attn_weight.type().is_cuda(), "attn_weight must be a CUDA tensor"); - - const int batch = value.size(0); - const int spatial_size = value.size(1); - const int num_heads = value.size(2); - const int channels = value.size(3); - - const int num_levels = spatial_shapes.size(0); - - const int num_query = sampling_loc.size(1); - const int num_point = sampling_loc.size(4); - - const int im2col_step_ = std::min(batch, im2col_step); - - AT_ASSERTM(batch % im2col_step_ == 0, "batch(%d) must divide im2col_step(%d)", batch, im2col_step_); - - auto output = at::zeros({batch, num_query, num_heads, channels}, value.options()); - - const int batch_n = im2col_step_; - auto output_n = output.view({batch/im2col_step_, batch_n, num_query, num_heads, channels}); - auto per_value_size = spatial_size * num_heads * channels; - auto per_sample_loc_size = num_query * num_heads * num_levels * num_point * 2; - auto per_attn_weight_size = num_query * num_heads * num_levels * num_point; - for (int n = 0; n < batch/im2col_step_; ++n) - { - auto columns = output_n.select(0, n); - AT_DISPATCH_FLOATING_TYPES(value.type(), "ms_deform_attn_forward_cuda", ([&] { - ms_deformable_im2col_cuda(at::cuda::getCurrentCUDAStream(), - value.data() + n * im2col_step_ * per_value_size, - spatial_shapes.data(), - level_start_index.data(), - sampling_loc.data() + n * im2col_step_ * per_sample_loc_size, - attn_weight.data() + n * im2col_step_ * per_attn_weight_size, - batch_n, spatial_size, num_heads, channels, num_levels, num_query, num_point, - columns.data()); - - })); - } - - output = output.view({batch, num_query, num_heads*channels}); - - return output; -} - - -std::vector ms_deform_attn_cuda_backward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const at::Tensor &grad_output, - const int im2col_step) -{ - - AT_ASSERTM(value.is_contiguous(), "value tensor has to be contiguous"); - AT_ASSERTM(spatial_shapes.is_contiguous(), "spatial_shapes tensor has to be contiguous"); - AT_ASSERTM(level_start_index.is_contiguous(), "level_start_index tensor has to be contiguous"); - AT_ASSERTM(sampling_loc.is_contiguous(), "sampling_loc tensor has to be contiguous"); - AT_ASSERTM(attn_weight.is_contiguous(), "attn_weight tensor has to be contiguous"); - AT_ASSERTM(grad_output.is_contiguous(), "grad_output tensor has to be contiguous"); - - AT_ASSERTM(value.type().is_cuda(), "value must be a CUDA tensor"); - AT_ASSERTM(spatial_shapes.type().is_cuda(), "spatial_shapes must be a CUDA tensor"); - AT_ASSERTM(level_start_index.type().is_cuda(), "level_start_index must be a CUDA tensor"); - AT_ASSERTM(sampling_loc.type().is_cuda(), "sampling_loc must be a CUDA tensor"); - AT_ASSERTM(attn_weight.type().is_cuda(), "attn_weight must be a CUDA tensor"); - AT_ASSERTM(grad_output.type().is_cuda(), "grad_output must be a CUDA tensor"); - - const int batch = value.size(0); - const int spatial_size = value.size(1); - const int num_heads = value.size(2); - const int channels = value.size(3); - - const int num_levels = spatial_shapes.size(0); - - const int num_query = sampling_loc.size(1); - const int num_point = sampling_loc.size(4); - - const int im2col_step_ = std::min(batch, im2col_step); - - AT_ASSERTM(batch % im2col_step_ == 0, "batch(%d) must divide im2col_step(%d)", batch, im2col_step_); - - auto grad_value = at::zeros_like(value); - auto grad_sampling_loc = at::zeros_like(sampling_loc); - auto grad_attn_weight = at::zeros_like(attn_weight); - - const int batch_n = im2col_step_; - auto per_value_size = spatial_size * num_heads * channels; - auto per_sample_loc_size = num_query * num_heads * num_levels * num_point * 2; - auto per_attn_weight_size = num_query * num_heads * num_levels * num_point; - auto grad_output_n = grad_output.view({batch/im2col_step_, batch_n, num_query, num_heads, channels}); - - for (int n = 0; n < batch/im2col_step_; ++n) - { - auto grad_output_g = grad_output_n.select(0, n); - AT_DISPATCH_FLOATING_TYPES(value.type(), "ms_deform_attn_backward_cuda", ([&] { - ms_deformable_col2im_cuda(at::cuda::getCurrentCUDAStream(), - grad_output_g.data(), - value.data() + n * im2col_step_ * per_value_size, - spatial_shapes.data(), - level_start_index.data(), - sampling_loc.data() + n * im2col_step_ * per_sample_loc_size, - attn_weight.data() + n * im2col_step_ * per_attn_weight_size, - batch_n, spatial_size, num_heads, channels, num_levels, num_query, num_point, - grad_value.data() + n * im2col_step_ * per_value_size, - grad_sampling_loc.data() + n * im2col_step_ * per_sample_loc_size, - grad_attn_weight.data() + n * im2col_step_ * per_attn_weight_size); - - })); - } - - return { - grad_value, grad_sampling_loc, grad_attn_weight - }; -} \ No newline at end of file diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_attn_cuda.h b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_attn_cuda.h deleted file mode 100644 index c7ae53f99c..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_attn_cuda.h +++ /dev/null @@ -1,30 +0,0 @@ -/*! -************************************************************************************************** -* Deformable DETR -* Copyright (c) 2020 SenseTime. All Rights Reserved. -* Licensed under the Apache License, Version 2.0 [see LICENSE for details] -************************************************************************************************** -* Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -************************************************************************************************** -*/ - -#pragma once -#include - -at::Tensor ms_deform_attn_cuda_forward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const int im2col_step); - -std::vector ms_deform_attn_cuda_backward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const at::Tensor &grad_output, - const int im2col_step); - diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_im2col_cuda.cuh b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_im2col_cuda.cuh deleted file mode 100644 index 6bc2acb7ae..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/cuda/ms_deform_im2col_cuda.cuh +++ /dev/null @@ -1,1327 +0,0 @@ -/*! -************************************************************************** -* Deformable DETR -* Copyright (c) 2020 SenseTime. All Rights Reserved. -* Licensed under the Apache License, Version 2.0 [see LICENSE for details] -************************************************************************** -* Modified from DCN (https://github.com/msracver/Deformable-ConvNets) -* Copyright (c) 2018 Microsoft -************************************************************************** -*/ - -#include -#include -#include - -#include -#include - -#include - -#define CUDA_KERNEL_LOOP(i, n) \ - for (int i = blockIdx.x * blockDim.x + threadIdx.x; \ - i < (n); \ - i += blockDim.x * gridDim.x) - -const int CUDA_NUM_THREADS = 1024; -inline int GET_BLOCKS(const int N, const int num_threads) -{ - return (N + num_threads - 1) / num_threads; -} - - -template -__device__ scalar_t ms_deform_attn_im2col_bilinear(const scalar_t* &bottom_data, - const int &height, const int &width, const int &nheads, const int &channels, - const scalar_t &h, const scalar_t &w, const int &m, const int &c) -{ - const int h_low = floor(h); - const int w_low = floor(w); - const int h_high = h_low + 1; - const int w_high = w_low + 1; - - const scalar_t lh = h - h_low; - const scalar_t lw = w - w_low; - const scalar_t hh = 1 - lh, hw = 1 - lw; - - const int w_stride = nheads * channels; - const int h_stride = width * w_stride; - const int h_low_ptr_offset = h_low * h_stride; - const int h_high_ptr_offset = h_low_ptr_offset + h_stride; - const int w_low_ptr_offset = w_low * w_stride; - const int w_high_ptr_offset = w_low_ptr_offset + w_stride; - const int base_ptr = m * channels + c; - - scalar_t v1 = 0; - if (h_low >= 0 && w_low >= 0) - { - const int ptr1 = h_low_ptr_offset + w_low_ptr_offset + base_ptr; - v1 = bottom_data[ptr1]; - } - scalar_t v2 = 0; - if (h_low >= 0 && w_high <= width - 1) - { - const int ptr2 = h_low_ptr_offset + w_high_ptr_offset + base_ptr; - v2 = bottom_data[ptr2]; - } - scalar_t v3 = 0; - if (h_high <= height - 1 && w_low >= 0) - { - const int ptr3 = h_high_ptr_offset + w_low_ptr_offset + base_ptr; - v3 = bottom_data[ptr3]; - } - scalar_t v4 = 0; - if (h_high <= height - 1 && w_high <= width - 1) - { - const int ptr4 = h_high_ptr_offset + w_high_ptr_offset + base_ptr; - v4 = bottom_data[ptr4]; - } - - const scalar_t w1 = hh * hw, w2 = hh * lw, w3 = lh * hw, w4 = lh * lw; - - const scalar_t val = (w1 * v1 + w2 * v2 + w3 * v3 + w4 * v4); - return val; -} - - -template -__device__ void ms_deform_attn_col2im_bilinear(const scalar_t* &bottom_data, - const int &height, const int &width, const int &nheads, const int &channels, - const scalar_t &h, const scalar_t &w, const int &m, const int &c, - const scalar_t &top_grad, - const scalar_t &attn_weight, - scalar_t* &grad_value, - scalar_t* grad_sampling_loc, - scalar_t* grad_attn_weight) -{ - const int h_low = floor(h); - const int w_low = floor(w); - const int h_high = h_low + 1; - const int w_high = w_low + 1; - - const scalar_t lh = h - h_low; - const scalar_t lw = w - w_low; - const scalar_t hh = 1 - lh, hw = 1 - lw; - - const int w_stride = nheads * channels; - const int h_stride = width * w_stride; - const int h_low_ptr_offset = h_low * h_stride; - const int h_high_ptr_offset = h_low_ptr_offset + h_stride; - const int w_low_ptr_offset = w_low * w_stride; - const int w_high_ptr_offset = w_low_ptr_offset + w_stride; - const int base_ptr = m * channels + c; - - const scalar_t w1 = hh * hw, w2 = hh * lw, w3 = lh * hw, w4 = lh * lw; - const scalar_t top_grad_value = top_grad * attn_weight; - scalar_t grad_h_weight = 0, grad_w_weight = 0; - - scalar_t v1 = 0; - if (h_low >= 0 && w_low >= 0) - { - const int ptr1 = h_low_ptr_offset + w_low_ptr_offset + base_ptr; - v1 = bottom_data[ptr1]; - grad_h_weight -= hw * v1; - grad_w_weight -= hh * v1; - atomicAdd(grad_value+ptr1, w1*top_grad_value); - } - scalar_t v2 = 0; - if (h_low >= 0 && w_high <= width - 1) - { - const int ptr2 = h_low_ptr_offset + w_high_ptr_offset + base_ptr; - v2 = bottom_data[ptr2]; - grad_h_weight -= lw * v2; - grad_w_weight += hh * v2; - atomicAdd(grad_value+ptr2, w2*top_grad_value); - } - scalar_t v3 = 0; - if (h_high <= height - 1 && w_low >= 0) - { - const int ptr3 = h_high_ptr_offset + w_low_ptr_offset + base_ptr; - v3 = bottom_data[ptr3]; - grad_h_weight += hw * v3; - grad_w_weight -= lh * v3; - atomicAdd(grad_value+ptr3, w3*top_grad_value); - } - scalar_t v4 = 0; - if (h_high <= height - 1 && w_high <= width - 1) - { - const int ptr4 = h_high_ptr_offset + w_high_ptr_offset + base_ptr; - v4 = bottom_data[ptr4]; - grad_h_weight += lw * v4; - grad_w_weight += lh * v4; - atomicAdd(grad_value+ptr4, w4*top_grad_value); - } - - const scalar_t val = (w1 * v1 + w2 * v2 + w3 * v3 + w4 * v4); - *grad_attn_weight = top_grad * val; - *grad_sampling_loc = width * grad_w_weight * top_grad_value; - *(grad_sampling_loc + 1) = height * grad_h_weight * top_grad_value; -} - - -template -__device__ void ms_deform_attn_col2im_bilinear_gm(const scalar_t* &bottom_data, - const int &height, const int &width, const int &nheads, const int &channels, - const scalar_t &h, const scalar_t &w, const int &m, const int &c, - const scalar_t &top_grad, - const scalar_t &attn_weight, - scalar_t* &grad_value, - scalar_t* grad_sampling_loc, - scalar_t* grad_attn_weight) -{ - const int h_low = floor(h); - const int w_low = floor(w); - const int h_high = h_low + 1; - const int w_high = w_low + 1; - - const scalar_t lh = h - h_low; - const scalar_t lw = w - w_low; - const scalar_t hh = 1 - lh, hw = 1 - lw; - - const int w_stride = nheads * channels; - const int h_stride = width * w_stride; - const int h_low_ptr_offset = h_low * h_stride; - const int h_high_ptr_offset = h_low_ptr_offset + h_stride; - const int w_low_ptr_offset = w_low * w_stride; - const int w_high_ptr_offset = w_low_ptr_offset + w_stride; - const int base_ptr = m * channels + c; - - const scalar_t w1 = hh * hw, w2 = hh * lw, w3 = lh * hw, w4 = lh * lw; - const scalar_t top_grad_value = top_grad * attn_weight; - scalar_t grad_h_weight = 0, grad_w_weight = 0; - - scalar_t v1 = 0; - if (h_low >= 0 && w_low >= 0) - { - const int ptr1 = h_low_ptr_offset + w_low_ptr_offset + base_ptr; - v1 = bottom_data[ptr1]; - grad_h_weight -= hw * v1; - grad_w_weight -= hh * v1; - atomicAdd(grad_value+ptr1, w1*top_grad_value); - } - scalar_t v2 = 0; - if (h_low >= 0 && w_high <= width - 1) - { - const int ptr2 = h_low_ptr_offset + w_high_ptr_offset + base_ptr; - v2 = bottom_data[ptr2]; - grad_h_weight -= lw * v2; - grad_w_weight += hh * v2; - atomicAdd(grad_value+ptr2, w2*top_grad_value); - } - scalar_t v3 = 0; - if (h_high <= height - 1 && w_low >= 0) - { - const int ptr3 = h_high_ptr_offset + w_low_ptr_offset + base_ptr; - v3 = bottom_data[ptr3]; - grad_h_weight += hw * v3; - grad_w_weight -= lh * v3; - atomicAdd(grad_value+ptr3, w3*top_grad_value); - } - scalar_t v4 = 0; - if (h_high <= height - 1 && w_high <= width - 1) - { - const int ptr4 = h_high_ptr_offset + w_high_ptr_offset + base_ptr; - v4 = bottom_data[ptr4]; - grad_h_weight += lw * v4; - grad_w_weight += lh * v4; - atomicAdd(grad_value+ptr4, w4*top_grad_value); - } - - const scalar_t val = (w1 * v1 + w2 * v2 + w3 * v3 + w4 * v4); - atomicAdd(grad_attn_weight, top_grad * val); - atomicAdd(grad_sampling_loc, width * grad_w_weight * top_grad_value); - atomicAdd(grad_sampling_loc + 1, height * grad_h_weight * top_grad_value); -} - - -template -__global__ void ms_deformable_im2col_gpu_kernel(const int n, - const scalar_t *data_value, - const int64_t *data_spatial_shapes, - const int64_t *data_level_start_index, - const scalar_t *data_sampling_loc, - const scalar_t *data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t *data_col) -{ - CUDA_KERNEL_LOOP(index, n) - { - int _temp = index; - const int c_col = _temp % channels; - _temp /= channels; - const int sampling_index = _temp; - const int m_col = _temp % num_heads; - _temp /= num_heads; - const int q_col = _temp % num_query; - _temp /= num_query; - const int b_col = _temp; - - scalar_t *data_col_ptr = data_col + index; - int data_weight_ptr = sampling_index * num_levels * num_point; - int data_loc_w_ptr = data_weight_ptr << 1; - const int qid_stride = num_heads * channels; - const int data_value_ptr_init_offset = b_col * spatial_size * qid_stride; - scalar_t col = 0; - - for (int l_col=0; l_col < num_levels; ++l_col) - { - const int level_start_id = data_level_start_index[l_col]; - const int spatial_h_ptr = l_col << 1; - const int spatial_h = data_spatial_shapes[spatial_h_ptr]; - const int spatial_w = data_spatial_shapes[spatial_h_ptr + 1]; - const scalar_t *data_value_ptr = data_value + (data_value_ptr_init_offset + level_start_id * qid_stride); - for (int p_col=0; p_col < num_point; ++p_col) - { - const scalar_t loc_w = data_sampling_loc[data_loc_w_ptr]; - const scalar_t loc_h = data_sampling_loc[data_loc_w_ptr + 1]; - const scalar_t weight = data_attn_weight[data_weight_ptr]; - - const scalar_t h_im = loc_h * spatial_h - 0.5; - const scalar_t w_im = loc_w * spatial_w - 0.5; - - if (h_im > -1 && w_im > -1 && h_im < spatial_h && w_im < spatial_w) - { - col += ms_deform_attn_im2col_bilinear(data_value_ptr, spatial_h, spatial_w, num_heads, channels, h_im, w_im, m_col, c_col) * weight; - } - - data_weight_ptr += 1; - data_loc_w_ptr += 2; - } - } - *data_col_ptr = col; - } -} - -template -__global__ void ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v1(const int n, - const scalar_t *grad_col, - const scalar_t *data_value, - const int64_t *data_spatial_shapes, - const int64_t *data_level_start_index, - const scalar_t *data_sampling_loc, - const scalar_t *data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t *grad_value, - scalar_t *grad_sampling_loc, - scalar_t *grad_attn_weight) -{ - CUDA_KERNEL_LOOP(index, n) - { - __shared__ scalar_t cache_grad_sampling_loc[blockSize * 2]; - __shared__ scalar_t cache_grad_attn_weight[blockSize]; - unsigned int tid = threadIdx.x; - int _temp = index; - const int c_col = _temp % channels; - _temp /= channels; - const int sampling_index = _temp; - const int m_col = _temp % num_heads; - _temp /= num_heads; - const int q_col = _temp % num_query; - _temp /= num_query; - const int b_col = _temp; - - const scalar_t top_grad = grad_col[index]; - - int data_weight_ptr = sampling_index * num_levels * num_point; - int data_loc_w_ptr = data_weight_ptr << 1; - const int grad_sampling_ptr = data_weight_ptr; - grad_sampling_loc += grad_sampling_ptr << 1; - grad_attn_weight += grad_sampling_ptr; - const int grad_weight_stride = 1; - const int grad_loc_stride = 2; - const int qid_stride = num_heads * channels; - const int data_value_ptr_init_offset = b_col * spatial_size * qid_stride; - - for (int l_col=0; l_col < num_levels; ++l_col) - { - const int level_start_id = data_level_start_index[l_col]; - const int spatial_h_ptr = l_col << 1; - const int spatial_h = data_spatial_shapes[spatial_h_ptr]; - const int spatial_w = data_spatial_shapes[spatial_h_ptr + 1]; - const int value_ptr_offset = data_value_ptr_init_offset + level_start_id * qid_stride; - const scalar_t *data_value_ptr = data_value + value_ptr_offset; - scalar_t *grad_value_ptr = grad_value + value_ptr_offset; - - for (int p_col=0; p_col < num_point; ++p_col) - { - const scalar_t loc_w = data_sampling_loc[data_loc_w_ptr]; - const scalar_t loc_h = data_sampling_loc[data_loc_w_ptr + 1]; - const scalar_t weight = data_attn_weight[data_weight_ptr]; - - const scalar_t h_im = loc_h * spatial_h - 0.5; - const scalar_t w_im = loc_w * spatial_w - 0.5; - *(cache_grad_sampling_loc+(threadIdx.x << 1)) = 0; - *(cache_grad_sampling_loc+((threadIdx.x << 1) + 1)) = 0; - *(cache_grad_attn_weight+threadIdx.x)=0; - if (h_im > -1 && w_im > -1 && h_im < spatial_h && w_im < spatial_w) - { - ms_deform_attn_col2im_bilinear( - data_value_ptr, spatial_h, spatial_w, num_heads, channels, h_im, w_im, m_col, c_col, - top_grad, weight, grad_value_ptr, - cache_grad_sampling_loc+(threadIdx.x << 1), cache_grad_attn_weight+threadIdx.x); - } - - __syncthreads(); - if (tid == 0) - { - scalar_t _grad_w=cache_grad_sampling_loc[0], _grad_h=cache_grad_sampling_loc[1], _grad_a=cache_grad_attn_weight[0]; - int sid=2; - for (unsigned int tid = 1; tid < blockSize; ++tid) - { - _grad_w += cache_grad_sampling_loc[sid]; - _grad_h += cache_grad_sampling_loc[sid + 1]; - _grad_a += cache_grad_attn_weight[tid]; - sid += 2; - } - - - *grad_sampling_loc = _grad_w; - *(grad_sampling_loc + 1) = _grad_h; - *grad_attn_weight = _grad_a; - } - __syncthreads(); - - data_weight_ptr += 1; - data_loc_w_ptr += 2; - grad_attn_weight += grad_weight_stride; - grad_sampling_loc += grad_loc_stride; - } - } - } -} - - -template -__global__ void ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v2(const int n, - const scalar_t *grad_col, - const scalar_t *data_value, - const int64_t *data_spatial_shapes, - const int64_t *data_level_start_index, - const scalar_t *data_sampling_loc, - const scalar_t *data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t *grad_value, - scalar_t *grad_sampling_loc, - scalar_t *grad_attn_weight) -{ - CUDA_KERNEL_LOOP(index, n) - { - __shared__ scalar_t cache_grad_sampling_loc[blockSize * 2]; - __shared__ scalar_t cache_grad_attn_weight[blockSize]; - unsigned int tid = threadIdx.x; - int _temp = index; - const int c_col = _temp % channels; - _temp /= channels; - const int sampling_index = _temp; - const int m_col = _temp % num_heads; - _temp /= num_heads; - const int q_col = _temp % num_query; - _temp /= num_query; - const int b_col = _temp; - - const scalar_t top_grad = grad_col[index]; - - int data_weight_ptr = sampling_index * num_levels * num_point; - int data_loc_w_ptr = data_weight_ptr << 1; - const int grad_sampling_ptr = data_weight_ptr; - grad_sampling_loc += grad_sampling_ptr << 1; - grad_attn_weight += grad_sampling_ptr; - const int grad_weight_stride = 1; - const int grad_loc_stride = 2; - const int qid_stride = num_heads * channels; - const int data_value_ptr_init_offset = b_col * spatial_size * qid_stride; - - for (int l_col=0; l_col < num_levels; ++l_col) - { - const int level_start_id = data_level_start_index[l_col]; - const int spatial_h_ptr = l_col << 1; - const int spatial_h = data_spatial_shapes[spatial_h_ptr]; - const int spatial_w = data_spatial_shapes[spatial_h_ptr + 1]; - const int value_ptr_offset = data_value_ptr_init_offset + level_start_id * qid_stride; - const scalar_t *data_value_ptr = data_value + value_ptr_offset; - scalar_t *grad_value_ptr = grad_value + value_ptr_offset; - - for (int p_col=0; p_col < num_point; ++p_col) - { - const scalar_t loc_w = data_sampling_loc[data_loc_w_ptr]; - const scalar_t loc_h = data_sampling_loc[data_loc_w_ptr + 1]; - const scalar_t weight = data_attn_weight[data_weight_ptr]; - - const scalar_t h_im = loc_h * spatial_h - 0.5; - const scalar_t w_im = loc_w * spatial_w - 0.5; - *(cache_grad_sampling_loc+(threadIdx.x << 1)) = 0; - *(cache_grad_sampling_loc+((threadIdx.x << 1) + 1)) = 0; - *(cache_grad_attn_weight+threadIdx.x)=0; - if (h_im > -1 && w_im > -1 && h_im < spatial_h && w_im < spatial_w) - { - ms_deform_attn_col2im_bilinear( - data_value_ptr, spatial_h, spatial_w, num_heads, channels, h_im, w_im, m_col, c_col, - top_grad, weight, grad_value_ptr, - cache_grad_sampling_loc+(threadIdx.x << 1), cache_grad_attn_weight+threadIdx.x); - } - - __syncthreads(); - - for (unsigned int s=blockSize/2; s>0; s>>=1) - { - if (tid < s) { - const unsigned int xid1 = tid << 1; - const unsigned int xid2 = (tid + s) << 1; - cache_grad_attn_weight[tid] += cache_grad_attn_weight[tid + s]; - cache_grad_sampling_loc[xid1] += cache_grad_sampling_loc[xid2]; - cache_grad_sampling_loc[xid1 + 1] += cache_grad_sampling_loc[xid2 + 1]; - } - __syncthreads(); - } - - if (tid == 0) - { - *grad_sampling_loc = cache_grad_sampling_loc[0]; - *(grad_sampling_loc + 1) = cache_grad_sampling_loc[1]; - *grad_attn_weight = cache_grad_attn_weight[0]; - } - __syncthreads(); - - data_weight_ptr += 1; - data_loc_w_ptr += 2; - grad_attn_weight += grad_weight_stride; - grad_sampling_loc += grad_loc_stride; - } - } - } -} - - -template -__global__ void ms_deformable_col2im_gpu_kernel_shm_reduce_v1(const int n, - const scalar_t *grad_col, - const scalar_t *data_value, - const int64_t *data_spatial_shapes, - const int64_t *data_level_start_index, - const scalar_t *data_sampling_loc, - const scalar_t *data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t *grad_value, - scalar_t *grad_sampling_loc, - scalar_t *grad_attn_weight) -{ - CUDA_KERNEL_LOOP(index, n) - { - extern __shared__ int _s[]; - scalar_t* cache_grad_sampling_loc = (scalar_t*)_s; - scalar_t* cache_grad_attn_weight = cache_grad_sampling_loc + 2 * blockDim.x; - unsigned int tid = threadIdx.x; - int _temp = index; - const int c_col = _temp % channels; - _temp /= channels; - const int sampling_index = _temp; - const int m_col = _temp % num_heads; - _temp /= num_heads; - const int q_col = _temp % num_query; - _temp /= num_query; - const int b_col = _temp; - - const scalar_t top_grad = grad_col[index]; - - int data_weight_ptr = sampling_index * num_levels * num_point; - int data_loc_w_ptr = data_weight_ptr << 1; - const int grad_sampling_ptr = data_weight_ptr; - grad_sampling_loc += grad_sampling_ptr << 1; - grad_attn_weight += grad_sampling_ptr; - const int grad_weight_stride = 1; - const int grad_loc_stride = 2; - const int qid_stride = num_heads * channels; - const int data_value_ptr_init_offset = b_col * spatial_size * qid_stride; - - for (int l_col=0; l_col < num_levels; ++l_col) - { - const int level_start_id = data_level_start_index[l_col]; - const int spatial_h_ptr = l_col << 1; - const int spatial_h = data_spatial_shapes[spatial_h_ptr]; - const int spatial_w = data_spatial_shapes[spatial_h_ptr + 1]; - const int value_ptr_offset = data_value_ptr_init_offset + level_start_id * qid_stride; - const scalar_t *data_value_ptr = data_value + value_ptr_offset; - scalar_t *grad_value_ptr = grad_value + value_ptr_offset; - - for (int p_col=0; p_col < num_point; ++p_col) - { - const scalar_t loc_w = data_sampling_loc[data_loc_w_ptr]; - const scalar_t loc_h = data_sampling_loc[data_loc_w_ptr + 1]; - const scalar_t weight = data_attn_weight[data_weight_ptr]; - - const scalar_t h_im = loc_h * spatial_h - 0.5; - const scalar_t w_im = loc_w * spatial_w - 0.5; - *(cache_grad_sampling_loc+(threadIdx.x << 1)) = 0; - *(cache_grad_sampling_loc+((threadIdx.x << 1) + 1)) = 0; - *(cache_grad_attn_weight+threadIdx.x)=0; - if (h_im > -1 && w_im > -1 && h_im < spatial_h && w_im < spatial_w) - { - ms_deform_attn_col2im_bilinear( - data_value_ptr, spatial_h, spatial_w, num_heads, channels, h_im, w_im, m_col, c_col, - top_grad, weight, grad_value_ptr, - cache_grad_sampling_loc+(threadIdx.x << 1), cache_grad_attn_weight+threadIdx.x); - } - - __syncthreads(); - if (tid == 0) - { - scalar_t _grad_w=cache_grad_sampling_loc[0], _grad_h=cache_grad_sampling_loc[1], _grad_a=cache_grad_attn_weight[0]; - int sid=2; - for (unsigned int tid = 1; tid < blockDim.x; ++tid) - { - _grad_w += cache_grad_sampling_loc[sid]; - _grad_h += cache_grad_sampling_loc[sid + 1]; - _grad_a += cache_grad_attn_weight[tid]; - sid += 2; - } - - - *grad_sampling_loc = _grad_w; - *(grad_sampling_loc + 1) = _grad_h; - *grad_attn_weight = _grad_a; - } - __syncthreads(); - - data_weight_ptr += 1; - data_loc_w_ptr += 2; - grad_attn_weight += grad_weight_stride; - grad_sampling_loc += grad_loc_stride; - } - } - } -} - -template -__global__ void ms_deformable_col2im_gpu_kernel_shm_reduce_v2(const int n, - const scalar_t *grad_col, - const scalar_t *data_value, - const int64_t *data_spatial_shapes, - const int64_t *data_level_start_index, - const scalar_t *data_sampling_loc, - const scalar_t *data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t *grad_value, - scalar_t *grad_sampling_loc, - scalar_t *grad_attn_weight) -{ - CUDA_KERNEL_LOOP(index, n) - { - extern __shared__ int _s[]; - scalar_t* cache_grad_sampling_loc = (scalar_t*)_s; - scalar_t* cache_grad_attn_weight = cache_grad_sampling_loc + 2 * blockDim.x; - unsigned int tid = threadIdx.x; - int _temp = index; - const int c_col = _temp % channels; - _temp /= channels; - const int sampling_index = _temp; - const int m_col = _temp % num_heads; - _temp /= num_heads; - const int q_col = _temp % num_query; - _temp /= num_query; - const int b_col = _temp; - - const scalar_t top_grad = grad_col[index]; - - int data_weight_ptr = sampling_index * num_levels * num_point; - int data_loc_w_ptr = data_weight_ptr << 1; - const int grad_sampling_ptr = data_weight_ptr; - grad_sampling_loc += grad_sampling_ptr << 1; - grad_attn_weight += grad_sampling_ptr; - const int grad_weight_stride = 1; - const int grad_loc_stride = 2; - const int qid_stride = num_heads * channels; - const int data_value_ptr_init_offset = b_col * spatial_size * qid_stride; - - for (int l_col=0; l_col < num_levels; ++l_col) - { - const int level_start_id = data_level_start_index[l_col]; - const int spatial_h_ptr = l_col << 1; - const int spatial_h = data_spatial_shapes[spatial_h_ptr]; - const int spatial_w = data_spatial_shapes[spatial_h_ptr + 1]; - const int value_ptr_offset = data_value_ptr_init_offset + level_start_id * qid_stride; - const scalar_t *data_value_ptr = data_value + value_ptr_offset; - scalar_t *grad_value_ptr = grad_value + value_ptr_offset; - - for (int p_col=0; p_col < num_point; ++p_col) - { - const scalar_t loc_w = data_sampling_loc[data_loc_w_ptr]; - const scalar_t loc_h = data_sampling_loc[data_loc_w_ptr + 1]; - const scalar_t weight = data_attn_weight[data_weight_ptr]; - - const scalar_t h_im = loc_h * spatial_h - 0.5; - const scalar_t w_im = loc_w * spatial_w - 0.5; - *(cache_grad_sampling_loc+(threadIdx.x << 1)) = 0; - *(cache_grad_sampling_loc+((threadIdx.x << 1) + 1)) = 0; - *(cache_grad_attn_weight+threadIdx.x)=0; - if (h_im > -1 && w_im > -1 && h_im < spatial_h && w_im < spatial_w) - { - ms_deform_attn_col2im_bilinear( - data_value_ptr, spatial_h, spatial_w, num_heads, channels, h_im, w_im, m_col, c_col, - top_grad, weight, grad_value_ptr, - cache_grad_sampling_loc+(threadIdx.x << 1), cache_grad_attn_weight+threadIdx.x); - } - - __syncthreads(); - - for (unsigned int s=blockDim.x/2, spre=blockDim.x; s>0; s>>=1, spre>>=1) - { - if (tid < s) { - const unsigned int xid1 = tid << 1; - const unsigned int xid2 = (tid + s) << 1; - cache_grad_attn_weight[tid] += cache_grad_attn_weight[tid + s]; - cache_grad_sampling_loc[xid1] += cache_grad_sampling_loc[xid2]; - cache_grad_sampling_loc[xid1 + 1] += cache_grad_sampling_loc[xid2 + 1]; - if (tid + (s << 1) < spre) - { - cache_grad_attn_weight[tid] += cache_grad_attn_weight[tid + (s << 1)]; - cache_grad_sampling_loc[xid1] += cache_grad_sampling_loc[xid2 + (s << 1)]; - cache_grad_sampling_loc[xid1 + 1] += cache_grad_sampling_loc[xid2 + 1 + (s << 1)]; - } - } - __syncthreads(); - } - - if (tid == 0) - { - *grad_sampling_loc = cache_grad_sampling_loc[0]; - *(grad_sampling_loc + 1) = cache_grad_sampling_loc[1]; - *grad_attn_weight = cache_grad_attn_weight[0]; - } - __syncthreads(); - - data_weight_ptr += 1; - data_loc_w_ptr += 2; - grad_attn_weight += grad_weight_stride; - grad_sampling_loc += grad_loc_stride; - } - } - } -} - -template -__global__ void ms_deformable_col2im_gpu_kernel_shm_reduce_v2_multi_blocks(const int n, - const scalar_t *grad_col, - const scalar_t *data_value, - const int64_t *data_spatial_shapes, - const int64_t *data_level_start_index, - const scalar_t *data_sampling_loc, - const scalar_t *data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t *grad_value, - scalar_t *grad_sampling_loc, - scalar_t *grad_attn_weight) -{ - CUDA_KERNEL_LOOP(index, n) - { - extern __shared__ int _s[]; - scalar_t* cache_grad_sampling_loc = (scalar_t*)_s; - scalar_t* cache_grad_attn_weight = cache_grad_sampling_loc + 2 * blockDim.x; - unsigned int tid = threadIdx.x; - int _temp = index; - const int c_col = _temp % channels; - _temp /= channels; - const int sampling_index = _temp; - const int m_col = _temp % num_heads; - _temp /= num_heads; - const int q_col = _temp % num_query; - _temp /= num_query; - const int b_col = _temp; - - const scalar_t top_grad = grad_col[index]; - - int data_weight_ptr = sampling_index * num_levels * num_point; - int data_loc_w_ptr = data_weight_ptr << 1; - const int grad_sampling_ptr = data_weight_ptr; - grad_sampling_loc += grad_sampling_ptr << 1; - grad_attn_weight += grad_sampling_ptr; - const int grad_weight_stride = 1; - const int grad_loc_stride = 2; - const int qid_stride = num_heads * channels; - const int data_value_ptr_init_offset = b_col * spatial_size * qid_stride; - - for (int l_col=0; l_col < num_levels; ++l_col) - { - const int level_start_id = data_level_start_index[l_col]; - const int spatial_h_ptr = l_col << 1; - const int spatial_h = data_spatial_shapes[spatial_h_ptr]; - const int spatial_w = data_spatial_shapes[spatial_h_ptr + 1]; - const int value_ptr_offset = data_value_ptr_init_offset + level_start_id * qid_stride; - const scalar_t *data_value_ptr = data_value + value_ptr_offset; - scalar_t *grad_value_ptr = grad_value + value_ptr_offset; - - for (int p_col=0; p_col < num_point; ++p_col) - { - const scalar_t loc_w = data_sampling_loc[data_loc_w_ptr]; - const scalar_t loc_h = data_sampling_loc[data_loc_w_ptr + 1]; - const scalar_t weight = data_attn_weight[data_weight_ptr]; - - const scalar_t h_im = loc_h * spatial_h - 0.5; - const scalar_t w_im = loc_w * spatial_w - 0.5; - *(cache_grad_sampling_loc+(threadIdx.x << 1)) = 0; - *(cache_grad_sampling_loc+((threadIdx.x << 1) + 1)) = 0; - *(cache_grad_attn_weight+threadIdx.x)=0; - if (h_im > -1 && w_im > -1 && h_im < spatial_h && w_im < spatial_w) - { - ms_deform_attn_col2im_bilinear( - data_value_ptr, spatial_h, spatial_w, num_heads, channels, h_im, w_im, m_col, c_col, - top_grad, weight, grad_value_ptr, - cache_grad_sampling_loc+(threadIdx.x << 1), cache_grad_attn_weight+threadIdx.x); - } - - __syncthreads(); - - for (unsigned int s=blockDim.x/2, spre=blockDim.x; s>0; s>>=1, spre>>=1) - { - if (tid < s) { - const unsigned int xid1 = tid << 1; - const unsigned int xid2 = (tid + s) << 1; - cache_grad_attn_weight[tid] += cache_grad_attn_weight[tid + s]; - cache_grad_sampling_loc[xid1] += cache_grad_sampling_loc[xid2]; - cache_grad_sampling_loc[xid1 + 1] += cache_grad_sampling_loc[xid2 + 1]; - if (tid + (s << 1) < spre) - { - cache_grad_attn_weight[tid] += cache_grad_attn_weight[tid + (s << 1)]; - cache_grad_sampling_loc[xid1] += cache_grad_sampling_loc[xid2 + (s << 1)]; - cache_grad_sampling_loc[xid1 + 1] += cache_grad_sampling_loc[xid2 + 1 + (s << 1)]; - } - } - __syncthreads(); - } - - if (tid == 0) - { - atomicAdd(grad_sampling_loc, cache_grad_sampling_loc[0]); - atomicAdd(grad_sampling_loc + 1, cache_grad_sampling_loc[1]); - atomicAdd(grad_attn_weight, cache_grad_attn_weight[0]); - } - __syncthreads(); - - data_weight_ptr += 1; - data_loc_w_ptr += 2; - grad_attn_weight += grad_weight_stride; - grad_sampling_loc += grad_loc_stride; - } - } - } -} - - -template -__global__ void ms_deformable_col2im_gpu_kernel_gm(const int n, - const scalar_t *grad_col, - const scalar_t *data_value, - const int64_t *data_spatial_shapes, - const int64_t *data_level_start_index, - const scalar_t *data_sampling_loc, - const scalar_t *data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t *grad_value, - scalar_t *grad_sampling_loc, - scalar_t *grad_attn_weight) -{ - CUDA_KERNEL_LOOP(index, n) - { - int _temp = index; - const int c_col = _temp % channels; - _temp /= channels; - const int sampling_index = _temp; - const int m_col = _temp % num_heads; - _temp /= num_heads; - const int q_col = _temp % num_query; - _temp /= num_query; - const int b_col = _temp; - - const scalar_t top_grad = grad_col[index]; - - int data_weight_ptr = sampling_index * num_levels * num_point; - int data_loc_w_ptr = data_weight_ptr << 1; - const int grad_sampling_ptr = data_weight_ptr; - grad_sampling_loc += grad_sampling_ptr << 1; - grad_attn_weight += grad_sampling_ptr; - const int grad_weight_stride = 1; - const int grad_loc_stride = 2; - const int qid_stride = num_heads * channels; - const int data_value_ptr_init_offset = b_col * spatial_size * qid_stride; - - for (int l_col=0; l_col < num_levels; ++l_col) - { - const int level_start_id = data_level_start_index[l_col]; - const int spatial_h_ptr = l_col << 1; - const int spatial_h = data_spatial_shapes[spatial_h_ptr]; - const int spatial_w = data_spatial_shapes[spatial_h_ptr + 1]; - const int value_ptr_offset = data_value_ptr_init_offset + level_start_id * qid_stride; - const scalar_t *data_value_ptr = data_value + value_ptr_offset; - scalar_t *grad_value_ptr = grad_value + value_ptr_offset; - - for (int p_col=0; p_col < num_point; ++p_col) - { - const scalar_t loc_w = data_sampling_loc[data_loc_w_ptr]; - const scalar_t loc_h = data_sampling_loc[data_loc_w_ptr + 1]; - const scalar_t weight = data_attn_weight[data_weight_ptr]; - - const scalar_t h_im = loc_h * spatial_h - 0.5; - const scalar_t w_im = loc_w * spatial_w - 0.5; - if (h_im > -1 && w_im > -1 && h_im < spatial_h && w_im < spatial_w) - { - ms_deform_attn_col2im_bilinear_gm( - data_value_ptr, spatial_h, spatial_w, num_heads, channels, h_im, w_im, m_col, c_col, - top_grad, weight, grad_value_ptr, - grad_sampling_loc, grad_attn_weight); - } - data_weight_ptr += 1; - data_loc_w_ptr += 2; - grad_attn_weight += grad_weight_stride; - grad_sampling_loc += grad_loc_stride; - } - } - } -} - - -template -void ms_deformable_im2col_cuda(cudaStream_t stream, - const scalar_t* data_value, - const int64_t* data_spatial_shapes, - const int64_t* data_level_start_index, - const scalar_t* data_sampling_loc, - const scalar_t* data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t* data_col) -{ - const int num_kernels = batch_size * num_query * num_heads * channels; - const int num_actual_kernels = batch_size * num_query * num_heads * channels; - const int num_threads = CUDA_NUM_THREADS; - ms_deformable_im2col_gpu_kernel - <<>>( - num_kernels, data_value, data_spatial_shapes, data_level_start_index, data_sampling_loc, data_attn_weight, - batch_size, spatial_size, num_heads, channels, num_levels, num_query, num_point, data_col); - - cudaError_t err = cudaGetLastError(); - if (err != cudaSuccess) - { - printf("error in ms_deformable_im2col_cuda: %s\n", cudaGetErrorString(err)); - } - -} - -template -void ms_deformable_col2im_cuda(cudaStream_t stream, - const scalar_t* grad_col, - const scalar_t* data_value, - const int64_t * data_spatial_shapes, - const int64_t * data_level_start_index, - const scalar_t * data_sampling_loc, - const scalar_t * data_attn_weight, - const int batch_size, - const int spatial_size, - const int num_heads, - const int channels, - const int num_levels, - const int num_query, - const int num_point, - scalar_t* grad_value, - scalar_t* grad_sampling_loc, - scalar_t* grad_attn_weight) -{ - const int num_threads = (channels > CUDA_NUM_THREADS)?CUDA_NUM_THREADS:channels; - const int num_kernels = batch_size * num_query * num_heads * channels; - const int num_actual_kernels = batch_size * num_query * num_heads * channels; - if (channels > 1024) - { - if ((channels & 1023) == 0) - { - ms_deformable_col2im_gpu_kernel_shm_reduce_v2_multi_blocks - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - } - else - { - ms_deformable_col2im_gpu_kernel_gm - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - } - } - else{ - switch(channels) - { - case 1: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v1 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 2: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v1 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 4: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v1 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 8: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v1 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 16: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v1 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 32: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v1 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 64: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v2 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 128: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v2 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 256: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v2 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 512: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v2 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - case 1024: - ms_deformable_col2im_gpu_kernel_shm_blocksize_aware_reduce_v2 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - break; - default: - if (channels < 64) - { - ms_deformable_col2im_gpu_kernel_shm_reduce_v1 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - } - else - { - ms_deformable_col2im_gpu_kernel_shm_reduce_v2 - <<>>( - num_kernels, - grad_col, - data_value, - data_spatial_shapes, - data_level_start_index, - data_sampling_loc, - data_attn_weight, - batch_size, - spatial_size, - num_heads, - channels, - num_levels, - num_query, - num_point, - grad_value, - grad_sampling_loc, - grad_attn_weight); - } - } - } - cudaError_t err = cudaGetLastError(); - if (err != cudaSuccess) - { - printf("error in ms_deformable_col2im_cuda: %s\n", cudaGetErrorString(err)); - } - -} \ No newline at end of file diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/ms_deform_attn.h b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/ms_deform_attn.h deleted file mode 100644 index ac0ef2ec25..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/ms_deform_attn.h +++ /dev/null @@ -1,62 +0,0 @@ -/*! -************************************************************************************************** -* Deformable DETR -* Copyright (c) 2020 SenseTime. All Rights Reserved. -* Licensed under the Apache License, Version 2.0 [see LICENSE for details] -************************************************************************************************** -* Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -************************************************************************************************** -*/ - -#pragma once - -#include "cpu/ms_deform_attn_cpu.h" - -#ifdef WITH_CUDA -#include "cuda/ms_deform_attn_cuda.h" -#endif - - -at::Tensor -ms_deform_attn_forward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const int im2col_step) -{ - if (value.type().is_cuda()) - { -#ifdef WITH_CUDA - return ms_deform_attn_cuda_forward( - value, spatial_shapes, level_start_index, sampling_loc, attn_weight, im2col_step); -#else - AT_ERROR("Not compiled with GPU support"); -#endif - } - AT_ERROR("Not implemented on the CPU"); -} - -std::vector -ms_deform_attn_backward( - const at::Tensor &value, - const at::Tensor &spatial_shapes, - const at::Tensor &level_start_index, - const at::Tensor &sampling_loc, - const at::Tensor &attn_weight, - const at::Tensor &grad_output, - const int im2col_step) -{ - if (value.type().is_cuda()) - { -#ifdef WITH_CUDA - return ms_deform_attn_cuda_backward( - value, spatial_shapes, level_start_index, sampling_loc, attn_weight, grad_output, im2col_step); -#else - AT_ERROR("Not compiled with GPU support"); -#endif - } - AT_ERROR("Not implemented on the CPU"); -} - diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/vision.cpp b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/vision.cpp deleted file mode 100644 index 2201f63a51..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/src/vision.cpp +++ /dev/null @@ -1,16 +0,0 @@ -/*! -************************************************************************************************** -* Deformable DETR -* Copyright (c) 2020 SenseTime. All Rights Reserved. -* Licensed under the Apache License, Version 2.0 [see LICENSE for details] -************************************************************************************************** -* Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -************************************************************************************************** -*/ - -#include "ms_deform_attn.h" - -PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { - m.def("ms_deform_attn_forward", &ms_deform_attn_forward, "ms_deform_attn_forward"); - m.def("ms_deform_attn_backward", &ms_deform_attn_backward, "ms_deform_attn_backward"); -} diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/test.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/test.py deleted file mode 100644 index 720d6473b2..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/test.py +++ /dev/null @@ -1,121 +0,0 @@ -# ------------------------------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------------------------------ -# Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 -# ------------------------------------------------------------------------------------------------ - - -from functions.ms_deform_attn_func import MSDeformAttnFunction, ms_deform_attn_core_pytorch -import torch -from torch.autograd import gradcheck - -N, M, D = 1, 2, 2 -Lq, L, P = 2, 2, 2 -shapes = torch.as_tensor([(6, 4), (3, 2)], dtype=torch.long).cuda() -level_start_index = torch.cat((shapes.new_zeros((1,)), shapes.prod(1).cumsum(0)[:-1])) -S = sum([(H * W).item() for H, W in shapes]) - - -torch.manual_seed(3) - - -@torch.no_grad() -def check_forward_equal_with_pytorch_double() -> None: - value = torch.rand(N, S, M, D).cuda() * 0.01 - sampling_locations = torch.rand(N, Lq, M, L, P, 2).cuda() - attention_weights = torch.rand(N, Lq, M, L, P).cuda() + 1e-5 - attention_weights /= attention_weights.sum(-1, keepdim=True).sum(-2, keepdim=True) - im2col_step = 2 - output_pytorch = ( - ms_deform_attn_core_pytorch( - value.double(), shapes, sampling_locations.double(), attention_weights.double() - ) - .detach() - .cpu() - ) - output_cuda = ( - MSDeformAttnFunction.apply( - value.double(), - shapes, - level_start_index, - sampling_locations.double(), - attention_weights.double(), - im2col_step, - ) - .detach() - .cpu() - ) - fwdok = torch.allclose(output_cuda, output_pytorch) - max_abs_err = (output_cuda - output_pytorch).abs().max() - max_rel_err = ((output_cuda - output_pytorch).abs() / output_pytorch.abs()).max() - - print( - f"* {fwdok} check_forward_equal_with_pytorch_double: max_abs_err {max_abs_err:.2e} max_rel_err {max_rel_err:.2e}" - ) - - -@torch.no_grad() -def check_forward_equal_with_pytorch_float() -> None: - value = torch.rand(N, S, M, D).cuda() * 0.01 - sampling_locations = torch.rand(N, Lq, M, L, P, 2).cuda() - attention_weights = torch.rand(N, Lq, M, L, P).cuda() + 1e-5 - attention_weights /= attention_weights.sum(-1, keepdim=True).sum(-2, keepdim=True) - im2col_step = 2 - output_pytorch = ( - ms_deform_attn_core_pytorch(value, shapes, sampling_locations, attention_weights) - .detach() - .cpu() - ) - output_cuda = ( - MSDeformAttnFunction.apply( - value, shapes, level_start_index, sampling_locations, attention_weights, im2col_step - ) - .detach() - .cpu() - ) - fwdok = torch.allclose(output_cuda, output_pytorch, rtol=1e-2, atol=1e-3) - max_abs_err = (output_cuda - output_pytorch).abs().max() - max_rel_err = ((output_cuda - output_pytorch).abs() / output_pytorch.abs()).max() - - print( - f"* {fwdok} check_forward_equal_with_pytorch_float: max_abs_err {max_abs_err:.2e} max_rel_err {max_rel_err:.2e}" - ) - - -def check_gradient_numerical( - channels: int=4, grad_value: bool=True, grad_sampling_loc: bool=True, grad_attn_weight: bool=True -) -> None: - value = torch.rand(N, S, M, channels).cuda() * 0.01 - sampling_locations = torch.rand(N, Lq, M, L, P, 2).cuda() - attention_weights = torch.rand(N, Lq, M, L, P).cuda() + 1e-5 - attention_weights /= attention_weights.sum(-1, keepdim=True).sum(-2, keepdim=True) - im2col_step = 2 - func = MSDeformAttnFunction.apply - - value.requires_grad = grad_value - sampling_locations.requires_grad = grad_sampling_loc - attention_weights.requires_grad = grad_attn_weight - - gradok = gradcheck( - func, - ( - value.double(), - shapes, - level_start_index, - sampling_locations.double(), - attention_weights.double(), - im2col_step, - ), - ) - - print(f"* {gradok} check_gradient_numerical(D={channels})") - - -if __name__ == "__main__": - check_forward_equal_with_pytorch_double() - check_forward_equal_with_pytorch_float() - - for channels in [30, 32, 64, 71, 1025, 2048, 3096]: - check_gradient_numerical(channels, True, True, True) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/position_encoding.py b/dimos/models/Detic/third_party/Deformable-DETR/models/position_encoding.py deleted file mode 100644 index 2ce5038e5e..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/position_encoding.py +++ /dev/null @@ -1,112 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Various positional encodings for the transformer. -""" - -import math - -import torch -from torch import nn -from util.misc import NestedTensor - - -class PositionEmbeddingSine(nn.Module): - """ - This is a more standard version of the position embedding, very similar to the one - used by the Attention is all you need paper, generalized to work on images. - """ - - def __init__(self, num_pos_feats: int=64, temperature: int=10000, normalize: bool=False, scale=None) -> None: - super().__init__() - self.num_pos_feats = num_pos_feats - self.temperature = temperature - self.normalize = normalize - if scale is not None and normalize is False: - raise ValueError("normalize should be True if scale is passed") - if scale is None: - scale = 2 * math.pi - self.scale = scale - - def forward(self, tensor_list: NestedTensor): - x = tensor_list.tensors - mask = tensor_list.mask - assert mask is not None - not_mask = ~mask - y_embed = not_mask.cumsum(1, dtype=torch.float32) - x_embed = not_mask.cumsum(2, dtype=torch.float32) - if self.normalize: - eps = 1e-6 - y_embed = (y_embed - 0.5) / (y_embed[:, -1:, :] + eps) * self.scale - x_embed = (x_embed - 0.5) / (x_embed[:, :, -1:] + eps) * self.scale - - dim_t = torch.arange(self.num_pos_feats, dtype=torch.float32, device=x.device) - dim_t = self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats) - - pos_x = x_embed[:, :, :, None] / dim_t - pos_y = y_embed[:, :, :, None] / dim_t - pos_x = torch.stack( - (pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4 - ).flatten(3) - pos_y = torch.stack( - (pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4 - ).flatten(3) - pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2) - return pos - - -class PositionEmbeddingLearned(nn.Module): - """ - Absolute pos embedding, learned. - """ - - def __init__(self, num_pos_feats: int=256) -> None: - super().__init__() - self.row_embed = nn.Embedding(50, num_pos_feats) - self.col_embed = nn.Embedding(50, num_pos_feats) - self.reset_parameters() - - def reset_parameters(self) -> None: - nn.init.uniform_(self.row_embed.weight) - nn.init.uniform_(self.col_embed.weight) - - def forward(self, tensor_list: NestedTensor): - x = tensor_list.tensors - h, w = x.shape[-2:] - i = torch.arange(w, device=x.device) - j = torch.arange(h, device=x.device) - x_emb = self.col_embed(i) - y_emb = self.row_embed(j) - pos = ( - torch.cat( - [ - x_emb.unsqueeze(0).repeat(h, 1, 1), - y_emb.unsqueeze(1).repeat(1, w, 1), - ], - dim=-1, - ) - .permute(2, 0, 1) - .unsqueeze(0) - .repeat(x.shape[0], 1, 1, 1) - ) - return pos - - -def build_position_encoding(args): - N_steps = args.hidden_dim // 2 - if args.position_embedding in ("v2", "sine"): - # TODO find a better way of exposing other arguments - position_embedding = PositionEmbeddingSine(N_steps, normalize=True) - elif args.position_embedding in ("v3", "learned"): - position_embedding = PositionEmbeddingLearned(N_steps) - else: - raise ValueError(f"not supported {args.position_embedding}") - - return position_embedding diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/segmentation.py b/dimos/models/Detic/third_party/Deformable-DETR/models/segmentation.py deleted file mode 100644 index 2450a5c447..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/segmentation.py +++ /dev/null @@ -1,398 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -This file provides the definition of the convolutional heads used to predict masks, as well as the losses -""" - -from collections import defaultdict -import io - -from PIL import Image -import torch -import torch.nn as nn -import torch.nn.functional as F -import util.box_ops as box_ops -from util.misc import NestedTensor, interpolate, nested_tensor_from_tensor_list -from typing import Optional, Sequence - -try: - from panopticapi.utils import id2rgb, rgb2id -except ImportError: - pass - - -class DETRsegm(nn.Module): - def __init__(self, detr, freeze_detr: bool=False) -> None: - super().__init__() - self.detr = detr - - if freeze_detr: - for p in self.parameters(): - p.requires_grad_(False) - - hidden_dim, nheads = detr.transformer.d_model, detr.transformer.nhead - self.bbox_attention = MHAttentionMap(hidden_dim, hidden_dim, nheads, dropout=0) - self.mask_head = MaskHeadSmallConv(hidden_dim + nheads, [1024, 512, 256], hidden_dim) - - def forward(self, samples: NestedTensor): - if not isinstance(samples, NestedTensor): - samples = nested_tensor_from_tensor_list(samples) - features, pos = self.detr.backbone(samples) - - bs = features[-1].tensors.shape[0] - - src, mask = features[-1].decompose() - src_proj = self.detr.input_proj(src) - hs, memory = self.detr.transformer(src_proj, mask, self.detr.query_embed.weight, pos[-1]) - - outputs_class = self.detr.class_embed(hs) - outputs_coord = self.detr.bbox_embed(hs).sigmoid() - out = {"pred_logits": outputs_class[-1], "pred_boxes": outputs_coord[-1]} - if self.detr.aux_loss: - out["aux_outputs"] = [ - {"pred_logits": a, "pred_boxes": b} - for a, b in zip(outputs_class[:-1], outputs_coord[:-1], strict=False) - ] - - # FIXME h_boxes takes the last one computed, keep this in mind - bbox_mask = self.bbox_attention(hs[-1], memory, mask=mask) - - seg_masks = self.mask_head( - src_proj, bbox_mask, [features[2].tensors, features[1].tensors, features[0].tensors] - ) - outputs_seg_masks = seg_masks.view( - bs, self.detr.num_queries, seg_masks.shape[-2], seg_masks.shape[-1] - ) - - out["pred_masks"] = outputs_seg_masks - return out - - -class MaskHeadSmallConv(nn.Module): - """ - Simple convolutional head, using group norm. - Upsampling is done using a FPN approach - """ - - def __init__(self, dim: int, fpn_dims, context_dim) -> None: - super().__init__() - - inter_dims = [ - dim, - context_dim // 2, - context_dim // 4, - context_dim // 8, - context_dim // 16, - context_dim // 64, - ] - self.lay1 = torch.nn.Conv2d(dim, dim, 3, padding=1) - self.gn1 = torch.nn.GroupNorm(8, dim) - self.lay2 = torch.nn.Conv2d(dim, inter_dims[1], 3, padding=1) - self.gn2 = torch.nn.GroupNorm(8, inter_dims[1]) - self.lay3 = torch.nn.Conv2d(inter_dims[1], inter_dims[2], 3, padding=1) - self.gn3 = torch.nn.GroupNorm(8, inter_dims[2]) - self.lay4 = torch.nn.Conv2d(inter_dims[2], inter_dims[3], 3, padding=1) - self.gn4 = torch.nn.GroupNorm(8, inter_dims[3]) - self.lay5 = torch.nn.Conv2d(inter_dims[3], inter_dims[4], 3, padding=1) - self.gn5 = torch.nn.GroupNorm(8, inter_dims[4]) - self.out_lay = torch.nn.Conv2d(inter_dims[4], 1, 3, padding=1) - - self.dim = dim - - self.adapter1 = torch.nn.Conv2d(fpn_dims[0], inter_dims[1], 1) - self.adapter2 = torch.nn.Conv2d(fpn_dims[1], inter_dims[2], 1) - self.adapter3 = torch.nn.Conv2d(fpn_dims[2], inter_dims[3], 1) - - for m in self.modules(): - if isinstance(m, nn.Conv2d): - nn.init.kaiming_uniform_(m.weight, a=1) - nn.init.constant_(m.bias, 0) - - def forward(self, x, bbox_mask, fpns): - def expand(tensor, length: int): - return tensor.unsqueeze(1).repeat(1, int(length), 1, 1, 1).flatten(0, 1) - - x = torch.cat([expand(x, bbox_mask.shape[1]), bbox_mask.flatten(0, 1)], 1) - - x = self.lay1(x) - x = self.gn1(x) - x = F.relu(x) - x = self.lay2(x) - x = self.gn2(x) - x = F.relu(x) - - cur_fpn = self.adapter1(fpns[0]) - if cur_fpn.size(0) != x.size(0): - cur_fpn = expand(cur_fpn, x.size(0) / cur_fpn.size(0)) - x = cur_fpn + F.interpolate(x, size=cur_fpn.shape[-2:], mode="nearest") - x = self.lay3(x) - x = self.gn3(x) - x = F.relu(x) - - cur_fpn = self.adapter2(fpns[1]) - if cur_fpn.size(0) != x.size(0): - cur_fpn = expand(cur_fpn, x.size(0) / cur_fpn.size(0)) - x = cur_fpn + F.interpolate(x, size=cur_fpn.shape[-2:], mode="nearest") - x = self.lay4(x) - x = self.gn4(x) - x = F.relu(x) - - cur_fpn = self.adapter3(fpns[2]) - if cur_fpn.size(0) != x.size(0): - cur_fpn = expand(cur_fpn, x.size(0) / cur_fpn.size(0)) - x = cur_fpn + F.interpolate(x, size=cur_fpn.shape[-2:], mode="nearest") - x = self.lay5(x) - x = self.gn5(x) - x = F.relu(x) - - x = self.out_lay(x) - return x - - -class MHAttentionMap(nn.Module): - """This is a 2D attention module, which only returns the attention softmax (no multiplication by value)""" - - def __init__(self, query_dim, hidden_dim, num_heads: int, dropout: int=0, bias: bool=True) -> None: - super().__init__() - self.num_heads = num_heads - self.hidden_dim = hidden_dim - self.dropout = nn.Dropout(dropout) - - self.q_linear = nn.Linear(query_dim, hidden_dim, bias=bias) - self.k_linear = nn.Linear(query_dim, hidden_dim, bias=bias) - - nn.init.zeros_(self.k_linear.bias) - nn.init.zeros_(self.q_linear.bias) - nn.init.xavier_uniform_(self.k_linear.weight) - nn.init.xavier_uniform_(self.q_linear.weight) - self.normalize_fact = float(hidden_dim / self.num_heads) ** -0.5 - - def forward(self, q, k, mask=None): - q = self.q_linear(q) - k = F.conv2d(k, self.k_linear.weight.unsqueeze(-1).unsqueeze(-1), self.k_linear.bias) - qh = q.view(q.shape[0], q.shape[1], self.num_heads, self.hidden_dim // self.num_heads) - kh = k.view( - k.shape[0], self.num_heads, self.hidden_dim // self.num_heads, k.shape[-2], k.shape[-1] - ) - weights = torch.einsum("bqnc,bnchw->bqnhw", qh * self.normalize_fact, kh) - - if mask is not None: - weights.masked_fill_(mask.unsqueeze(1).unsqueeze(1), float("-inf")) - weights = F.softmax(weights.flatten(2), dim=-1).view_as(weights) - weights = self.dropout(weights) - return weights - - -def dice_loss(inputs, targets, num_boxes: int): - """ - Compute the DICE loss, similar to generalized IOU for masks - Args: - inputs: A float tensor of arbitrary shape. - The predictions for each example. - targets: A float tensor with the same shape as inputs. Stores the binary - classification label for each element in inputs - (0 for the negative class and 1 for the positive class). - """ - inputs = inputs.sigmoid() - inputs = inputs.flatten(1) - numerator = 2 * (inputs * targets).sum(1) - denominator = inputs.sum(-1) + targets.sum(-1) - loss = 1 - (numerator + 1) / (denominator + 1) - return loss.sum() / num_boxes - - -def sigmoid_focal_loss(inputs, targets, num_boxes: int, alpha: float = 0.25, gamma: float = 2): - """ - Loss used in RetinaNet for dense detection: https://arxiv.org/abs/1708.02002. - Args: - inputs: A float tensor of arbitrary shape. - The predictions for each example. - targets: A float tensor with the same shape as inputs. Stores the binary - classification label for each element in inputs - (0 for the negative class and 1 for the positive class). - alpha: (optional) Weighting factor in range (0,1) to balance - positive vs negative examples. Default = -1 (no weighting). - gamma: Exponent of the modulating factor (1 - p_t) to - balance easy vs hard examples. - Returns: - Loss tensor - """ - prob = inputs.sigmoid() - ce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") - p_t = prob * targets + (1 - prob) * (1 - targets) - loss = ce_loss * ((1 - p_t) ** gamma) - - if alpha >= 0: - alpha_t = alpha * targets + (1 - alpha) * (1 - targets) - loss = alpha_t * loss - - return loss.mean(1).sum() / num_boxes - - -class PostProcessSegm(nn.Module): - def __init__(self, threshold: float=0.5) -> None: - super().__init__() - self.threshold = threshold - - @torch.no_grad() - def forward(self, results, outputs, orig_target_sizes: Sequence[int], max_target_sizes: Sequence[int]): - assert len(orig_target_sizes) == len(max_target_sizes) - max_h, max_w = max_target_sizes.max(0)[0].tolist() - outputs_masks = outputs["pred_masks"].squeeze(2) - outputs_masks = F.interpolate( - outputs_masks, size=(max_h, max_w), mode="bilinear", align_corners=False - ) - outputs_masks = (outputs_masks.sigmoid() > self.threshold).cpu() - - for i, (cur_mask, t, tt) in enumerate( - zip(outputs_masks, max_target_sizes, orig_target_sizes, strict=False) - ): - img_h, img_w = t[0], t[1] - results[i]["masks"] = cur_mask[:, :img_h, :img_w].unsqueeze(1) - results[i]["masks"] = F.interpolate( - results[i]["masks"].float(), size=tuple(tt.tolist()), mode="nearest" - ).byte() - - return results - - -class PostProcessPanoptic(nn.Module): - """This class converts the output of the model to the final panoptic result, in the format expected by the - coco panoptic API""" - - def __init__(self, is_thing_map: bool, threshold: float=0.85) -> None: - """ - Parameters: - is_thing_map: This is a whose keys are the class ids, and the values a boolean indicating whether - the class is a thing (True) or a stuff (False) class - threshold: confidence threshold: segments with confidence lower than this will be deleted - """ - super().__init__() - self.threshold = threshold - self.is_thing_map = is_thing_map - - def forward(self, outputs, processed_sizes: Sequence[int], target_sizes: Optional[Sequence[int]]=None): - """This function computes the panoptic prediction from the model's predictions. - Parameters: - outputs: This is a dict coming directly from the model. See the model doc for the content. - processed_sizes: This is a list of tuples (or torch tensors) of sizes of the images that were passed to the - model, ie the size after data augmentation but before batching. - target_sizes: This is a list of tuples (or torch tensors) corresponding to the requested final size - of each prediction. If left to None, it will default to the processed_sizes - """ - if target_sizes is None: - target_sizes = processed_sizes - assert len(processed_sizes) == len(target_sizes) - out_logits, raw_masks, raw_boxes = ( - outputs["pred_logits"], - outputs["pred_masks"], - outputs["pred_boxes"], - ) - assert len(out_logits) == len(raw_masks) == len(target_sizes) - preds = [] - - def to_tuple(tup): - if isinstance(tup, tuple): - return tup - return tuple(tup.cpu().tolist()) - - for cur_logits, cur_masks, cur_boxes, size, target_size in zip( - out_logits, raw_masks, raw_boxes, processed_sizes, target_sizes, strict=False - ): - # we filter empty queries and detection below threshold - scores, labels = cur_logits.softmax(-1).max(-1) - keep = labels.ne(outputs["pred_logits"].shape[-1] - 1) & (scores > self.threshold) - cur_scores, cur_classes = cur_logits.softmax(-1).max(-1) - cur_scores = cur_scores[keep] - cur_classes = cur_classes[keep] - cur_masks = cur_masks[keep] - cur_masks = interpolate(cur_masks[None], to_tuple(size), mode="bilinear").squeeze(0) - cur_boxes = box_ops.box_cxcywh_to_xyxy(cur_boxes[keep]) - - h, w = cur_masks.shape[-2:] - assert len(cur_boxes) == len(cur_classes) - - # It may be that we have several predicted masks for the same stuff class. - # In the following, we track the list of masks ids for each stuff class (they are merged later on) - cur_masks = cur_masks.flatten(1) - stuff_equiv_classes = defaultdict(lambda: []) - for k, label in enumerate(cur_classes): - if not self.is_thing_map[label.item()]: - stuff_equiv_classes[label.item()].append(k) - - def get_ids_area(masks, scores, dedup: bool=False): - # This helper function creates the final panoptic segmentation image - # It also returns the area of the masks that appears on the image - - m_id = masks.transpose(0, 1).softmax(-1) - - if m_id.shape[-1] == 0: - # We didn't detect any mask :( - m_id = torch.zeros((h, w), dtype=torch.long, device=m_id.device) - else: - m_id = m_id.argmax(-1).view(h, w) - - if dedup: - # Merge the masks corresponding to the same stuff class - for equiv in stuff_equiv_classes.values(): - if len(equiv) > 1: - for eq_id in equiv: - m_id.masked_fill_(m_id.eq(eq_id), equiv[0]) - - final_h, final_w = to_tuple(target_size) - - seg_img = Image.fromarray(id2rgb(m_id.view(h, w).cpu().numpy())) - seg_img = seg_img.resize(size=(final_w, final_h), resample=Image.NEAREST) - - np_seg_img = ( - torch.ByteTensor(torch.ByteStorage.from_buffer(seg_img.tobytes())) - .view(final_h, final_w, 3) - .numpy() - ) - m_id = torch.from_numpy(rgb2id(np_seg_img)) - - area = [] - for i in range(len(scores)): - area.append(m_id.eq(i).sum().item()) - return area, seg_img - - area, seg_img = get_ids_area(cur_masks, cur_scores, dedup=True) - if cur_classes.numel() > 0: - # We know filter empty masks as long as we find some - while True: - filtered_small = torch.as_tensor( - [area[i] <= 4 for i, c in enumerate(cur_classes)], - dtype=torch.bool, - device=keep.device, - ) - if filtered_small.any().item(): - cur_scores = cur_scores[~filtered_small] - cur_classes = cur_classes[~filtered_small] - cur_masks = cur_masks[~filtered_small] - area, seg_img = get_ids_area(cur_masks, cur_scores) - else: - break - - else: - cur_classes = torch.ones(1, dtype=torch.long, device=cur_classes.device) - - segments_info = [] - for i, a in enumerate(area): - cat = cur_classes[i].item() - segments_info.append( - {"id": i, "isthing": self.is_thing_map[cat], "category_id": cat, "area": a} - ) - del cur_classes - - with io.BytesIO() as out: - seg_img.save(out, format="PNG") - predictions = {"png_string": out.getvalue(), "segments_info": segments_info} - preds.append(predictions) - return preds diff --git a/dimos/models/Detic/third_party/Deformable-DETR/requirements.txt b/dimos/models/Detic/third_party/Deformable-DETR/requirements.txt deleted file mode 100644 index fd846723be..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pycocotools -tqdm -cython -scipy diff --git a/dimos/models/Detic/third_party/Deformable-DETR/tools/launch.py b/dimos/models/Detic/third_party/Deformable-DETR/tools/launch.py deleted file mode 100644 index 1d60ae4994..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/tools/launch.py +++ /dev/null @@ -1,204 +0,0 @@ -# -------------------------------------------------------------------------------------------------------------------------- -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# -------------------------------------------------------------------------------------------------------------------------- -# Modified from https://github.com/pytorch/pytorch/blob/173f224570017b4b1a3a1a13d0bff280a54d9cd9/torch/distributed/launch.py -# -------------------------------------------------------------------------------------------------------------------------- - -r""" -`torch.distributed.launch` is a module that spawns up multiple distributed -training processes on each of the training nodes. -The utility can be used for single-node distributed training, in which one or -more processes per node will be spawned. The utility can be used for either -CPU training or GPU training. If the utility is used for GPU training, -each distributed process will be operating on a single GPU. This can achieve -well-improved single-node training performance. It can also be used in -multi-node distributed training, by spawning up multiple processes on each node -for well-improved multi-node distributed training performance as well. -This will especially be benefitial for systems with multiple Infiniband -interfaces that have direct-GPU support, since all of them can be utilized for -aggregated communication bandwidth. -In both cases of single-node distributed training or multi-node distributed -training, this utility will launch the given number of processes per node -(``--nproc_per_node``). If used for GPU training, this number needs to be less -or euqal to the number of GPUs on the current system (``nproc_per_node``), -and each process will be operating on a single GPU from *GPU 0 to -GPU (nproc_per_node - 1)*. -**How to use this module:** -1. Single-Node multi-process distributed training -:: - >>> python -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE - YOUR_TRAINING_SCRIPT.py (--arg1 --arg2 --arg3 and all other - arguments of your training script) -2. Multi-Node multi-process distributed training: (e.g. two nodes) -Node 1: *(IP: 192.168.1.1, and has a free port: 1234)* -:: - >>> python -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE - --nnodes=2 --node_rank=0 --master_addr="192.168.1.1" - --master_port=1234 YOUR_TRAINING_SCRIPT.py (--arg1 --arg2 --arg3 - and all other arguments of your training script) -Node 2: -:: - >>> python -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE - --nnodes=2 --node_rank=1 --master_addr="192.168.1.1" - --master_port=1234 YOUR_TRAINING_SCRIPT.py (--arg1 --arg2 --arg3 - and all other arguments of your training script) -3. To look up what optional arguments this module offers: -:: - >>> python -m torch.distributed.launch --help -**Important Notices:** -1. This utilty and multi-process distributed (single-node or -multi-node) GPU training currently only achieves the best performance using -the NCCL distributed backend. Thus NCCL backend is the recommended backend to -use for GPU training. -2. In your training program, you must parse the command-line argument: -``--local_rank=LOCAL_PROCESS_RANK``, which will be provided by this module. -If your training program uses GPUs, you should ensure that your code only -runs on the GPU device of LOCAL_PROCESS_RANK. This can be done by: -Parsing the local_rank argument -:: - >>> import argparse - >>> parser = argparse.ArgumentParser() - >>> parser.add_argument("--local_rank", type=int) - >>> args = parser.parse_args() -Set your device to local rank using either -:: - >>> torch.cuda.set_device(arg.local_rank) # before your code runs -or -:: - >>> with torch.cuda.device(arg.local_rank): - >>> # your code to run -3. In your training program, you are supposed to call the following function -at the beginning to start the distributed backend. You need to make sure that -the init_method uses ``env://``, which is the only supported ``init_method`` -by this module. -:: - torch.distributed.init_process_group(backend='YOUR BACKEND', - init_method='env://') -4. In your training program, you can either use regular distributed functions -or use :func:`torch.nn.parallel.DistributedDataParallel` module. If your -training program uses GPUs for training and you would like to use -:func:`torch.nn.parallel.DistributedDataParallel` module, -here is how to configure it. -:: - model = torch.nn.parallel.DistributedDataParallel(model, - device_ids=[arg.local_rank], - output_device=arg.local_rank) -Please ensure that ``device_ids`` argument is set to be the only GPU device id -that your code will be operating on. This is generally the local rank of the -process. In other words, the ``device_ids`` needs to be ``[args.local_rank]``, -and ``output_device`` needs to be ``args.local_rank`` in order to use this -utility -5. Another way to pass ``local_rank`` to the subprocesses via environment variable -``LOCAL_RANK``. This behavior is enabled when you launch the script with -``--use_env=True``. You must adjust the subprocess example above to replace -``args.local_rank`` with ``os.environ['LOCAL_RANK']``; the launcher -will not pass ``--local_rank`` when you specify this flag. -.. warning:: - ``local_rank`` is NOT globally unique: it is only unique per process - on a machine. Thus, don't use it to decide if you should, e.g., - write to a networked filesystem. See - https://github.com/pytorch/pytorch/issues/12042 for an example of - how things can go wrong if you don't do this correctly. -""" - -from argparse import REMAINDER, ArgumentParser -import os -import subprocess - - -def parse_args(): - """ - Helper function parsing the command line options - @retval ArgumentParser - """ - parser = ArgumentParser( - description="PyTorch distributed training launch " - "helper utilty that will spawn up " - "multiple distributed processes" - ) - - # Optional arguments for the launch helper - parser.add_argument( - "--nnodes", type=int, default=1, help="The number of nodes to use for distributed training" - ) - parser.add_argument( - "--node_rank", - type=int, - default=0, - help="The rank of the node for multi-node distributed training", - ) - parser.add_argument( - "--nproc_per_node", - type=int, - default=1, - help="The number of processes to launch on each node, " - "for GPU training, this is recommended to be set " - "to the number of GPUs in your system so that " - "each process can be bound to a single GPU.", - ) - parser.add_argument( - "--master_addr", - default="127.0.0.1", - type=str, - help="Master node (rank 0)'s address, should be either " - "the IP address or the hostname of node 0, for " - "single node multi-proc training, the " - "--master_addr can simply be 127.0.0.1", - ) - parser.add_argument( - "--master_port", - default=29500, - type=int, - help="Master node (rank 0)'s free port that needs to be used for communciation during distributed training", - ) - - # positional - parser.add_argument( - "training_script", - type=str, - help="The full path to the single GPU training " - "program/script to be launched in parallel, " - "followed by all the arguments for the " - "training script", - ) - - # rest from the training program - parser.add_argument("training_script_args", nargs=REMAINDER) - return parser.parse_args() - - -def main(): - args = parse_args() - - # world size in terms of number of processes - dist_world_size = args.nproc_per_node * args.nnodes - - # set PyTorch distributed related environmental variables - current_env = os.environ.copy() - current_env["MASTER_ADDR"] = args.master_addr - current_env["MASTER_PORT"] = str(args.master_port) - current_env["WORLD_SIZE"] = str(dist_world_size) - - processes = [] - - for local_rank in range(0, args.nproc_per_node): - # each process's rank - dist_rank = args.nproc_per_node * args.node_rank + local_rank - current_env["RANK"] = str(dist_rank) - current_env["LOCAL_RANK"] = str(local_rank) - - cmd = [args.training_script, *args.training_script_args] - - process = subprocess.Popen(cmd, env=current_env) - processes.append(process) - - for process in processes: - process.wait() - if process.returncode != 0: - raise subprocess.CalledProcessError(returncode=process.returncode, cmd=process.args) - - -if __name__ == "__main__": - main() diff --git a/dimos/models/Detic/third_party/Deformable-DETR/tools/run_dist_launch.sh b/dimos/models/Detic/third_party/Deformable-DETR/tools/run_dist_launch.sh deleted file mode 100755 index f6f6c4fb6f..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/tools/run_dist_launch.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ - -set -x - -GPUS=$1 -RUN_COMMAND=${@:2} -if [ $GPUS -lt 8 ]; then - GPUS_PER_NODE=${GPUS_PER_NODE:-$GPUS} -else - GPUS_PER_NODE=${GPUS_PER_NODE:-8} -fi -MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} -MASTER_PORT=${MASTER_PORT:-"29500"} -NODE_RANK=${NODE_RANK:-0} - -let "NNODES=GPUS/GPUS_PER_NODE" - -python ./tools/launch.py \ - --nnodes ${NNODES} \ - --node_rank ${NODE_RANK} \ - --master_addr ${MASTER_ADDR} \ - --master_port ${MASTER_PORT} \ - --nproc_per_node ${GPUS_PER_NODE} \ - ${RUN_COMMAND} \ No newline at end of file diff --git a/dimos/models/Detic/third_party/Deformable-DETR/tools/run_dist_slurm.sh b/dimos/models/Detic/third_party/Deformable-DETR/tools/run_dist_slurm.sh deleted file mode 100755 index bd73d0bbb7..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/tools/run_dist_slurm.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# -------------------------------------------------------------------------------------------------------------------------- -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# -------------------------------------------------------------------------------------------------------------------------- -# Modified from https://github.com/open-mmlab/mmdetection/blob/3b53fe15d87860c6941f3dda63c0f27422da6266/tools/slurm_train.sh -# -------------------------------------------------------------------------------------------------------------------------- - -set -x - -PARTITION=$1 -JOB_NAME=$2 -GPUS=$3 -RUN_COMMAND=${@:4} -if [ $GPUS -lt 8 ]; then - GPUS_PER_NODE=${GPUS_PER_NODE:-$GPUS} -else - GPUS_PER_NODE=${GPUS_PER_NODE:-8} -fi -CPUS_PER_TASK=${CPUS_PER_TASK:-4} -SRUN_ARGS=${SRUN_ARGS:-""} - -srun -p ${PARTITION} \ - --job-name=${JOB_NAME} \ - --gres=gpu:${GPUS_PER_NODE} \ - --ntasks=${GPUS} \ - --ntasks-per-node=${GPUS_PER_NODE} \ - --cpus-per-task=${CPUS_PER_TASK} \ - --kill-on-bad-exit=1 \ - ${SRUN_ARGS} \ - ${RUN_COMMAND} - diff --git a/dimos/models/Detic/third_party/Deformable-DETR/util/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/util/__init__.py deleted file mode 100644 index 4ebdc90b7f..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/util/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ diff --git a/dimos/models/Detic/third_party/Deformable-DETR/util/box_ops.py b/dimos/models/Detic/third_party/Deformable-DETR/util/box_ops.py deleted file mode 100644 index 5864b68d3b..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/util/box_ops.py +++ /dev/null @@ -1,95 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Utilities for bounding box manipulation and GIoU. -""" - -import torch -from torchvision.ops.boxes import box_area - - -def box_cxcywh_to_xyxy(x): - x_c, y_c, w, h = x.unbind(-1) - b = [(x_c - 0.5 * w), (y_c - 0.5 * h), (x_c + 0.5 * w), (y_c + 0.5 * h)] - return torch.stack(b, dim=-1) - - -def box_xyxy_to_cxcywh(x): - x0, y0, x1, y1 = x.unbind(-1) - b = [(x0 + x1) / 2, (y0 + y1) / 2, (x1 - x0), (y1 - y0)] - return torch.stack(b, dim=-1) - - -# modified from torchvision to also return the union -def box_iou(boxes1, boxes2): - area1 = box_area(boxes1) - area2 = box_area(boxes2) - - lt = torch.max(boxes1[:, None, :2], boxes2[:, :2]) # [N,M,2] - rb = torch.min(boxes1[:, None, 2:], boxes2[:, 2:]) # [N,M,2] - - wh = (rb - lt).clamp(min=0) # [N,M,2] - inter = wh[:, :, 0] * wh[:, :, 1] # [N,M] - - union = area1[:, None] + area2 - inter - - iou = inter / union - return iou, union - - -def generalized_box_iou(boxes1, boxes2): - """ - Generalized IoU from https://giou.stanford.edu/ - - The boxes should be in [x0, y0, x1, y1] format - - Returns a [N, M] pairwise matrix, where N = len(boxes1) - and M = len(boxes2) - """ - # degenerate boxes gives inf / nan results - # so do an early check - assert (boxes1[:, 2:] >= boxes1[:, :2]).all() - assert (boxes2[:, 2:] >= boxes2[:, :2]).all() - iou, union = box_iou(boxes1, boxes2) - - lt = torch.min(boxes1[:, None, :2], boxes2[:, :2]) - rb = torch.max(boxes1[:, None, 2:], boxes2[:, 2:]) - - wh = (rb - lt).clamp(min=0) # [N,M,2] - area = wh[:, :, 0] * wh[:, :, 1] - - return iou - (area - union) / area - - -def masks_to_boxes(masks): - """Compute the bounding boxes around the provided masks - - The masks should be in format [N, H, W] where N is the number of masks, (H, W) are the spatial dimensions. - - Returns a [N, 4] tensors, with the boxes in xyxy format - """ - if masks.numel() == 0: - return torch.zeros((0, 4), device=masks.device) - - h, w = masks.shape[-2:] - - y = torch.arange(0, h, dtype=torch.float) - x = torch.arange(0, w, dtype=torch.float) - y, x = torch.meshgrid(y, x) - - x_mask = masks * x.unsqueeze(0) - x_max = x_mask.flatten(1).max(-1)[0] - x_min = x_mask.masked_fill(~(masks.bool()), 1e8).flatten(1).min(-1)[0] - - y_mask = masks * y.unsqueeze(0) - y_max = y_mask.flatten(1).max(-1)[0] - y_min = y_mask.masked_fill(~(masks.bool()), 1e8).flatten(1).min(-1)[0] - - return torch.stack([x_min, y_min, x_max, y_max], 1) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/util/misc.py b/dimos/models/Detic/third_party/Deformable-DETR/util/misc.py deleted file mode 100644 index 0615de5b5f..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/util/misc.py +++ /dev/null @@ -1,538 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Misc functions, including distributed helpers. - -Mostly copy-paste from torchvision references. -""" - -from collections import defaultdict, deque -import datetime -import os -import pickle -import subprocess -import time -from typing import List, Optional - -import torch -from torch import Tensor -import torch.distributed as dist - -# needed due to empty tensor bug in pytorch and torchvision 0.5 -import torchvision - -if float(torchvision.__version__[:3]) < 0.5: - import math - - from torchvision.ops.misc import _NewEmptyTensorOp - - def _check_size_scale_factor(dim: int, size: int, scale_factor): - # type: (int, Optional[List[int]], Optional[float]) -> None - if size is None and scale_factor is None: - raise ValueError("either size or scale_factor should be defined") - if size is not None and scale_factor is not None: - raise ValueError("only one of size or scale_factor should be defined") - if not (scale_factor is not None and len(scale_factor) != dim): - raise ValueError( - f"scale_factor shape must match input shape. Input is {dim}D, scale_factor size is {len(scale_factor)}" - ) - - def _output_size(dim: int, input, size: int, scale_factor): - # type: (int, Tensor, Optional[List[int]], Optional[float]) -> List[int] - assert dim == 2 - _check_size_scale_factor(dim, size, scale_factor) - if size is not None: - return size - # if dim is not 2 or scale_factor is iterable use _ntuple instead of concat - assert scale_factor is not None and isinstance(scale_factor, int | float) - scale_factors = [scale_factor, scale_factor] - # math.floor might return float in py2.7 - return [math.floor(input.size(i + 2) * scale_factors[i]) for i in range(dim)] -elif float(torchvision.__version__[:3]) < 0.7: - from torchvision.ops import _new_empty_tensor - from torchvision.ops.misc import _output_size - - -class SmoothedValue: - """Track a series of values and provide access to smoothed values over a - window or the global series average. - """ - - def __init__(self, window_size: int=20, fmt=None) -> None: - if fmt is None: - fmt = "{median:.4f} ({global_avg:.4f})" - self.deque = deque(maxlen=window_size) - self.total = 0.0 - self.count = 0 - self.fmt = fmt - - def update(self, value, n: int=1) -> None: - self.deque.append(value) - self.count += n - self.total += value * n - - def synchronize_between_processes(self) -> None: - """ - Warning: does not synchronize the deque! - """ - if not is_dist_avail_and_initialized(): - return - t = torch.tensor([self.count, self.total], dtype=torch.float64, device="cuda") - dist.barrier() - dist.all_reduce(t) - t = t.tolist() - self.count = int(t[0]) - self.total = t[1] - - @property - def median(self): - d = torch.tensor(list(self.deque)) - return d.median().item() - - @property - def avg(self): - d = torch.tensor(list(self.deque), dtype=torch.float32) - return d.mean().item() - - @property - def global_avg(self): - return self.total / self.count - - @property - def max(self): - return max(self.deque) - - @property - def value(self): - return self.deque[-1] - - def __str__(self) -> str: - return self.fmt.format( - median=self.median, - avg=self.avg, - global_avg=self.global_avg, - max=self.max, - value=self.value, - ) - - -def all_gather(data): - """ - Run all_gather on arbitrary picklable data (not necessarily tensors) - Args: - data: any picklable object - Returns: - list[data]: list of data gathered from each rank - """ - world_size = get_world_size() - if world_size == 1: - return [data] - - # serialized to a Tensor - buffer = pickle.dumps(data) - storage = torch.ByteStorage.from_buffer(buffer) - tensor = torch.ByteTensor(storage).to("cuda") - - # obtain Tensor size of each rank - local_size = torch.tensor([tensor.numel()], device="cuda") - size_list = [torch.tensor([0], device="cuda") for _ in range(world_size)] - dist.all_gather(size_list, local_size) - size_list = [int(size.item()) for size in size_list] - max_size = max(size_list) - - # receiving Tensor from all ranks - # we pad the tensor because torch all_gather does not support - # gathering tensors of different shapes - tensor_list = [] - for _ in size_list: - tensor_list.append(torch.empty((max_size,), dtype=torch.uint8, device="cuda")) - if local_size != max_size: - padding = torch.empty(size=(max_size - local_size,), dtype=torch.uint8, device="cuda") - tensor = torch.cat((tensor, padding), dim=0) - dist.all_gather(tensor_list, tensor) - - data_list = [] - for size, tensor in zip(size_list, tensor_list, strict=False): - buffer = tensor.cpu().numpy().tobytes()[:size] - data_list.append(pickle.loads(buffer)) - - return data_list - - -def reduce_dict(input_dict, average: bool=True): - """ - Args: - input_dict (dict): all the values will be reduced - average (bool): whether to do average or sum - Reduce the values in the dictionary from all processes so that all processes - have the averaged results. Returns a dict with the same fields as - input_dict, after reduction. - """ - world_size = get_world_size() - if world_size < 2: - return input_dict - with torch.no_grad(): - names = [] - values = [] - # sort the keys so that they are consistent across processes - for k in sorted(input_dict.keys()): - names.append(k) - values.append(input_dict[k]) - values = torch.stack(values, dim=0) - dist.all_reduce(values) - if average: - values /= world_size - reduced_dict = {k: v for k, v in zip(names, values, strict=False)} - return reduced_dict - - -class MetricLogger: - def __init__(self, delimiter: str="\t") -> None: - self.meters = defaultdict(SmoothedValue) - self.delimiter = delimiter - - def update(self, **kwargs) -> None: - for k, v in kwargs.items(): - if isinstance(v, torch.Tensor): - v = v.item() - assert isinstance(v, float | int) - self.meters[k].update(v) - - def __getattr__(self, attr): - if attr in self.meters: - return self.meters[attr] - if attr in self.__dict__: - return self.__dict__[attr] - raise AttributeError(f"'{type(self).__name__}' object has no attribute '{attr}'") - - def __str__(self) -> str: - loss_str = [] - for name, meter in self.meters.items(): - loss_str.append(f"{name}: {meter!s}") - return self.delimiter.join(loss_str) - - def synchronize_between_processes(self) -> None: - for meter in self.meters.values(): - meter.synchronize_between_processes() - - def add_meter(self, name: str, meter) -> None: - self.meters[name] = meter - - def log_every(self, iterable, print_freq, header=None): - i = 0 - if not header: - header = "" - start_time = time.time() - end = time.time() - iter_time = SmoothedValue(fmt="{avg:.4f}") - data_time = SmoothedValue(fmt="{avg:.4f}") - space_fmt = ":" + str(len(str(len(iterable)))) + "d" - if torch.cuda.is_available(): - log_msg = self.delimiter.join( - [ - header, - "[{0" + space_fmt + "}/{1}]", - "eta: {eta}", - "{meters}", - "time: {time}", - "data: {data}", - "max mem: {memory:.0f}", - ] - ) - else: - log_msg = self.delimiter.join( - [ - header, - "[{0" + space_fmt + "}/{1}]", - "eta: {eta}", - "{meters}", - "time: {time}", - "data: {data}", - ] - ) - MB = 1024.0 * 1024.0 - for obj in iterable: - data_time.update(time.time() - end) - yield obj - iter_time.update(time.time() - end) - if i % print_freq == 0 or i == len(iterable) - 1: - eta_seconds = iter_time.global_avg * (len(iterable) - i) - eta_string = str(datetime.timedelta(seconds=int(eta_seconds))) - if torch.cuda.is_available(): - print( - log_msg.format( - i, - len(iterable), - eta=eta_string, - meters=str(self), - time=str(iter_time), - data=str(data_time), - memory=torch.cuda.max_memory_allocated() / MB, - ) - ) - else: - print( - log_msg.format( - i, - len(iterable), - eta=eta_string, - meters=str(self), - time=str(iter_time), - data=str(data_time), - ) - ) - i += 1 - end = time.time() - total_time = time.time() - start_time - total_time_str = str(datetime.timedelta(seconds=int(total_time))) - print( - f"{header} Total time: {total_time_str} ({total_time / len(iterable):.4f} s / it)" - ) - - -def get_sha(): - cwd = os.path.dirname(os.path.abspath(__file__)) - - def _run(command): - return subprocess.check_output(command, cwd=cwd).decode("ascii").strip() - - sha = "N/A" - diff = "clean" - branch = "N/A" - try: - sha = _run(["git", "rev-parse", "HEAD"]) - subprocess.check_output(["git", "diff"], cwd=cwd) - diff = _run(["git", "diff-index", "HEAD"]) - diff = "has uncommited changes" if diff else "clean" - branch = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"]) - except Exception: - pass - message = f"sha: {sha}, status: {diff}, branch: {branch}" - return message - - -def collate_fn(batch): - batch = list(zip(*batch, strict=False)) - batch[0] = nested_tensor_from_tensor_list(batch[0]) - return tuple(batch) - - -def _max_by_axis(the_list): - # type: (List[List[int]]) -> List[int] - maxes = the_list[0] - for sublist in the_list[1:]: - for index, item in enumerate(sublist): - maxes[index] = max(maxes[index], item) - return maxes - - -def nested_tensor_from_tensor_list(tensor_list: list[Tensor]): - # TODO make this more general - if tensor_list[0].ndim == 3: - # TODO make it support different-sized images - max_size = _max_by_axis([list(img.shape) for img in tensor_list]) - # min_size = tuple(min(s) for s in zip(*[img.shape for img in tensor_list])) - batch_shape = [len(tensor_list), *max_size] - b, c, h, w = batch_shape - dtype = tensor_list[0].dtype - device = tensor_list[0].device - tensor = torch.zeros(batch_shape, dtype=dtype, device=device) - mask = torch.ones((b, h, w), dtype=torch.bool, device=device) - for img, pad_img, m in zip(tensor_list, tensor, mask, strict=False): - pad_img[: img.shape[0], : img.shape[1], : img.shape[2]].copy_(img) - m[: img.shape[1], : img.shape[2]] = False - else: - raise ValueError("not supported") - return NestedTensor(tensor, mask) - - -class NestedTensor: - def __init__(self, tensors, mask: Tensor | None) -> None: - self.tensors = tensors - self.mask = mask - - def to(self, device, non_blocking: bool=False): - # type: (Device) -> NestedTensor - cast_tensor = self.tensors.to(device, non_blocking=non_blocking) - mask = self.mask - if mask is not None: - assert mask is not None - cast_mask = mask.to(device, non_blocking=non_blocking) - else: - cast_mask = None - return NestedTensor(cast_tensor, cast_mask) - - def record_stream(self, *args, **kwargs) -> None: - self.tensors.record_stream(*args, **kwargs) - if self.mask is not None: - self.mask.record_stream(*args, **kwargs) - - def decompose(self): - return self.tensors, self.mask - - def __repr__(self) -> str: - return str(self.tensors) - - -def setup_for_distributed(is_master: bool) -> None: - """ - This function disables printing when not in master process - """ - import builtins as __builtin__ - - builtin_print = __builtin__.print - - def print(*args, **kwargs) -> None: - force = kwargs.pop("force", False) - if is_master or force: - builtin_print(*args, **kwargs) - - __builtin__.print = print - - -def is_dist_avail_and_initialized() -> bool: - if not dist.is_available(): - return False - if not dist.is_initialized(): - return False - return True - - -def get_world_size(): - if not is_dist_avail_and_initialized(): - return 1 - return dist.get_world_size() - - -def get_rank(): - if not is_dist_avail_and_initialized(): - return 0 - return dist.get_rank() - - -def get_local_size(): - if not is_dist_avail_and_initialized(): - return 1 - return int(os.environ["LOCAL_SIZE"]) - - -def get_local_rank(): - if not is_dist_avail_and_initialized(): - return 0 - return int(os.environ["LOCAL_RANK"]) - - -def is_main_process(): - return get_rank() == 0 - - -def save_on_master(*args, **kwargs) -> None: - if is_main_process(): - torch.save(*args, **kwargs) - - -def init_distributed_mode(args) -> None: - if "RANK" in os.environ and "WORLD_SIZE" in os.environ: - args.rank = int(os.environ["RANK"]) - args.world_size = int(os.environ["WORLD_SIZE"]) - args.gpu = int(os.environ["LOCAL_RANK"]) - args.dist_url = "env://" - os.environ["LOCAL_SIZE"] = str(torch.cuda.device_count()) - elif "SLURM_PROCID" in os.environ: - proc_id = int(os.environ["SLURM_PROCID"]) - ntasks = int(os.environ["SLURM_NTASKS"]) - node_list = os.environ["SLURM_NODELIST"] - num_gpus = torch.cuda.device_count() - addr = subprocess.getoutput(f"scontrol show hostname {node_list} | head -n1") - os.environ["MASTER_PORT"] = os.environ.get("MASTER_PORT", "29500") - os.environ["MASTER_ADDR"] = addr - os.environ["WORLD_SIZE"] = str(ntasks) - os.environ["RANK"] = str(proc_id) - os.environ["LOCAL_RANK"] = str(proc_id % num_gpus) - os.environ["LOCAL_SIZE"] = str(num_gpus) - args.dist_url = "env://" - args.world_size = ntasks - args.rank = proc_id - args.gpu = proc_id % num_gpus - else: - print("Not using distributed mode") - args.distributed = False - return - - args.distributed = True - - torch.cuda.set_device(args.gpu) - args.dist_backend = "nccl" - print(f"| distributed init (rank {args.rank}): {args.dist_url}", flush=True) - torch.distributed.init_process_group( - backend=args.dist_backend, - init_method=args.dist_url, - world_size=args.world_size, - rank=args.rank, - ) - torch.distributed.barrier() - setup_for_distributed(args.rank == 0) - - -@torch.no_grad() -def accuracy(output, target, topk=(1,)): - """Computes the precision@k for the specified values of k""" - if target.numel() == 0: - return [torch.zeros([], device=output.device)] - maxk = max(topk) - batch_size = target.size(0) - - _, pred = output.topk(maxk, 1, True, True) - pred = pred.t() - correct = pred.eq(target.view(1, -1).expand_as(pred)) - - res = [] - for k in topk: - correct_k = correct[:k].view(-1).float().sum(0) - res.append(correct_k.mul_(100.0 / batch_size)) - return res - - -def interpolate(input, size: Optional[int]=None, scale_factor=None, mode: str="nearest", align_corners=None): - # type: (Tensor, Optional[List[int]], Optional[float], str, Optional[bool]) -> Tensor - """ - Equivalent to nn.functional.interpolate, but with support for empty batch sizes. - This will eventually be supported natively by PyTorch, and this - class can go away. - """ - if float(torchvision.__version__[:3]) < 0.7: - if input.numel() > 0: - return torch.nn.functional.interpolate(input, size, scale_factor, mode, align_corners) - - output_shape = _output_size(2, input, size, scale_factor) - output_shape = list(input.shape[:-2]) + list(output_shape) - if float(torchvision.__version__[:3]) < 0.5: - return _NewEmptyTensorOp.apply(input, output_shape) - return _new_empty_tensor(input, output_shape) - else: - return torchvision.ops.misc.interpolate(input, size, scale_factor, mode, align_corners) - - -def get_total_grad_norm(parameters, norm_type: int=2): - parameters = list(filter(lambda p: p.grad is not None, parameters)) - norm_type = float(norm_type) - device = parameters[0].grad.device - total_norm = torch.norm( - torch.stack([torch.norm(p.grad.detach(), norm_type).to(device) for p in parameters]), - norm_type, - ) - return total_norm - - -def inverse_sigmoid(x, eps: float=1e-5): - x = x.clamp(min=0, max=1) - x1 = x.clamp(min=eps) - x2 = (1 - x).clamp(min=eps) - return torch.log(x1 / x2) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/util/plot_utils.py b/dimos/models/Detic/third_party/Deformable-DETR/util/plot_utils.py deleted file mode 100644 index 0af3b9e5e6..0000000000 --- a/dimos/models/Detic/third_party/Deformable-DETR/util/plot_utils.py +++ /dev/null @@ -1,120 +0,0 @@ -# ------------------------------------------------------------------------ -# Deformable DETR -# Copyright (c) 2020 SenseTime. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 [see LICENSE for details] -# ------------------------------------------------------------------------ -# Modified from DETR (https://github.com/facebookresearch/detr) -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# ------------------------------------------------------------------------ - -""" -Plotting utilities to visualize training logs. -""" - -from pathlib import Path, PurePath - -import matplotlib.pyplot as plt -import pandas as pd -import seaborn as sns -import torch - - -def plot_logs( - logs, fields=("class_error", "loss_bbox_unscaled", "mAP"), ewm_col: int=0, log_name: str="log.txt" -): - """ - Function to plot specific fields from training log(s). Plots both training and test results. - - :: Inputs - logs = list containing Path objects, each pointing to individual dir with a log file - - fields = which results to plot from each log file - plots both training and test for each field. - - ewm_col = optional, which column to use as the exponential weighted smoothing of the plots - - log_name = optional, name of log file if different than default 'log.txt'. - - :: Outputs - matplotlib plots of results in fields, color coded for each log file. - - solid lines are training results, dashed lines are test results. - - """ - func_name = "plot_utils.py::plot_logs" - - # verify logs is a list of Paths (list[Paths]) or single Pathlib object Path, - # convert single Path to list to avoid 'not iterable' error - - if not isinstance(logs, list): - if isinstance(logs, PurePath): - logs = [logs] - print(f"{func_name} info: logs param expects a list argument, converted to list[Path].") - else: - raise ValueError( - f"{func_name} - invalid argument for logs parameter.\n \ - Expect list[Path] or single Path obj, received {type(logs)}" - ) - - # verify valid dir(s) and that every item in list is Path object - for _i, dir in enumerate(logs): - if not isinstance(dir, PurePath): - raise ValueError( - f"{func_name} - non-Path object in logs argument of {type(dir)}: \n{dir}" - ) - if dir.exists(): - continue - raise ValueError(f"{func_name} - invalid directory in logs argument:\n{dir}") - - # load log file(s) and plot - dfs = [pd.read_json(Path(p) / log_name, lines=True) for p in logs] - - fig, axs = plt.subplots(ncols=len(fields), figsize=(16, 5)) - - for df, color in zip(dfs, sns.color_palette(n_colors=len(logs)), strict=False): - for j, field in enumerate(fields): - if field == "mAP": - coco_eval = ( - pd.DataFrame(pd.np.stack(df.test_coco_eval.dropna().values)[:, 1]) - .ewm(com=ewm_col) - .mean() - ) - axs[j].plot(coco_eval, c=color) - else: - df.interpolate().ewm(com=ewm_col).mean().plot( - y=[f"train_{field}", f"test_{field}"], - ax=axs[j], - color=[color] * 2, - style=["-", "--"], - ) - for ax, field in zip(axs, fields, strict=False): - ax.legend([Path(p).name for p in logs]) - ax.set_title(field) - - -def plot_precision_recall(files, naming_scheme: str="iter"): - if naming_scheme == "exp_id": - # name becomes exp_id - names = [f.parts[-3] for f in files] - elif naming_scheme == "iter": - names = [f.stem for f in files] - else: - raise ValueError(f"not supported {naming_scheme}") - fig, axs = plt.subplots(ncols=2, figsize=(16, 5)) - for f, color, name in zip(files, sns.color_palette("Blues", n_colors=len(files)), names, strict=False): - data = torch.load(f) - # precision is n_iou, n_points, n_cat, n_area, max_det - precision = data["precision"] - recall = data["params"].recThrs - scores = data["scores"] - # take precision for all classes, all areas and 100 detections - precision = precision[0, :, :, 0, -1].mean(1) - scores = scores[0, :, :, 0, -1].mean(1) - prec = precision.mean() - rec = data["recall"][0, :, 0, -1].mean() - print( - f"{naming_scheme} {name}: mAP@50={prec * 100: 05.1f}, " - + f"score={scores.mean():0.3f}, " - + f"f1={2 * prec * rec / (prec + rec + 1e-8):0.3f}" - ) - axs[0].plot(recall, precision, c=color) - axs[1].plot(recall, scores, c=color) - - axs[0].set_title("Precision / Recall") - axs[0].legend(names) - axs[1].set_title("Scores / Recall") - axs[1].legend(names) - return fig, axs diff --git a/dimos/models/Detic/tools/convert-thirdparty-pretrained-model-to-d2.py b/dimos/models/Detic/tools/convert-thirdparty-pretrained-model-to-d2.py deleted file mode 100644 index 567e71f7c4..0000000000 --- a/dimos/models/Detic/tools/convert-thirdparty-pretrained-model-to-d2.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -import argparse -import pickle - -import torch - -""" -Usage: - -cd DETIC_ROOT/models/ -wget https://miil-public-eu.oss-eu-central-1.aliyuncs.com/model-zoo/ImageNet_21K_P/models/resnet50_miil_21k.pth -python ../tools/convert-thirdparty-pretrained-model-to-d2.py --path resnet50_miil_21k.pth - -wget https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_base_patch4_window7_224_22k.pth -python ../tools/convert-thirdparty-pretrained-model-to-d2.py --path swin_base_patch4_window7_224_22k.pth - -""" - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--path", default="") - args = parser.parse_args() - - print("Loading", args.path) - model = torch.load(args.path, map_location="cpu") - # import pdb; pdb.set_trace() - if "model" in model: - model = model["model"] - if "state_dict" in model: - model = model["state_dict"] - ret = {"model": model, "__author__": "third_party", "matching_heuristics": True} - out_path = args.path.replace(".pth", ".pkl") - print("Saving to", out_path) - pickle.dump(ret, open(out_path, "wb")) diff --git a/dimos/models/Detic/tools/create_imagenetlvis_json.py b/dimos/models/Detic/tools/create_imagenetlvis_json.py deleted file mode 100644 index 4f53874421..0000000000 --- a/dimos/models/Detic/tools/create_imagenetlvis_json.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import json -import os - -from detectron2.data.detection_utils import read_image -from nltk.corpus import wordnet - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--imagenet_path", default="datasets/imagenet/ImageNet-LVIS") - parser.add_argument("--lvis_meta_path", default="datasets/lvis/lvis_v1_val.json") - parser.add_argument( - "--out_path", default="datasets/imagenet/annotations/imagenet_lvis_image_info.json" - ) - args = parser.parse_args() - - print("Loading LVIS meta") - data = json.load(open(args.lvis_meta_path)) - print("Done") - synset2cat = {x["synset"]: x for x in data["categories"]} - count = 0 - images = [] - image_counts = {} - folders = sorted(os.listdir(args.imagenet_path)) - for i, folder in enumerate(folders): - class_path = args.imagenet_path + folder - files = sorted(os.listdir(class_path)) - synset = wordnet.synset_from_pos_and_offset("n", int(folder[1:])).name() - cat = synset2cat[synset] - cat_id = cat["id"] - cat_name = cat["name"] - cat_images = [] - for file in files: - count = count + 1 - file_name = f"{folder}/{file}" - # img = cv2.imread('{}/{}'.format(args.imagenet_path, file_name)) - img = read_image(f"{args.imagenet_path}/{file_name}") - h, w = img.shape[:2] - image = { - "id": count, - "file_name": file_name, - "pos_category_ids": [cat_id], - "width": w, - "height": h, - } - cat_images.append(image) - images.extend(cat_images) - image_counts[cat_id] = len(cat_images) - print(i, cat_name, len(cat_images)) - print("# Images", len(images)) - for x in data["categories"]: - x["image_count"] = image_counts[x["id"]] if x["id"] in image_counts else 0 - out = {"categories": data["categories"], "images": images, "annotations": []} - print("Writing to", args.out_path) - json.dump(out, open(args.out_path, "w")) diff --git a/dimos/models/Detic/tools/create_lvis_21k.py b/dimos/models/Detic/tools/create_lvis_21k.py deleted file mode 100644 index a1f24446ac..0000000000 --- a/dimos/models/Detic/tools/create_lvis_21k.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import copy -import json - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--imagenet_path", default="datasets/imagenet/annotations/imagenet-21k_image_info.json" - ) - parser.add_argument("--lvis_path", default="datasets/lvis/lvis_v1_train.json") - parser.add_argument("--save_categories", default="") - parser.add_argument("--not_save_imagenet", action="store_true") - parser.add_argument("--not_save_lvis", action="store_true") - parser.add_argument("--mark", default="lvis-21k") - args = parser.parse_args() - - print("Loading", args.imagenet_path) - in_data = json.load(open(args.imagenet_path)) - print("Loading", args.lvis_path) - lvis_data = json.load(open(args.lvis_path)) - - categories = copy.deepcopy(lvis_data["categories"]) - cat_count = max(x["id"] for x in categories) - synset2id = {x["synset"]: x["id"] for x in categories} - name2id = {x["name"]: x["id"] for x in categories} - in_id_map = {} - for x in in_data["categories"]: - if x["synset"] in synset2id: - in_id_map[x["id"]] = synset2id[x["synset"]] - elif x["name"] in name2id: - in_id_map[x["id"]] = name2id[x["name"]] - x["id"] = name2id[x["name"]] - else: - cat_count = cat_count + 1 - name2id[x["name"]] = cat_count - in_id_map[x["id"]] = cat_count - x["id"] = cat_count - categories.append(x) - - print("lvis cats", len(lvis_data["categories"])) - print("imagenet cats", len(in_data["categories"])) - print("merge cats", len(categories)) - - filtered_images = [] - for x in in_data["images"]: - x["pos_category_ids"] = [in_id_map[xx] for xx in x["pos_category_ids"]] - x["pos_category_ids"] = [xx for xx in sorted(set(x["pos_category_ids"])) if xx >= 0] - if len(x["pos_category_ids"]) > 0: - filtered_images.append(x) - - in_data["categories"] = categories - lvis_data["categories"] = categories - - if not args.not_save_imagenet: - in_out_path = args.imagenet_path[:-5] + f"_{args.mark}.json" - for k, v in in_data.items(): - print("imagenet", k, len(v)) - print("Saving Imagenet to", in_out_path) - json.dump(in_data, open(in_out_path, "w")) - - if not args.not_save_lvis: - lvis_out_path = args.lvis_path[:-5] + f"_{args.mark}.json" - for k, v in lvis_data.items(): - print("lvis", k, len(v)) - print("Saving LVIS to", lvis_out_path) - json.dump(lvis_data, open(lvis_out_path, "w")) - - if args.save_categories != "": - for x in categories: - for k in ["image_count", "instance_count", "synonyms", "def"]: - if k in x: - del x[k] - CATEGORIES = repr(categories) + " # noqa" - open(args.save_categories, "w").write(f"CATEGORIES = {CATEGORIES}") diff --git a/dimos/models/Detic/tools/download_cc.py b/dimos/models/Detic/tools/download_cc.py deleted file mode 100644 index ef7b4b0f7d..0000000000 --- a/dimos/models/Detic/tools/download_cc.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import json -import os - -import numpy as np -from PIL import Image - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--ann", default="datasets/cc3m/Train_GCC-training.tsv") - parser.add_argument("--save_image_path", default="datasets/cc3m/training/") - parser.add_argument("--cat_info", default="datasets/lvis/lvis_v1_val.json") - parser.add_argument("--out_path", default="datasets/cc3m/train_image_info.json") - parser.add_argument("--not_download_image", action="store_true") - args = parser.parse_args() - categories = json.load(open(args.cat_info))["categories"] - images = [] - if not os.path.exists(args.save_image_path): - os.makedirs(args.save_image_path) - f = open(args.ann) - for i, line in enumerate(f): - cap, path = line[:-1].split("\t") - print(i, cap, path) - if not args.not_download_image: - os.system(f"wget {path} -O {args.save_image_path}/{i + 1}.jpg") - try: - img = Image.open(open(f"{args.save_image_path}/{i + 1}.jpg", "rb")) - img = np.asarray(img.convert("RGB")) - h, w = img.shape[:2] - except: - continue - image_info = { - "id": i + 1, - "file_name": f"{i + 1}.jpg", - "height": h, - "width": w, - "captions": [cap], - } - images.append(image_info) - data = {"categories": categories, "images": images, "annotations": []} - for k, v in data.items(): - print(k, len(v)) - print("Saving to", args.out_path) - json.dump(data, open(args.out_path, "w")) diff --git a/dimos/models/Detic/tools/dump_clip_features.py b/dimos/models/Detic/tools/dump_clip_features.py deleted file mode 100644 index 31be161f6d..0000000000 --- a/dimos/models/Detic/tools/dump_clip_features.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import itertools -import json - -from nltk.corpus import wordnet -import numpy as np -import torch - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--ann", default="datasets/lvis/lvis_v1_val.json") - parser.add_argument("--out_path", default="") - parser.add_argument("--prompt", default="a") - parser.add_argument("--model", default="clip") - parser.add_argument("--clip_model", default="ViT-B/32") - parser.add_argument("--fix_space", action="store_true") - parser.add_argument("--use_underscore", action="store_true") - parser.add_argument("--avg_synonyms", action="store_true") - parser.add_argument("--use_wn_name", action="store_true") - args = parser.parse_args() - - print("Loading", args.ann) - data = json.load(open(args.ann)) - cat_names = [x["name"] for x in sorted(data["categories"], key=lambda x: x["id"])] - if "synonyms" in data["categories"][0]: - if args.use_wn_name: - synonyms = [ - [xx.name() for xx in wordnet.synset(x["synset"]).lemmas()] - if x["synset"] != "stop_sign.n.01" - else ["stop_sign"] - for x in sorted(data["categories"], key=lambda x: x["id"]) - ] - else: - synonyms = [x["synonyms"] for x in sorted(data["categories"], key=lambda x: x["id"])] - else: - synonyms = [] - if args.fix_space: - cat_names = [x.replace("_", " ") for x in cat_names] - if args.use_underscore: - cat_names = [x.strip().replace("/ ", "/").replace(" ", "_") for x in cat_names] - print("cat_names", cat_names) - device = "cuda" if torch.cuda.is_available() else "cpu" - - if args.prompt == "a": - sentences = ["a " + x for x in cat_names] - sentences_synonyms = [["a " + xx for xx in x] for x in synonyms] - if args.prompt == "none": - sentences = [x for x in cat_names] - sentences_synonyms = [[xx for xx in x] for x in synonyms] - elif args.prompt == "photo": - sentences = [f"a photo of a {x}" for x in cat_names] - sentences_synonyms = [[f"a photo of a {xx}" for xx in x] for x in synonyms] - elif args.prompt == "scene": - sentences = [f"a photo of a {x} in the scene" for x in cat_names] - sentences_synonyms = [ - [f"a photo of a {xx} in the scene" for xx in x] for x in synonyms - ] - - print("sentences_synonyms", len(sentences_synonyms), sum(len(x) for x in sentences_synonyms)) - if args.model == "clip": - import clip - - print("Loading CLIP") - model, preprocess = clip.load(args.clip_model, device=device) - if args.avg_synonyms: - sentences = list(itertools.chain.from_iterable(sentences_synonyms)) - print("flattened_sentences", len(sentences)) - text = clip.tokenize(sentences).to(device) - with torch.no_grad(): - if len(text) > 10000: - text_features = torch.cat( - [ - model.encode_text(text[: len(text) // 2]), - model.encode_text(text[len(text) // 2 :]), - ], - dim=0, - ) - else: - text_features = model.encode_text(text) - print("text_features.shape", text_features.shape) - if args.avg_synonyms: - synonyms_per_cat = [len(x) for x in sentences_synonyms] - text_features = text_features.split(synonyms_per_cat, dim=0) - text_features = [x.mean(dim=0) for x in text_features] - text_features = torch.stack(text_features, dim=0) - print("after stack", text_features.shape) - text_features = text_features.cpu().numpy() - elif args.model in ["bert", "roberta"]: - from transformers import AutoModel, AutoTokenizer - - if args.model == "bert": - model_name = "bert-large-uncased" - if args.model == "roberta": - model_name = "roberta-large" - tokenizer = AutoTokenizer.from_pretrained(model_name) - model = AutoModel.from_pretrained(model_name) - model.eval() - if args.avg_synonyms: - sentences = list(itertools.chain.from_iterable(sentences_synonyms)) - print("flattened_sentences", len(sentences)) - inputs = tokenizer(sentences, padding=True, return_tensors="pt") - with torch.no_grad(): - model_outputs = model(**inputs) - outputs = model_outputs.pooler_output - text_features = outputs.detach().cpu() - if args.avg_synonyms: - synonyms_per_cat = [len(x) for x in sentences_synonyms] - text_features = text_features.split(synonyms_per_cat, dim=0) - text_features = [x.mean(dim=0) for x in text_features] - text_features = torch.stack(text_features, dim=0) - print("after stack", text_features.shape) - text_features = text_features.numpy() - print("text_features.shape", text_features.shape) - else: - assert 0, args.model - if args.out_path != "": - print("saveing to", args.out_path) - np.save(open(args.out_path, "wb"), text_features) - import pdb - - pdb.set_trace() diff --git a/dimos/models/Detic/tools/fix_o365_names.py b/dimos/models/Detic/tools/fix_o365_names.py deleted file mode 100644 index 5aee27a14f..0000000000 --- a/dimos/models/Detic/tools/fix_o365_names.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import copy -import json - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--ann", default="datasets/objects365/annotations/zhiyuan_objv2_val.json") - parser.add_argument("--fix_name_map", default="datasets/metadata/Objects365_names_fix.csv") - args = parser.parse_args() - - new_names = {} - old_names = {} - with open(args.fix_name_map) as f: - for line in f: - tmp = line.strip().split(",") - old_names[int(tmp[0])] = tmp[1] - new_names[int(tmp[0])] = tmp[2] - data = json.load(open(args.ann)) - - cat_info = copy.deepcopy(data["categories"]) - - for x in cat_info: - if old_names[x["id"]].strip() != x["name"].strip(): - print("{} {} {}".format(x, old_names[x["id"]], new_names[x["id"]])) - import pdb - - pdb.set_trace() - if old_names[x["id"]] != new_names[x["id"]]: - print("Renaming", x["id"], x["name"], new_names[x["id"]]) - x["name"] = new_names[x["id"]] - - data["categories"] = cat_info - out_name = args.ann[:-5] + "_fixname.json" - print("Saving to", out_name) - json.dump(data, open(out_name, "w")) diff --git a/dimos/models/Detic/tools/fix_o365_path.py b/dimos/models/Detic/tools/fix_o365_path.py deleted file mode 100644 index c43358fff0..0000000000 --- a/dimos/models/Detic/tools/fix_o365_path.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import json -import os - -import path - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--ann", default="datasets/objects365/annotations/zhiyuan_objv2_train_fixname.json" - ) - parser.add_argument("--img_dir", default="datasets/objects365/train/") - args = parser.parse_args() - - print("Loading", args.ann) - data = json.load(open(args.ann)) - images = [] - count = 0 - for x in data["images"]: - path = "{}/{}".format(args.img_dir, x["file_name"]) - if os.path.exists(path): - images.append(x) - else: - print(path) - count = count + 1 - print("Missing", count, "images") - data["images"] = images - out_name = args.ann[:-5] + "_fixmiss.json" - print("Saving to", out_name) - json.dump(data, open(out_name, "w")) diff --git a/dimos/models/Detic/tools/get_cc_tags.py b/dimos/models/Detic/tools/get_cc_tags.py deleted file mode 100644 index 0a5cdab8ec..0000000000 --- a/dimos/models/Detic/tools/get_cc_tags.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -from collections import defaultdict -import json - -from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - -# This mapping is extracted from the official LVIS mapping: -# https://github.com/lvis-dataset/lvis-api/blob/master/data/coco_to_synset.json -COCO_SYNSET_CATEGORIES = [ - {"synset": "person.n.01", "coco_cat_id": 1}, - {"synset": "bicycle.n.01", "coco_cat_id": 2}, - {"synset": "car.n.01", "coco_cat_id": 3}, - {"synset": "motorcycle.n.01", "coco_cat_id": 4}, - {"synset": "airplane.n.01", "coco_cat_id": 5}, - {"synset": "bus.n.01", "coco_cat_id": 6}, - {"synset": "train.n.01", "coco_cat_id": 7}, - {"synset": "truck.n.01", "coco_cat_id": 8}, - {"synset": "boat.n.01", "coco_cat_id": 9}, - {"synset": "traffic_light.n.01", "coco_cat_id": 10}, - {"synset": "fireplug.n.01", "coco_cat_id": 11}, - {"synset": "stop_sign.n.01", "coco_cat_id": 13}, - {"synset": "parking_meter.n.01", "coco_cat_id": 14}, - {"synset": "bench.n.01", "coco_cat_id": 15}, - {"synset": "bird.n.01", "coco_cat_id": 16}, - {"synset": "cat.n.01", "coco_cat_id": 17}, - {"synset": "dog.n.01", "coco_cat_id": 18}, - {"synset": "horse.n.01", "coco_cat_id": 19}, - {"synset": "sheep.n.01", "coco_cat_id": 20}, - {"synset": "beef.n.01", "coco_cat_id": 21}, - {"synset": "elephant.n.01", "coco_cat_id": 22}, - {"synset": "bear.n.01", "coco_cat_id": 23}, - {"synset": "zebra.n.01", "coco_cat_id": 24}, - {"synset": "giraffe.n.01", "coco_cat_id": 25}, - {"synset": "backpack.n.01", "coco_cat_id": 27}, - {"synset": "umbrella.n.01", "coco_cat_id": 28}, - {"synset": "bag.n.04", "coco_cat_id": 31}, - {"synset": "necktie.n.01", "coco_cat_id": 32}, - {"synset": "bag.n.06", "coco_cat_id": 33}, - {"synset": "frisbee.n.01", "coco_cat_id": 34}, - {"synset": "ski.n.01", "coco_cat_id": 35}, - {"synset": "snowboard.n.01", "coco_cat_id": 36}, - {"synset": "ball.n.06", "coco_cat_id": 37}, - {"synset": "kite.n.03", "coco_cat_id": 38}, - {"synset": "baseball_bat.n.01", "coco_cat_id": 39}, - {"synset": "baseball_glove.n.01", "coco_cat_id": 40}, - {"synset": "skateboard.n.01", "coco_cat_id": 41}, - {"synset": "surfboard.n.01", "coco_cat_id": 42}, - {"synset": "tennis_racket.n.01", "coco_cat_id": 43}, - {"synset": "bottle.n.01", "coco_cat_id": 44}, - {"synset": "wineglass.n.01", "coco_cat_id": 46}, - {"synset": "cup.n.01", "coco_cat_id": 47}, - {"synset": "fork.n.01", "coco_cat_id": 48}, - {"synset": "knife.n.01", "coco_cat_id": 49}, - {"synset": "spoon.n.01", "coco_cat_id": 50}, - {"synset": "bowl.n.03", "coco_cat_id": 51}, - {"synset": "banana.n.02", "coco_cat_id": 52}, - {"synset": "apple.n.01", "coco_cat_id": 53}, - {"synset": "sandwich.n.01", "coco_cat_id": 54}, - {"synset": "orange.n.01", "coco_cat_id": 55}, - {"synset": "broccoli.n.01", "coco_cat_id": 56}, - {"synset": "carrot.n.01", "coco_cat_id": 57}, - # {"synset": "frank.n.02", "coco_cat_id": 58}, - {"synset": "sausage.n.01", "coco_cat_id": 58}, - {"synset": "pizza.n.01", "coco_cat_id": 59}, - {"synset": "doughnut.n.02", "coco_cat_id": 60}, - {"synset": "cake.n.03", "coco_cat_id": 61}, - {"synset": "chair.n.01", "coco_cat_id": 62}, - {"synset": "sofa.n.01", "coco_cat_id": 63}, - {"synset": "pot.n.04", "coco_cat_id": 64}, - {"synset": "bed.n.01", "coco_cat_id": 65}, - {"synset": "dining_table.n.01", "coco_cat_id": 67}, - {"synset": "toilet.n.02", "coco_cat_id": 70}, - {"synset": "television_receiver.n.01", "coco_cat_id": 72}, - {"synset": "laptop.n.01", "coco_cat_id": 73}, - {"synset": "mouse.n.04", "coco_cat_id": 74}, - {"synset": "remote_control.n.01", "coco_cat_id": 75}, - {"synset": "computer_keyboard.n.01", "coco_cat_id": 76}, - {"synset": "cellular_telephone.n.01", "coco_cat_id": 77}, - {"synset": "microwave.n.02", "coco_cat_id": 78}, - {"synset": "oven.n.01", "coco_cat_id": 79}, - {"synset": "toaster.n.02", "coco_cat_id": 80}, - {"synset": "sink.n.01", "coco_cat_id": 81}, - {"synset": "electric_refrigerator.n.01", "coco_cat_id": 82}, - {"synset": "book.n.01", "coco_cat_id": 84}, - {"synset": "clock.n.01", "coco_cat_id": 85}, - {"synset": "vase.n.01", "coco_cat_id": 86}, - {"synset": "scissors.n.01", "coco_cat_id": 87}, - {"synset": "teddy.n.01", "coco_cat_id": 88}, - {"synset": "hand_blower.n.01", "coco_cat_id": 89}, - {"synset": "toothbrush.n.01", "coco_cat_id": 90}, -] - - -def map_name(x): - x = x.replace("_", " ") - if "(" in x: - x = x[: x.find("(")] - return x.lower().strip() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--cc_ann", default="datasets/cc3m/train_image_info.json") - parser.add_argument("--out_path", default="datasets/cc3m/train_image_info_tags.json") - parser.add_argument("--keep_images", action="store_true") - parser.add_argument("--allcaps", action="store_true") - parser.add_argument("--cat_path", default="") - parser.add_argument("--convert_caption", action="store_true") - # parser.add_argument('--lvis_ann', default='datasets/lvis/lvis_v1_val.json') - args = parser.parse_args() - - # lvis_data = json.load(open(args.lvis_ann, 'r')) - cc_data = json.load(open(args.cc_ann)) - if args.convert_caption: - num_caps = 0 - caps = defaultdict(list) - for x in cc_data["annotations"]: - caps[x["image_id"]].append(x["caption"]) - for x in cc_data["images"]: - x["captions"] = caps[x["id"]] - num_caps += len(x["captions"]) - print("# captions", num_caps) - - if args.cat_path != "": - print("Loading", args.cat_path) - cats = json.load(open(args.cat_path))["categories"] - if "synonyms" not in cats[0]: - cocoid2synset = {x["coco_cat_id"]: x["synset"] for x in COCO_SYNSET_CATEGORIES} - synset2synonyms = {x["synset"]: x["synonyms"] for x in LVIS_CATEGORIES} - for x in cats: - synonyms = synset2synonyms[cocoid2synset[x["id"]]] - x["synonyms"] = synonyms - x["frequency"] = "f" - cc_data["categories"] = cats - - id2cat = {x["id"]: x for x in cc_data["categories"]} - class_count = {x["id"]: 0 for x in cc_data["categories"]} - class_data = { - x["id"]: [" " + map_name(xx) + " " for xx in x["synonyms"]] for x in cc_data["categories"] - } - num_examples = 5 - examples = {x["id"]: [] for x in cc_data["categories"]} - - print("class_data", class_data) - - images = [] - for i, x in enumerate(cc_data["images"]): - if i % 10000 == 0: - print(i, len(cc_data["images"])) - if args.allcaps: - caption = (" ".join(x["captions"])).lower() - else: - caption = x["captions"][0].lower() - x["pos_category_ids"] = [] - for cat_id, cat_names in class_data.items(): - find = False - for c in cat_names: - if c in caption or caption.startswith(c[1:]) or caption.endswith(c[:-1]): - find = True - break - if find: - x["pos_category_ids"].append(cat_id) - class_count[cat_id] += 1 - if len(examples[cat_id]) < num_examples: - examples[cat_id].append(caption) - if len(x["pos_category_ids"]) > 0 or args.keep_images: - images.append(x) - - zero_class = [] - for cat_id, count in class_count.items(): - print(id2cat[cat_id]["name"], count, end=", ") - if count == 0: - zero_class.append(id2cat[cat_id]) - print("==") - print("zero class", zero_class) - - # for freq in ['r', 'c', 'f']: - # print('#cats', freq, len([x for x in cc_data['categories'] \ - # if x['frequency'] == freq] and class_count[x['id']] > 0)) - - for freq in ["r", "c", "f"]: - print( - "#Images", - freq, - sum([v for k, v in class_count.items() if id2cat[k]["frequency"] == freq]), - ) - - try: - out_data = {"images": images, "categories": cc_data["categories"], "annotations": []} - for k, v in out_data.items(): - print(k, len(v)) - if args.keep_images and not args.out_path.endswith("_full.json"): - args.out_path = args.out_path[:-5] + "_full.json" - print("Writing to", args.out_path) - json.dump(out_data, open(args.out_path, "w")) - except: - pass diff --git a/dimos/models/Detic/tools/get_coco_zeroshot_oriorder.py b/dimos/models/Detic/tools/get_coco_zeroshot_oriorder.py deleted file mode 100644 index 688b0a92e5..0000000000 --- a/dimos/models/Detic/tools/get_coco_zeroshot_oriorder.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import json - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--data_path", default="datasets/coco/annotations/instances_val2017_unseen_2.json" - ) - parser.add_argument("--cat_path", default="datasets/coco/annotations/instances_val2017.json") - args = parser.parse_args() - print("Loading", args.cat_path) - cat = json.load(open(args.cat_path))["categories"] - - print("Loading", args.data_path) - data = json.load(open(args.data_path)) - data["categories"] = cat - out_path = args.data_path[:-5] + "_oriorder.json" - print("Saving to", out_path) - json.dump(data, open(out_path, "w")) diff --git a/dimos/models/Detic/tools/get_imagenet_21k_full_tar_json.py b/dimos/models/Detic/tools/get_imagenet_21k_full_tar_json.py deleted file mode 100644 index 00502db11f..0000000000 --- a/dimos/models/Detic/tools/get_imagenet_21k_full_tar_json.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import json -import operator -import sys -import time - -from nltk.corpus import wordnet -import numpy as np -import torch -from tqdm import tqdm - -sys.path.insert(0, "third_party/CenterNet2/") -sys.path.insert(0, "third_party/Deformable-DETR") -from detic.data.tar_dataset import DiskTarDataset - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--imagenet_dir", default="datasets/imagenet/ImageNet-21k/") - parser.add_argument("--tarfile_path", default="datasets/imagenet/metadata-22k/tar_files.npy") - parser.add_argument("--tar_index_dir", default="datasets/imagenet/metadata-22k/tarindex_npy") - parser.add_argument( - "--out_path", default="datasets/imagenet/annotations/imagenet-22k_image_info.json" - ) - parser.add_argument("--workers", default=16, type=int) - args = parser.parse_args() - - start_time = time.time() - print("Building dataset") - dataset = DiskTarDataset(args.tarfile_path, args.tar_index_dir) - end_time = time.time() - print(f"Took {end_time - start_time} seconds to make the dataset.") - print(f"Have {len(dataset)} samples.") - print("dataset", dataset) - - tar_files = np.load(args.tarfile_path) - categories = [] - for i, tar_file in enumerate(tar_files): - wnid = tar_file[-13:-4] - synset = wordnet.synset_from_pos_and_offset("n", int(wnid[1:])) - synonyms = [x.name() for x in synset.lemmas()] - category = { - "id": i + 1, - "synset": synset.name(), - "name": synonyms[0], - "def": synset.definition(), - "synonyms": synonyms, - } - categories.append(category) - print("categories", len(categories)) - - data_loader = torch.utils.data.DataLoader( - dataset, - batch_size=1, - shuffle=False, - num_workers=args.workers, - collate_fn=operator.itemgetter(0), - ) - images = [] - for img, label, index in tqdm(data_loader): - if label == -1: - continue - image = { - "id": int(index) + 1, - "pos_category_ids": [int(label) + 1], - "height": int(img.height), - "width": int(img.width), - "tar_index": int(index), - } - images.append(image) - - data = {"categories": categories, "images": images, "annotations": []} - try: - for k, v in data.items(): - print(k, len(v)) - print("Saving to ", args.out_path) - json.dump(data, open(args.out_path, "w")) - except: - pass - import pdb - - pdb.set_trace() diff --git a/dimos/models/Detic/tools/get_lvis_cat_info.py b/dimos/models/Detic/tools/get_lvis_cat_info.py deleted file mode 100644 index 414a615b8a..0000000000 --- a/dimos/models/Detic/tools/get_lvis_cat_info.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import json - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--ann", default="datasets/lvis/lvis_v1_train.json") - parser.add_argument("--add_freq", action="store_true") - parser.add_argument("--r_thresh", type=int, default=10) - parser.add_argument("--c_thresh", type=int, default=100) - args = parser.parse_args() - - print("Loading", args.ann) - data = json.load(open(args.ann)) - cats = data["categories"] - image_count = {x["id"]: set() for x in cats} - ann_count = {x["id"]: 0 for x in cats} - for x in data["annotations"]: - image_count[x["category_id"]].add(x["image_id"]) - ann_count[x["category_id"]] += 1 - num_freqs = {x: 0 for x in ["r", "f", "c"]} - for x in cats: - x["image_count"] = len(image_count[x["id"]]) - x["instance_count"] = ann_count[x["id"]] - if args.add_freq: - freq = "f" - if x["image_count"] < args.c_thresh: - freq = "c" - if x["image_count"] < args.r_thresh: - freq = "r" - x["frequency"] = freq - num_freqs[freq] += 1 - print(cats) - image_counts = sorted([x["image_count"] for x in cats]) - # print('image count', image_counts) - # import pdb; pdb.set_trace() - if args.add_freq: - for x in ["r", "c", "f"]: - print(x, num_freqs[x]) - out = cats # {'categories': cats} - out_path = args.ann[:-5] + "_cat_info.json" - print("Saving to", out_path) - json.dump(out, open(out_path, "w")) diff --git a/dimos/models/Detic/tools/merge_lvis_coco.py b/dimos/models/Detic/tools/merge_lvis_coco.py deleted file mode 100644 index 1a76a02f0b..0000000000 --- a/dimos/models/Detic/tools/merge_lvis_coco.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from collections import defaultdict -import json - -from detectron2.structures import Boxes, pairwise_iou -import torch - -COCO_PATH = "datasets/coco/annotations/instances_train2017.json" -IMG_PATH = "datasets/coco/train2017/" -LVIS_PATH = "datasets/lvis/lvis_v1_train.json" -NO_SEG = False -if NO_SEG: - SAVE_PATH = "datasets/lvis/lvis_v1_train+coco_box.json" -else: - SAVE_PATH = "datasets/lvis/lvis_v1_train+coco_mask.json" -THRESH = 0.7 -DEBUG = False - -# This mapping is extracted from the official LVIS mapping: -# https://github.com/lvis-dataset/lvis-api/blob/master/data/coco_to_synset.json -COCO_SYNSET_CATEGORIES = [ - {"synset": "person.n.01", "coco_cat_id": 1}, - {"synset": "bicycle.n.01", "coco_cat_id": 2}, - {"synset": "car.n.01", "coco_cat_id": 3}, - {"synset": "motorcycle.n.01", "coco_cat_id": 4}, - {"synset": "airplane.n.01", "coco_cat_id": 5}, - {"synset": "bus.n.01", "coco_cat_id": 6}, - {"synset": "train.n.01", "coco_cat_id": 7}, - {"synset": "truck.n.01", "coco_cat_id": 8}, - {"synset": "boat.n.01", "coco_cat_id": 9}, - {"synset": "traffic_light.n.01", "coco_cat_id": 10}, - {"synset": "fireplug.n.01", "coco_cat_id": 11}, - {"synset": "stop_sign.n.01", "coco_cat_id": 13}, - {"synset": "parking_meter.n.01", "coco_cat_id": 14}, - {"synset": "bench.n.01", "coco_cat_id": 15}, - {"synset": "bird.n.01", "coco_cat_id": 16}, - {"synset": "cat.n.01", "coco_cat_id": 17}, - {"synset": "dog.n.01", "coco_cat_id": 18}, - {"synset": "horse.n.01", "coco_cat_id": 19}, - {"synset": "sheep.n.01", "coco_cat_id": 20}, - {"synset": "beef.n.01", "coco_cat_id": 21}, - {"synset": "elephant.n.01", "coco_cat_id": 22}, - {"synset": "bear.n.01", "coco_cat_id": 23}, - {"synset": "zebra.n.01", "coco_cat_id": 24}, - {"synset": "giraffe.n.01", "coco_cat_id": 25}, - {"synset": "backpack.n.01", "coco_cat_id": 27}, - {"synset": "umbrella.n.01", "coco_cat_id": 28}, - {"synset": "bag.n.04", "coco_cat_id": 31}, - {"synset": "necktie.n.01", "coco_cat_id": 32}, - {"synset": "bag.n.06", "coco_cat_id": 33}, - {"synset": "frisbee.n.01", "coco_cat_id": 34}, - {"synset": "ski.n.01", "coco_cat_id": 35}, - {"synset": "snowboard.n.01", "coco_cat_id": 36}, - {"synset": "ball.n.06", "coco_cat_id": 37}, - {"synset": "kite.n.03", "coco_cat_id": 38}, - {"synset": "baseball_bat.n.01", "coco_cat_id": 39}, - {"synset": "baseball_glove.n.01", "coco_cat_id": 40}, - {"synset": "skateboard.n.01", "coco_cat_id": 41}, - {"synset": "surfboard.n.01", "coco_cat_id": 42}, - {"synset": "tennis_racket.n.01", "coco_cat_id": 43}, - {"synset": "bottle.n.01", "coco_cat_id": 44}, - {"synset": "wineglass.n.01", "coco_cat_id": 46}, - {"synset": "cup.n.01", "coco_cat_id": 47}, - {"synset": "fork.n.01", "coco_cat_id": 48}, - {"synset": "knife.n.01", "coco_cat_id": 49}, - {"synset": "spoon.n.01", "coco_cat_id": 50}, - {"synset": "bowl.n.03", "coco_cat_id": 51}, - {"synset": "banana.n.02", "coco_cat_id": 52}, - {"synset": "apple.n.01", "coco_cat_id": 53}, - {"synset": "sandwich.n.01", "coco_cat_id": 54}, - {"synset": "orange.n.01", "coco_cat_id": 55}, - {"synset": "broccoli.n.01", "coco_cat_id": 56}, - {"synset": "carrot.n.01", "coco_cat_id": 57}, - # {"synset": "frank.n.02", "coco_cat_id": 58}, - {"synset": "sausage.n.01", "coco_cat_id": 58}, - {"synset": "pizza.n.01", "coco_cat_id": 59}, - {"synset": "doughnut.n.02", "coco_cat_id": 60}, - {"synset": "cake.n.03", "coco_cat_id": 61}, - {"synset": "chair.n.01", "coco_cat_id": 62}, - {"synset": "sofa.n.01", "coco_cat_id": 63}, - {"synset": "pot.n.04", "coco_cat_id": 64}, - {"synset": "bed.n.01", "coco_cat_id": 65}, - {"synset": "dining_table.n.01", "coco_cat_id": 67}, - {"synset": "toilet.n.02", "coco_cat_id": 70}, - {"synset": "television_receiver.n.01", "coco_cat_id": 72}, - {"synset": "laptop.n.01", "coco_cat_id": 73}, - {"synset": "mouse.n.04", "coco_cat_id": 74}, - {"synset": "remote_control.n.01", "coco_cat_id": 75}, - {"synset": "computer_keyboard.n.01", "coco_cat_id": 76}, - {"synset": "cellular_telephone.n.01", "coco_cat_id": 77}, - {"synset": "microwave.n.02", "coco_cat_id": 78}, - {"synset": "oven.n.01", "coco_cat_id": 79}, - {"synset": "toaster.n.02", "coco_cat_id": 80}, - {"synset": "sink.n.01", "coco_cat_id": 81}, - {"synset": "electric_refrigerator.n.01", "coco_cat_id": 82}, - {"synset": "book.n.01", "coco_cat_id": 84}, - {"synset": "clock.n.01", "coco_cat_id": 85}, - {"synset": "vase.n.01", "coco_cat_id": 86}, - {"synset": "scissors.n.01", "coco_cat_id": 87}, - {"synset": "teddy.n.01", "coco_cat_id": 88}, - {"synset": "hand_blower.n.01", "coco_cat_id": 89}, - {"synset": "toothbrush.n.01", "coco_cat_id": 90}, -] - - -def get_bbox(ann): - bbox = ann["bbox"] - return [bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]] - - -if __name__ == "__main__": - file_name_key = "file_name" if "v0.5" in LVIS_PATH else "coco_url" - coco_data = json.load(open(COCO_PATH)) - lvis_data = json.load(open(LVIS_PATH)) - - coco_cats = coco_data["categories"] - lvis_cats = lvis_data["categories"] - - num_find = 0 - num_not_find = 0 - num_twice = 0 - coco2lviscats = {} - synset2lvisid = {x["synset"]: x["id"] for x in lvis_cats} - # cocoid2synset = {x['coco_cat_id']: x['synset'] for x in COCO_SYNSET_CATEGORIES} - coco2lviscats = { - x["coco_cat_id"]: synset2lvisid[x["synset"]] - for x in COCO_SYNSET_CATEGORIES - if x["synset"] in synset2lvisid - } - print(len(coco2lviscats)) - - lvis_file2id = {x[file_name_key][-16:]: x["id"] for x in lvis_data["images"]} - lvis_id2img = {x["id"]: x for x in lvis_data["images"]} - lvis_catid2name = {x["id"]: x["name"] for x in lvis_data["categories"]} - - coco_file2anns = {} - coco_id2img = {x["id"]: x for x in coco_data["images"]} - coco_img2anns = defaultdict(list) - for ann in coco_data["annotations"]: - coco_img = coco_id2img[ann["image_id"]] - file_name = coco_img["file_name"][-16:] - if ann["category_id"] in coco2lviscats and file_name in lvis_file2id: - lvis_image_id = lvis_file2id[file_name] - lvis_image = lvis_id2img[lvis_image_id] - lvis_cat_id = coco2lviscats[ann["category_id"]] - if lvis_cat_id in lvis_image["neg_category_ids"]: - continue - if DEBUG: - import cv2 - - img_path = IMG_PATH + file_name - img = cv2.imread(img_path) - print(lvis_catid2name[lvis_cat_id]) - print("neg", [lvis_catid2name[x] for x in lvis_image["neg_category_ids"]]) - cv2.imshow("img", img) - cv2.waitKey() - ann["category_id"] = lvis_cat_id - ann["image_id"] = lvis_image_id - coco_img2anns[file_name].append(ann) - - lvis_img2anns = defaultdict(list) - for ann in lvis_data["annotations"]: - lvis_img = lvis_id2img[ann["image_id"]] - file_name = lvis_img[file_name_key][-16:] - lvis_img2anns[file_name].append(ann) - - ann_id_count = 0 - anns = [] - for file_name in lvis_img2anns: - coco_anns = coco_img2anns[file_name] - lvis_anns = lvis_img2anns[file_name] - ious = pairwise_iou( - Boxes(torch.tensor([get_bbox(x) for x in coco_anns])), - Boxes(torch.tensor([get_bbox(x) for x in lvis_anns])), - ) - - for ann in lvis_anns: - ann_id_count = ann_id_count + 1 - ann["id"] = ann_id_count - anns.append(ann) - - for i, ann in enumerate(coco_anns): - if len(ious[i]) == 0 or ious[i].max() < THRESH: - ann_id_count = ann_id_count + 1 - ann["id"] = ann_id_count - anns.append(ann) - else: - duplicated = False - for j in range(len(ious[i])): - if ( - ious[i, j] >= THRESH - and coco_anns[i]["category_id"] == lvis_anns[j]["category_id"] - ): - duplicated = True - if not duplicated: - ann_id_count = ann_id_count + 1 - ann["id"] = ann_id_count - anns.append(ann) - if NO_SEG: - for ann in anns: - del ann["segmentation"] - lvis_data["annotations"] = anns - - print("# Images", len(lvis_data["images"])) - print("# Anns", len(lvis_data["annotations"])) - json.dump(lvis_data, open(SAVE_PATH, "w")) diff --git a/dimos/models/Detic/tools/preprocess_imagenet22k.py b/dimos/models/Detic/tools/preprocess_imagenet22k.py deleted file mode 100644 index edf2d2bbf7..0000000000 --- a/dimos/models/Detic/tools/preprocess_imagenet22k.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Facebook, Inc. and its affiliates. - -import os -import sys - -import numpy as np - -sys.path.insert(0, "third_party/CenterNet2/") -sys.path.insert(0, "third_party/Deformable-DETR") -import gzip -import io -import time - -from detic.data.tar_dataset import _TarDataset - - -class _RawTarDataset: - def __init__(self, filename, indexname: str, preload: bool=False) -> None: - self.filename = filename - self.names = [] - self.offsets = [] - - for l in open(indexname): - ll = l.split() - a, b, c = ll[:3] - offset = int(b[:-1]) - if l.endswith("** Block of NULs **\n"): - self.offsets.append(offset) - break - else: - if c.endswith("JPEG"): - self.names.append(c) - self.offsets.append(offset) - else: - # ignore directories - pass - if preload: - self.data = np.memmap(filename, mode="r", dtype="uint8") - else: - self.data = None - - def __len__(self) -> int: - return len(self.names) - - def __getitem__(self, idx: int): - if self.data is None: - self.data = np.memmap(self.filename, mode="r", dtype="uint8") - ofs = self.offsets[idx] * 512 - fsize = 512 * (self.offsets[idx + 1] - self.offsets[idx]) - data = self.data[ofs : ofs + fsize] - - if data[:13].tostring() == "././@LongLink": - data = data[3 * 512 :] - else: - data = data[512:] - - # just to make it more fun a few JPEGs are GZIP compressed... - # catch this case - if tuple(data[:2]) == (0x1F, 0x8B): - s = io.StringIO(data.tostring()) - g = gzip.GzipFile(None, "r", 0, s) - sdata = g.read() - else: - sdata = data.tostring() - return sdata - - -def preprocess() -> None: - # Follow https://github.com/Alibaba-MIIL/ImageNet21K/blob/main/dataset_preprocessing/processing_script.sh - # Expect 12358684 samples with 11221 classes - # ImageNet folder has 21841 classes (synsets) - - i22kdir = "/datasets01/imagenet-22k/062717/" - i22ktarlogs = "/checkpoint/imisra/datasets/imagenet-22k/tarindex" - class_names_file = "/checkpoint/imisra/datasets/imagenet-22k/words.txt" - - output_dir = "/checkpoint/zhouxy/Datasets/ImageNet/metadata-22k/" - i22knpytarlogs = "/checkpoint/zhouxy/Datasets/ImageNet/metadata-22k/tarindex_npy" - print("Listing dir") - log_files = os.listdir(i22ktarlogs) - log_files = [x for x in log_files if x.endswith(".tarlog")] - log_files.sort() - dataset_lens = [] - min_count = 0 - create_npy_tarlogs = True - print("Creating folders") - if create_npy_tarlogs: - os.makedirs(i22knpytarlogs, exist_ok=True) - for log_file in log_files: - syn = log_file.replace(".tarlog", "") - dataset = _RawTarDataset( - os.path.join(i22kdir, syn + ".tar"), - os.path.join(i22ktarlogs, syn + ".tarlog"), - preload=False, - ) - names = np.array(dataset.names) - offsets = np.array(dataset.offsets, dtype=np.int64) - np.save(os.path.join(i22knpytarlogs, f"{syn}_names.npy"), names) - np.save(os.path.join(i22knpytarlogs, f"{syn}_offsets.npy"), offsets) - - os.makedirs(output_dir, exist_ok=True) - - start_time = time.time() - for log_file in log_files: - syn = log_file.replace(".tarlog", "") - dataset = _TarDataset(os.path.join(i22kdir, syn + ".tar"), i22knpytarlogs) - # dataset = _RawTarDataset(os.path.join(i22kdir, syn + ".tar"), - # os.path.join(i22ktarlogs, syn + ".tarlog"), - # preload=False) - dataset_lens.append(len(dataset)) - end_time = time.time() - print(f"Time {end_time - start_time}") - - dataset_lens = np.array(dataset_lens) - dataset_valid = dataset_lens > min_count - - syn2class = {} - with open(class_names_file) as fh: - for line in fh: - line = line.strip().split("\t") - syn2class[line[0]] = line[1] - - tarlog_files = [] - class_names = [] - tar_files = [] - for k in range(len(dataset_valid)): - if not dataset_valid[k]: - continue - syn = log_files[k].replace(".tarlog", "") - tarlog_files.append(os.path.join(i22ktarlogs, syn + ".tarlog")) - tar_files.append(os.path.join(i22kdir, syn + ".tar")) - class_names.append(syn2class[syn]) - - tarlog_files = np.array(tarlog_files) - tar_files = np.array(tar_files) - class_names = np.array(class_names) - print(f"Have {len(class_names)} classes and {dataset_lens[dataset_valid].sum()} samples") - - np.save(os.path.join(output_dir, "tarlog_files.npy"), tarlog_files) - np.save(os.path.join(output_dir, "tar_files.npy"), tar_files) - np.save(os.path.join(output_dir, "class_names.npy"), class_names) - np.save(os.path.join(output_dir, "tar_files.npy"), tar_files) - - -if __name__ == "__main__": - preprocess() diff --git a/dimos/models/Detic/tools/remove_lvis_rare.py b/dimos/models/Detic/tools/remove_lvis_rare.py deleted file mode 100644 index 423dd6e6e2..0000000000 --- a/dimos/models/Detic/tools/remove_lvis_rare.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import json - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--ann", default="datasets/lvis/lvis_v1_train.json") - args = parser.parse_args() - - print("Loading", args.ann) - data = json.load(open(args.ann)) - catid2freq = {x["id"]: x["frequency"] for x in data["categories"]} - print("ori #anns", len(data["annotations"])) - exclude = ["r"] - data["annotations"] = [ - x for x in data["annotations"] if catid2freq[x["category_id"]] not in exclude - ] - print("filtered #anns", len(data["annotations"])) - out_path = args.ann[:-5] + "_norare.json" - print("Saving to", out_path) - json.dump(data, open(out_path, "w")) diff --git a/dimos/models/Detic/tools/unzip_imagenet_lvis.py b/dimos/models/Detic/tools/unzip_imagenet_lvis.py deleted file mode 100644 index fd969c28bb..0000000000 --- a/dimos/models/Detic/tools/unzip_imagenet_lvis.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import argparse -import os - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--src_path", default="datasets/imagenet/ImageNet-21K/") - parser.add_argument("--dst_path", default="datasets/imagenet/ImageNet-LVIS/") - parser.add_argument("--data_path", default="datasets/imagenet_lvis_wnid.txt") - args = parser.parse_args() - - f = open(args.data_path) - for i, line in enumerate(f): - cmd = "mkdir {x} && tar -xf {src}/{l}.tar -C {x}".format( - src=args.src_path, l=line.strip(), x=args.dst_path + "/" + line.strip() - ) - print(i, cmd) - os.system(cmd) diff --git a/dimos/models/Detic/train_net.py b/dimos/models/Detic/train_net.py deleted file mode 100644 index 54ab6136f4..0000000000 --- a/dimos/models/Detic/train_net.py +++ /dev/null @@ -1,266 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -from collections import OrderedDict -import datetime -import logging -import os -import sys -import time - -from detectron2.checkpoint import DetectionCheckpointer, PeriodicCheckpointer -from detectron2.config import get_cfg -from detectron2.data import ( - MetadataCatalog, - build_detection_test_loader, -) -from detectron2.data.build import build_detection_train_loader -from detectron2.data.dataset_mapper import DatasetMapper -from detectron2.engine import default_argument_parser, default_setup, launch -from detectron2.evaluation import ( - COCOEvaluator, - LVISEvaluator, - inference_on_dataset, - print_csv_format, -) -from detectron2.modeling import build_model -from detectron2.solver import build_lr_scheduler, build_optimizer -import detectron2.utils.comm as comm -from detectron2.utils.events import ( - CommonMetricPrinter, - EventStorage, - JSONWriter, - TensorboardXWriter, -) -from detectron2.utils.logger import setup_logger -from fvcore.common.timer import Timer -import torch -from torch.cuda.amp import GradScaler -from torch.nn.parallel import DistributedDataParallel - -sys.path.insert(0, "third_party/CenterNet2/") -from centernet.config import add_centernet_config - -sys.path.insert(0, "third_party/Deformable-DETR") -from detic.config import add_detic_config -from detic.custom_solver import build_custom_optimizer -from detic.data.custom_build_augmentation import build_custom_augmentation -from detic.data.custom_dataset_dataloader import build_custom_train_loader -from detic.data.custom_dataset_mapper import CustomDatasetMapper, DetrDatasetMapper -from detic.evaluation.custom_coco_eval import CustomCOCOEvaluator -from detic.evaluation.oideval import OIDEvaluator -from detic.modeling.utils import reset_cls_test - -logger = logging.getLogger("detectron2") - - -def do_test(cfg, model): - results = OrderedDict() - for d, dataset_name in enumerate(cfg.DATASETS.TEST): - if cfg.MODEL.RESET_CLS_TESTS: - reset_cls_test(model, cfg.MODEL.TEST_CLASSIFIERS[d], cfg.MODEL.TEST_NUM_CLASSES[d]) - mapper = ( - None - if cfg.INPUT.TEST_INPUT_TYPE == "default" - else DatasetMapper(cfg, False, augmentations=build_custom_augmentation(cfg, False)) - ) - data_loader = build_detection_test_loader(cfg, dataset_name, mapper=mapper) - output_folder = os.path.join(cfg.OUTPUT_DIR, f"inference_{dataset_name}") - evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type - - if evaluator_type == "lvis" or cfg.GEN_PSEDO_LABELS: - evaluator = LVISEvaluator(dataset_name, cfg, True, output_folder) - elif evaluator_type == "coco": - if dataset_name == "coco_generalized_zeroshot_val": - # Additionally plot mAP for 'seen classes' and 'unseen classes' - evaluator = CustomCOCOEvaluator(dataset_name, cfg, True, output_folder) - else: - evaluator = COCOEvaluator(dataset_name, cfg, True, output_folder) - elif evaluator_type == "oid": - evaluator = OIDEvaluator(dataset_name, cfg, True, output_folder) - else: - assert 0, evaluator_type - - results[dataset_name] = inference_on_dataset(model, data_loader, evaluator) - if comm.is_main_process(): - logger.info(f"Evaluation results for {dataset_name} in csv format:") - print_csv_format(results[dataset_name]) - if len(results) == 1: - results = next(iter(results.values())) - return results - - -def do_train(cfg, model, resume: bool=False) -> None: - model.train() - if cfg.SOLVER.USE_CUSTOM_SOLVER: - optimizer = build_custom_optimizer(cfg, model) - else: - assert cfg.SOLVER.OPTIMIZER == "SGD" - assert cfg.SOLVER.CLIP_GRADIENTS.CLIP_TYPE != "full_model" - assert cfg.SOLVER.BACKBONE_MULTIPLIER == 1.0 - optimizer = build_optimizer(cfg, model) - scheduler = build_lr_scheduler(cfg, optimizer) - - checkpointer = DetectionCheckpointer( - model, cfg.OUTPUT_DIR, optimizer=optimizer, scheduler=scheduler - ) - - start_iter = ( - checkpointer.resume_or_load(cfg.MODEL.WEIGHTS, resume=resume).get("iteration", -1) + 1 - ) - if not resume: - start_iter = 0 - max_iter = cfg.SOLVER.MAX_ITER if cfg.SOLVER.TRAIN_ITER < 0 else cfg.SOLVER.TRAIN_ITER - - periodic_checkpointer = PeriodicCheckpointer( - checkpointer, cfg.SOLVER.CHECKPOINT_PERIOD, max_iter=max_iter - ) - - writers = ( - [ - CommonMetricPrinter(max_iter), - JSONWriter(os.path.join(cfg.OUTPUT_DIR, "metrics.json")), - TensorboardXWriter(cfg.OUTPUT_DIR), - ] - if comm.is_main_process() - else [] - ) - - use_custom_mapper = cfg.WITH_IMAGE_LABELS - MapperClass = CustomDatasetMapper if use_custom_mapper else DatasetMapper - mapper = ( - MapperClass(cfg, True) - if cfg.INPUT.CUSTOM_AUG == "" - else DetrDatasetMapper(cfg, True) - if cfg.INPUT.CUSTOM_AUG == "DETR" - else MapperClass(cfg, True, augmentations=build_custom_augmentation(cfg, True)) - ) - if cfg.DATALOADER.SAMPLER_TRAIN in ["TrainingSampler", "RepeatFactorTrainingSampler"]: - data_loader = build_detection_train_loader(cfg, mapper=mapper) - else: - data_loader = build_custom_train_loader(cfg, mapper=mapper) - - if cfg.FP16: - scaler = GradScaler() - - logger.info(f"Starting training from iteration {start_iter}") - with EventStorage(start_iter) as storage: - step_timer = Timer() - data_timer = Timer() - start_time = time.perf_counter() - for data, iteration in zip(data_loader, range(start_iter, max_iter), strict=False): - data_time = data_timer.seconds() - storage.put_scalars(data_time=data_time) - step_timer.reset() - iteration = iteration + 1 - storage.step() - loss_dict = model(data) - - losses = sum(loss for k, loss in loss_dict.items()) - assert torch.isfinite(losses).all(), loss_dict - - loss_dict_reduced = {k: v.item() for k, v in comm.reduce_dict(loss_dict).items()} - losses_reduced = sum(loss for loss in loss_dict_reduced.values()) - if comm.is_main_process(): - storage.put_scalars(total_loss=losses_reduced, **loss_dict_reduced) - - optimizer.zero_grad() - if cfg.FP16: - scaler.scale(losses).backward() - scaler.step(optimizer) - scaler.update() - else: - losses.backward() - optimizer.step() - - storage.put_scalar("lr", optimizer.param_groups[0]["lr"], smoothing_hint=False) - - step_time = step_timer.seconds() - storage.put_scalars(time=step_time) - data_timer.reset() - scheduler.step() - - if ( - cfg.TEST.EVAL_PERIOD > 0 - and iteration % cfg.TEST.EVAL_PERIOD == 0 - and iteration != max_iter - ): - do_test(cfg, model) - comm.synchronize() - - if iteration - start_iter > 5 and (iteration % 20 == 0 or iteration == max_iter): - for writer in writers: - writer.write() - periodic_checkpointer.step(iteration) - - total_time = time.perf_counter() - start_time - logger.info( - f"Total training time: {datetime.timedelta(seconds=int(total_time))!s}" - ) - - -def setup(args): - """ - Create configs and perform basic setups. - """ - cfg = get_cfg() - add_centernet_config(cfg) - add_detic_config(cfg) - cfg.merge_from_file(args.config_file) - cfg.merge_from_list(args.opts) - if "/auto" in cfg.OUTPUT_DIR: - file_name = os.path.basename(args.config_file)[:-5] - cfg.OUTPUT_DIR = cfg.OUTPUT_DIR.replace("/auto", f"/{file_name}") - logger.info(f"OUTPUT_DIR: {cfg.OUTPUT_DIR}") - cfg.freeze() - default_setup(cfg, args) - setup_logger(output=cfg.OUTPUT_DIR, distributed_rank=comm.get_rank(), name="detic") - return cfg - - -def main(args): - cfg = setup(args) - - model = build_model(cfg) - logger.info(f"Model:\n{model}") - if args.eval_only: - DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load( - cfg.MODEL.WEIGHTS, resume=args.resume - ) - - return do_test(cfg, model) - - distributed = comm.get_world_size() > 1 - if distributed: - model = DistributedDataParallel( - model, - device_ids=[comm.get_local_rank()], - broadcast_buffers=False, - find_unused_parameters=cfg.FIND_UNUSED_PARAM, - ) - - do_train(cfg, model, resume=args.resume) - return do_test(cfg, model) - - -if __name__ == "__main__": - args = default_argument_parser() - args = args.parse_args() - if args.num_machines == 1: - args.dist_url = f"tcp://127.0.0.1:{torch.randint(11111, 60000, (1,))[0].item()}" - else: - if args.dist_url == "host": - args.dist_url = "tcp://{}:12345".format(os.environ["SLURM_JOB_NODELIST"]) - elif not args.dist_url.startswith("tcp"): - tmp = os.popen( - f"echo $(scontrol show job {args.dist_url} | grep BatchHost)" - ).read() - tmp = tmp[tmp.find("=") + 1 : -1] - args.dist_url = f"tcp://{tmp}:12345" - print("Command Line Args:", args) - launch( - main, - args.num_gpus, - num_machines=args.num_machines, - machine_rank=args.machine_rank, - dist_url=args.dist_url, - args=(args,), - ) diff --git a/dimos/models/__init__.py b/dimos/models/__init__.py index e69de29bb2..d8e2e14341 100644 --- a/dimos/models/__init__.py +++ b/dimos/models/__init__.py @@ -0,0 +1,3 @@ +from dimos.models.base import HuggingFaceModel, LocalModel + +__all__ = ["HuggingFaceModel", "LocalModel"] diff --git a/dimos/models/base.py b/dimos/models/base.py new file mode 100644 index 0000000000..2269a6d0b8 --- /dev/null +++ b/dimos/models/base.py @@ -0,0 +1,199 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base classes for local GPU models.""" + +from __future__ import annotations + +from dataclasses import dataclass +from functools import cached_property +from typing import Annotated, Any + +import torch + +from dimos.core.resource import Resource +from dimos.protocol.service import Configurable # type: ignore[attr-defined] + +# Device string type - 'cuda', 'cpu', 'cuda:0', 'cuda:1', etc. +DeviceType = Annotated[str, "Device identifier (e.g., 'cuda', 'cpu', 'cuda:0')"] + + +@dataclass +class LocalModelConfig: + device: DeviceType = "cuda" if torch.cuda.is_available() else "cpu" + dtype: torch.dtype = torch.float32 + warmup: bool = False + autostart: bool = False + + +class LocalModel(Resource, Configurable[LocalModelConfig]): + """Base class for all local GPU/CPU models. + + Implements Resource interface for lifecycle management. + + Subclasses MUST override: + - _model: @cached_property that loads and returns the model + + Subclasses MAY override: + - start() for custom initialization logic + - stop() for custom cleanup logic + """ + + default_config = LocalModelConfig + config: LocalModelConfig + + def __init__(self, **kwargs: object) -> None: + """Initialize local model with device and dtype configuration. + + Args: + device: Device to run on ('cuda', 'cpu', 'cuda:0', etc.). + Auto-detects CUDA availability if None. + dtype: Model dtype (torch.float16, torch.bfloat16, etc.). + Uses class _default_dtype if None. + autostart: If True, immediately load the model. + If False (default), model loads lazily on first use. + """ + super().__init__(**kwargs) + if self.config.warmup or self.config.autostart: + self.start() + + @property + def device(self) -> str: + """The device this model runs on.""" + return self.config.device + + @property + def dtype(self) -> torch.dtype: + """The dtype used by this model.""" + return self.config.dtype + + @cached_property + def _model(self) -> Any: + """Lazily loaded model. Subclasses must override this property.""" + raise NotImplementedError(f"{self.__class__.__name__} must override _model property") + + def start(self) -> None: + """Load the model (Resource interface). + + Subclasses should override to add custom initialization. + """ + _ = self._model + + def stop(self) -> None: + """Release model and free GPU memory (Resource interface). + + Subclasses should override and call super().stop() for custom cleanup. + """ + import gc + + if "_model" in self.__dict__: + del self.__dict__["_model"] + + # Reset torch.compile caches to free memory from compiled models + # See: https://github.com/pytorch/pytorch/issues/105181 + try: + import torch._dynamo + + torch._dynamo.reset() + except (ImportError, AttributeError): + pass + + gc.collect() + if self.config.device.startswith("cuda") and torch.cuda.is_available(): + torch.cuda.empty_cache() + + def _ensure_cuda_initialized(self) -> None: + """Initialize CUDA context to prevent cuBLAS allocation failures. + + Some models (CLIP, TorchReID) fail if they are the first to use CUDA. + Call this before model loading if needed. + """ + if self.config.device.startswith("cuda") and torch.cuda.is_available(): + try: + _ = torch.zeros(1, 1, device="cuda") @ torch.zeros(1, 1, device="cuda") + torch.cuda.synchronize() + except Exception: + pass + + +@dataclass +class HuggingFaceModelConfig(LocalModelConfig): + model_name: str = "" + trust_remote_code: bool = True + dtype: torch.dtype = torch.float16 + + +class HuggingFaceModel(LocalModel): + """Base class for HuggingFace transformers-based models. + + Provides common patterns for loading models from the HuggingFace Hub + using from_pretrained(). + + Subclasses SHOULD set: + - _model_class: The AutoModel class to use (e.g., AutoModelForCausalLM) + + Subclasses MAY override: + - _model: @cached_property for custom model loading + """ + + default_config = HuggingFaceModelConfig + config: HuggingFaceModelConfig + _model_class: Any = None # e.g., AutoModelForCausalLM + + @property + def model_name(self) -> str: + """The HuggingFace model identifier.""" + return self.config.model_name + + @cached_property + def _model(self) -> Any: + """Load the HuggingFace model using _model_class. + + Override this property for custom loading logic. + """ + if self._model_class is None: + raise NotImplementedError( + f"{self.__class__.__name__} must set _model_class or override _model property" + ) + model = self._model_class.from_pretrained( + self.config.model_name, + trust_remote_code=self.config.trust_remote_code, + torch_dtype=self.config.dtype, + ) + return model.to(self.config.device) + + def _move_inputs_to_device( + self, + inputs: dict[str, torch.Tensor], + apply_dtype: bool = True, + ) -> dict[str, torch.Tensor]: + """Move input tensors to model device with appropriate dtype. + + Args: + inputs: Dictionary of input tensors + apply_dtype: Whether to apply model dtype to floating point tensors + + Returns: + Dictionary with tensors moved to device + """ + result = {} + for k, v in inputs.items(): + if isinstance(v, torch.Tensor): + if apply_dtype and v.is_floating_point(): + result[k] = v.to(self.config.device, dtype=self.config.dtype) + else: + result[k] = v.to(self.config.device) + else: + result[k] = v + return result diff --git a/dimos/models/depth/metric3d.py b/dimos/models/depth/metric3d.py index e22c546dc3..41b5086991 100644 --- a/dimos/models/depth/metric3d.py +++ b/dimos/models/depth/metric3d.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,41 +12,57 @@ # See the License for the specific language governing permissions and # limitations under the License. +from dataclasses import dataclass, field +from functools import cached_property +from typing import Any + import cv2 -import numpy as np -from PIL import Image import torch -# May need to add this back for import to work -# external_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'external', 'Metric3D')) -# if external_path not in sys.path: -# sys.path.append(external_path) +from dimos.models.base import LocalModel, LocalModelConfig + + +@dataclass +class Metric3DConfig(LocalModelConfig): + """Configuration for Metric3D depth estimation model.""" + + camera_intrinsics: list[float] = field(default_factory=lambda: [500.0, 500.0, 320.0, 240.0]) + """Camera intrinsics [fx, fy, cx, cy].""" + + gt_depth_scale: float = 256.0 + """Scale factor for ground truth depth.""" + + device: str = "cuda" if torch.cuda.is_available() else "cpu" + """Device to run the model on.""" -class Metric3D: - def __init__(self, camera_intrinsics=None, gt_depth_scale: float=256.0) -> None: - # self.conf = get_config("zoedepth", "infer") - # self.depth_model = build_model(self.conf) - self.depth_model = torch.hub.load( +class Metric3D(LocalModel): + default_config = Metric3DConfig + config: Metric3DConfig + + def __init__(self, **kwargs: object) -> None: + super().__init__(**kwargs) + self.intrinsic = self.config.camera_intrinsics + self.intrinsic_scaled: list[float] | None = None + self.gt_depth_scale = self.config.gt_depth_scale + self.pad_info: list[int] | None = None + self.rgb_origin: Any = None + + @cached_property + def _model(self) -> Any: + model = torch.hub.load( # type: ignore[no-untyped-call] "yvanyin/metric3d", "metric3d_vit_small", pretrain=True - ).cuda() - if torch.cuda.device_count() > 1: - print(f"Using {torch.cuda.device_count()} GPUs!") - # self.depth_model = torch.nn.DataParallel(self.depth_model) - self.depth_model.eval() - - self.intrinsic = camera_intrinsics - self.intrinsic_scaled = None - self.gt_depth_scale = gt_depth_scale # And this - self.pad_info = None - self.rgb_origin = None + ) + model = model.to(self.device) + model.eval() + return model """ Input: Single image in RGB format Output: Depth map """ - def update_intrinsic(self, intrinsic): + def update_intrinsic(self, intrinsic): # type: ignore[no-untyped-def] """ Update the intrinsic parameters dynamically. Ensure that the input intrinsic is valid. @@ -56,7 +72,7 @@ def update_intrinsic(self, intrinsic): self.intrinsic = intrinsic print(f"Intrinsics updated to: {self.intrinsic}") - def infer_depth(self, img, debug: bool=False): + def infer_depth(self, img, debug: bool = False): # type: ignore[no-untyped-def] if debug: print(f"Input image: {img}") try: @@ -69,17 +85,17 @@ def infer_depth(self, img, debug: bool=False): except Exception as e: print(f"Error parsing into infer_depth: {e}") - img = self.rescale_input(img, self.rgb_origin) + img = self.rescale_input(img, self.rgb_origin) # type: ignore[no-untyped-call] with torch.no_grad(): - pred_depth, confidence, output_dict = self.depth_model.inference({"input": img}) + pred_depth, confidence, output_dict = self._model.inference({"input": img}) # Convert to PIL format - depth_image = self.unpad_transform_depth(pred_depth) + depth_image = self.unpad_transform_depth(pred_depth) # type: ignore[no-untyped-call] return depth_image.cpu().numpy() - def save_depth(self, pred_depth) -> None: + def save_depth(self, pred_depth) -> None: # type: ignore[no-untyped-def] # Save the depth map to a file pred_depth_np = pred_depth.cpu().numpy() output_depth_file = "output_depth_map.png" @@ -87,7 +103,7 @@ def save_depth(self, pred_depth) -> None: print(f"Depth map saved to {output_depth_file}") # Adjusts input size to fit pretrained ViT model - def rescale_input(self, rgb, rgb_origin): + def rescale_input(self, rgb, rgb_origin): # type: ignore[no-untyped-def] #### ajust input size to fit pretrained model # keep ratio resize input_size = (616, 1064) # for vit model @@ -127,41 +143,38 @@ def rescale_input(self, rgb, rgb_origin): std = torch.tensor([58.395, 57.12, 57.375]).float()[:, None, None] rgb = torch.from_numpy(rgb.transpose((2, 0, 1))).float() rgb = torch.div((rgb - mean), std) - rgb = rgb[None, :, :, :].cuda() + rgb = rgb[None, :, :, :].to(self.device) return rgb - def unpad_transform_depth(self, pred_depth): + def unpad_transform_depth(self, pred_depth): # type: ignore[no-untyped-def] # un pad pred_depth = pred_depth.squeeze() pred_depth = pred_depth[ - self.pad_info[0] : pred_depth.shape[0] - self.pad_info[1], - self.pad_info[2] : pred_depth.shape[1] - self.pad_info[3], + self.pad_info[0] : pred_depth.shape[0] - self.pad_info[1], # type: ignore[index] + self.pad_info[2] : pred_depth.shape[1] - self.pad_info[3], # type: ignore[index] ] # upsample to original size pred_depth = torch.nn.functional.interpolate( - pred_depth[None, None, :, :], self.rgb_origin.shape[:2], mode="bilinear" + pred_depth[None, None, :, :], + self.rgb_origin.shape[:2], + mode="bilinear", ).squeeze() ###################### canonical camera space ###################### #### de-canonical transform canonical_to_real_scale = ( - self.intrinsic_scaled[0] / 1000.0 + self.intrinsic_scaled[0] / 1000.0 # type: ignore[index] ) # 1000.0 is the focal length of canonical camera pred_depth = pred_depth * canonical_to_real_scale # now the depth is metric pred_depth = torch.clamp(pred_depth, 0, 1000) return pred_depth - """Set new intrinsic value.""" - - def update_intrinsic(self, intrinsic) -> None: - self.intrinsic = intrinsic - - def eval_predicted_depth(self, depth_file, pred_depth) -> None: + def eval_predicted_depth(self, depth_file, pred_depth) -> None: # type: ignore[no-untyped-def] if depth_file is not None: gt_depth = cv2.imread(depth_file, -1) - gt_depth = gt_depth / self.gt_depth_scale - gt_depth = torch.from_numpy(gt_depth).float().cuda() + gt_depth = gt_depth / self.gt_depth_scale # type: ignore[assignment] + gt_depth = torch.from_numpy(gt_depth).float().to(self.device) # type: ignore[assignment] assert gt_depth.shape == pred_depth.shape mask = gt_depth > 1e-8 diff --git a/dimos/models/depth/test_metric3d.py b/dimos/models/depth/test_metric3d.py new file mode 100644 index 0000000000..050100047b --- /dev/null +++ b/dimos/models/depth/test_metric3d.py @@ -0,0 +1,87 @@ +import numpy as np +import pytest + +from dimos.models.depth.metric3d import Metric3D +from dimos.msgs.sensor_msgs import Image +from dimos.utils.data import get_data + + +@pytest.fixture +def sample_intrinsics() -> list[float]: + """Sample camera intrinsics [fx, fy, cx, cy].""" + return [500.0, 500.0, 320.0, 240.0] + + +@pytest.mark.gpu +def test_metric3d_init(sample_intrinsics: list[float]) -> None: + """Test Metric3D initialization.""" + model = Metric3D(camera_intrinsics=sample_intrinsics) + assert model.config.camera_intrinsics == sample_intrinsics + assert model.config.gt_depth_scale == 256.0 + assert model.device == "cuda" + + +@pytest.mark.gpu +def test_metric3d_update_intrinsic(sample_intrinsics: list[float]) -> None: + """Test updating camera intrinsics.""" + model = Metric3D(camera_intrinsics=sample_intrinsics) + + new_intrinsics = [600.0, 600.0, 400.0, 300.0] + model.update_intrinsic(new_intrinsics) + assert model.intrinsic == new_intrinsics + + +@pytest.mark.gpu +def test_metric3d_update_intrinsic_invalid(sample_intrinsics: list[float]) -> None: + """Test that invalid intrinsics raise an error.""" + model = Metric3D(camera_intrinsics=sample_intrinsics) + + with pytest.raises(ValueError, match="Intrinsic must be a list"): + model.update_intrinsic([1.0, 2.0]) # Only 2 values + + +@pytest.mark.gpu +def test_metric3d_infer_depth(sample_intrinsics: list[float]) -> None: + """Test depth inference on a sample image.""" + model = Metric3D(camera_intrinsics=sample_intrinsics) + model.start() + + # Load test image + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + rgb_array = image.data + + # Run inference + depth_map = model.infer_depth(rgb_array) + + # Verify output + assert isinstance(depth_map, np.ndarray) + assert depth_map.shape[:2] == rgb_array.shape[:2] # Same spatial dimensions + assert depth_map.dtype in [np.float32, np.float64] + assert depth_map.min() >= 0 # Depth should be non-negative + + print(f"Depth map shape: {depth_map.shape}") + print(f"Depth range: [{depth_map.min():.2f}, {depth_map.max():.2f}]") + + model.stop() + + +@pytest.mark.gpu +def test_metric3d_multiple_inferences(sample_intrinsics: list[float]) -> None: + """Test multiple depth inferences.""" + model = Metric3D(camera_intrinsics=sample_intrinsics) + model.start() + + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + rgb_array = image.data + + # Run multiple inferences + depths = [] + for _ in range(3): + depth = model.infer_depth(rgb_array) + depths.append(depth) + + # Results should be consistent + for i in range(1, len(depths)): + assert np.allclose(depths[0], depths[i], rtol=1e-5) + + model.stop() diff --git a/dimos/models/embedding/base.py b/dimos/models/embedding/base.py index 99a8d8fd15..eba5e45894 100644 --- a/dimos/models/embedding/base.py +++ b/dimos/models/embedding/base.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,18 +15,34 @@ from __future__ import annotations from abc import ABC, abstractmethod +from dataclasses import dataclass import time -from typing import TYPE_CHECKING, Generic, Optional, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar import numpy as np import torch +from dimos.models.base import HuggingFaceModelConfig, LocalModelConfig from dimos.types.timestamped import Timestamped if TYPE_CHECKING: from dimos.msgs.sensor_msgs import Image +@dataclass +class EmbeddingModelConfig(LocalModelConfig): + """Base config for embedding models.""" + + normalize: bool = True + + +@dataclass +class HuggingFaceEmbeddingModelConfig(HuggingFaceModelConfig): + """Base config for HuggingFace-based embedding models.""" + + normalize: bool = True + + class Embedding(Timestamped): """Base class for embeddings with vector data. @@ -34,9 +50,9 @@ class Embedding(Timestamped): Embeddings are kept as torch.Tensor on device by default for efficiency. """ - vector: torch.Tensor | np.ndarray + vector: torch.Tensor | np.ndarray # type: ignore[type-arg] - def __init__(self, vector: torch.Tensor | np.ndarray, timestamp: float | None = None) -> None: + def __init__(self, vector: torch.Tensor | np.ndarray, timestamp: float | None = None) -> None: # type: ignore[type-arg] self.vector = vector if timestamp: self.timestamp = timestamp @@ -51,7 +67,7 @@ def __matmul__(self, other: Embedding) -> float: return result.item() return float(self.vector @ other.to_numpy()) - def to_numpy(self) -> np.ndarray: + def to_numpy(self) -> np.ndarray: # type: ignore[type-arg] """Convert to numpy array (moves to CPU if needed).""" if isinstance(self.vector, torch.Tensor): return self.vector.detach().cpu().numpy() @@ -81,7 +97,6 @@ class EmbeddingModel(ABC, Generic[E]): """Abstract base class for embedding models supporting vision and language.""" device: str - normalize: bool = True @abstractmethod def embed(self, *images: Image) -> E | list[E]: diff --git a/dimos/models/embedding/clip.py b/dimos/models/embedding/clip.py index 23ab5e94f2..d8a62efcb2 100644 --- a/dimos/models/embedding/clip.py +++ b/dimos/models/embedding/clip.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,43 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. +from dataclasses import dataclass +from functools import cached_property + from PIL import Image as PILImage import torch import torch.nn.functional as F -from transformers import CLIPModel as HFCLIPModel, CLIPProcessor +from transformers import CLIPModel as HFCLIPModel, CLIPProcessor # type: ignore[import-untyped] -from dimos.models.embedding.base import Embedding, EmbeddingModel +from dimos.models.base import HuggingFaceModel +from dimos.models.embedding.base import Embedding, EmbeddingModel, HuggingFaceEmbeddingModelConfig from dimos.msgs.sensor_msgs import Image -_CUDA_INITIALIZED = False - class CLIPEmbedding(Embedding): ... -class CLIPModel(EmbeddingModel[CLIPEmbedding]): +@dataclass +class CLIPModelConfig(HuggingFaceEmbeddingModelConfig): + model_name: str = "openai/clip-vit-base-patch32" + dtype: torch.dtype = torch.float32 + + +class CLIPModel(EmbeddingModel[CLIPEmbedding], HuggingFaceModel): """CLIP embedding model for vision-language re-identification.""" - def __init__( - self, - model_name: str = "openai/clip-vit-base-patch32", - device: str | None = None, - normalize: bool = False, - ) -> None: - """ - Initialize CLIP model. + default_config = CLIPModelConfig + config: CLIPModelConfig + _model_class = HFCLIPModel - Args: - model_name: HuggingFace model name (e.g., "openai/clip-vit-base-patch32") - device: Device to run on (cuda/cpu), auto-detects if None - normalize: Whether to L2 normalize embeddings - """ - self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") - self.normalize = normalize + @cached_property + def _model(self) -> HFCLIPModel: + self._ensure_cuda_initialized() + return HFCLIPModel.from_pretrained(self.config.model_name).eval().to(self.config.device) - # Load model and processor - self.model = HFCLIPModel.from_pretrained(model_name).eval().to(self.device) - self.processor = CLIPProcessor.from_pretrained(model_name) + @cached_property + def _processor(self) -> CLIPProcessor: + return CLIPProcessor.from_pretrained(self.config.model_name) def embed(self, *images: Image) -> CLIPEmbedding | list[CLIPEmbedding]: """Embed one or more images. @@ -60,10 +60,10 @@ def embed(self, *images: Image) -> CLIPEmbedding | list[CLIPEmbedding]: # Process images with torch.inference_mode(): - inputs = self.processor(images=pil_images, return_tensors="pt").to(self.device) - image_features = self.model.get_image_features(**inputs) + inputs = self._processor(images=pil_images, return_tensors="pt").to(self.config.device) + image_features = self._model.get_image_features(**inputs) - if self.normalize: + if self.config.normalize: image_features = F.normalize(image_features, dim=-1) # Create embeddings (keep as torch.Tensor on device) @@ -80,12 +80,12 @@ def embed_text(self, *texts: str) -> CLIPEmbedding | list[CLIPEmbedding]: Returns embeddings as torch.Tensor on device for efficient GPU comparisons. """ with torch.inference_mode(): - inputs = self.processor(text=list(texts), return_tensors="pt", padding=True).to( - self.device + inputs = self._processor(text=list(texts), return_tensors="pt", padding=True).to( + self.config.device ) - text_features = self.model.get_text_features(**inputs) + text_features = self._model.get_text_features(**inputs) - if self.normalize: + if self.config.normalize: text_features = F.normalize(text_features, dim=-1) # Create embeddings (keep as torch.Tensor on device) @@ -95,28 +95,21 @@ def embed_text(self, *texts: str) -> CLIPEmbedding | list[CLIPEmbedding]: return embeddings[0] if len(texts) == 1 else embeddings - def warmup(self) -> None: - """Warmup the model with a dummy forward pass.""" - # WORKAROUND: HuggingFace CLIP fails with CUBLAS_STATUS_ALLOC_FAILED when it's - # the first model to use CUDA. Initialize CUDA context with a dummy operation. - # This only needs to happen once per process. - global _CUDA_INITIALIZED - if self.device == "cuda" and not _CUDA_INITIALIZED: - try: - # Initialize CUDA with a small matmul operation to setup cuBLAS properly - _ = torch.zeros(1, 1, device="cuda") @ torch.zeros(1, 1, device="cuda") - torch.cuda.synchronize() - _CUDA_INITIALIZED = True - except Exception: - # If initialization fails, continue anyway - the warmup might still work - pass - - dummy_image = torch.randn(1, 3, 224, 224).to(self.device) - dummy_text_inputs = self.processor(text=["warmup"], return_tensors="pt", padding=True).to( - self.device + def start(self) -> None: + """Start the model with a dummy forward pass.""" + super().start() + + dummy_image = torch.randn(1, 3, 224, 224).to(self.config.device) + dummy_text_inputs = self._processor(text=["warmup"], return_tensors="pt", padding=True).to( + self.config.device ) with torch.inference_mode(): - # Use pixel_values directly for image warmup - self.model.get_image_features(pixel_values=dummy_image) - self.model.get_text_features(**dummy_text_inputs) + self._model.get_image_features(pixel_values=dummy_image) + self._model.get_text_features(**dummy_text_inputs) + + def stop(self) -> None: + """Release model and free GPU memory.""" + if "_processor" in self.__dict__: + del self.__dict__["_processor"] + super().stop() diff --git a/dimos/models/embedding/embedding_models_disabled_tests.py b/dimos/models/embedding/embedding_models_disabled_tests.py deleted file mode 100644 index bb1f038410..0000000000 --- a/dimos/models/embedding/embedding_models_disabled_tests.py +++ /dev/null @@ -1,404 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest - -from dimos.msgs.sensor_msgs import Image -from dimos.utils.data import get_data - - -@pytest.fixture(scope="session", params=["clip", "mobileclip", "treid"]) -def embedding_model(request): - """Load embedding model once for all tests. Parametrized for different models.""" - if request.param == "mobileclip": - from dimos.models.embedding.mobileclip import MobileCLIPModel - - model_path = get_data("models_mobileclip") / "mobileclip2_s0.pt" - model = MobileCLIPModel(model_name="MobileCLIP2-S0", model_path=model_path) - elif request.param == "clip": - from dimos.models.embedding.clip import CLIPModel - - model = CLIPModel(model_name="openai/clip-vit-base-patch32") - elif request.param == "treid": - from dimos.models.embedding.treid import TorchReIDModel - - model = TorchReIDModel(model_name="osnet_x1_0") - else: - raise ValueError(f"Unknown model: {request.param}") - - model.warmup() - return model - - -@pytest.fixture(scope="session") -def test_image(): - """Load test image.""" - return Image.from_file(get_data("cafe.jpg")).to_rgb() - - -@pytest.mark.heavy -def test_single_image_embedding(embedding_model, test_image) -> None: - """Test embedding a single image.""" - embedding = embedding_model.embed(test_image) - - # Embedding should be torch.Tensor on device - import torch - - assert isinstance(embedding.vector, torch.Tensor), "Embedding should be torch.Tensor" - assert embedding.vector.device.type in ["cuda", "cpu"], "Should be on valid device" - - # Test conversion to numpy - vector_np = embedding.to_numpy() - print(f"\nEmbedding shape: {vector_np.shape}") - print(f"Embedding dtype: {vector_np.dtype}") - print(f"Embedding norm: {np.linalg.norm(vector_np):.4f}") - - assert vector_np.shape[0] > 0, "Embedding should have features" - assert np.isfinite(vector_np).all(), "Embedding should contain finite values" - - # Check L2 normalization - norm = np.linalg.norm(vector_np) - assert abs(norm - 1.0) < 0.01, f"Embedding should be L2 normalized, got norm={norm}" - - -@pytest.mark.heavy -def test_batch_image_embedding(embedding_model, test_image) -> None: - """Test embedding multiple images at once.""" - embeddings = embedding_model.embed(test_image, test_image, test_image) - - assert isinstance(embeddings, list), "Batch embedding should return list" - assert len(embeddings) == 3, "Should return 3 embeddings" - - # Check all embeddings are similar (same image) - sim_01 = embeddings[0] @ embeddings[1] - sim_02 = embeddings[0] @ embeddings[2] - - print(f"\nSimilarity between same images: {sim_01:.6f}, {sim_02:.6f}") - - assert sim_01 > 0.99, f"Same image embeddings should be very similar, got {sim_01}" - assert sim_02 > 0.99, f"Same image embeddings should be very similar, got {sim_02}" - - -@pytest.mark.heavy -def test_single_text_embedding(embedding_model) -> None: - """Test embedding a single text string.""" - import torch - - if not hasattr(embedding_model, "embed_text"): - pytest.skip("Model does not support text embeddings") - - embedding = embedding_model.embed_text("a cafe") - - # Should be torch.Tensor - assert isinstance(embedding.vector, torch.Tensor), "Text embedding should be torch.Tensor" - - vector_np = embedding.to_numpy() - print(f"\nText embedding shape: {vector_np.shape}") - print(f"Text embedding norm: {np.linalg.norm(vector_np):.4f}") - - assert vector_np.shape[0] > 0, "Text embedding should have features" - assert np.isfinite(vector_np).all(), "Text embedding should contain finite values" - - # Check L2 normalization - norm = np.linalg.norm(vector_np) - assert abs(norm - 1.0) < 0.01, f"Text embedding should be L2 normalized, got norm={norm}" - - -@pytest.mark.heavy -def test_batch_text_embedding(embedding_model) -> None: - """Test embedding multiple text strings at once.""" - import torch - - if not hasattr(embedding_model, "embed_text"): - pytest.skip("Model does not support text embeddings") - - embeddings = embedding_model.embed_text("a cafe", "a person", "a dog") - - assert isinstance(embeddings, list), "Batch text embedding should return list" - assert len(embeddings) == 3, "Should return 3 text embeddings" - - # All should be torch.Tensor and normalized - for i, emb in enumerate(embeddings): - assert isinstance(emb.vector, torch.Tensor), f"Embedding {i} should be torch.Tensor" - norm = np.linalg.norm(emb.to_numpy()) - assert abs(norm - 1.0) < 0.01, f"Text embedding {i} should be L2 normalized" - - -@pytest.mark.heavy -def test_text_image_similarity(embedding_model, test_image) -> None: - """Test cross-modal text-image similarity using @ operator.""" - if not hasattr(embedding_model, "embed_text"): - pytest.skip("Model does not support text embeddings") - - img_embedding = embedding_model.embed(test_image) - - # Embed text queries - queries = ["a cafe", "a person", "a car", "a dog", "potato", "food"] - text_embeddings = embedding_model.embed_text(*queries) - - # Compute similarities using @ operator - similarities = {} - for query, text_emb in zip(queries, text_embeddings, strict=False): - similarity = img_embedding @ text_emb - similarities[query] = similarity - print(f"\n'{query}': {similarity:.4f}") - - # Cafe image should match "a cafe" better than "a dog" - assert similarities["a cafe"] > similarities["a dog"], "Should recognize cafe scene" - assert similarities["a person"] > similarities["a car"], "Should detect people in cafe" - - -@pytest.mark.heavy -def test_cosine_distance(embedding_model, test_image) -> None: - """Test cosine distance computation (1 - similarity).""" - emb1 = embedding_model.embed(test_image) - emb2 = embedding_model.embed(test_image) - - # Similarity using @ operator - similarity = emb1 @ emb2 - - # Distance is 1 - similarity - distance = 1.0 - similarity - - print(f"\nSimilarity (same image): {similarity:.6f}") - print(f"Distance (same image): {distance:.6f}") - - assert similarity > 0.99, f"Same image should have high similarity, got {similarity}" - assert distance < 0.01, f"Same image should have low distance, got {distance}" - - -@pytest.mark.heavy -def test_query_functionality(embedding_model, test_image) -> None: - """Test query method for top-k retrieval.""" - if not hasattr(embedding_model, "embed_text"): - pytest.skip("Model does not support text embeddings") - - # Create a query and some candidates - query_text = embedding_model.embed_text("a cafe") - - # Create candidate embeddings - candidate_texts = ["a cafe", "a restaurant", "a person", "a dog", "a car"] - candidates = embedding_model.embed_text(*candidate_texts) - - # Query for top-3 - results = embedding_model.query(query_text, candidates, top_k=3) - - print("\nTop-3 results:") - for idx, sim in results: - print(f" {candidate_texts[idx]}: {sim:.4f}") - - assert len(results) == 3, "Should return top-3 results" - assert results[0][0] == 0, "Top match should be 'a cafe' itself" - assert results[0][1] > results[1][1], "Results should be sorted by similarity" - assert results[1][1] > results[2][1], "Results should be sorted by similarity" - - -@pytest.mark.heavy -def test_embedding_operator(embedding_model, test_image) -> None: - """Test that @ operator works on embeddings.""" - emb1 = embedding_model.embed(test_image) - emb2 = embedding_model.embed(test_image) - - # Use @ operator - similarity = emb1 @ emb2 - - assert isinstance(similarity, float), "@ operator should return float" - assert 0.0 <= similarity <= 1.0, "Cosine similarity should be in [0, 1]" - assert similarity > 0.99, "Same image should have similarity near 1.0" - - -@pytest.mark.heavy -def test_warmup(embedding_model) -> None: - """Test that warmup runs without error.""" - # Warmup is already called in fixture, but test it explicitly - embedding_model.warmup() - # Just verify no exceptions raised - assert True - - -@pytest.mark.heavy -def test_compare_one_to_many(embedding_model, test_image) -> None: - """Test GPU-accelerated one-to-many comparison.""" - import torch - - # Create query and gallery - query_emb = embedding_model.embed(test_image) - gallery_embs = embedding_model.embed(test_image, test_image, test_image) - - # Compare on GPU - similarities = embedding_model.compare_one_to_many(query_emb, gallery_embs) - - print(f"\nOne-to-many similarities: {similarities}") - - # Should return torch.Tensor - assert isinstance(similarities, torch.Tensor), "Should return torch.Tensor" - assert similarities.shape == (3,), "Should have 3 similarities" - assert similarities.device.type in ["cuda", "cpu"], "Should be on device" - - # All should be ~1.0 (same image) - similarities_np = similarities.cpu().numpy() - assert np.all(similarities_np > 0.99), "Same images should have similarity ~1.0" - - -@pytest.mark.heavy -def test_compare_many_to_many(embedding_model) -> None: - """Test GPU-accelerated many-to-many comparison.""" - import torch - - if not hasattr(embedding_model, "embed_text"): - pytest.skip("Model does not support text embeddings") - - # Create queries and candidates - queries = embedding_model.embed_text("a cafe", "a person") - candidates = embedding_model.embed_text("a cafe", "a restaurant", "a dog") - - # Compare on GPU - similarities = embedding_model.compare_many_to_many(queries, candidates) - - print(f"\nMany-to-many similarities:\n{similarities}") - - # Should return torch.Tensor - assert isinstance(similarities, torch.Tensor), "Should return torch.Tensor" - assert similarities.shape == (2, 3), "Should be (2, 3) similarity matrix" - assert similarities.device.type in ["cuda", "cpu"], "Should be on device" - - # First query should match first candidate best - similarities_np = similarities.cpu().numpy() - assert similarities_np[0, 0] > similarities_np[0, 2], "Cafe should match cafe better than dog" - - -@pytest.mark.heavy -def test_gpu_query_performance(embedding_model, test_image) -> None: - """Test that query method uses GPU acceleration.""" - # Create a larger gallery - gallery_size = 20 - gallery_images = [test_image] * gallery_size - gallery_embs = embedding_model.embed(*gallery_images) - - query_emb = embedding_model.embed(test_image) - - # Query should use GPU-accelerated comparison - results = embedding_model.query(query_emb, gallery_embs, top_k=5) - - print(f"\nTop-5 results from gallery of {gallery_size}") - for idx, sim in results: - print(f" Index {idx}: {sim:.4f}") - - assert len(results) == 5, "Should return top-5 results" - # All should be high similarity (same image, allow some variation for image preprocessing) - for idx, sim in results: - assert sim > 0.90, f"Same images should have high similarity, got {sim}" - - -@pytest.mark.heavy -def test_embedding_performance(embedding_model) -> None: - """Measure embedding performance over multiple real video frames.""" - import time - - from dimos.utils.testing import TimedSensorReplay - - # Load actual video frames - data_dir = "unitree_go2_lidar_corrected" - get_data(data_dir) - - video_replay = TimedSensorReplay(f"{data_dir}/video") - - # Collect 10 real frames from the video - test_images = [] - for _ts, frame in video_replay.iterate_ts(duration=1.0): - test_images.append(frame.to_rgb()) - if len(test_images) >= 10: - break - - if len(test_images) < 10: - pytest.skip(f"Not enough video frames found (got {len(test_images)})") - - # Measure single image embedding time - times = [] - for img in test_images: - start = time.perf_counter() - _ = embedding_model.embed(img) - end = time.perf_counter() - elapsed_ms = (end - start) * 1000 - times.append(elapsed_ms) - - # Calculate statistics - avg_time = sum(times) / len(times) - min_time = min(times) - max_time = max(times) - std_time = (sum((t - avg_time) ** 2 for t in times) / len(times)) ** 0.5 - - print("\n" + "=" * 60) - print("Embedding Performance Statistics:") - print("=" * 60) - print(f"Number of images: {len(test_images)}") - print(f"Average time: {avg_time:.2f} ms") - print(f"Min time: {min_time:.2f} ms") - print(f"Max time: {max_time:.2f} ms") - print(f"Std dev: {std_time:.2f} ms") - print(f"Throughput: {1000 / avg_time:.1f} images/sec") - print("=" * 60) - - # Also test batch embedding performance - start = time.perf_counter() - batch_embeddings = embedding_model.embed(*test_images) - end = time.perf_counter() - batch_time = (end - start) * 1000 - batch_per_image = batch_time / len(test_images) - - print("\nBatch Embedding Performance:") - print(f"Total batch time: {batch_time:.2f} ms") - print(f"Time per image (batched): {batch_per_image:.2f} ms") - print(f"Batch throughput: {1000 / batch_per_image:.1f} images/sec") - print(f"Speedup vs single: {avg_time / batch_per_image:.2f}x") - print("=" * 60) - - # Verify embeddings are valid - assert len(batch_embeddings) == len(test_images) - assert all(e.vector is not None for e in batch_embeddings) - - # Sanity check: verify embeddings are meaningful by testing text-image similarity - # Skip for models that don't support text embeddings - if hasattr(embedding_model, "embed_text"): - print("\n" + "=" * 60) - print("Sanity Check: Text-Image Similarity on First Frame") - print("=" * 60) - first_frame_emb = batch_embeddings[0] - - # Test common object/scene queries - test_queries = [ - "indoor scene", - "outdoor scene", - "a person", - "a dog", - "a robot", - "grass and trees", - "furniture", - "a car", - ] - - text_embeddings = embedding_model.embed_text(*test_queries) - similarities = [] - for query, text_emb in zip(test_queries, text_embeddings, strict=False): - sim = first_frame_emb @ text_emb - similarities.append((query, sim)) - - # Sort by similarity - similarities.sort(key=lambda x: x[1], reverse=True) - - print("Top matching concepts:") - for query, sim in similarities[:5]: - print(f" '{query}': {sim:.4f}") - print("=" * 60) diff --git a/dimos/models/embedding/mobileclip.py b/dimos/models/embedding/mobileclip.py index 8ddefd3c87..7c3d7adc69 100644 --- a/dimos/models/embedding/mobileclip.py +++ b/dimos/models/embedding/mobileclip.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,55 +12,55 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +from dataclasses import dataclass +from functools import cached_property +from typing import Any import open_clip from PIL import Image as PILImage import torch import torch.nn.functional as F -from dimos.models.embedding.base import Embedding, EmbeddingModel +from dimos.models.base import LocalModel +from dimos.models.embedding.base import Embedding, EmbeddingModel, EmbeddingModelConfig from dimos.msgs.sensor_msgs import Image +from dimos.utils.data import get_data class MobileCLIPEmbedding(Embedding): ... -class MobileCLIPModel(EmbeddingModel[MobileCLIPEmbedding]): - """MobileCLIP embedding model for vision-language re-identification.""" +@dataclass +class MobileCLIPModelConfig(EmbeddingModelConfig): + model_name: str = "MobileCLIP2-S4" - def __init__( - self, - model_name: str = "MobileCLIP2-S4", - model_path: Path | str | None = None, - device: str | None = None, - normalize: bool = True, - ) -> None: - """ - Initialize MobileCLIP model. - Args: - model_name: Name of the model architecture - model_path: Path to pretrained weights - device: Device to run on (cuda/cpu), auto-detects if None - normalize: Whether to L2 normalize embeddings - """ - if not OPEN_CLIP_AVAILABLE: - raise ImportError( - "open_clip is required for MobileCLIPModel. " - "Install it with: pip install open-clip-torch" - ) +class MobileCLIPModel(EmbeddingModel[MobileCLIPEmbedding], LocalModel): + """MobileCLIP embedding model for vision-language re-identification.""" - self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") - self.normalize = normalize + default_config = MobileCLIPModelConfig + config: MobileCLIPModelConfig - # Load model - pretrained = str(model_path) if model_path else None - self.model, _, self.preprocess = open_clip.create_model_and_transforms( - model_name, pretrained=pretrained + @cached_property + def _model_and_preprocess(self) -> tuple[Any, Any]: + """Load model and transforms (open_clip returns them together).""" + model_path = get_data("models_mobileclip") / (self.config.model_name + ".pt") + model, _, preprocess = open_clip.create_model_and_transforms( + self.config.model_name, pretrained=str(model_path) ) - self.tokenizer = open_clip.get_tokenizer(model_name) - self.model = self.model.eval().to(self.device) + return model.eval().to(self.config.device), preprocess + + @cached_property + def _model(self) -> Any: + return self._model_and_preprocess[0] + + @cached_property + def _preprocess(self) -> Any: + return self._model_and_preprocess[1] + + @cached_property + def _tokenizer(self) -> Any: + return open_clip.get_tokenizer(self.config.model_name) def embed(self, *images: Image) -> MobileCLIPEmbedding | list[MobileCLIPEmbedding]: """Embed one or more images. @@ -72,9 +72,11 @@ def embed(self, *images: Image) -> MobileCLIPEmbedding | list[MobileCLIPEmbeddin # Preprocess and batch with torch.inference_mode(): - batch = torch.stack([self.preprocess(img) for img in pil_images]).to(self.device) - feats = self.model.encode_image(batch) - if self.normalize: + batch = torch.stack([self._preprocess(img) for img in pil_images]).to( + self.config.device + ) + feats = self._model.encode_image(batch) + if self.config.normalize: feats = F.normalize(feats, dim=-1) # Create embeddings (keep as torch.Tensor on device) @@ -91,9 +93,9 @@ def embed_text(self, *texts: str) -> MobileCLIPEmbedding | list[MobileCLIPEmbedd Returns embeddings as torch.Tensor on device for efficient GPU comparisons. """ with torch.inference_mode(): - text_tokens = self.tokenizer(list(texts)).to(self.device) - feats = self.model.encode_text(text_tokens) - if self.normalize: + text_tokens = self._tokenizer(list(texts)).to(self.config.device) + feats = self._model.encode_text(text_tokens) + if self.config.normalize: feats = F.normalize(feats, dim=-1) # Create embeddings (keep as torch.Tensor on device) @@ -103,10 +105,18 @@ def embed_text(self, *texts: str) -> MobileCLIPEmbedding | list[MobileCLIPEmbedd return embeddings[0] if len(texts) == 1 else embeddings - def warmup(self) -> None: - """Warmup the model with a dummy forward pass.""" - dummy_image = torch.randn(1, 3, 224, 224).to(self.device) - dummy_text = self.tokenizer(["warmup"]).to(self.device) + def start(self) -> None: + """Start the model with a dummy forward pass.""" + super().start() + dummy_image = torch.randn(1, 3, 224, 224).to(self.config.device) + dummy_text = self._tokenizer(["warmup"]).to(self.config.device) with torch.inference_mode(): - self.model.encode_image(dummy_image) - self.model.encode_text(dummy_text) + self._model.encode_image(dummy_image) + self._model.encode_text(dummy_text) + + def stop(self) -> None: + """Release model and free GPU memory.""" + for attr in ("_model_and_preprocess", "_model", "_preprocess", "_tokenizer"): + if attr in self.__dict__: + del self.__dict__[attr] + super().stop() diff --git a/dimos/models/embedding/test_embedding.py b/dimos/models/embedding/test_embedding.py new file mode 100644 index 0000000000..a87a2f5a57 --- /dev/null +++ b/dimos/models/embedding/test_embedding.py @@ -0,0 +1,152 @@ +import time +from typing import Any + +import pytest +import torch + +from dimos.models.embedding.clip import CLIPModel +from dimos.models.embedding.mobileclip import MobileCLIPModel +from dimos.models.embedding.treid import TorchReIDModel +from dimos.msgs.sensor_msgs import Image +from dimos.utils.data import get_data + + +@pytest.mark.parametrize( + "model_class,model_name,supports_text", + [ + (CLIPModel, "CLIP", True), + pytest.param(MobileCLIPModel, "MobileCLIP", True), + (TorchReIDModel, "TorchReID", False), + ], + ids=["clip", "mobileclip", "treid"], +) +@pytest.mark.gpu +def test_embedding_model(model_class: type, model_name: str, supports_text: bool) -> None: + """Test embedding functionality across different model types.""" + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + + print(f"\nTesting {model_name} embedding model") + + # Initialize model + print(f"Loading {model_name} model...") + model: Any = model_class() + model.start() + + # Test single image embedding + print("Embedding single image...") + start_time = time.time() + embedding = model.embed(image) + embed_time = time.time() - start_time + + print(f" Vector shape: {embedding.vector.shape}") + print(f" Time: {embed_time:.3f}s") + + assert embedding.vector is not None + assert len(embedding.vector.shape) == 1 # Should be 1D vector + + # Test batch embedding + print("\nTesting batch embedding (3 images)...") + start_time = time.time() + embeddings = model.embed(image, image, image) + batch_time = time.time() - start_time + + print(f" Batch size: {len(embeddings)}") + print(f" Total time: {batch_time:.3f}s") + print(f" Per image: {batch_time / 3:.3f}s") + + assert len(embeddings) == 3 + assert all(e.vector is not None for e in embeddings) + + # Test similarity computation + print("\nTesting similarity computation...") + sim = embedding @ embeddings[0] + print(f" Self-similarity: {sim:.4f}") + # Self-similarity should be ~1.0 for normalized embeddings + assert sim > 0.99, "Self-similarity should be ~1.0 for normalized embeddings" + + # Test text embedding if supported + if supports_text: + print("\nTesting text embedding...") + start_time = time.time() + text_embedding = model.embed_text("a photo of a cafe") + text_time = time.time() - start_time + + print(f" Text vector shape: {text_embedding.vector.shape}") + print(f" Time: {text_time:.3f}s") + + # Test cross-modal similarity + cross_sim = embedding @ text_embedding + print(f" Image-text similarity: {cross_sim:.4f}") + + assert text_embedding.vector is not None + assert embedding.vector.shape == text_embedding.vector.shape + else: + print(f"\nSkipping text embedding (not supported by {model_name})") + + print(f"\n{model_name} embedding test passed!") + + +@pytest.mark.parametrize( + "model_class,model_name", + [ + (CLIPModel, "CLIP"), + pytest.param(MobileCLIPModel, "MobileCLIP"), + ], + ids=["clip", "mobileclip"], +) +@pytest.mark.gpu +def test_text_image_retrieval(model_class: type, model_name: str) -> None: + """Test text-to-image retrieval using embedding similarity.""" + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + + print(f"\nTesting {model_name} text-image retrieval") + + model: Any = model_class(normalize=True) + model.start() + + # Embed images + image_embeddings = model.embed(image, image, image) + + # Embed text queries + queries = ["a cafe", "a dog", "a car"] + text_embeddings = model.embed_text(*queries) + + # Compute similarities + print("\nSimilarity matrix (text x image):") + for query, text_emb in zip(queries, text_embeddings, strict=False): + sims = [text_emb @ img_emb for img_emb in image_embeddings] + print(f" '{query}': {[f'{s:.3f}' for s in sims]}") + + # The cafe query should have highest similarity + cafe_sims = [text_embeddings[0] @ img_emb for img_emb in image_embeddings] + other_sims = [text_embeddings[1] @ img_emb for img_emb in image_embeddings] + + assert cafe_sims[0] > other_sims[0], "Cafe query should match cafe image better than dog query" + + print(f"\n{model_name} retrieval test passed!") + + +@pytest.mark.gpu +def test_embedding_device_transfer() -> None: + """Test embedding device transfer operations.""" + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + + model = CLIPModel() + embedding = model.embed(image) + assert not isinstance(embedding, list) + + # Test to_numpy + np_vec = embedding.to_numpy() + assert not isinstance(np_vec, torch.Tensor) + print(f"NumPy vector shape: {np_vec.shape}") + + # Test to_torch + torch_vec = embedding.to_torch() + assert isinstance(torch_vec, torch.Tensor) + print(f"Torch vector shape: {torch_vec.shape}, device: {torch_vec.device}") + + # Test to_cpu + embedding.to_cpu() + assert isinstance(embedding.vector, torch.Tensor) + assert embedding.vector.device == torch.device("cpu") + print("Successfully moved to CPU") diff --git a/dimos/models/embedding/treid.py b/dimos/models/embedding/treid.py index b00ad11250..a8893d38e4 100644 --- a/dimos/models/embedding/treid.py +++ b/dimos/models/embedding/treid.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,54 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +from dataclasses import dataclass +from functools import cached_property import torch import torch.nn.functional as F from torchreid import utils as torchreid_utils -from dimos.models.embedding.base import Embedding, EmbeddingModel +from dimos.models.base import LocalModel +from dimos.models.embedding.base import Embedding, EmbeddingModel, EmbeddingModelConfig from dimos.msgs.sensor_msgs import Image - -_CUDA_INITIALIZED = False +from dimos.utils.data import get_data class TorchReIDEmbedding(Embedding): ... -class TorchReIDModel(EmbeddingModel[TorchReIDEmbedding]): +# osnet models downloaded from https://kaiyangzhou.github.io/deep-person-reid/MODEL_ZOO.html +# into dimos/data/models_torchreid/ +# feel free to add more +@dataclass +class TorchReIDModelConfig(EmbeddingModelConfig): + model_name: str = "osnet_x1_0" + + +class TorchReIDModel(EmbeddingModel[TorchReIDEmbedding], LocalModel): """TorchReID embedding model for person re-identification.""" - def __init__( - self, - model_name: str = "se_resnext101_32x4d", - model_path: Path | str | None = None, - device: str | None = None, - normalize: bool = False, - ) -> None: - """ - Initialize TorchReID model. + default_config = TorchReIDModelConfig + config: TorchReIDModelConfig - Args: - model_name: Name of the model architecture (e.g., "osnet_x1_0", "osnet_x0_75") - model_path: Path to pretrained weights (.pth.tar file) - device: Device to run on (cuda/cpu), auto-detects if None - normalize: Whether to L2 normalize embeddings - """ - if not TORCHREID_AVAILABLE: - raise ImportError( - "torchreid is required for TorchReIDModel. Install it with: pip install torchreid" - ) - - self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") - self.normalize = normalize - - # Load model using torchreid's FeatureExtractor - model_path_str = str(model_path) if model_path else "" - self.extractor = torchreid_utils.FeatureExtractor( - model_name=model_name, - model_path=model_path_str, - device=self.device, + @cached_property + def _model(self) -> torchreid_utils.FeatureExtractor: + self._ensure_cuda_initialized() + return torchreid_utils.FeatureExtractor( + model_name=self.config.model_name, + model_path=str(get_data("models_torchreid") / (self.config.model_name + ".pth")), + device=self.config.device, ) def embed(self, *images: Image) -> TorchReIDEmbedding | list[TorchReIDEmbedding]: @@ -72,15 +61,15 @@ def embed(self, *images: Image) -> TorchReIDEmbedding | list[TorchReIDEmbedding] # Extract features with torch.inference_mode(): - features = self.extractor(np_images) + features = self._model(np_images) # torchreid may return either numpy array or torch tensor depending on configuration if isinstance(features, torch.Tensor): - features_tensor = features.to(self.device) + features_tensor = features.to(self.config.device) else: - features_tensor = torch.from_numpy(features).to(self.device) + features_tensor = torch.from_numpy(features).to(self.config.device) - if self.normalize: + if self.config.normalize: features_tensor = F.normalize(features_tensor, dim=-1) # Create embeddings (keep as torch.Tensor on device) @@ -102,24 +91,17 @@ def embed_text(self, *texts: str) -> TorchReIDEmbedding | list[TorchReIDEmbeddin "Use CLIP or MobileCLIP for text-image similarity." ) - def warmup(self) -> None: - """Warmup the model with a dummy forward pass.""" - # WORKAROUND: TorchReID can fail with CUBLAS errors when it's the first model to use CUDA. - # Initialize CUDA context with a dummy operation. This only needs to happen once per process. - global _CUDA_INITIALIZED - if self.device == "cuda" and not _CUDA_INITIALIZED: - try: - # Initialize CUDA with a small matmul operation to setup cuBLAS properly - _ = torch.zeros(1, 1, device="cuda") @ torch.zeros(1, 1, device="cuda") - torch.cuda.synchronize() - _CUDA_INITIALIZED = True - except Exception: - # If initialization fails, continue anyway - the warmup might still work - pass + def start(self) -> None: + """Start the model with a dummy forward pass.""" + super().start() # Create a dummy 256x128 image (typical person ReID input size) as numpy array import numpy as np dummy_image = np.random.randint(0, 256, (256, 128, 3), dtype=np.uint8) with torch.inference_mode(): - _ = self.extractor([dummy_image]) + _ = self._model([dummy_image]) + + def stop(self) -> None: + """Release model and free GPU memory.""" + super().stop() diff --git a/dimos/models/labels/__init__.py b/dimos/models/labels/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/models/labels/llava-34b.py b/dimos/models/labels/llava-34b.py deleted file mode 100644 index 52e28ac24e..0000000000 --- a/dimos/models/labels/llava-34b.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import os - -# llava v1.6 -from llama_cpp import Llama -from llama_cpp.llama_chat_format import Llava15ChatHandler -from vqasynth.datasets.utils import image_to_base64_data_uri - - -class Llava: - def __init__( - self, - mmproj: str=f"{os.getcwd()}/models/mmproj-model-f16.gguf", - model_path: str=f"{os.getcwd()}/models/llava-v1.6-34b.Q4_K_M.gguf", - gpu: bool=True, - ) -> None: - chat_handler = Llava15ChatHandler(clip_model_path=mmproj, verbose=True) - n_gpu_layers = 0 - if gpu: - n_gpu_layers = -1 - self.llm = Llama( - model_path=model_path, - chat_handler=chat_handler, - n_ctx=2048, - logits_all=True, - n_gpu_layers=n_gpu_layers, - ) - - def run_inference(self, image, prompt: str, return_json: bool=True): - data_uri = image_to_base64_data_uri(image) - res = self.llm.create_chat_completion( - messages=[ - { - "role": "system", - "content": "You are an assistant who perfectly describes images.", - }, - { - "role": "user", - "content": [ - {"type": "image_url", "image_url": {"url": data_uri}}, - {"type": "text", "text": prompt}, - ], - }, - ] - ) - if return_json: - return list( - set( - self.extract_descriptions_from_incomplete_json( - res["choices"][0]["message"]["content"] - ) - ) - ) - - return res["choices"][0]["message"]["content"] - - def extract_descriptions_from_incomplete_json(self, json_like_str): - last_object_idx = json_like_str.rfind(',"object') - - if last_object_idx != -1: - json_str = json_like_str[:last_object_idx] + "}" - else: - json_str = json_like_str.strip() - if not json_str.endswith("}"): - json_str += "}" - - try: - json_obj = json.loads(json_str) - descriptions = [ - details["description"].replace(".", "") - for key, details in json_obj.items() - if "description" in details - ] - - return descriptions - except json.JSONDecodeError as e: - raise ValueError(f"Error parsing JSON: {e}") diff --git a/dimos/models/manipulation/contact_graspnet_pytorch/inference.py b/dimos/models/manipulation/contact_graspnet_pytorch/inference.py index 4241392d8e..0769fc150d 100644 --- a/dimos/models/manipulation/contact_graspnet_pytorch/inference.py +++ b/dimos/models/manipulation/contact_graspnet_pytorch/inference.py @@ -2,18 +2,20 @@ import glob import os -from contact_graspnet_pytorch import config_utils -from contact_graspnet_pytorch.checkpoints import CheckpointIO -from contact_graspnet_pytorch.contact_grasp_estimator import GraspEstimator -from contact_graspnet_pytorch.data import load_available_input_data -from contact_graspnet_pytorch.visualization_utils_o3d import show_image, visualize_grasps +from contact_graspnet_pytorch import config_utils # type: ignore[import-not-found] +from contact_graspnet_pytorch.checkpoints import CheckpointIO # type: ignore[import-not-found] +from contact_graspnet_pytorch.contact_grasp_estimator import ( # type: ignore[import-not-found] + GraspEstimator, +) +from contact_graspnet_pytorch.data import ( # type: ignore[import-not-found] + load_available_input_data, +) import numpy as np -import torch from dimos.utils.data import get_data -def inference(global_config, +def inference(global_config, # type: ignore[no-untyped-def] ckpt_dir, input_paths, local_regions: bool=True, diff --git a/dimos/models/manipulation/contact_graspnet_pytorch/test_contact_graspnet.py b/dimos/models/manipulation/contact_graspnet_pytorch/test_contact_graspnet.py index b006c98603..7964a24954 100644 --- a/dimos/models/manipulation/contact_graspnet_pytorch/test_contact_graspnet.py +++ b/dimos/models/manipulation/contact_graspnet_pytorch/test_contact_graspnet.py @@ -1,7 +1,5 @@ import glob -import importlib.util import os -import sys import numpy as np import pytest diff --git a/dimos/models/pointcloud/__init__.py b/dimos/models/pointcloud/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/models/pointcloud/pointcloud_utils.py b/dimos/models/pointcloud/pointcloud_utils.py deleted file mode 100644 index 33b4b59607..0000000000 --- a/dimos/models/pointcloud/pointcloud_utils.py +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import random - -import numpy as np -import open3d as o3d - - -def save_pointcloud(pcd, file_path) -> None: - """ - Save a point cloud to a file using Open3D. - """ - o3d.io.write_point_cloud(file_path, pcd) - - -def restore_pointclouds(pointcloud_paths): - restored_pointclouds = [] - for path in pointcloud_paths: - restored_pointclouds.append(o3d.io.read_point_cloud(path)) - return restored_pointclouds - - -def create_point_cloud_from_rgbd(rgb_image, depth_image, intrinsic_parameters): - rgbd_image = o3d.geometry.RGBDImage.create_from_color_and_depth( - o3d.geometry.Image(rgb_image), - o3d.geometry.Image(depth_image), - depth_scale=0.125, # 1000.0, - depth_trunc=10.0, # 10.0, - convert_rgb_to_intensity=False, - ) - intrinsic = o3d.camera.PinholeCameraIntrinsic() - intrinsic.set_intrinsics( - intrinsic_parameters["width"], - intrinsic_parameters["height"], - intrinsic_parameters["fx"], - intrinsic_parameters["fy"], - intrinsic_parameters["cx"], - intrinsic_parameters["cy"], - ) - pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd_image, intrinsic) - return pcd - - -def canonicalize_point_cloud(pcd, canonicalize_threshold: float=0.3): - # Segment the largest plane, assumed to be the floor - plane_model, inliers = pcd.segment_plane( - distance_threshold=0.01, ransac_n=3, num_iterations=1000 - ) - - canonicalized = False - if len(inliers) / len(pcd.points) > canonicalize_threshold: - canonicalized = True - - # Ensure the plane normal points upwards - if np.dot(plane_model[:3], [0, 1, 0]) < 0: - plane_model = -plane_model - - # Normalize the plane normal vector - normal = plane_model[:3] / np.linalg.norm(plane_model[:3]) - - # Compute the new basis vectors - new_y = normal - new_x = np.cross(new_y, [0, 0, -1]) - new_x /= np.linalg.norm(new_x) - new_z = np.cross(new_x, new_y) - - # Create the transformation matrix - transformation = np.identity(4) - transformation[:3, :3] = np.vstack((new_x, new_y, new_z)).T - transformation[:3, 3] = -np.dot(transformation[:3, :3], pcd.points[inliers[0]]) - - # Apply the transformation - pcd.transform(transformation) - - # Additional 180-degree rotation around the Z-axis - rotation_z_180 = np.array( - [[np.cos(np.pi), -np.sin(np.pi), 0], [np.sin(np.pi), np.cos(np.pi), 0], [0, 0, 1]] - ) - pcd.rotate(rotation_z_180, center=(0, 0, 0)) - - return pcd, canonicalized, transformation - else: - return pcd, canonicalized, None - - -# Distance calculations -def human_like_distance(distance_meters) -> str: - # Define the choices with units included, focusing on the 0.1 to 10 meters range - if distance_meters < 1: # For distances less than 1 meter - choices = [ - ( - round(distance_meters * 100, 2), - "centimeters", - 0.2, - ), # Centimeters for very small distances - ( - round(distance_meters * 39.3701, 2), - "inches", - 0.8, - ), # Inches for the majority of cases under 1 meter - ] - elif distance_meters < 3: # For distances less than 3 meters - choices = [ - (round(distance_meters, 2), "meters", 0.5), - ( - round(distance_meters * 3.28084, 2), - "feet", - 0.5, - ), # Feet as a common unit within indoor spaces - ] - else: # For distances from 3 up to 10 meters - choices = [ - ( - round(distance_meters, 2), - "meters", - 0.7, - ), # Meters for clarity and international understanding - ( - round(distance_meters * 3.28084, 2), - "feet", - 0.3, - ), # Feet for additional context - ] - - # Normalize probabilities and make a selection - total_probability = sum(prob for _, _, prob in choices) - cumulative_distribution = [] - cumulative_sum = 0 - for value, unit, probability in choices: - cumulative_sum += probability / total_probability # Normalize probabilities - cumulative_distribution.append((cumulative_sum, value, unit)) - - # Randomly choose based on the cumulative distribution - r = random.random() - for cumulative_prob, value, unit in cumulative_distribution: - if r < cumulative_prob: - return f"{value} {unit}" - - # Fallback to the last choice if something goes wrong - return f"{choices[-1][0]} {choices[-1][1]}" - - -def calculate_distances_between_point_clouds(A, B): - dist_pcd1_to_pcd2 = np.asarray(A.compute_point_cloud_distance(B)) - dist_pcd2_to_pcd1 = np.asarray(B.compute_point_cloud_distance(A)) - combined_distances = np.concatenate((dist_pcd1_to_pcd2, dist_pcd2_to_pcd1)) - avg_dist = np.mean(combined_distances) - return human_like_distance(avg_dist) - - -def calculate_centroid(pcd): - """Calculate the centroid of a point cloud.""" - points = np.asarray(pcd.points) - centroid = np.mean(points, axis=0) - return centroid - - -def calculate_relative_positions(centroids): - """Calculate the relative positions between centroids of point clouds.""" - num_centroids = len(centroids) - relative_positions_info = [] - - for i in range(num_centroids): - for j in range(i + 1, num_centroids): - relative_vector = centroids[j] - centroids[i] - - distance = np.linalg.norm(relative_vector) - relative_positions_info.append( - {"pcd_pair": (i, j), "relative_vector": relative_vector, "distance": distance} - ) - - return relative_positions_info - - -def get_bounding_box_height(pcd): - """ - Compute the height of the bounding box for a given point cloud. - - Parameters: - pcd (open3d.geometry.PointCloud): The input point cloud. - - Returns: - float: The height of the bounding box. - """ - aabb = pcd.get_axis_aligned_bounding_box() - return aabb.get_extent()[1] # Assuming the Y-axis is the up-direction - - -def compare_bounding_box_height(pcd_i, pcd_j): - """ - Compare the bounding box heights of two point clouds. - - Parameters: - pcd_i (open3d.geometry.PointCloud): The first point cloud. - pcd_j (open3d.geometry.PointCloud): The second point cloud. - - Returns: - bool: True if the bounding box of pcd_i is taller than that of pcd_j, False otherwise. - """ - height_i = get_bounding_box_height(pcd_i) - height_j = get_bounding_box_height(pcd_j) - - return height_i > height_j diff --git a/dimos/models/qwen/video_query.py b/dimos/models/qwen/video_query.py index 0f8a3b8f9c..7ba80ae069 100644 --- a/dimos/models/qwen/video_query.py +++ b/dimos/models/qwen/video_query.py @@ -2,26 +2,25 @@ import json import os -from typing import Optional, Tuple import numpy as np from openai import OpenAI from reactivex import Observable, operators as ops from reactivex.subject import Subject -from dimos.agents.agent import OpenAIAgent -from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer +from dimos.agents_deprecated.agent import OpenAIAgent +from dimos.agents_deprecated.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer from dimos.utils.threadpool import get_scheduler BBox = tuple[float, float, float, float] # (x1, y1, x2, y2) def query_single_frame_observable( - video_observable: Observable, + video_observable: Observable, # type: ignore[type-arg] query: str, api_key: str | None = None, model_name: str = "qwen2.5-vl-72b-instruct", -) -> Observable: +) -> Observable: # type: ignore[type-arg] """Process a single frame from a video observable with Qwen model. Args: @@ -55,7 +54,7 @@ def query_single_frame_observable( ) # Create response subject - response_subject = Subject() + response_subject = Subject() # type: ignore[var-annotated] # Create temporary agent for processing agent = OpenAIAgent( @@ -88,7 +87,7 @@ def query_single_frame_observable( def query_single_frame( - image: np.ndarray, + image: np.ndarray, # type: ignore[type-arg] query: str = "Return the center coordinates of the fridge handle as a tuple (x,y)", api_key: str | None = None, model_name: str = "qwen2.5-vl-72b-instruct", @@ -141,7 +140,7 @@ def query_single_frame( frame = image # Create a Subject that will emit the image once - frame_subject = Subject() + frame_subject = Subject() # type: ignore[var-annotated] # Subscribe to frame processing agent.subscribe_to_image_processing(frame_subject) @@ -159,11 +158,11 @@ def query_single_frame( # Clean up agent.dispose_all() - return response + return response # type: ignore[no-any-return] def get_bbox_from_qwen( - video_stream: Observable, object_name: str | None = None + video_stream: Observable, object_name: str | None = None # type: ignore[type-arg] ) -> tuple[BBox, float] | None: """Get bounding box coordinates from Qwen for a specific object or any object. @@ -202,7 +201,7 @@ def get_bbox_from_qwen( return None -def get_bbox_from_qwen_frame(frame, object_name: str | None = None) -> BBox | None: +def get_bbox_from_qwen_frame(frame, object_name: str | None = None) -> BBox | None: # type: ignore[no-untyped-def] """Get bounding box coordinates from Qwen for a specific object or any object using a single frame. Args: diff --git a/dimos/models/segmentation/__init__.py b/dimos/models/segmentation/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/models/segmentation/clipseg.py b/dimos/models/segmentation/clipseg.py deleted file mode 100644 index ca8fbeb6fc..0000000000 --- a/dimos/models/segmentation/clipseg.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from transformers import AutoProcessor, CLIPSegForImageSegmentation - - -class CLIPSeg: - def __init__(self, model_name: str="CIDAS/clipseg-rd64-refined") -> None: - self.clipseg_processor = AutoProcessor.from_pretrained(model_name) - self.clipseg_model = CLIPSegForImageSegmentation.from_pretrained(model_name) - - def run_inference(self, image, text_descriptions): - inputs = self.clipseg_processor( - text=text_descriptions, - images=[image] * len(text_descriptions), - padding=True, - return_tensors="pt", - ) - outputs = self.clipseg_model(**inputs) - logits = outputs.logits - return logits.detach().unsqueeze(1) diff --git a/dimos/models/segmentation/sam.py b/dimos/models/segmentation/sam.py deleted file mode 100644 index 96b23bf984..0000000000 --- a/dimos/models/segmentation/sam.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import torch -from transformers import SamModel, SamProcessor - - -class SAM: - def __init__(self, model_name: str="facebook/sam-vit-huge", device: str="cuda") -> None: - self.device = device - self.sam_model = SamModel.from_pretrained(model_name).to(self.device) - self.sam_processor = SamProcessor.from_pretrained(model_name) - - def run_inference_from_points(self, image, points): - sam_inputs = self.sam_processor(image, input_points=points, return_tensors="pt").to( - self.device - ) - with torch.no_grad(): - sam_outputs = self.sam_model(**sam_inputs) - return self.sam_processor.image_processor.post_process_masks( - sam_outputs.pred_masks.cpu(), - sam_inputs["original_sizes"].cpu(), - sam_inputs["reshaped_input_sizes"].cpu(), - ) diff --git a/dimos/models/segmentation/segment_utils.py b/dimos/models/segmentation/segment_utils.py deleted file mode 100644 index 9b15f353e4..0000000000 --- a/dimos/models/segmentation/segment_utils.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import torch - - -def find_medoid_and_closest_points(points, num_closest: int=5): - """ - Find the medoid from a collection of points and the closest points to the medoid. - - Parameters: - points (np.array): A numpy array of shape (N, D) where N is the number of points and D is the dimensionality. - num_closest (int): Number of closest points to return. - - Returns: - np.array: The medoid point. - np.array: The closest points to the medoid. - """ - distances = np.sqrt(((points[:, np.newaxis, :] - points[np.newaxis, :, :]) ** 2).sum(axis=-1)) - distance_sums = distances.sum(axis=1) - medoid_idx = np.argmin(distance_sums) - medoid = points[medoid_idx] - sorted_indices = np.argsort(distances[medoid_idx]) - closest_indices = sorted_indices[1 : num_closest + 1] - return medoid, points[closest_indices] - - -def sample_points_from_heatmap(heatmap, original_size: int, num_points: int=5, percentile: float=0.95): - """ - Sample points from the given heatmap, focusing on areas with higher values. - """ - width, height = original_size - threshold = np.percentile(heatmap.numpy(), percentile) - masked_heatmap = torch.where(heatmap > threshold, heatmap, torch.tensor(0.0)) - probabilities = torch.softmax(masked_heatmap.flatten(), dim=0) - - attn = torch.sigmoid(heatmap) - w = attn.shape[0] - sampled_indices = torch.multinomial( - torch.tensor(probabilities.ravel()), num_points, replacement=True - ) - - sampled_coords = np.array(np.unravel_index(sampled_indices, attn.shape)).T - medoid, sampled_coords = find_medoid_and_closest_points(sampled_coords) - pts = [] - for pt in sampled_coords.tolist(): - x, y = pt - x = height * x / w - y = width * y / w - pts.append([y, x]) - return pts - - -def apply_mask_to_image(image, mask): - """ - Apply a binary mask to an image. The mask should be a binary array where the regions to keep are True. - """ - masked_image = image.copy() - for c in range(masked_image.shape[2]): - masked_image[:, :, c] = masked_image[:, :, c] * mask - return masked_image diff --git a/dimos/models/test_base.py b/dimos/models/test_base.py new file mode 100644 index 0000000000..3ae6f116ac --- /dev/null +++ b/dimos/models/test_base.py @@ -0,0 +1,136 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for LocalModel and HuggingFaceModel base classes.""" + +from functools import cached_property + +import torch + +from dimos.models.base import HuggingFaceModel, LocalModel + + +class ConcreteLocalModel(LocalModel): + """Concrete implementation for testing.""" + + @cached_property + def _model(self) -> str: + return "loaded_model" + + +class ConcreteHuggingFaceModel(HuggingFaceModel): + """Concrete implementation for testing.""" + + @cached_property + def _model(self) -> str: + return f"hf_model:{self.model_name}" + + +def test_local_model_device_auto_detection() -> None: + """Test that device is auto-detected based on CUDA availability.""" + model = ConcreteLocalModel() + expected = "cuda" if torch.cuda.is_available() else "cpu" + assert model.device == expected + + +def test_local_model_explicit_device() -> None: + """Test that explicit device is respected.""" + model = ConcreteLocalModel(device="cpu") + assert model.device == "cpu" + + +def test_local_model_default_dtype() -> None: + """Test that default dtype is float32 for LocalModel.""" + model = ConcreteLocalModel() + assert model.dtype == torch.float32 + + +def test_local_model_explicit_dtype() -> None: + """Test that explicit dtype is respected.""" + model = ConcreteLocalModel(dtype=torch.float16) + assert model.dtype == torch.float16 + + +def test_local_model_lazy_loading() -> None: + """Test that model is lazily loaded.""" + model = ConcreteLocalModel() + # Model not loaded yet + assert "_model" not in model.__dict__ + # Access triggers loading + _ = model._model + # Now it's cached + assert "_model" in model.__dict__ + assert model._model == "loaded_model" + + +def test_local_model_start_triggers_loading() -> None: + """Test that start() triggers model loading.""" + model = ConcreteLocalModel() + assert "_model" not in model.__dict__ + model.start() + assert "_model" in model.__dict__ + + +def test_huggingface_model_inherits_local_model() -> None: + """Test that HuggingFaceModel inherits from LocalModel.""" + assert issubclass(HuggingFaceModel, LocalModel) + + +def test_huggingface_model_default_dtype() -> None: + """Test that default dtype is float16 for HuggingFaceModel.""" + model = ConcreteHuggingFaceModel(model_name="test/model") + assert model.dtype == torch.float16 + + +def test_huggingface_model_name() -> None: + """Test model_name property.""" + model = ConcreteHuggingFaceModel(model_name="microsoft/Florence-2-large") + assert model.model_name == "microsoft/Florence-2-large" + + +def test_huggingface_model_trust_remote_code() -> None: + """Test trust_remote_code defaults to True.""" + model = ConcreteHuggingFaceModel(model_name="test/model") + assert model.config.trust_remote_code is True + + model2 = ConcreteHuggingFaceModel(model_name="test/model", trust_remote_code=False) + assert model2.config.trust_remote_code is False + + +def test_huggingface_start_loads_model() -> None: + """Test that start() loads model.""" + model = ConcreteHuggingFaceModel(model_name="test/model") + assert "_model" not in model.__dict__ + model.start() + assert "_model" in model.__dict__ + + +def test_move_inputs_to_device() -> None: + """Test _move_inputs_to_device helper.""" + model = ConcreteHuggingFaceModel(model_name="test/model", device="cpu") + + inputs = { + "input_ids": torch.tensor([1, 2, 3]), + "attention_mask": torch.tensor([1, 1, 1]), + "pixel_values": torch.randn(1, 3, 224, 224), + "labels": "not_a_tensor", + } + + moved = model._move_inputs_to_device(inputs) + + assert moved["input_ids"].device.type == "cpu" + assert moved["attention_mask"].device.type == "cpu" + assert moved["pixel_values"].device.type == "cpu" + assert moved["pixel_values"].dtype == torch.float16 # dtype applied + assert moved["labels"] == "not_a_tensor" # non-tensor unchanged diff --git a/dimos/models/vl/README.md b/dimos/models/vl/README.md index 3a8353c69a..c252d47957 100644 --- a/dimos/models/vl/README.md +++ b/dimos/models/vl/README.md @@ -20,3 +20,48 @@ image = Image.from_file("path/to/your/image.jpg") response = model.query(image.data, "What do you see in this image?") print(response) ``` + +## Moondream Hosted Model + +The `MoondreamHostedVlModel` class provides access to the hosted Moondream API for fast vision-language tasks. + +**Prerequisites:** + +You must export your API key before using the model: +```bash +export MOONDREAM_API_KEY="your_api_key_here" +``` + +### Capabilities + +The model supports four modes of operation: + +1. **Caption**: Generate a description of the image. +2. **Query**: Ask natural language questions about the image. +3. **Detect**: Find bounding boxes for specific objects. +4. **Point**: Locate the center points of specific objects. + +### Example Usage + +```python +from dimos.models.vl.moondream_hosted import MoondreamHostedVlModel +from dimos.msgs.sensor_msgs import Image + +model = MoondreamHostedVlModel() +image = Image.from_file("path/to/image.jpg") + +# 1. Caption +print(f"Caption: {model.caption(image)}") + +# 2. Query +print(f"Answer: {model.query(image, 'Is there a person in the image?')}") + +# 3. Detect (returns ImageDetections2D) +detections = model.query_detections(image, "person") +for det in detections.detections: + print(f"Found person at {det.bbox}") + +# 4. Point (returns list of (x, y) coordinates) +points = model.point(image, "person") +print(f"Person centers: {points}") +``` diff --git a/dimos/models/vl/__init__.py b/dimos/models/vl/__init__.py index 8cb0a7944b..6f120f9141 100644 --- a/dimos/models/vl/__init__.py +++ b/dimos/models/vl/__init__.py @@ -1,2 +1,14 @@ -from dimos.models.vl.base import VlModel +from dimos.models.vl.base import Captioner, VlModel +from dimos.models.vl.florence import Florence2Model +from dimos.models.vl.moondream import MoondreamVlModel +from dimos.models.vl.moondream_hosted import MoondreamHostedVlModel from dimos.models.vl.qwen import QwenVlModel + +__all__ = [ + "Captioner", + "Florence2Model", + "MoondreamHostedVlModel", + "MoondreamVlModel", + "QwenVlModel", + "VlModel", +] diff --git a/dimos/models/vl/base.py b/dimos/models/vl/base.py index 7e162b3ccf..93caba4de7 100644 --- a/dimos/models/vl/base.py +++ b/dimos/models/vl/base.py @@ -1,9 +1,13 @@ from abc import ABC, abstractmethod +from dataclasses import dataclass import json import logging +import warnings +from dimos.core.resource import Resource from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type import Detection2DBBox, ImageDetections2D +from dimos.perception.detection.type import Detection2DBBox, Detection2DPoint, ImageDetections2D +from dimos.protocol.service import Configurable # type: ignore[attr-defined] from dimos.utils.data import get_data from dimos.utils.decorators import retry from dimos.utils.llm_utils import extract_json @@ -11,22 +15,58 @@ logger = logging.getLogger(__name__) +class Captioner(ABC): + """Interface for models that can generate image captions.""" + + @abstractmethod + def caption(self, image: Image) -> str: + """Generate a text description of the image. + + Args: + image: Input image to caption + + Returns: + Text description of the image + """ + ... + + def caption_batch(self, *images: Image) -> list[str]: + """Generate captions for multiple images. + + Default implementation calls caption() for each image. + Subclasses may override for more efficient batching. + + Args: + images: Input images to caption + + Returns: + List of text descriptions + """ + return [self.caption(img) for img in images] + + +# Type alias for VLM detection format: [label, x1, y1, x2, y2] +VlmDetection = tuple[str, float, float, float, float] + + def vlm_detection_to_detection2d( - vlm_detection: list, track_id: int, image: Image + vlm_detection: VlmDetection | list[str | float], + track_id: int, + image: Image, ) -> Detection2DBBox | None: """Convert a single VLM detection [label, x1, y1, x2, y2] to Detection2DBBox. Args: - vlm_detection: Single detection list containing [label, x1, y1, x2, y2] + vlm_detection: Single detection tuple/list containing [label, x1, y1, x2, y2] track_id: Track ID to assign to this detection image: Source image for the detection Returns: Detection2DBBox instance or None if invalid """ - # Validate list structure - if not isinstance(vlm_detection, list): - logger.debug(f"VLM detection is not a list: {type(vlm_detection)}") + # Validate list/tuple structure + if not isinstance(vlm_detection, (list, tuple)): + logger.debug(f"VLM detection is not a list/tuple: {type(vlm_detection)}") return None if len(vlm_detection) != 5: @@ -40,12 +80,12 @@ def vlm_detection_to_detection2d( # Validate and convert coordinates try: - coords = [float(x) for x in vlm_detection[1:]] + coords = [float(vlm_detection[i]) for i in range(1, 5)] except (ValueError, TypeError) as e: logger.debug(f"Invalid VLM detection coordinates: {vlm_detection[1:]}. Error: {e}") return None - bbox = tuple(coords) + bbox = (coords[0], coords[1], coords[2], coords[3]) # Use -1 for class_id since VLM doesn't provide it # confidence defaults to 1.0 for VLM @@ -60,47 +100,243 @@ def vlm_detection_to_detection2d( ) -class VlModel(ABC): +# Type alias for VLM point format: [label, x, y] +VlmPoint = tuple[str, float, float] + + +def vlm_point_to_detection2d_point( + vlm_point: VlmPoint | list[str | float], + track_id: int, + image: Image, +) -> Detection2DPoint | None: + """Convert a single VLM point [label, x, y] to Detection2DPoint. + + Args: + vlm_point: Single point tuple/list containing [label, x, y] + track_id: Track ID to assign to this detection + image: Source image for the detection + + Returns: + Detection2DPoint instance or None if invalid + """ + # Validate list/tuple structure + if not isinstance(vlm_point, (list, tuple)): + logger.debug(f"VLM point is not a list/tuple: {type(vlm_point)}") + return None + + if len(vlm_point) != 3: + logger.debug(f"Invalid VLM point length: {len(vlm_point)}, expected 3. Got: {vlm_point}") + return None + + # Extract label + name = str(vlm_point[0]) + + # Validate and convert coordinates + try: + x = float(vlm_point[1]) + y = float(vlm_point[2]) + except (ValueError, TypeError) as e: + logger.debug(f"Invalid VLM point coordinates: {vlm_point[1:]}. Error: {e}") + return None + + return Detection2DPoint( + x=x, + y=y, + name=name, + ts=image.ts, + image=image, + track_id=track_id, + ) + + +@dataclass +class VlModelConfig: + """Configuration for VlModel.""" + + auto_resize: tuple[int, int] | None = None + """Optional (width, height) tuple. If set, images are resized to fit.""" + + +class VlModel(Captioner, Resource, Configurable[VlModelConfig]): + """Vision-language model that can answer questions about images. + + Inherits from Captioner, providing a default caption() implementation + that uses query() with a standard captioning prompt. + + Implements Resource interface for lifecycle management. + """ + + default_config = VlModelConfig + config: VlModelConfig + + def _prepare_image(self, image: Image) -> tuple[Image, float]: + """Prepare image for inference, applying any configured transformations. + + Returns: + Tuple of (prepared_image, scale_factor). Scale factor is 1.0 if no resize. + """ + if self.config.auto_resize is not None: + max_w, max_h = self.config.auto_resize + return image.resize_to_fit(max_w, max_h) + return image, 1.0 + @abstractmethod - def query(self, image: Image, query: str, **kwargs) -> str: ... + def query(self, image: Image, query: str, **kwargs) -> str: ... # type: ignore[no-untyped-def] + + def query_batch(self, images: list[Image], query: str, **kwargs) -> list[str]: # type: ignore[no-untyped-def] + """Query multiple images with the same question. + + Default implementation calls query() for each image sequentially. + Subclasses may override for more efficient batched inference. + + Args: + images: List of input images + query: Question to ask about each image + + Returns: + List of responses, one per image + """ + warnings.warn( + f"{self.__class__.__name__}.query_batch() is using default sequential implementation. " + "Override for efficient batched inference.", + stacklevel=2, + ) + return [self.query(image, query, **kwargs) for image in images] + + def query_multi(self, image: Image, queries: list[str], **kwargs) -> list[str]: # type: ignore[no-untyped-def] + """Query a single image with multiple different questions. + + Default implementation calls query() for each question sequentially. + Subclasses may override for more efficient inference (e.g., by + encoding the image once and reusing it for all queries). + + Args: + image: Input image + queries: List of questions to ask about the image - def warmup(self) -> None: + Returns: + List of responses, one per query + """ + warnings.warn( + f"{self.__class__.__name__}.query_multi() is using default sequential implementation. " + "Override for efficient batched inference.", + stacklevel=2, + ) + return [self.query(image, q, **kwargs) for q in queries] + + def caption(self, image: Image) -> str: + """Generate a caption by querying the VLM with a standard prompt.""" + return self.query(image, "Describe this image concisely.") + + def start(self) -> None: + """Start the model by running a simple query (Resource interface).""" try: image = Image.from_file(get_data("cafe-smol.jpg")).to_rgb() - self._model.detect(image, "person", settings={"max_objects": 1}) + self.query(image, "What is this?") except Exception: pass # requery once if JSON parsing fails - @retry(max_retries=2, on_exception=json.JSONDecodeError, delay=0.0) - def query_json(self, image: Image, query: str) -> dict: + @retry(max_retries=2, on_exception=json.JSONDecodeError, delay=0.0) # type: ignore[untyped-decorator] + def query_json(self, image: Image, query: str) -> dict: # type: ignore[type-arg] response = self.query(image, query) - return extract_json(response) + return extract_json(response) # type: ignore[return-value] - def query_detections(self, image: Image, query: str, **kwargs) -> ImageDetections2D: + def query_detections( + self, image: Image, query: str, **kwargs: object + ) -> ImageDetections2D[Detection2DBBox]: full_query = f"""show me bounding boxes in pixels for this query: `{query}` format should be: - `[ - [label, x1, y1, x2, y2] + ```json + [ + ["label1", x1, y1, x2, y2] + ["label2", x1, y1, x2, y2] ... ]` (etc, multiple matches are possible) If there's no match return `[]`. Label is whatever you think is appropriate - Only respond with the coordinates, no other text.""" + Only respond with JSON, no other text. + """ image_detections = ImageDetections2D(image) + # Get scaled image and scale factor for coordinate rescaling + scaled_image, scale = self._prepare_image(image) + try: - detection_tuples = self.query_json(image, full_query) + detection_tuples = self.query_json(scaled_image, full_query) except Exception: return image_detections for track_id, detection_tuple in enumerate(detection_tuples): + # Scale coordinates back to original image size if resized + if ( + scale != 1.0 + and isinstance(detection_tuple, (list, tuple)) + and len(detection_tuple) == 5 + ): + detection_tuple = [ + detection_tuple[0], # label + detection_tuple[1] / scale, # x1 + detection_tuple[2] / scale, # y1 + detection_tuple[3] / scale, # x2 + detection_tuple[4] / scale, # y2 + ] detection2d = vlm_detection_to_detection2d(detection_tuple, track_id, image) if detection2d is not None and detection2d.is_valid(): image_detections.detections.append(detection2d) return image_detections + + def query_points( + self, image: Image, query: str, **kwargs: object + ) -> ImageDetections2D[Detection2DPoint]: + """Query the VLM for point locations matching the query. + + Args: + image: Input image to query + query: Description of what points to find (e.g., "center of the red ball") + + Returns: + ImageDetections2D containing Detection2DPoint instances + """ + full_query = f"""Show me point coordinates in pixels for this query: `{query}` + + The format should be: + ```json + [ + ["label 1", x, y], + ["label 2", x, y], + ... + ] + + If there's no match return `[]`. Label is whatever you think is appropriate. + Only respond with the JSON, no other text. + """ + + image_detections: ImageDetections2D[Detection2DPoint] = ImageDetections2D(image) + + # Get scaled image and scale factor for coordinate rescaling + scaled_image, scale = self._prepare_image(image) + + try: + point_tuples = self.query_json(scaled_image, full_query) + except Exception: + return image_detections + + for track_id, point_tuple in enumerate(point_tuples): + # Scale coordinates back to original image size if resized + if scale != 1.0 and isinstance(point_tuple, (list, tuple)) and len(point_tuple) == 3: + point_tuple = [ + point_tuple[0], # label + point_tuple[1] / scale, # x + point_tuple[2] / scale, # y + ] + point2d = vlm_point_to_detection2d_point(point_tuple, track_id, image) + if point2d is not None and point2d.is_valid(): + image_detections.detections.append(point2d) + + return image_detections diff --git a/dimos/models/vl/florence.py b/dimos/models/vl/florence.py new file mode 100644 index 0000000000..2e6cf822a8 --- /dev/null +++ b/dimos/models/vl/florence.py @@ -0,0 +1,170 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cached_property + +from PIL import Image as PILImage +import torch +from transformers import AutoModelForCausalLM, AutoProcessor # type: ignore[import-untyped] + +from dimos.models.base import HuggingFaceModel +from dimos.models.vl.base import Captioner +from dimos.msgs.sensor_msgs import Image + + +class Florence2Model(HuggingFaceModel, Captioner): + """Florence-2 captioning model from Microsoft. + + A lightweight, fast captioning model optimized for generating image descriptions + without requiring a text prompt. Supports multiple caption detail levels. + """ + + _model_class = AutoModelForCausalLM + + def __init__( + self, + model_name: str = "microsoft/Florence-2-base", + **kwargs: object, + ) -> None: + """Initialize Florence-2 model. + + Args: + model_name: HuggingFace model name. Options: + - "microsoft/Florence-2-base" (~0.2B, fastest) + - "microsoft/Florence-2-large" (~0.8B, better quality) + **kwargs: Additional config options (device, dtype, warmup, etc.) + """ + super().__init__(model_name=model_name, **kwargs) + + @cached_property + def _processor(self) -> AutoProcessor: + return AutoProcessor.from_pretrained( + self.config.model_name, trust_remote_code=self.config.trust_remote_code + ) + + def caption(self, image: Image, detail: str = "normal") -> str: + """Generate a caption for the image. + + Args: + image: Input image to caption + detail: Level of detail for caption: + - "brief": Short, concise caption + - "normal": Standard caption (default) + - "detailed": More detailed description + + Returns: + Text description of the image + """ + # Map detail level to Florence-2 task prompts + task_prompts = { + "brief": "", + "normal": "", + "detailed": "", + "more_detailed": "", + } + task_prompt = task_prompts.get(detail, "") + + # Convert to PIL + pil_image = PILImage.fromarray(image.to_rgb().data) + + # Process inputs + inputs = self._processor(text=task_prompt, images=pil_image, return_tensors="pt") + inputs = self._move_inputs_to_device(inputs) + + # Generate + with torch.inference_mode(): + generated_ids = self._model.generate( + **inputs, + max_new_tokens=256, + num_beams=3, + do_sample=False, + ) + + # Decode + generated_text = self._processor.batch_decode(generated_ids, skip_special_tokens=False)[0] + + # Parse output - Florence returns structured output + parsed = self._processor.post_process_generation( + generated_text, task=task_prompt, image_size=pil_image.size + ) + + # Extract caption from parsed output + caption: str = parsed.get(task_prompt, generated_text) + return caption.strip() + + def caption_batch(self, *images: Image) -> list[str]: + """Generate captions for multiple images efficiently. + + Args: + images: Input images to caption + + Returns: + List of text descriptions + """ + if not images: + return [] + + task_prompt = "" + + # Convert all to PIL + pil_images = [PILImage.fromarray(img.to_rgb().data) for img in images] + + # Process batch + inputs = self._processor( + text=[task_prompt] * len(images), images=pil_images, return_tensors="pt", padding=True + ) + inputs = self._move_inputs_to_device(inputs) + + # Generate + with torch.inference_mode(): + generated_ids = self._model.generate( + **inputs, + max_new_tokens=256, + num_beams=3, + do_sample=False, + ) + + # Decode all + generated_texts = self._processor.batch_decode(generated_ids, skip_special_tokens=False) + + # Parse outputs + captions = [] + for text, pil_img in zip(generated_texts, pil_images, strict=True): + parsed = self._processor.post_process_generation( + text, task=task_prompt, image_size=pil_img.size + ) + captions.append(parsed.get(task_prompt, text).strip()) + + return captions + + def start(self) -> None: + """Start the model with a dummy forward pass.""" + # Load model and processor via base class + super().start() + + # Run a small inference + dummy = PILImage.new("RGB", (224, 224), color="gray") + inputs = self._processor(text="", images=dummy, return_tensors="pt") + inputs = self._move_inputs_to_device(inputs) + + with torch.inference_mode(): + self._model.generate(**inputs, max_new_tokens=10) + + def stop(self) -> None: + """Release model and free GPU memory.""" + # Clean up processor cached property + if "_processor" in self.__dict__: + del self.__dict__["_processor"] + # Call parent which handles _model cleanup + super().stop() diff --git a/dimos/models/vl/moondream.py b/dimos/models/vl/moondream.py index ce63c70238..f31611e867 100644 --- a/dimos/models/vl/moondream.py +++ b/dimos/models/vl/moondream.py @@ -1,68 +1,133 @@ +from dataclasses import dataclass from functools import cached_property -from typing import Optional +from typing import Any import warnings import numpy as np from PIL import Image as PILImage import torch -from transformers import AutoModelForCausalLM +from transformers import AutoModelForCausalLM # type: ignore[import-untyped] +from dimos.models.base import HuggingFaceModel, HuggingFaceModelConfig from dimos.models.vl.base import VlModel from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type import Detection2DBBox, ImageDetections2D +from dimos.perception.detection.type import Detection2DBBox, Detection2DPoint, ImageDetections2D +# Moondream works well with 512x512 max +MOONDREAM_DEFAULT_AUTO_RESIZE = (512, 512) -class MoondreamVlModel(VlModel): - _model_name: str - _device: str - _dtype: torch.dtype - def __init__( - self, - model_name: str = "vikhyatk/moondream2", - device: str | None = None, - dtype: torch.dtype = torch.bfloat16, - ) -> None: - self._model_name = model_name - self._device = device or ("cuda" if torch.cuda.is_available() else "cpu") - self._dtype = dtype +@dataclass +class MoondreamConfig(HuggingFaceModelConfig): + """Configuration for MoondreamVlModel.""" + + model_name: str = "vikhyatk/moondream2" + dtype: torch.dtype = torch.bfloat16 + auto_resize: tuple[int, int] | None = MOONDREAM_DEFAULT_AUTO_RESIZE + + +class MoondreamVlModel(HuggingFaceModel, VlModel): + _model_class = AutoModelForCausalLM + default_config = MoondreamConfig # type: ignore[assignment] + config: MoondreamConfig # type: ignore[assignment] @cached_property def _model(self) -> AutoModelForCausalLM: + """Load model with compile() for optimization.""" model = AutoModelForCausalLM.from_pretrained( - self._model_name, - trust_remote_code=True, - torch_dtype=self._dtype, - ) - model = model.to(self._device) + self.config.model_name, + trust_remote_code=self.config.trust_remote_code, + torch_dtype=self.config.dtype, + ).to(self.config.device) model.compile() - return model - def query(self, image: Image | np.ndarray, query: str, **kwargs) -> str: + def _to_pil(self, image: Image | np.ndarray[Any, Any]) -> PILImage.Image: + """Convert dimos Image or numpy array to PIL Image, applying auto_resize.""" if isinstance(image, np.ndarray): warnings.warn( - "MoondreamVlModel.query should receive standard dimos Image type, not a numpy array", + "MoondreamVlModel should receive standard dimos Image type, not a numpy array", DeprecationWarning, stacklevel=2, ) image = Image.from_numpy(image) - # Convert dimos Image to PIL Image - # dimos Image stores data in RGB/BGR format, convert to RGB for PIL + image, _ = self._prepare_image(image) rgb_image = image.to_rgb() - pil_image = PILImage.fromarray(rgb_image.data) + return PILImage.fromarray(rgb_image.data) + + def query(self, image: Image | np.ndarray, query: str, **kwargs) -> str: # type: ignore[no-untyped-def, type-arg] + pil_image = self._to_pil(image) # Query the model result = self._model.query(image=pil_image, question=query, reasoning=False) # Handle both dict and string responses if isinstance(result, dict): - return result.get("answer", str(result)) + return result.get("answer", str(result)) # type: ignore[no-any-return] return str(result) - def query_detections(self, image: Image, query: str, **kwargs) -> ImageDetections2D: + def query_batch(self, images: list[Image], query: str, **kwargs) -> list[str]: # type: ignore[no-untyped-def] + """Query multiple images with the same question. + + Note: moondream2's batch_answer is not truly batched - it processes + images sequentially. No speedup over sequential calls. + + Args: + images: List of input images + query: Question to ask about each image + + Returns: + List of responses, one per image + """ + warnings.warn( + "MoondreamVlModel.query_batch() uses moondream's batch_answer which is not " + "truly batched - images are processed sequentially with no speedup.", + stacklevel=2, + ) + if not images: + return [] + + pil_images = [self._to_pil(img) for img in images] + prompts = [query] * len(images) + result: list[str] = self._model.batch_answer(pil_images, prompts) + return result + + def query_multi(self, image: Image, queries: list[str], **kwargs) -> list[str]: # type: ignore[no-untyped-def] + """Query a single image with multiple different questions. + + Optimized implementation that encodes the image once and reuses + the encoded representation for all queries. + + Args: + image: Input image + queries: List of questions to ask about the image + + Returns: + List of responses, one per query + """ + if not queries: + return [] + + # Encode image once + pil_image = self._to_pil(image) + encoded_image = self._model.encode_image(pil_image) + + # Query with each question, reusing the encoded image + results = [] + for query in queries: + result = self._model.query(image=encoded_image, question=query, reasoning=False) + if isinstance(result, dict): + results.append(result.get("answer", str(result))) + else: + results.append(str(result)) + + return results + + def query_detections( + self, image: Image, query: str, **kwargs: object + ) -> ImageDetections2D[Detection2DBBox]: """Detect objects using Moondream's native detect method. Args: @@ -73,7 +138,7 @@ def query_detections(self, image: Image, query: str, **kwargs) -> ImageDetection Returns: ImageDetections2D containing detected bounding boxes """ - pil_image = PILImage.fromarray(image.data) + pil_image = self._to_pil(image) settings = {"max_objects": kwargs.get("max_objects", 5)} result = self._model.detect(pil_image, query, settings=settings) @@ -112,3 +177,44 @@ def query_detections(self, image: Image, query: str, **kwargs) -> ImageDetection image_detections.detections.append(detection) return image_detections + + def query_points( + self, image: Image, query: str, **kwargs: object + ) -> ImageDetections2D[Detection2DPoint]: + """Detect point locations using Moondream's native point method. + + Args: + image: Input image + query: Object query (e.g., "person's head", "center of the ball") + + Returns: + ImageDetections2D containing detected points + """ + pil_image = self._to_pil(image) + + result = self._model.point(pil_image, query) + + # Convert to ImageDetections2D + image_detections: ImageDetections2D[Detection2DPoint] = ImageDetections2D(image) + + # Get image dimensions for converting normalized coords to pixels + height, width = image.height, image.width + + for track_id, point in enumerate(result.get("points", [])): + # Convert normalized coordinates (0-1) to pixel coordinates + x = point["x"] * width + y = point["y"] * height + + detection = Detection2DPoint( + x=x, + y=y, + name=query, + ts=image.ts, + image=image, + track_id=track_id, + ) + + if detection.is_valid(): + image_detections.detections.append(detection) + + return image_detections diff --git a/dimos/models/vl/moondream_hosted.py b/dimos/models/vl/moondream_hosted.py new file mode 100644 index 0000000000..c28a12363f --- /dev/null +++ b/dimos/models/vl/moondream_hosted.py @@ -0,0 +1,136 @@ +from functools import cached_property +import os +import warnings + +import moondream as md # type: ignore[import-untyped] +import numpy as np +from PIL import Image as PILImage + +from dimos.models.vl.base import VlModel +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.type import Detection2DBBox, ImageDetections2D + + +class MoondreamHostedVlModel(VlModel): + _api_key: str | None + + def __init__(self, api_key: str | None = None) -> None: + self._api_key = api_key + + @cached_property + def _client(self) -> md.vl: + api_key = self._api_key or os.getenv("MOONDREAM_API_KEY") + if not api_key: + raise ValueError( + "Moondream API key must be provided or set in MOONDREAM_API_KEY environment variable" + ) + return md.vl(api_key=api_key) + + def _to_pil_image(self, image: Image | np.ndarray) -> PILImage.Image: # type: ignore[type-arg] + if isinstance(image, np.ndarray): + warnings.warn( + "MoondreamHostedVlModel should receive standard dimos Image type, not a numpy array", + DeprecationWarning, + stacklevel=3, + ) + image = Image.from_numpy(image) + + rgb_image = image.to_rgb() + return PILImage.fromarray(rgb_image.data) + + def query(self, image: Image | np.ndarray, query: str, **kwargs) -> str: # type: ignore[no-untyped-def, type-arg] + pil_image = self._to_pil_image(image) + + result = self._client.query(pil_image, query) + return result.get("answer", str(result)) # type: ignore[no-any-return] + + def caption(self, image: Image | np.ndarray, length: str = "normal") -> str: # type: ignore[type-arg] + """Generate a caption for the image. + + Args: + image: Input image + length: Caption length ("normal", "short", "long") + """ + pil_image = self._to_pil_image(image) + result = self._client.caption(pil_image, length=length) + return result.get("caption", str(result)) # type: ignore[no-any-return] + + def query_detections(self, image: Image, query: str, **kwargs) -> ImageDetections2D[Detection2DBBox]: # type: ignore[no-untyped-def] + """Detect objects using Moondream's hosted detect method. + + Args: + image: Input image + query: Object query (e.g., "person", "car") + max_objects: Maximum number of objects to detect (not directly supported by hosted API args in docs, + but we handle the output) + + Returns: + ImageDetections2D containing detected bounding boxes + """ + pil_image = self._to_pil_image(image) + + # API docs: detect(image, object) -> {"objects": [...]} + result = self._client.detect(pil_image, query) + objects = result.get("objects", []) + + # Convert to ImageDetections2D + image_detections = ImageDetections2D(image) + height, width = image.height, image.width + + for track_id, obj in enumerate(objects): + # Expected format from docs: Region with x_min, y_min, x_max, y_max + # Assuming normalized coordinates as per local model and standard VLM behavior + x_min_norm = obj.get("x_min", 0.0) + y_min_norm = obj.get("y_min", 0.0) + x_max_norm = obj.get("x_max", 1.0) + y_max_norm = obj.get("y_max", 1.0) + + x1 = x_min_norm * width + y1 = y_min_norm * height + x2 = x_max_norm * width + y2 = y_max_norm * height + + bbox = (x1, y1, x2, y2) + + detection = Detection2DBBox( + bbox=bbox, + track_id=track_id, + class_id=-1, + confidence=1.0, + name=query, + ts=image.ts, + image=image, + ) + + if detection.is_valid(): + image_detections.detections.append(detection) + + return image_detections + + def point(self, image: Image, query: str) -> list[tuple[float, float]]: + """Get coordinates of specific objects in an image. + + Args: + image: Input image + query: Object query + + Returns: + List of (x, y) pixel coordinates + """ + pil_image = self._to_pil_image(image) + result = self._client.point(pil_image, query) + points = result.get("points", []) + + pixel_points = [] + height, width = image.height, image.width + + for p in points: + x_norm = p.get("x", 0.0) + y_norm = p.get("y", 0.0) + pixel_points.append((x_norm * width, y_norm * height)) + + return pixel_points + + def stop(self) -> None: + pass + diff --git a/dimos/models/vl/qwen.py b/dimos/models/vl/qwen.py index c302d12c22..b1d3d6f036 100644 --- a/dimos/models/vl/qwen.py +++ b/dimos/models/vl/qwen.py @@ -1,25 +1,29 @@ +from dataclasses import dataclass from functools import cached_property import os -from typing import Optional import numpy as np from openai import OpenAI -from dimos.models.vl.base import VlModel +from dimos.models.vl.base import VlModel, VlModelConfig from dimos.msgs.sensor_msgs import Image -class QwenVlModel(VlModel): - _model_name: str - _api_key: str | None +@dataclass +class QwenVlModelConfig(VlModelConfig): + """Configuration for Qwen VL model.""" + + model_name: str = "qwen2.5-vl-72b-instruct" + api_key: str | None = None + - def __init__(self, api_key: str | None = None, model_name: str = "qwen2.5-vl-72b-instruct") -> None: - self._model_name = model_name - self._api_key = api_key +class QwenVlModel(VlModel): + default_config = QwenVlModelConfig + config: QwenVlModelConfig @cached_property def _client(self) -> OpenAI: - api_key = self._api_key or os.getenv("ALIBABA_API_KEY") + api_key = self.config.api_key or os.getenv("ALIBABA_API_KEY") if not api_key: raise ValueError( "Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable" @@ -30,7 +34,7 @@ def _client(self) -> OpenAI: api_key=api_key, ) - def query(self, image: Image | np.ndarray, query: str) -> str: + def query(self, image: Image | np.ndarray, query: str) -> str: # type: ignore[override, type-arg] if isinstance(image, np.ndarray): import warnings @@ -42,10 +46,13 @@ def query(self, image: Image | np.ndarray, query: str) -> str: image = Image.from_numpy(image) + # Apply auto_resize if configured + image, _ = self._prepare_image(image) + img_base64 = image.to_base64() response = self._client.chat.completions.create( - model=self._model_name, + model=self.config.model_name, messages=[ { "role": "user", @@ -60,4 +67,9 @@ def query(self, image: Image | np.ndarray, query: str) -> str: ], ) - return response.choices[0].message.content + return response.choices[0].message.content # type: ignore[return-value] + + def stop(self) -> None: + """Release the OpenAI client.""" + if "_client" in self.__dict__: + del self.__dict__["_client"] diff --git a/dimos/models/vl/test_base.py b/dimos/models/vl/test_base.py index 3d8575fab3..a7296bd87b 100644 --- a/dimos/models/vl/test_base.py +++ b/dimos/models/vl/test_base.py @@ -1,10 +1,13 @@ import os from unittest.mock import MagicMock +from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations import pytest +from dimos.core import LCMTransport +from dimos.models.vl.moondream import MoondreamVlModel from dimos.models.vl.qwen import QwenVlModel -from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs import Image, ImageFormat from dimos.perception.detection.type import ImageDetections2D from dimos.utils.data import get_data @@ -103,3 +106,41 @@ def test_query_detections_real() -> None: assert detection.is_valid() print(f"Found {len(detections.detections)} detections for query '{query}'") + + +@pytest.mark.tool +def test_query_points() -> None: + """Test query_points with real API calls (requires API key).""" + # Load test image + image = Image.from_file(get_data("cafe.jpg"), format=ImageFormat.RGB).to_rgb() + + # Initialize the model (will use real API) + model = MoondreamVlModel() + + # Query for points in the image + query = "center of each person's head" + detections = model.query_points(image, query) + + assert isinstance(detections, ImageDetections2D) + print(detections) + + # Check that detections were found + if detections.detections: + for point in detections.detections: + # Verify each point has expected attributes + assert hasattr(point, "x") + assert hasattr(point, "y") + assert point.name + assert point.confidence == 1.0 + assert point.class_id == -1 # VLM detections use -1 for class_id + assert point.is_valid() + + print(f"Found {len(detections.detections)} points for query '{query}'") + + image_topic: LCMTransport[Image] = LCMTransport("/image", Image) + image_topic.publish(image) + image_topic.lcm.stop() + + annotations: LCMTransport[ImageAnnotations] = LCMTransport("/annotations", ImageAnnotations) + annotations.publish(detections.to_foxglove_annotations()) + annotations.lcm.stop() diff --git a/dimos/models/vl/test_captioner.py b/dimos/models/vl/test_captioner.py new file mode 100644 index 0000000000..081f3bcefc --- /dev/null +++ b/dimos/models/vl/test_captioner.py @@ -0,0 +1,90 @@ +from collections.abc import Generator +import time +from typing import Protocol, TypeVar + +import pytest + +from dimos.models.vl.florence import Florence2Model +from dimos.models.vl.moondream import MoondreamVlModel +from dimos.msgs.sensor_msgs import Image +from dimos.utils.data import get_data + + +class CaptionerModel(Protocol): + """Intersection of Captioner and Resource for testing.""" + + def caption(self, image: Image) -> str: ... + def caption_batch(self, *images: Image) -> list[str]: ... + def start(self) -> None: ... + def stop(self) -> None: ... + + +M = TypeVar("M", bound=CaptionerModel) + + +@pytest.fixture(scope="module") +def test_image() -> Image: + return Image.from_file(get_data("cafe.jpg")).to_rgb() + + +def generic_model_fixture(model_type: type[M]) -> Generator[M, None, None]: + model_instance = model_type() + model_instance.start() + yield model_instance + model_instance.stop() + + +@pytest.fixture(params=[Florence2Model, MoondreamVlModel]) +def captioner_model(request: pytest.FixtureRequest) -> Generator[CaptionerModel, None, None]: + yield from generic_model_fixture(request.param) + + +@pytest.fixture(params=[Florence2Model]) +def florence2_model(request: pytest.FixtureRequest) -> Generator[Florence2Model, None, None]: + yield from generic_model_fixture(request.param) + + +@pytest.mark.gpu +def test_captioner(captioner_model: CaptionerModel, test_image: Image) -> None: + """Test captioning functionality across different model types.""" + # Test single caption + start_time = time.time() + caption = captioner_model.caption(test_image) + caption_time = time.time() - start_time + + print(f" Caption: {caption}") + print(f" Time: {caption_time:.3f}s") + + assert isinstance(caption, str) + assert len(caption) > 0 + + # Test batch captioning + print("\nTesting batch captioning (3 images)...") + start_time = time.time() + captions = captioner_model.caption_batch(test_image, test_image, test_image) + batch_time = time.time() - start_time + + print(f" Captions: {captions}") + print(f" Total time: {batch_time:.3f}s") + print(f" Per image: {batch_time / 3:.3f}s") + + assert len(captions) == 3 + assert all(isinstance(c, str) and len(c) > 0 for c in captions) + + +@pytest.mark.gpu +def test_florence2_detail_levels(florence2_model: Florence2Model, test_image: Image) -> None: + """Test Florence-2 different detail levels.""" + detail_levels = ["brief", "normal", "detailed", "more_detailed"] + + for detail in detail_levels: + print(f"\nDetail level: {detail}") + start_time = time.time() + caption = florence2_model.caption(test_image, detail=detail) + caption_time = time.time() - start_time + + print(f" Caption ({len(caption)} chars): {caption[:100]}...") + print(f" Time: {caption_time:.3f}s") + + assert isinstance(caption, str) + assert len(caption) > 0 diff --git a/dimos/models/vl/test_models.py b/dimos/models/vl/test_models.py index a30951669c..e69de29bb2 100644 --- a/dimos/models/vl/test_models.py +++ b/dimos/models/vl/test_models.py @@ -1,92 +0,0 @@ -import time -from typing import TYPE_CHECKING - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations -import pytest - -from dimos.core import LCMTransport -from dimos.models.vl.moondream import MoondreamVlModel -from dimos.models.vl.qwen import QwenVlModel -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.detectors.yolo import Yolo2DDetector -from dimos.perception.detection.type import ImageDetections2D -from dimos.utils.data import get_data - -if TYPE_CHECKING: - from dimos.models.vl.base import VlModel - - -@pytest.mark.parametrize( - "model_class,model_name", - [ - (MoondreamVlModel, "Moondream"), - (QwenVlModel, "Qwen"), - ], - ids=["moondream", "qwen"], -) -@pytest.mark.gpu -def test_vlm(model_class, model_name: str) -> None: - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - - print(f"Testing {model_name}") - - # Initialize model - print(f"Loading {model_name} model...") - model: VlModel = model_class() - model.warmup() - - queries = [ - "glasses", - "blue shirt", - "bulb", - "cigarette", - "reflection of a car", - "knee", - "flowers on the left table", - "shoes", - "leftmost persons ear", - "rightmost arm", - ] - - all_detections = ImageDetections2D(image) - query_times = [] - - # # First, run YOLO detection - # print("\nRunning YOLO detection...") - # yolo_detector = Yolo2DDetector() - # yolo_detections = yolo_detector.process_image(image) - # print(f" YOLO found {len(yolo_detections.detections)} objects") - # all_detections.detections.extend(yolo_detections.detections) - # annotations_transport.publish(all_detections.to_foxglove_annotations()) - - # Publish to LCM with model-specific channel names - annotations_transport: LCMTransport[ImageAnnotations] = LCMTransport( - "/annotations", ImageAnnotations - ) - - image_transport: LCMTransport[Image] = LCMTransport("/image", Image) - - image_transport.publish(image) - - # Then run VLM queries - for query in queries: - print(f"\nQuerying for: {query}") - start_time = time.time() - detections = model.query_detections(image, query, max_objects=5) - query_time = time.time() - start_time - query_times.append(query_time) - - print(f" Found {len(detections)} detections in {query_time:.3f}s") - all_detections.detections.extend(detections.detections) - annotations_transport.publish(all_detections.to_foxglove_annotations()) - - avg_time = sum(query_times) / len(query_times) if query_times else 0 - print(f"\n{model_name} Results:") - print(f" Average query time: {avg_time:.3f}s") - print(f" Total detections: {len(all_detections)}") - print(all_detections) - - annotations_transport.publish(all_detections.to_foxglove_annotations()) - - annotations_transport.lcm.stop() - image_transport.lcm.stop() diff --git a/dimos/models/vl/test_vlm.py b/dimos/models/vl/test_vlm.py new file mode 100644 index 0000000000..1bf20eb680 --- /dev/null +++ b/dimos/models/vl/test_vlm.py @@ -0,0 +1,306 @@ +import time +from typing import TYPE_CHECKING + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + ImageAnnotations, +) +import pytest + +from dimos.core import LCMTransport +from dimos.models.vl.moondream import MoondreamVlModel +from dimos.models.vl.qwen import QwenVlModel +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.type import ImageDetections2D +from dimos.utils.cli.plot import bar +from dimos.utils.data import get_data + +if TYPE_CHECKING: + from dimos.models.vl.base import VlModel + + +# For these tests you can run foxglove-bridge to visualize results +# You can also run lcm-spy to confirm that messages are being published + + +@pytest.mark.parametrize( + "model_class,model_name", + [ + (MoondreamVlModel, "Moondream"), + (QwenVlModel, "Qwen"), + ], +) +@pytest.mark.gpu +def test_vlm_bbox_detections(model_class: "type[VlModel]", model_name: str) -> None: + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + + print(f"Testing {model_name}") + + # Initialize model + print(f"Loading {model_name} model...") + model: VlModel = model_class() + model.start() + + queries = [ + "glasses", + "blue shirt", + "bulb", + "cigarette", + "reflection of a car", + "knee", + "flowers on the left table", + "shoes", + "leftmost persons ear", + "rightmost arm", + ] + + all_detections = ImageDetections2D(image) + query_times = [] + + # Publish to LCM with model-specific channel names + annotations_transport: LCMTransport[ImageAnnotations] = LCMTransport( + "/annotations", ImageAnnotations + ) + + image_transport: LCMTransport[Image] = LCMTransport("/image", Image) + + image_transport.publish(image) + + # Then run VLM queries + for query in queries: + print(f"\nQuerying for: {query}") + start_time = time.time() + detections = model.query_detections(image, query, max_objects=5) + query_time = time.time() - start_time + query_times.append(query_time) + + print(f" Found {len(detections)} detections in {query_time:.3f}s") + all_detections.detections.extend(detections.detections) + annotations_transport.publish(all_detections.to_foxglove_annotations()) + + avg_time = sum(query_times) / len(query_times) if query_times else 0 + print(f"\n{model_name} Results:") + print(f" Average query time: {avg_time:.3f}s") + print(f" Total detections: {len(all_detections)}") + print(all_detections) + + annotations_transport.publish(all_detections.to_foxglove_annotations()) + + annotations_transport.lcm.stop() + image_transport.lcm.stop() + model.stop() + + +@pytest.mark.parametrize( + "model_class,model_name", + [ + (MoondreamVlModel, "Moondream"), + (QwenVlModel, "Qwen"), + ], +) +@pytest.mark.gpu +def test_vlm_point_detections(model_class: "type[VlModel]", model_name: str) -> None: + """Test VLM point detection capabilities.""" + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + + print(f"Testing {model_name} point detection") + + # Initialize model + print(f"Loading {model_name} model...") + model: VlModel = model_class() + model.start() + + queries = [ + "center of each person's head", + "tip of the nose", + "center of the glasses", + "cigarette tip", + "center of each light bulb", + "center of each shoe", + ] + + all_detections = ImageDetections2D(image) + query_times = [] + + # Publish to LCM with model-specific channel names + annotations_transport: LCMTransport[ImageAnnotations] = LCMTransport( + "/annotations", ImageAnnotations + ) + + image_transport: LCMTransport[Image] = LCMTransport("/image", Image) + + image_transport.publish(image) + + # Then run VLM queries + for query in queries: + print(f"\nQuerying for: {query}") + start_time = time.time() + detections = model.query_points(image, query) + query_time = time.time() - start_time + query_times.append(query_time) + + print(f" Found {len(detections)} points in {query_time:.3f}s") + all_detections.detections.extend(detections.detections) + annotations_transport.publish(all_detections.to_foxglove_annotations()) + + avg_time = sum(query_times) / len(query_times) if query_times else 0 + print(f"\n{model_name} Results:") + print(f" Average query time: {avg_time:.3f}s") + print(f" Total points: {len(all_detections)}") + print(all_detections) + + annotations_transport.publish(all_detections.to_foxglove_annotations()) + + annotations_transport.lcm.stop() + image_transport.lcm.stop() + model.stop() + + +@pytest.mark.parametrize( + "model_class,model_name", + [ + (MoondreamVlModel, "Moondream"), + ], +) +@pytest.mark.gpu +def test_vlm_query_multi(model_class: "type[VlModel]", model_name: str) -> None: + """Test query_multi optimization - single image, multiple queries.""" + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + + print(f"\nTesting {model_name} query_multi optimization") + + model: VlModel = model_class() + model.start() + + queries = [ + "How many people are in this image?", + "What color is the leftmost person's shirt?", + "Are there any glasses visible?", + "What's on the table?", + ] + + # Sequential queries + print("\nSequential queries:") + start_time = time.time() + sequential_results = [model.query(image, q) for q in queries] + sequential_time = time.time() - start_time + print(f" Time: {sequential_time:.3f}s") + + # Batched queries (encode image once) + print("\nBatched queries (query_multi):") + start_time = time.time() + batch_results = model.query_multi(image, queries) + batch_time = time.time() - start_time + print(f" Time: {batch_time:.3f}s") + + speedup_pct = (sequential_time - batch_time) / sequential_time * 100 + print(f"\nSpeedup: {speedup_pct:.1f}%") + + # Print results + for q, seq_r, batch_r in zip(queries, sequential_results, batch_results, strict=True): + print(f"\nQ: {q}") + print(f" Sequential: {seq_r[:120]}...") + print(f" Batch: {batch_r[:120]}...") + + model.stop() + + +@pytest.mark.parametrize( + "model_class,model_name", + [ + (MoondreamVlModel, "Moondream"), + ], +) +@pytest.mark.tool +@pytest.mark.gpu +def test_vlm_query_batch(model_class: "type[VlModel]", model_name: str) -> None: + """Test query_batch optimization - multiple images, same query.""" + from dimos.utils.testing import TimedSensorReplay + + # Load 5 frames at 1-second intervals using TimedSensorReplay + replay = TimedSensorReplay[Image]("unitree_go2_office_walk2/video") + images = [replay.find_closest_seek(i).to_rgb() for i in range(0, 10, 2)] + + print(f"\nTesting {model_name} query_batch with {len(images)} images") + + model: VlModel = model_class() + model.start() + + query = "Describe this image in a short sentence" + + # Sequential queries (print as they come in) + print("\nSequential queries:") + sequential_results = [] + start_time = time.time() + for i, img in enumerate(images): + result = model.query(img, query) + sequential_results.append(result) + print(f" [{i}] {result[:120]}...") + sequential_time = time.time() - start_time + print(f" Time: {sequential_time:.3f}s") + + # Batched queries (pre-encode all images) + print("\nBatched queries (query_batch):") + start_time = time.time() + batch_results = model.query_batch(images, query) + batch_time = time.time() - start_time + for i, result in enumerate(batch_results): + print(f" [{i}] {result[:120]}...") + print(f" Time: {batch_time:.3f}s") + + speedup_pct = (sequential_time - batch_time) / sequential_time * 100 + print(f"\nSpeedup: {speedup_pct:.1f}%") + + # Verify results are valid strings + assert len(batch_results) == len(images) + assert all(isinstance(r, str) and len(r) > 0 for r in batch_results) + + model.stop() + + +@pytest.mark.parametrize( + "model_class,sizes", + [ + (MoondreamVlModel, [None, (512, 512), (256, 256)]), + (QwenVlModel, [None, (512, 512), (256, 256)]), + ], +) +@pytest.mark.gpu +def test_vlm_resize( + model_class: "type[VlModel]", + sizes: list[tuple[int, int] | None], +) -> None: + """Test VLM auto_resize effect on performance.""" + from dimos.utils.testing import TimedSensorReplay + + replay = TimedSensorReplay[Image]("unitree_go2_office_walk2/video") + image = replay.find_closest_seek(0).to_rgb() + + labels: list[str] = [] + avg_times: list[float] = [] + + for auto_resize in sizes: + resize_str = f"{auto_resize[0]}x{auto_resize[1]}" if auto_resize else "full" + print(f"\nOriginal image: {image.width}x{image.height}, auto_resize: {resize_str}") + + model: VlModel = model_class(auto_resize=auto_resize) + model.start() + + times = [] + for i in range(3): + start = time.time() + result = model.query_detections(image, "box") + elapsed = time.time() - start + times.append(elapsed) + print(f" [{i}] ({elapsed:.2f}s)", result) + + avg = sum(times) / len(times) + print(f"Avg time: {avg:.2f}s") + labels.append(resize_str) + avg_times.append(avg) + + # Free GPU memory before next model + model.stop() + + # Plot results + print(f"\n{model_class.__name__} resize performance:") + bar(labels, avg_times, title=f"{model_class.__name__} Query Time", ylabel="seconds") diff --git a/dimos/msgs/foxglove_msgs/Color.py b/dimos/msgs/foxglove_msgs/Color.py index ed19911eb7..954b10c8b9 100644 --- a/dimos/msgs/foxglove_msgs/Color.py +++ b/dimos/msgs/foxglove_msgs/Color.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ from dimos_lcm.foxglove_msgs import Color as LCMColor -class Color(LCMColor): +class Color(LCMColor): # type: ignore[misc] """Color with convenience methods.""" @classmethod diff --git a/dimos/msgs/foxglove_msgs/ImageAnnotations.py b/dimos/msgs/foxglove_msgs/ImageAnnotations.py index 1f58b09d73..aff7c5f7cb 100644 --- a/dimos/msgs/foxglove_msgs/ImageAnnotations.py +++ b/dimos/msgs/foxglove_msgs/ImageAnnotations.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,22 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations as FoxgloveImageAnnotations +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + ImageAnnotations as FoxgloveImageAnnotations, +) -class ImageAnnotations(FoxgloveImageAnnotations): +class ImageAnnotations(FoxgloveImageAnnotations): # type: ignore[misc] def __add__(self, other: "ImageAnnotations") -> "ImageAnnotations": points = self.points + other.points texts = self.texts + other.texts + circles = self.circles + other.circles return ImageAnnotations( texts=texts, texts_length=len(texts), points=points, points_length=len(points), + circles=circles, + circles_length=len(circles), ) def agent_encode(self) -> str: if len(self.texts) == 0: - return None - return list(map(lambda t: t.text, self.texts)) + return None # type: ignore[return-value] + return list(map(lambda t: t.text, self.texts)) # type: ignore[return-value] diff --git a/dimos/msgs/foxglove_msgs/__init__.py b/dimos/msgs/foxglove_msgs/__init__.py index 36698f5484..945ebf94c9 100644 --- a/dimos/msgs/foxglove_msgs/__init__.py +++ b/dimos/msgs/foxglove_msgs/__init__.py @@ -1 +1,3 @@ from dimos.msgs.foxglove_msgs.ImageAnnotations import ImageAnnotations + +__all__ = ["ImageAnnotations"] diff --git a/dimos/msgs/geometry_msgs/Pose.py b/dimos/msgs/geometry_msgs/Pose.py index 0bb69d84bf..bf6a821cc8 100644 --- a/dimos/msgs/geometry_msgs/Pose.py +++ b/dimos/msgs/geometry_msgs/Pose.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,14 +16,21 @@ from typing import TypeAlias -from dimos_lcm.geometry_msgs import Pose as LCMPose, Transform as LCMTransform +from dimos_lcm.geometry_msgs import ( + Pose as LCMPose, + Transform as LCMTransform, +) try: - from geometry_msgs.msg import Point as ROSPoint, Pose as ROSPose, Quaternion as ROSQuaternion + from geometry_msgs.msg import ( # type: ignore[attr-defined] + Point as ROSPoint, + Pose as ROSPose, + Quaternion as ROSQuaternion, + ) except ImportError: - ROSPose = None - ROSPoint = None - ROSQuaternion = None + ROSPose = None # type: ignore[assignment, misc] + ROSPoint = None # type: ignore[assignment, misc] + ROSQuaternion = None # type: ignore[assignment, misc] from plum import dispatch @@ -40,7 +47,7 @@ ) -class Pose(LCMPose): +class Pose(LCMPose): # type: ignore[misc] position: Vector3 orientation: Quaternion msg_name = "geometry_msgs.Pose" @@ -51,13 +58,13 @@ def __init__(self) -> None: self.position = Vector3(0.0, 0.0, 0.0) self.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, x: int | float, y: int | float, z: int | float) -> None: """Initialize a pose with position and identity orientation.""" self.position = Vector3(x, y, z) self.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( self, x: int | float, @@ -72,7 +79,7 @@ def __init__( self.position = Vector3(x, y, z) self.orientation = Quaternion(qx, qy, qz, qw) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( self, position: VectorConvertable | Vector3 | None = None, @@ -86,25 +93,25 @@ def __init__( self.position = Vector3(position) self.orientation = Quaternion(orientation) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, pose_tuple: tuple[VectorConvertable, QuaternionConvertable]) -> None: """Initialize from a tuple of (position, orientation).""" self.position = Vector3(pose_tuple[0]) self.orientation = Quaternion(pose_tuple[1]) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, pose_dict: dict[str, VectorConvertable | QuaternionConvertable]) -> None: """Initialize from a dictionary with 'position' and 'orientation' keys.""" self.position = Vector3(pose_dict["position"]) self.orientation = Quaternion(pose_dict["orientation"]) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, pose: Pose) -> None: """Initialize from another Pose (copy constructor).""" self.position = Vector3(pose.position) self.orientation = Quaternion(pose.orientation) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, lcm_pose: LCMPose) -> None: """Initialize from an LCM Pose.""" self.position = Vector3(lcm_pose.position.x, lcm_pose.position.y, lcm_pose.position.z) @@ -155,7 +162,7 @@ def __str__(self) -> str: f"quaternion=[{self.orientation}])" ) - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two poses are equal.""" if not isinstance(other, Pose): return False @@ -240,11 +247,11 @@ def to_ros_msg(self) -> ROSPose: Returns: ROS Pose message """ - ros_msg = ROSPose() - ros_msg.position = ROSPoint( + ros_msg = ROSPose() # type: ignore[no-untyped-call] + ros_msg.position = ROSPoint( # type: ignore[no-untyped-call] x=float(self.position.x), y=float(self.position.y), z=float(self.position.z) ) - ros_msg.orientation = ROSQuaternion( + ros_msg.orientation = ROSQuaternion( # type: ignore[no-untyped-call] x=float(self.orientation.x), y=float(self.orientation.y), z=float(self.orientation.z), @@ -259,7 +266,7 @@ def to_pose(value: Pose) -> Pose: return value -@dispatch +@dispatch # type: ignore[no-redef] def to_pose(value: PoseConvertable) -> Pose: """Convert a pose-compatible value to a Pose object.""" return Pose(value) diff --git a/dimos/msgs/geometry_msgs/PoseStamped.py b/dimos/msgs/geometry_msgs/PoseStamped.py index 770f41b641..406c5d7ac7 100644 --- a/dimos/msgs/geometry_msgs/PoseStamped.py +++ b/dimos/msgs/geometry_msgs/PoseStamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,17 +14,20 @@ from __future__ import annotations +import math import time from typing import BinaryIO, TypeAlias from dimos_lcm.geometry_msgs import PoseStamped as LCMPoseStamped try: - from geometry_msgs.msg import PoseStamped as ROSPoseStamped + from geometry_msgs.msg import ( # type: ignore[attr-defined] + PoseStamped as ROSPoseStamped, + ) except ImportError: - ROSPoseStamped = None - + ROSPoseStamped = None # type: ignore[assignment, misc] from plum import dispatch +import rerun as rr from dimos.msgs.geometry_msgs.Pose import Pose from dimos.msgs.geometry_msgs.Quaternion import Quaternion, QuaternionConvertable @@ -40,7 +43,7 @@ ) -def sec_nsec(ts): +def sec_nsec(ts): # type: ignore[no-untyped-def] s = int(ts) return [s, int((ts - s) * 1_000_000_000)] @@ -51,7 +54,7 @@ class PoseStamped(Pose, Timestamped): frame_id: str @dispatch - def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: + def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: # type: ignore[no-untyped-def] self.frame_id = frame_id self.ts = ts if ts != 0 else time.time() super().__init__(**kwargs) @@ -59,9 +62,9 @@ def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: def lcm_encode(self) -> bytes: lcm_mgs = LCMPoseStamped() lcm_mgs.pose = self - [lcm_mgs.header.stamp.sec, lcm_mgs.header.stamp.nsec] = sec_nsec(self.ts) + [lcm_mgs.header.stamp.sec, lcm_mgs.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] lcm_mgs.header.frame_id = self.frame_id - return lcm_mgs.lcm_encode() + return lcm_mgs.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes | BinaryIO) -> PoseStamped: @@ -81,9 +84,34 @@ def lcm_decode(cls, data: bytes | BinaryIO) -> PoseStamped: def __str__(self) -> str: return ( f"PoseStamped(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " - f"euler=[{self.roll:.3f}, {self.pitch:.3f}, {self.yaw:.3f}])" + f"euler=[{math.degrees(self.roll):.1f}, {math.degrees(self.pitch):.1f}, {math.degrees(self.yaw):.1f}])" + ) + + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to rerun Transform3D format. + + Returns a Transform3D that can be logged to Rerun to position + child entities in the transform hierarchy. + """ + return rr.Transform3D( + translation=[self.x, self.y, self.z], + rotation=rr.Quaternion( + xyzw=[ + self.orientation.x, + self.orientation.y, + self.orientation.z, + self.orientation.w, + ] + ), ) + def to_rerun_arrow(self, length: float = 0.5): # type: ignore[no-untyped-def] + """Convert to rerun Arrows3D format for visualization.""" + origin = [[self.x, self.y, self.z]] + forward = self.orientation.rotate_vector(Vector3(length, 0, 0)) + vector = [[forward.x, forward.y, forward.z]] + return rr.Arrows3D(origins=origin, vectors=vector) + def new_transform_to(self, name: str) -> Transform: return self.find_transform( PoseStamped( @@ -113,7 +141,7 @@ def find_transform(self, other: PoseStamped) -> Transform: ) @classmethod - def from_ros_msg(cls, ros_msg: ROSPoseStamped) -> PoseStamped: + def from_ros_msg(cls, ros_msg: ROSPoseStamped) -> PoseStamped: # type: ignore[override] """Create a PoseStamped from a ROS geometry_msgs/PoseStamped message. Args: @@ -135,13 +163,13 @@ def from_ros_msg(cls, ros_msg: ROSPoseStamped) -> PoseStamped: orientation=pose.orientation, ) - def to_ros_msg(self) -> ROSPoseStamped: + def to_ros_msg(self) -> ROSPoseStamped: # type: ignore[override] """Convert to a ROS geometry_msgs/PoseStamped message. Returns: ROS PoseStamped message """ - ros_msg = ROSPoseStamped() + ros_msg = ROSPoseStamped() # type: ignore[no-untyped-call] # Set header ros_msg.header.frame_id = self.frame_id diff --git a/dimos/msgs/geometry_msgs/PoseWithCovariance.py b/dimos/msgs/geometry_msgs/PoseWithCovariance.py index ba2c360935..b619679a78 100644 --- a/dimos/msgs/geometry_msgs/PoseWithCovariance.py +++ b/dimos/msgs/geometry_msgs/PoseWithCovariance.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,14 +16,18 @@ from typing import TYPE_CHECKING, TypeAlias -from dimos_lcm.geometry_msgs import PoseWithCovariance as LCMPoseWithCovariance +from dimos_lcm.geometry_msgs import ( + PoseWithCovariance as LCMPoseWithCovariance, +) import numpy as np from plum import dispatch try: - from geometry_msgs.msg import PoseWithCovariance as ROSPoseWithCovariance + from geometry_msgs.msg import ( # type: ignore[attr-defined] + PoseWithCovariance as ROSPoseWithCovariance, + ) except ImportError: - ROSPoseWithCovariance = None + ROSPoseWithCovariance = None # type: ignore[assignment, misc] from dimos.msgs.geometry_msgs.Pose import Pose, PoseConvertable @@ -33,13 +37,13 @@ # Types that can be converted to/from PoseWithCovariance PoseWithCovarianceConvertable: TypeAlias = ( - tuple[PoseConvertable, list[float] | np.ndarray] + tuple[PoseConvertable, list[float] | np.ndarray] # type: ignore[type-arg] | LCMPoseWithCovariance - | dict[str, PoseConvertable | list[float] | np.ndarray] + | dict[str, PoseConvertable | list[float] | np.ndarray] # type: ignore[type-arg] ) -class PoseWithCovariance(LCMPoseWithCovariance): +class PoseWithCovariance(LCMPoseWithCovariance): # type: ignore[misc] pose: Pose msg_name = "geometry_msgs.PoseWithCovariance" @@ -49,9 +53,11 @@ def __init__(self) -> None: self.pose = Pose() self.covariance = np.zeros(36) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( - self, pose: Pose | PoseConvertable, covariance: list[float] | np.ndarray | None = None + self, + pose: Pose | PoseConvertable, + covariance: list[float] | np.ndarray | None = None, # type: ignore[type-arg] ) -> None: """Initialize with pose and optional covariance.""" self.pose = Pose(pose) if not isinstance(pose, Pose) else pose @@ -60,20 +66,20 @@ def __init__( else: self.covariance = np.array(covariance, dtype=float).reshape(36) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, pose_with_cov: PoseWithCovariance) -> None: """Initialize from another PoseWithCovariance (copy constructor).""" self.pose = Pose(pose_with_cov.pose) self.covariance = np.array(pose_with_cov.covariance).copy() - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, lcm_pose_with_cov: LCMPoseWithCovariance) -> None: """Initialize from an LCM PoseWithCovariance.""" self.pose = Pose(lcm_pose_with_cov.pose) self.covariance = np.array(lcm_pose_with_cov.covariance) - @dispatch - def __init__(self, pose_dict: dict[str, PoseConvertable | list[float] | np.ndarray]) -> None: + @dispatch # type: ignore[no-redef] + def __init__(self, pose_dict: dict[str, PoseConvertable | list[float] | np.ndarray]) -> None: # type: ignore[type-arg] """Initialize from a dictionary with 'pose' and 'covariance' keys.""" self.pose = Pose(pose_dict["pose"]) covariance = pose_dict.get("covariance") @@ -82,13 +88,13 @@ def __init__(self, pose_dict: dict[str, PoseConvertable | list[float] | np.ndarr else: self.covariance = np.array(covariance, dtype=float).reshape(36) - @dispatch - def __init__(self, pose_tuple: tuple[PoseConvertable, list[float] | np.ndarray]) -> None: + @dispatch # type: ignore[no-redef] + def __init__(self, pose_tuple: tuple[PoseConvertable, list[float] | np.ndarray]) -> None: # type: ignore[type-arg] """Initialize from a tuple of (pose, covariance).""" self.pose = Pose(pose_tuple[0]) self.covariance = np.array(pose_tuple[1], dtype=float).reshape(36) - def __getattribute__(self, name: str): + def __getattribute__(self, name: str): # type: ignore[no-untyped-def] """Override to ensure covariance is always returned as numpy array.""" if name == "covariance": cov = object.__getattribute__(self, "covariance") @@ -97,7 +103,7 @@ def __getattribute__(self, name: str): return cov return super().__getattribute__(name) - def __setattr__(self, name: str, value) -> None: + def __setattr__(self, name: str, value) -> None: # type: ignore[no-untyped-def] """Override to ensure covariance is stored as numpy array.""" if name == "covariance": if not isinstance(value, np.ndarray): @@ -145,17 +151,17 @@ def yaw(self) -> float: return self.pose.yaw @property - def covariance_matrix(self) -> np.ndarray: + def covariance_matrix(self) -> np.ndarray: # type: ignore[type-arg] """Get covariance as 6x6 matrix.""" - return self.covariance.reshape(6, 6) + return self.covariance.reshape(6, 6) # type: ignore[has-type, no-any-return] @covariance_matrix.setter - def covariance_matrix(self, value: np.ndarray) -> None: + def covariance_matrix(self, value: np.ndarray) -> None: # type: ignore[type-arg] """Set covariance from 6x6 matrix.""" - self.covariance = np.array(value).reshape(36) + self.covariance = np.array(value).reshape(36) # type: ignore[has-type] def __repr__(self) -> str: - return f"PoseWithCovariance(pose={self.pose!r}, covariance=<{self.covariance.shape[0] if isinstance(self.covariance, np.ndarray) else len(self.covariance)} elements>)" + return f"PoseWithCovariance(pose={self.pose!r}, covariance=<{self.covariance.shape[0] if isinstance(self.covariance, np.ndarray) else len(self.covariance)} elements>)" # type: ignore[has-type] def __str__(self) -> str: return ( @@ -164,22 +170,22 @@ def __str__(self) -> str: f"cov_trace={np.trace(self.covariance_matrix):.3f})" ) - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two PoseWithCovariance are equal.""" if not isinstance(other, PoseWithCovariance): return False - return self.pose == other.pose and np.allclose(self.covariance, other.covariance) + return self.pose == other.pose and np.allclose(self.covariance, other.covariance) # type: ignore[has-type] def lcm_encode(self) -> bytes: """Encode to LCM binary format.""" lcm_msg = LCMPoseWithCovariance() lcm_msg.pose = self.pose # LCM expects list, not numpy array - if isinstance(self.covariance, np.ndarray): - lcm_msg.covariance = self.covariance.tolist() + if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] + lcm_msg.covariance = self.covariance.tolist() # type: ignore[has-type] else: - lcm_msg.covariance = list(self.covariance) - return lcm_msg.lcm_encode() + lcm_msg.covariance = list(self.covariance) # type: ignore[has-type] + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes) -> PoseWithCovariance: @@ -217,11 +223,11 @@ def to_ros_msg(self) -> ROSPoseWithCovariance: ROS PoseWithCovariance message """ - ros_msg = ROSPoseWithCovariance() + ros_msg = ROSPoseWithCovariance() # type: ignore[no-untyped-call] ros_msg.pose = self.pose.to_ros_msg() # ROS expects list, not numpy array - if isinstance(self.covariance, np.ndarray): - ros_msg.covariance = self.covariance.tolist() + if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] + ros_msg.covariance = self.covariance.tolist() # type: ignore[has-type] else: - ros_msg.covariance = list(self.covariance) + ros_msg.covariance = list(self.covariance) # type: ignore[has-type] return ros_msg diff --git a/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py index 3683a15fbd..c6138fd064 100644 --- a/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py +++ b/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,14 +17,18 @@ import time from typing import TypeAlias -from dimos_lcm.geometry_msgs import PoseWithCovarianceStamped as LCMPoseWithCovarianceStamped +from dimos_lcm.geometry_msgs import ( + PoseWithCovarianceStamped as LCMPoseWithCovarianceStamped, +) import numpy as np from plum import dispatch try: - from geometry_msgs.msg import PoseWithCovarianceStamped as ROSPoseWithCovarianceStamped + from geometry_msgs.msg import ( # type: ignore[attr-defined] + PoseWithCovarianceStamped as ROSPoseWithCovarianceStamped, + ) except ImportError: - ROSPoseWithCovarianceStamped = None + ROSPoseWithCovarianceStamped = None # type: ignore[assignment, misc] from dimos.msgs.geometry_msgs.Pose import Pose, PoseConvertable from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance @@ -32,13 +36,13 @@ # Types that can be converted to/from PoseWithCovarianceStamped PoseWithCovarianceStampedConvertable: TypeAlias = ( - tuple[PoseConvertable, list[float] | np.ndarray] + tuple[PoseConvertable, list[float] | np.ndarray] # type: ignore[type-arg] | LCMPoseWithCovarianceStamped - | dict[str, PoseConvertable | list[float] | np.ndarray | float | str] + | dict[str, PoseConvertable | list[float] | np.ndarray | float | str] # type: ignore[type-arg] ) -def sec_nsec(ts): +def sec_nsec(ts): # type: ignore[no-untyped-def] s = int(ts) return [s, int((ts - s) * 1_000_000_000)] @@ -55,13 +59,13 @@ def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: self.ts = ts if ts != 0 else time.time() super().__init__(**kwargs) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( self, ts: float = 0.0, frame_id: str = "", pose: Pose | PoseConvertable | None = None, - covariance: list[float] | np.ndarray | None = None, + covariance: list[float] | np.ndarray | None = None, # type: ignore[type-arg] ) -> None: """Initialize with timestamp, frame_id, pose and covariance.""" self.frame_id = frame_id @@ -75,13 +79,13 @@ def lcm_encode(self) -> bytes: lcm_msg = LCMPoseWithCovarianceStamped() lcm_msg.pose.pose = self.pose # LCM expects list, not numpy array - if isinstance(self.covariance, np.ndarray): - lcm_msg.pose.covariance = self.covariance.tolist() + if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] + lcm_msg.pose.covariance = self.covariance.tolist() # type: ignore[has-type] else: - lcm_msg.pose.covariance = list(self.covariance) - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + lcm_msg.pose.covariance = list(self.covariance) # type: ignore[has-type] + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] lcm_msg.header.frame_id = self.frame_id - return lcm_msg.lcm_encode() + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes) -> PoseWithCovarianceStamped: @@ -113,7 +117,7 @@ def __str__(self) -> str: ) @classmethod - def from_ros_msg(cls, ros_msg: ROSPoseWithCovarianceStamped) -> PoseWithCovarianceStamped: + def from_ros_msg(cls, ros_msg: ROSPoseWithCovarianceStamped) -> PoseWithCovarianceStamped: # type: ignore[override] """Create a PoseWithCovarianceStamped from a ROS geometry_msgs/PoseWithCovarianceStamped message. Args: @@ -133,17 +137,17 @@ def from_ros_msg(cls, ros_msg: ROSPoseWithCovarianceStamped) -> PoseWithCovarian ts=ts, frame_id=ros_msg.header.frame_id, pose=pose_with_cov.pose, - covariance=pose_with_cov.covariance, + covariance=pose_with_cov.covariance, # type: ignore[has-type] ) - def to_ros_msg(self) -> ROSPoseWithCovarianceStamped: + def to_ros_msg(self) -> ROSPoseWithCovarianceStamped: # type: ignore[override] """Convert to a ROS geometry_msgs/PoseWithCovarianceStamped message. Returns: ROS PoseWithCovarianceStamped message """ - ros_msg = ROSPoseWithCovarianceStamped() + ros_msg = ROSPoseWithCovarianceStamped() # type: ignore[no-untyped-call] # Set header ros_msg.header.frame_id = self.frame_id @@ -153,9 +157,9 @@ def to_ros_msg(self) -> ROSPoseWithCovarianceStamped: # Set pose with covariance ros_msg.pose.pose = self.pose.to_ros_msg() # ROS expects list, not numpy array - if isinstance(self.covariance, np.ndarray): - ros_msg.pose.covariance = self.covariance.tolist() + if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] + ros_msg.pose.covariance = self.covariance.tolist() # type: ignore[has-type] else: - ros_msg.pose.covariance = list(self.covariance) + ros_msg.pose.covariance = list(self.covariance) # type: ignore[has-type] return ros_msg diff --git a/dimos/msgs/geometry_msgs/Quaternion.py b/dimos/msgs/geometry_msgs/Quaternion.py index 6ce8c3bf2d..d19436d441 100644 --- a/dimos/msgs/geometry_msgs/Quaternion.py +++ b/dimos/msgs/geometry_msgs/Quaternion.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,15 +22,15 @@ from dimos_lcm.geometry_msgs import Quaternion as LCMQuaternion import numpy as np from plum import dispatch -from scipy.spatial.transform import Rotation as R +from scipy.spatial.transform import Rotation as R # type: ignore[import-untyped] from dimos.msgs.geometry_msgs.Vector3 import Vector3 # Types that can be converted to/from Quaternion -QuaternionConvertable: TypeAlias = Sequence[int | float] | LCMQuaternion | np.ndarray +QuaternionConvertable: TypeAlias = Sequence[int | float] | LCMQuaternion | np.ndarray # type: ignore[type-arg] -class Quaternion(LCMQuaternion): +class Quaternion(LCMQuaternion): # type: ignore[misc] x: float = 0.0 y: float = 0.0 z: float = 0.0 @@ -38,29 +38,29 @@ class Quaternion(LCMQuaternion): msg_name = "geometry_msgs.Quaternion" @classmethod - def lcm_decode(cls, data: bytes | BinaryIO): + def lcm_decode(cls, data: bytes | BinaryIO): # type: ignore[no-untyped-def] if not hasattr(data, "read"): data = BytesIO(data) if data.read(8) != cls._get_packed_fingerprint(): raise ValueError("Decode error") - return cls._lcm_decode_one(data) + return cls._lcm_decode_one(data) # type: ignore[no-untyped-call] @classmethod - def _lcm_decode_one(cls, buf): + def _lcm_decode_one(cls, buf): # type: ignore[no-untyped-def] return cls(struct.unpack(">dddd", buf.read(32))) @dispatch def __init__(self) -> None: ... - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, x: int | float, y: int | float, z: int | float, w: int | float) -> None: self.x = float(x) self.y = float(y) self.z = float(z) self.w = float(w) - @dispatch - def __init__(self, sequence: Sequence[int | float] | np.ndarray) -> None: + @dispatch # type: ignore[no-redef] + def __init__(self, sequence: Sequence[int | float] | np.ndarray) -> None: # type: ignore[type-arg] if isinstance(sequence, np.ndarray): if sequence.size != 4: raise ValueError("Quaternion requires exactly 4 components [x, y, z, w]") @@ -73,12 +73,12 @@ def __init__(self, sequence: Sequence[int | float] | np.ndarray) -> None: self.z = sequence[2] self.w = sequence[3] - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, quaternion: Quaternion) -> None: """Initialize from another Quaternion (copy constructor).""" self.x, self.y, self.z, self.w = quaternion.x, quaternion.y, quaternion.z, quaternion.w - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, lcm_quaternion: LCMQuaternion) -> None: """Initialize from an LCM Quaternion.""" self.x, self.y, self.z, self.w = ( @@ -96,7 +96,7 @@ def to_list(self) -> list[float]: """List representation of the quaternion (x, y, z, w).""" return [self.x, self.y, self.z, self.w] - def to_numpy(self) -> np.ndarray: + def to_numpy(self) -> np.ndarray: # type: ignore[type-arg] """Numpy array representation of the quaternion (x, y, z, w).""" return np.array([self.x, self.y, self.z, self.w]) @@ -170,7 +170,7 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.__repr__() - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] if not isinstance(other, Quaternion): return False return self.x == other.x and self.y == other.y and self.z == other.z and self.w == other.w diff --git a/dimos/msgs/geometry_msgs/Transform.py b/dimos/msgs/geometry_msgs/Transform.py index b168eceaa5..3a52f5a8c0 100644 --- a/dimos/msgs/geometry_msgs/Transform.py +++ b/dimos/msgs/geometry_msgs/Transform.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,17 +23,18 @@ ) try: - from geometry_msgs.msg import ( + from geometry_msgs.msg import ( # type: ignore[attr-defined] Quaternion as ROSQuaternion, Transform as ROSTransform, TransformStamped as ROSTransformStamped, Vector3 as ROSVector3, ) except ImportError: - ROSTransformStamped = None - ROSTransform = None - ROSVector3 = None - ROSQuaternion = None + ROSTransformStamped = None # type: ignore[assignment, misc] + ROSTransform = None # type: ignore[assignment, misc] + ROSVector3 = None # type: ignore[assignment, misc] + ROSQuaternion = None # type: ignore[assignment, misc] +import rerun as rr from dimos.msgs.geometry_msgs.Quaternion import Quaternion from dimos.msgs.geometry_msgs.Vector3 import Vector3 @@ -49,7 +50,7 @@ class Transform(Timestamped): child_frame_id: str msg_name = "tf2_msgs.TFMessage" - def __init__( + def __init__( # type: ignore[no-untyped-def] self, translation: Vector3 | None = None, rotation: Quaternion | None = None, @@ -64,13 +65,23 @@ def __init__( self.translation = translation if translation is not None else Vector3() self.rotation = rotation if rotation is not None else Quaternion() + def now(self) -> Transform: + """Return a copy of this Transform with the current timestamp.""" + return Transform( + translation=self.translation, + rotation=self.rotation, + frame_id=self.frame_id, + child_frame_id=self.child_frame_id, + ts=time.time(), + ) + def __repr__(self) -> str: return f"Transform(translation={self.translation!r}, rotation={self.rotation!r})" def __str__(self) -> str: - return f"Transform:\n {self.frame_id} -> {self.child_frame_id} Translation: {self.translation}\n Rotation: {self.rotation}" + return f"{self.frame_id} -> {self.child_frame_id}\n Translation: {self.translation}\n Rotation: {self.rotation}" - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two transforms are equal.""" if not isinstance(other, Transform): return False @@ -199,7 +210,7 @@ def to_ros_transform_stamped(self) -> ROSTransformStamped: ROS TransformStamped message """ - ros_msg = ROSTransformStamped() + ros_msg = ROSTransformStamped() # type: ignore[no-untyped-call] # Set header ros_msg.header.frame_id = self.frame_id @@ -210,10 +221,10 @@ def to_ros_transform_stamped(self) -> ROSTransformStamped: ros_msg.child_frame_id = self.child_frame_id # Set transform - ros_msg.transform.translation = ROSVector3( + ros_msg.transform.translation = ROSVector3( # type: ignore[no-untyped-call] x=self.translation.x, y=self.translation.y, z=self.translation.z ) - ros_msg.transform.rotation = ROSQuaternion( + ros_msg.transform.rotation = ROSQuaternion( # type: ignore[no-untyped-call] x=self.rotation.x, y=self.rotation.y, z=self.rotation.z, w=self.rotation.w ) @@ -224,7 +235,7 @@ def __neg__(self) -> Transform: return self.inverse() @classmethod - def from_pose(cls, frame_id: str, pose: Pose | PoseStamped) -> Transform: + def from_pose(cls, frame_id: str, pose: Pose | PoseStamped) -> Transform: # type: ignore[name-defined] """Create a Transform from a Pose or PoseStamped. Args: @@ -255,7 +266,7 @@ def from_pose(cls, frame_id: str, pose: Pose | PoseStamped) -> Transform: else: raise TypeError(f"Expected Pose or PoseStamped, got {type(pose).__name__}") - def to_pose(self, **kwargs) -> PoseStamped: + def to_pose(self, **kwargs) -> PoseStamped: # type: ignore[name-defined, no-untyped-def] """Create a Transform from a Pose or PoseStamped. Args: @@ -277,7 +288,7 @@ def to_pose(self, **kwargs) -> PoseStamped: **kwargs, ) - def to_matrix(self) -> np.ndarray: + def to_matrix(self) -> np.ndarray: # type: ignore[name-defined] """Convert Transform to a 4x4 transformation matrix. Returns a homogeneous transformation matrix that represents both @@ -349,3 +360,18 @@ def lcm_decode(cls, data: bytes | BinaryIO) -> Transform: child_frame_id=lcm_transform_stamped.child_frame_id, ts=ts, ) + + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to rerun Transform3D format with frame IDs. + + Returns: + rr.Transform3D archetype for logging to rerun with parent/child frames + """ + return rr.Transform3D( + translation=[self.translation.x, self.translation.y, self.translation.z], + rotation=rr.Quaternion( + xyzw=[self.rotation.x, self.rotation.y, self.rotation.z, self.rotation.w] + ), + parent_frame=self.frame_id, # type: ignore[call-arg] + child_frame=self.child_frame_id, # type: ignore[call-arg] + ) diff --git a/dimos/msgs/geometry_msgs/Twist.py b/dimos/msgs/geometry_msgs/Twist.py index e824e1cfbf..5184afc5f7 100644 --- a/dimos/msgs/geometry_msgs/Twist.py +++ b/dimos/msgs/geometry_msgs/Twist.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,10 +18,13 @@ from plum import dispatch try: - from geometry_msgs.msg import Twist as ROSTwist, Vector3 as ROSVector3 + from geometry_msgs.msg import ( # type: ignore[attr-defined] + Twist as ROSTwist, + Vector3 as ROSVector3, + ) except ImportError: - ROSTwist = None - ROSVector3 = None + ROSTwist = None # type: ignore[assignment, misc] + ROSVector3 = None # type: ignore[assignment, misc] # Import Quaternion at runtime for beartype compatibility # (beartype needs to resolve forward references at runtime) @@ -29,7 +32,7 @@ from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorLike -class Twist(LCMTwist): +class Twist(LCMTwist): # type: ignore[misc] linear: Vector3 angular: Vector3 msg_name = "geometry_msgs.Twist" @@ -40,32 +43,32 @@ def __init__(self) -> None: self.linear = Vector3() self.angular = Vector3() - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, linear: VectorLike, angular: VectorLike) -> None: """Initialize a twist from linear and angular velocities.""" self.linear = Vector3(linear) self.angular = Vector3(angular) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, linear: VectorLike, angular: Quaternion) -> None: """Initialize a twist from linear velocity and angular as quaternion (converted to euler).""" self.linear = Vector3(linear) self.angular = angular.to_euler() - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, twist: Twist) -> None: """Initialize from another Twist (copy constructor).""" self.linear = Vector3(twist.linear) self.angular = Vector3(twist.angular) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, lcm_twist: LCMTwist) -> None: """Initialize from an LCM Twist.""" self.linear = Vector3(lcm_twist.linear) self.angular = Vector3(lcm_twist.angular) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, **kwargs) -> None: """Handle keyword arguments for LCM compatibility.""" linear = kwargs.get("linear", Vector3()) @@ -79,7 +82,7 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"Twist:\n Linear: {self.linear}\n Angular: {self.angular}" - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two twists are equal.""" if not isinstance(other, Twist): return False @@ -127,9 +130,9 @@ def to_ros_msg(self) -> ROSTwist: ROS Twist message """ - ros_msg = ROSTwist() - ros_msg.linear = ROSVector3(x=self.linear.x, y=self.linear.y, z=self.linear.z) - ros_msg.angular = ROSVector3(x=self.angular.x, y=self.angular.y, z=self.angular.z) + ros_msg = ROSTwist() # type: ignore[no-untyped-call] + ros_msg.linear = ROSVector3(x=self.linear.x, y=self.linear.y, z=self.linear.z) # type: ignore[no-untyped-call] + ros_msg.angular = ROSVector3(x=self.angular.x, y=self.angular.y, z=self.angular.z) # type: ignore[no-untyped-call] return ros_msg diff --git a/dimos/msgs/geometry_msgs/TwistStamped.py b/dimos/msgs/geometry_msgs/TwistStamped.py index 1a14d8cb0d..f5305509e5 100644 --- a/dimos/msgs/geometry_msgs/TwistStamped.py +++ b/dimos/msgs/geometry_msgs/TwistStamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,9 +21,11 @@ from plum import dispatch try: - from geometry_msgs.msg import TwistStamped as ROSTwistStamped + from geometry_msgs.msg import ( # type: ignore[attr-defined] + TwistStamped as ROSTwistStamped, + ) except ImportError: - ROSTwistStamped = None + ROSTwistStamped = None # type: ignore[assignment, misc] from dimos.msgs.geometry_msgs.Twist import Twist from dimos.msgs.geometry_msgs.Vector3 import VectorConvertable @@ -35,7 +37,7 @@ ) -def sec_nsec(ts): +def sec_nsec(ts): # type: ignore[no-untyped-def] s = int(ts) return [s, int((ts - s) * 1_000_000_000)] @@ -46,7 +48,7 @@ class TwistStamped(Twist, Timestamped): frame_id: str @dispatch - def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: + def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: # type: ignore[no-untyped-def] self.frame_id = frame_id self.ts = ts if ts != 0 else time.time() super().__init__(**kwargs) @@ -54,9 +56,9 @@ def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: def lcm_encode(self) -> bytes: lcm_msg = LCMTwistStamped() lcm_msg.twist = self - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] lcm_msg.header.frame_id = self.frame_id - return lcm_msg.lcm_encode() + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes | BinaryIO) -> TwistStamped: @@ -75,7 +77,7 @@ def __str__(self) -> str: ) @classmethod - def from_ros_msg(cls, ros_msg: ROSTwistStamped) -> TwistStamped: + def from_ros_msg(cls, ros_msg: ROSTwistStamped) -> TwistStamped: # type: ignore[override] """Create a TwistStamped from a ROS geometry_msgs/TwistStamped message. Args: @@ -98,14 +100,14 @@ def from_ros_msg(cls, ros_msg: ROSTwistStamped) -> TwistStamped: angular=twist.angular, ) - def to_ros_msg(self) -> ROSTwistStamped: + def to_ros_msg(self) -> ROSTwistStamped: # type: ignore[override] """Convert to a ROS geometry_msgs/TwistStamped message. Returns: ROS TwistStamped message """ - ros_msg = ROSTwistStamped() + ros_msg = ROSTwistStamped() # type: ignore[no-untyped-call] # Set header ros_msg.header.frame_id = self.frame_id diff --git a/dimos/msgs/geometry_msgs/TwistWithCovariance.py b/dimos/msgs/geometry_msgs/TwistWithCovariance.py index 53e77beaf7..1abbe54468 100644 --- a/dimos/msgs/geometry_msgs/TwistWithCovariance.py +++ b/dimos/msgs/geometry_msgs/TwistWithCovariance.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,27 +16,31 @@ from typing import TypeAlias -from dimos_lcm.geometry_msgs import TwistWithCovariance as LCMTwistWithCovariance +from dimos_lcm.geometry_msgs import ( + TwistWithCovariance as LCMTwistWithCovariance, +) import numpy as np from plum import dispatch try: - from geometry_msgs.msg import TwistWithCovariance as ROSTwistWithCovariance + from geometry_msgs.msg import ( # type: ignore[attr-defined] + TwistWithCovariance as ROSTwistWithCovariance, + ) except ImportError: - ROSTwistWithCovariance = None + ROSTwistWithCovariance = None # type: ignore[assignment, misc] from dimos.msgs.geometry_msgs.Twist import Twist from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorConvertable # Types that can be converted to/from TwistWithCovariance TwistWithCovarianceConvertable: TypeAlias = ( - tuple[Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray] + tuple[Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray] # type: ignore[type-arg] | LCMTwistWithCovariance - | dict[str, Twist | tuple[VectorConvertable, VectorConvertable] | list[float] | np.ndarray] + | dict[str, Twist | tuple[VectorConvertable, VectorConvertable] | list[float] | np.ndarray] # type: ignore[type-arg] ) -class TwistWithCovariance(LCMTwistWithCovariance): +class TwistWithCovariance(LCMTwistWithCovariance): # type: ignore[misc] twist: Twist msg_name = "geometry_msgs.TwistWithCovariance" @@ -46,11 +50,11 @@ def __init__(self) -> None: self.twist = Twist() self.covariance = np.zeros(36) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( self, twist: Twist | tuple[VectorConvertable, VectorConvertable], - covariance: list[float] | np.ndarray | None = None, + covariance: list[float] | np.ndarray | None = None, # type: ignore[type-arg] ) -> None: """Initialize with twist and optional covariance.""" if isinstance(twist, Twist): @@ -64,22 +68,22 @@ def __init__( else: self.covariance = np.array(covariance, dtype=float).reshape(36) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, twist_with_cov: TwistWithCovariance) -> None: """Initialize from another TwistWithCovariance (copy constructor).""" self.twist = Twist(twist_with_cov.twist) self.covariance = np.array(twist_with_cov.covariance).copy() - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, lcm_twist_with_cov: LCMTwistWithCovariance) -> None: """Initialize from an LCM TwistWithCovariance.""" self.twist = Twist(lcm_twist_with_cov.twist) self.covariance = np.array(lcm_twist_with_cov.covariance) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( self, - twist_dict: dict[ + twist_dict: dict[ # type: ignore[type-arg] str, Twist | tuple[VectorConvertable, VectorConvertable] | list[float] | np.ndarray ], ) -> None: @@ -97,10 +101,10 @@ def __init__( else: self.covariance = np.array(covariance, dtype=float).reshape(36) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( self, - twist_tuple: tuple[ + twist_tuple: tuple[ # type: ignore[type-arg] Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray ], ) -> None: @@ -113,7 +117,7 @@ def __init__( self.twist = Twist(twist[0], twist[1]) self.covariance = np.array(twist_tuple[1], dtype=float).reshape(36) - def __getattribute__(self, name: str): + def __getattribute__(self, name: str): # type: ignore[no-untyped-def] """Override to ensure covariance is always returned as numpy array.""" if name == "covariance": cov = object.__getattribute__(self, "covariance") @@ -122,7 +126,7 @@ def __getattribute__(self, name: str): return cov return super().__getattribute__(name) - def __setattr__(self, name: str, value) -> None: + def __setattr__(self, name: str, value) -> None: # type: ignore[no-untyped-def] """Override to ensure covariance is stored as numpy array.""" if name == "covariance": if not isinstance(value, np.ndarray): @@ -140,17 +144,17 @@ def angular(self) -> Vector3: return self.twist.angular @property - def covariance_matrix(self) -> np.ndarray: + def covariance_matrix(self) -> np.ndarray: # type: ignore[type-arg] """Get covariance as 6x6 matrix.""" - return self.covariance.reshape(6, 6) + return self.covariance.reshape(6, 6) # type: ignore[has-type, no-any-return] @covariance_matrix.setter - def covariance_matrix(self, value: np.ndarray) -> None: + def covariance_matrix(self, value: np.ndarray) -> None: # type: ignore[type-arg] """Set covariance from 6x6 matrix.""" - self.covariance = np.array(value).reshape(36) + self.covariance = np.array(value).reshape(36) # type: ignore[has-type] def __repr__(self) -> str: - return f"TwistWithCovariance(twist={self.twist!r}, covariance=<{self.covariance.shape[0] if isinstance(self.covariance, np.ndarray) else len(self.covariance)} elements>)" + return f"TwistWithCovariance(twist={self.twist!r}, covariance=<{self.covariance.shape[0] if isinstance(self.covariance, np.ndarray) else len(self.covariance)} elements>)" # type: ignore[has-type] def __str__(self) -> str: return ( @@ -159,11 +163,11 @@ def __str__(self) -> str: f"cov_trace={np.trace(self.covariance_matrix):.3f})" ) - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two TwistWithCovariance are equal.""" if not isinstance(other, TwistWithCovariance): return False - return self.twist == other.twist and np.allclose(self.covariance, other.covariance) + return self.twist == other.twist and np.allclose(self.covariance, other.covariance) # type: ignore[has-type] def is_zero(self) -> bool: """Check if this is a zero twist (no linear or angular velocity).""" @@ -178,11 +182,11 @@ def lcm_encode(self) -> bytes: lcm_msg = LCMTwistWithCovariance() lcm_msg.twist = self.twist # LCM expects list, not numpy array - if isinstance(self.covariance, np.ndarray): - lcm_msg.covariance = self.covariance.tolist() + if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] + lcm_msg.covariance = self.covariance.tolist() # type: ignore[has-type] else: - lcm_msg.covariance = list(self.covariance) - return lcm_msg.lcm_encode() + lcm_msg.covariance = list(self.covariance) # type: ignore[has-type] + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes) -> TwistWithCovariance: @@ -215,11 +219,11 @@ def to_ros_msg(self) -> ROSTwistWithCovariance: ROS TwistWithCovariance message """ - ros_msg = ROSTwistWithCovariance() + ros_msg = ROSTwistWithCovariance() # type: ignore[no-untyped-call] ros_msg.twist = self.twist.to_ros_msg() # ROS expects list, not numpy array - if isinstance(self.covariance, np.ndarray): - ros_msg.covariance = self.covariance.tolist() + if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] + ros_msg.covariance = self.covariance.tolist() # type: ignore[has-type] else: - ros_msg.covariance = list(self.covariance) + ros_msg.covariance = list(self.covariance) # type: ignore[has-type] return ros_msg diff --git a/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py index 20684d9375..3b1df6819b 100644 --- a/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py +++ b/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,14 +17,18 @@ import time from typing import TypeAlias -from dimos_lcm.geometry_msgs import TwistWithCovarianceStamped as LCMTwistWithCovarianceStamped +from dimos_lcm.geometry_msgs import ( + TwistWithCovarianceStamped as LCMTwistWithCovarianceStamped, +) import numpy as np from plum import dispatch try: - from geometry_msgs.msg import TwistWithCovarianceStamped as ROSTwistWithCovarianceStamped + from geometry_msgs.msg import ( # type: ignore[attr-defined] + TwistWithCovarianceStamped as ROSTwistWithCovarianceStamped, + ) except ImportError: - ROSTwistWithCovarianceStamped = None + ROSTwistWithCovarianceStamped = None # type: ignore[assignment, misc] from dimos.msgs.geometry_msgs.Twist import Twist from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance @@ -33,21 +37,21 @@ # Types that can be converted to/from TwistWithCovarianceStamped TwistWithCovarianceStampedConvertable: TypeAlias = ( - tuple[Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray] + tuple[Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray] # type: ignore[type-arg] | LCMTwistWithCovarianceStamped | dict[ str, Twist | tuple[VectorConvertable, VectorConvertable] | list[float] - | np.ndarray + | np.ndarray # type: ignore[type-arg] | float | str, ] ) -def sec_nsec(ts): +def sec_nsec(ts): # type: ignore[no-untyped-def] s = int(ts) return [s, int((ts - s) * 1_000_000_000)] @@ -64,13 +68,13 @@ def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: self.ts = ts if ts != 0 else time.time() super().__init__(**kwargs) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( self, ts: float = 0.0, frame_id: str = "", twist: Twist | tuple[VectorConvertable, VectorConvertable] | None = None, - covariance: list[float] | np.ndarray | None = None, + covariance: list[float] | np.ndarray | None = None, # type: ignore[type-arg] ) -> None: """Initialize with timestamp, frame_id, twist and covariance.""" self.frame_id = frame_id @@ -84,13 +88,13 @@ def lcm_encode(self) -> bytes: lcm_msg = LCMTwistWithCovarianceStamped() lcm_msg.twist.twist = self.twist # LCM expects list, not numpy array - if isinstance(self.covariance, np.ndarray): - lcm_msg.twist.covariance = self.covariance.tolist() + if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] + lcm_msg.twist.covariance = self.covariance.tolist() # type: ignore[has-type] else: - lcm_msg.twist.covariance = list(self.covariance) - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + lcm_msg.twist.covariance = list(self.covariance) # type: ignore[has-type] + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] lcm_msg.header.frame_id = self.frame_id - return lcm_msg.lcm_encode() + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes) -> TwistWithCovarianceStamped: @@ -121,7 +125,7 @@ def __str__(self) -> str: ) @classmethod - def from_ros_msg(cls, ros_msg: ROSTwistWithCovarianceStamped) -> TwistWithCovarianceStamped: + def from_ros_msg(cls, ros_msg: ROSTwistWithCovarianceStamped) -> TwistWithCovarianceStamped: # type: ignore[override] """Create a TwistWithCovarianceStamped from a ROS geometry_msgs/TwistWithCovarianceStamped message. Args: @@ -141,17 +145,17 @@ def from_ros_msg(cls, ros_msg: ROSTwistWithCovarianceStamped) -> TwistWithCovari ts=ts, frame_id=ros_msg.header.frame_id, twist=twist_with_cov.twist, - covariance=twist_with_cov.covariance, + covariance=twist_with_cov.covariance, # type: ignore[has-type] ) - def to_ros_msg(self) -> ROSTwistWithCovarianceStamped: + def to_ros_msg(self) -> ROSTwistWithCovarianceStamped: # type: ignore[override] """Convert to a ROS geometry_msgs/TwistWithCovarianceStamped message. Returns: ROS TwistWithCovarianceStamped message """ - ros_msg = ROSTwistWithCovarianceStamped() + ros_msg = ROSTwistWithCovarianceStamped() # type: ignore[no-untyped-call] # Set header ros_msg.header.frame_id = self.frame_id @@ -161,9 +165,9 @@ def to_ros_msg(self) -> ROSTwistWithCovarianceStamped: # Set twist with covariance ros_msg.twist.twist = self.twist.to_ros_msg() # ROS expects list, not numpy array - if isinstance(self.covariance, np.ndarray): - ros_msg.twist.covariance = self.covariance.tolist() + if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] + ros_msg.twist.covariance = self.covariance.tolist() # type: ignore[has-type] else: - ros_msg.twist.covariance = list(self.covariance) + ros_msg.twist.covariance = list(self.covariance) # type: ignore[has-type] return ros_msg diff --git a/dimos/msgs/geometry_msgs/Vector3.py b/dimos/msgs/geometry_msgs/Vector3.py index 05d3340a42..907079d5c1 100644 --- a/dimos/msgs/geometry_msgs/Vector3.py +++ b/dimos/msgs/geometry_msgs/Vector3.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,10 +22,10 @@ from plum import dispatch # Types that can be converted to/from Vector -VectorConvertable: TypeAlias = Sequence[int | float] | LCMVector3 | np.ndarray +VectorConvertable: TypeAlias = Sequence[int | float] | LCMVector3 | np.ndarray # type: ignore[type-arg] -def _ensure_3d(data: np.ndarray) -> np.ndarray: +def _ensure_3d(data: np.ndarray) -> np.ndarray: # type: ignore[type-arg] """Ensure the data array is exactly 3D by padding with zeros or raising an exception if too long.""" if len(data) == 3: return data @@ -39,7 +39,7 @@ def _ensure_3d(data: np.ndarray) -> np.ndarray: ) -class Vector3(LCMVector3): +class Vector3(LCMVector3): # type: ignore[misc] x: float = 0.0 y: float = 0.0 z: float = 0.0 @@ -52,28 +52,28 @@ def __init__(self) -> None: self.y = 0.0 self.z = 0.0 - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, x: int | float) -> None: """Initialize a 3D vector from a single numeric value (x, 0, 0).""" self.x = float(x) self.y = 0.0 self.z = 0.0 - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, x: int | float, y: int | float) -> None: """Initialize a 3D vector from x, y components (z=0).""" self.x = float(x) self.y = float(y) self.z = 0.0 - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, x: int | float, y: int | float, z: int | float) -> None: """Initialize a 3D vector from x, y, z components.""" self.x = float(x) self.y = float(y) self.z = float(z) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, sequence: Sequence[int | float]) -> None: """Initialize from a sequence (list, tuple) of numbers, ensuring 3D.""" data = _ensure_3d(np.array(sequence, dtype=float)) @@ -81,22 +81,22 @@ def __init__(self, sequence: Sequence[int | float]) -> None: self.y = float(data[1]) self.z = float(data[2]) - @dispatch - def __init__(self, array: np.ndarray) -> None: + @dispatch # type: ignore[no-redef] + def __init__(self, array: np.ndarray) -> None: # type: ignore[type-arg] """Initialize from a numpy array, ensuring 3D.""" data = _ensure_3d(np.array(array, dtype=float)) self.x = float(data[0]) self.y = float(data[1]) self.z = float(data[2]) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, vector: Vector3) -> None: """Initialize from another Vector3 (copy constructor).""" self.x = vector.x self.y = vector.y self.z = vector.z - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, lcm_vector: LCMVector3) -> None: """Initialize from an LCM Vector3.""" self.x = float(lcm_vector.x) @@ -120,11 +120,11 @@ def roll(self) -> float: return self.x @property - def data(self) -> np.ndarray: + def data(self) -> np.ndarray: # type: ignore[type-arg] """Get the underlying numpy array.""" return np.array([self.x, self.y, self.z], dtype=float) - def __getitem__(self, idx: int): + def __getitem__(self, idx: int): # type: ignore[no-untyped-def] if idx == 0: return self.x elif idx == 1: @@ -138,7 +138,7 @@ def __repr__(self) -> str: return f"Vector({self.data})" def __str__(self) -> str: - def getArrow(): + def getArrow(): # type: ignore[no-untyped-def] repr = ["←", "↖", "↑", "↗", "→", "ā†˜", "↓", "↙"] if self.x == 0 and self.y == 0: @@ -151,17 +151,17 @@ def getArrow(): # Get directional arrow symbol return repr[dir_index] - return f"{getArrow()} Vector {self.__repr__()}" + return f"{getArrow()} Vector {self.__repr__()}" # type: ignore[no-untyped-call] - def agent_encode(self) -> dict: + def agent_encode(self) -> dict: # type: ignore[type-arg] """Encode the vector for agent communication.""" return {"x": self.x, "y": self.y, "z": self.z} - def serialize(self) -> dict: + def serialize(self) -> dict: # type: ignore[type-arg] """Serialize the vector to a tuple.""" return {"type": "vector", "c": (self.x, self.y, self.z)} - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two vectors are equal using numpy's allclose for floating point comparison.""" if not isinstance(other, Vector3): return False @@ -194,7 +194,7 @@ def __neg__(self) -> Vector3: def dot(self, other: VectorConvertable | Vector3) -> float: """Compute dot product.""" other_vector = to_vector(other) - return self.x * other_vector.x + self.y * other_vector.y + self.z * other_vector.z + return self.x * other_vector.x + self.y * other_vector.y + self.z * other_vector.z # type: ignore[no-any-return] def cross(self, other: VectorConvertable | Vector3) -> Vector3: """Compute cross product (3D vectors only).""" @@ -278,12 +278,6 @@ def project(self, onto: VectorConvertable | Vector3) -> Vector3: scalar_projection * onto_vector.z, ) - # this is here to test ros_observable_topic - # doesn't happen irl afaik that we want a vector from ros message - @classmethod - def from_msg(cls, msg) -> Vector3: - return cls(*msg) - @classmethod def zeros(cls) -> Vector3: """Create a zero 3D vector.""" @@ -317,7 +311,7 @@ def to_tuple(self) -> tuple[float, float, float]: """Convert the vector to a tuple.""" return (self.x, self.y, self.z) - def to_numpy(self) -> np.ndarray: + def to_numpy(self) -> np.ndarray: # type: ignore[type-arg] """Convert the vector to a numpy array.""" return np.array([self.x, self.y, self.z], dtype=float) @@ -330,10 +324,10 @@ def is_zero(self) -> bool: return np.allclose([self.x, self.y, self.z], 0.0) @property - def quaternion(self): - return self.to_quaternion() + def quaternion(self): # type: ignore[no-untyped-def] + return self.to_quaternion() # type: ignore[no-untyped-call] - def to_quaternion(self): + def to_quaternion(self): # type: ignore[no-untyped-def] """Convert Vector3 representing Euler angles (roll, pitch, yaw) to a Quaternion. Assumes this Vector3 contains Euler angles in radians: @@ -384,19 +378,19 @@ def __bool__(self) -> bool: @dispatch -def to_numpy(value: Vector3) -> np.ndarray: +def to_numpy(value: Vector3) -> np.ndarray: # type: ignore[type-arg] """Convert a Vector3 to a numpy array.""" return value.to_numpy() -@dispatch -def to_numpy(value: np.ndarray) -> np.ndarray: +@dispatch # type: ignore[no-redef] +def to_numpy(value: np.ndarray) -> np.ndarray: # type: ignore[type-arg] """Pass through numpy arrays.""" return value -@dispatch -def to_numpy(value: Sequence[int | float]) -> np.ndarray: +@dispatch # type: ignore[no-redef] +def to_numpy(value: Sequence[int | float]) -> np.ndarray: # type: ignore[type-arg] """Convert a sequence to a numpy array.""" return np.array(value, dtype=float) @@ -407,7 +401,7 @@ def to_vector(value: Vector3) -> Vector3: return value -@dispatch +@dispatch # type: ignore[no-redef] def to_vector(value: VectorConvertable | Vector3) -> Vector3: """Convert a vector-compatible value to a Vector3 object.""" return Vector3(value) @@ -419,13 +413,13 @@ def to_tuple(value: Vector3) -> tuple[float, float, float]: return value.to_tuple() -@dispatch -def to_tuple(value: np.ndarray) -> tuple[float, ...]: +@dispatch # type: ignore[no-redef] +def to_tuple(value: np.ndarray) -> tuple[float, ...]: # type: ignore[type-arg] """Convert a numpy array to a tuple.""" return tuple(value.tolist()) -@dispatch +@dispatch # type: ignore[no-redef] def to_tuple(value: Sequence[int | float]) -> tuple[float, ...]: """Convert a sequence to a tuple.""" if isinstance(value, tuple): @@ -440,13 +434,13 @@ def to_list(value: Vector3) -> list[float]: return value.to_list() -@dispatch -def to_list(value: np.ndarray) -> list[float]: +@dispatch # type: ignore[no-redef] +def to_list(value: np.ndarray) -> list[float]: # type: ignore[type-arg] """Convert a numpy array to a list.""" return value.tolist() -@dispatch +@dispatch # type: ignore[no-redef] def to_list(value: Sequence[int | float]) -> list[float]: """Convert a sequence to a list.""" if isinstance(value, list): diff --git a/dimos/msgs/geometry_msgs/Wrench.py b/dimos/msgs/geometry_msgs/Wrench.py new file mode 100644 index 0000000000..c0e1273771 --- /dev/null +++ b/dimos/msgs/geometry_msgs/Wrench.py @@ -0,0 +1,40 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass + +from dimos.msgs.geometry_msgs.Vector3 import Vector3 + + +@dataclass +class Wrench: + """ + Represents a force and torque in 3D space. + + This is equivalent to ROS geometry_msgs/Wrench. + """ + + force: Vector3 = None # type: ignore[assignment] # Force vector (N) + torque: Vector3 = None # type: ignore[assignment] # Torque vector (Nm) + + def __post_init__(self) -> None: + if self.force is None: + self.force = Vector3(0.0, 0.0, 0.0) + if self.torque is None: + self.torque = Vector3(0.0, 0.0, 0.0) + + def __repr__(self) -> str: + return f"Wrench(force={self.force}, torque={self.torque})" diff --git a/dimos/msgs/geometry_msgs/WrenchStamped.py b/dimos/msgs/geometry_msgs/WrenchStamped.py new file mode 100644 index 0000000000..d01d663194 --- /dev/null +++ b/dimos/msgs/geometry_msgs/WrenchStamped.py @@ -0,0 +1,75 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +import time + +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.msgs.geometry_msgs.Wrench import Wrench +from dimos.types.timestamped import Timestamped + + +@dataclass +class WrenchStamped(Timestamped): + """ + Represents a stamped force/torque measurement. + + This is equivalent to ROS geometry_msgs/WrenchStamped. + """ + + msg_name = "geometry_msgs.WrenchStamped" + ts: float = 0.0 + frame_id: str = "" + wrench: Wrench = None # type: ignore[assignment] + + def __post_init__(self) -> None: + if self.ts == 0.0: + self.ts = time.time() + if self.wrench is None: + self.wrench = Wrench() + + @classmethod + def from_force_torque_array( # type: ignore[no-untyped-def] + cls, + ft_data: list, # type: ignore[type-arg] + frame_id: str = "ft_sensor", + ts: float | None = None, + ): + """ + Create WrenchStamped from a 6-element force/torque array. + + Args: + ft_data: [fx, fy, fz, tx, ty, tz] + frame_id: Reference frame + ts: Timestamp (defaults to current time) + + Returns: + WrenchStamped instance + """ + if len(ft_data) != 6: + raise ValueError(f"Expected 6 elements, got {len(ft_data)}") + + return cls( + ts=ts if ts is not None else time.time(), + frame_id=frame_id, + wrench=Wrench( + force=Vector3(x=ft_data[0], y=ft_data[1], z=ft_data[2]), + torque=Vector3(x=ft_data[3], y=ft_data[4], z=ft_data[5]), + ), + ) + + def __repr__(self) -> str: + return f"WrenchStamped(ts={self.ts}, frame_id='{self.frame_id}', wrench={self.wrench})" diff --git a/dimos/msgs/geometry_msgs/__init__.py b/dimos/msgs/geometry_msgs/__init__.py index de46a0a079..fd47d5f0ed 100644 --- a/dimos/msgs/geometry_msgs/__init__.py +++ b/dimos/msgs/geometry_msgs/__init__.py @@ -9,3 +9,24 @@ from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance from dimos.msgs.geometry_msgs.TwistWithCovarianceStamped import TwistWithCovarianceStamped from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorLike +from dimos.msgs.geometry_msgs.Wrench import Wrench +from dimos.msgs.geometry_msgs.WrenchStamped import WrenchStamped + +__all__ = [ + "Pose", + "PoseLike", + "PoseStamped", + "PoseWithCovariance", + "PoseWithCovarianceStamped", + "Quaternion", + "Transform", + "Twist", + "TwistStamped", + "TwistWithCovariance", + "TwistWithCovarianceStamped", + "Vector3", + "VectorLike", + "Wrench", + "WrenchStamped", + "to_pose", +] diff --git a/dimos/msgs/geometry_msgs/test_Pose.py b/dimos/msgs/geometry_msgs/test_Pose.py index e5c373e166..50bfaf1388 100644 --- a/dimos/msgs/geometry_msgs/test_Pose.py +++ b/dimos/msgs/geometry_msgs/test_Pose.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_PoseStamped.py b/dimos/msgs/geometry_msgs/test_PoseStamped.py index 6224b6548a..603723b610 100644 --- a/dimos/msgs/geometry_msgs/test_PoseStamped.py +++ b/dimos/msgs/geometry_msgs/test_PoseStamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py b/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py index ea455ba488..d62ca6e806 100644 --- a/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py +++ b/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py index 25a246495d..1d04bd8e87 100644 --- a/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py +++ b/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_Quaternion.py b/dimos/msgs/geometry_msgs/test_Quaternion.py index 501f5a0271..21c1e8caeb 100644 --- a/dimos/msgs/geometry_msgs/test_Quaternion.py +++ b/dimos/msgs/geometry_msgs/test_Quaternion.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_Transform.py b/dimos/msgs/geometry_msgs/test_Transform.py index b61e92ae01..2a1daff684 100644 --- a/dimos/msgs/geometry_msgs/test_Transform.py +++ b/dimos/msgs/geometry_msgs/test_Transform.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ def test_transform_string_representations() -> None: # Test str str_str = str(tf) - assert "Transform:" in str_str assert "Translation:" in str_str assert "Rotation:" in str_str diff --git a/dimos/msgs/geometry_msgs/test_Twist.py b/dimos/msgs/geometry_msgs/test_Twist.py index 49631a5372..f83ffa3fdd 100644 --- a/dimos/msgs/geometry_msgs/test_Twist.py +++ b/dimos/msgs/geometry_msgs/test_Twist.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_TwistStamped.py b/dimos/msgs/geometry_msgs/test_TwistStamped.py index 385523a284..7ba2f59e7d 100644 --- a/dimos/msgs/geometry_msgs/test_TwistStamped.py +++ b/dimos/msgs/geometry_msgs/test_TwistStamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py b/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py index 19b992baf4..746b0c3646 100644 --- a/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py +++ b/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py index 93c7a7b23f..f0d7e5b4ab 100644 --- a/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py +++ b/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_Vector3.py b/dimos/msgs/geometry_msgs/test_Vector3.py index 7ad4e67f16..099e35eb19 100644 --- a/dimos/msgs/geometry_msgs/test_Vector3.py +++ b/dimos/msgs/geometry_msgs/test_Vector3.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/geometry_msgs/test_publish.py b/dimos/msgs/geometry_msgs/test_publish.py index 50578346ae..b3d2324af0 100644 --- a/dimos/msgs/geometry_msgs/test_publish.py +++ b/dimos/msgs/geometry_msgs/test_publish.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ def _loop() -> None: lc.handle() # loop 10000 times for _ in range(10000000): - 3 + 3 + 3 + 3 # noqa: B018 except Exception as e: print(f"Error in LCM handling: {e}") diff --git a/dimos/msgs/nav_msgs/OccupancyGrid.py b/dimos/msgs/nav_msgs/OccupancyGrid.py index 3e144de74f..3876b44fab 100644 --- a/dimos/msgs/nav_msgs/OccupancyGrid.py +++ b/dimos/msgs/nav_msgs/OccupancyGrid.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,19 +15,34 @@ from __future__ import annotations from enum import IntEnum +from functools import lru_cache import time -from typing import TYPE_CHECKING, BinaryIO - -from dimos_lcm.nav_msgs import MapMetaData, OccupancyGrid as LCMOccupancyGrid -from dimos_lcm.std_msgs import Time as LCMTime +from typing import TYPE_CHECKING, Any, BinaryIO + +from dimos_lcm.nav_msgs import ( + MapMetaData, + OccupancyGrid as LCMOccupancyGrid, +) +from dimos_lcm.std_msgs import Time as LCMTime # type: ignore[import-untyped] +import matplotlib.pyplot as plt import numpy as np -from scipy import ndimage +from PIL import Image +import rerun as rr from dimos.msgs.geometry_msgs import Pose, Vector3, VectorLike from dimos.types.timestamped import Timestamped + +@lru_cache(maxsize=16) +def _get_matplotlib_cmap(name: str): # type: ignore[no-untyped-def] + """Get a matplotlib colormap by name (cached for performance).""" + return plt.get_cmap(name) + + if TYPE_CHECKING: - from dimos.msgs.sensor_msgs import PointCloud2 + from pathlib import Path + + from numpy.typing import NDArray class CostValues(IntEnum): @@ -56,11 +71,11 @@ class OccupancyGrid(Timestamped): ts: float frame_id: str info: MapMetaData - grid: np.ndarray + grid: NDArray[np.int8] def __init__( self, - grid: np.ndarray | None = None, + grid: NDArray[np.int8] | None = None, width: int | None = None, height: int | None = None, resolution: float = 0.05, @@ -89,7 +104,7 @@ def __init__( raise ValueError("Grid must be a 2D array") height, width = grid.shape self.info = MapMetaData( - map_load_time=self._to_lcm_time(), + map_load_time=self._to_lcm_time(), # type: ignore[no-untyped-call] resolution=resolution, width=width, height=height, @@ -99,7 +114,7 @@ def __init__( elif width is not None and height is not None: # Initialize with dimensions self.info = MapMetaData( - map_load_time=self._to_lcm_time(), + map_load_time=self._to_lcm_time(), # type: ignore[no-untyped-call] resolution=resolution, width=width, height=height, @@ -108,10 +123,10 @@ def __init__( self.grid = np.full((height, width), -1, dtype=np.int8) else: # Initialize empty - self.info = MapMetaData(map_load_time=self._to_lcm_time()) + self.info = MapMetaData(map_load_time=self._to_lcm_time()) # type: ignore[no-untyped-call] self.grid = np.array([], dtype=np.int8) - def _to_lcm_time(self): + def _to_lcm_time(self): # type: ignore[no-untyped-def] """Convert timestamp to LCM Time.""" s = int(self.ts) @@ -120,22 +135,22 @@ def _to_lcm_time(self): @property def width(self) -> int: """Width of the grid in cells.""" - return self.info.width + return self.info.width # type: ignore[no-any-return] @property def height(self) -> int: """Height of the grid in cells.""" - return self.info.height + return self.info.height # type: ignore[no-any-return] @property def resolution(self) -> float: """Grid resolution in meters/cell.""" - return self.info.resolution + return self.info.resolution # type: ignore[no-any-return] @property def origin(self) -> Pose: """Origin pose of the grid.""" - return self.info.origin + return self.info.origin # type: ignore[no-any-return] @property def total_cells(self) -> int: @@ -172,40 +187,16 @@ def unknown_percent(self) -> float: """Percentage of cells that are unknown.""" return (self.unknown_cells / self.total_cells * 100) if self.total_cells > 0 else 0.0 - def inflate(self, radius: float) -> OccupancyGrid: - """Inflate obstacles by a given radius (binary inflation). - Args: - radius: Inflation radius in meters - Returns: - New OccupancyGrid with inflated obstacles - """ - # Convert radius to grid cells - cell_radius = int(np.ceil(radius / self.resolution)) - - # Get grid as numpy array - grid_array = self.grid - - # Create circular kernel for binary inflation - 2 * cell_radius + 1 - y, x = np.ogrid[-cell_radius : cell_radius + 1, -cell_radius : cell_radius + 1] - kernel = (x**2 + y**2 <= cell_radius**2).astype(np.uint8) - - # Find occupied cells - occupied_mask = grid_array >= CostValues.OCCUPIED - - # Binary inflation - inflated = ndimage.binary_dilation(occupied_mask, structure=kernel) - result_grid = grid_array.copy() - result_grid[inflated] = CostValues.OCCUPIED - - # Create new OccupancyGrid with inflated data using numpy constructor - return OccupancyGrid( - grid=result_grid, - resolution=self.resolution, - origin=self.origin, - frame_id=self.frame_id, - ts=self.ts, - ) + @classmethod + def from_path(cls, path: Path) -> OccupancyGrid: + match path.suffix.lower(): + case ".npy": + return cls(grid=np.load(path)) + case ".png": + img = Image.open(path).convert("L") + return cls(grid=np.array(img).astype(np.int8)) + case _: + raise NotImplementedError(f"Unsupported file format: {path.suffix}") def world_to_grid(self, point: VectorLike) -> Vector3: """Convert world coordinates to grid coordinates. @@ -296,7 +287,7 @@ def lcm_encode(self) -> bytes: lcm_msg.data_length = 0 lcm_msg.data = [] - return lcm_msg.lcm_encode() + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes | BinaryIO) -> OccupancyGrid: @@ -326,201 +317,6 @@ def lcm_decode(cls, data: bytes | BinaryIO) -> OccupancyGrid: instance.info = lcm_msg.info return instance - @classmethod - def from_pointcloud( - cls, - cloud: PointCloud2, - resolution: float = 0.05, - min_height: float = 0.1, - max_height: float = 2.0, - frame_id: str | None = None, - mark_free_radius: float = 0.4, - ) -> OccupancyGrid: - """Create an OccupancyGrid from a PointCloud2 message. - - Args: - cloud: PointCloud2 message containing 3D points - resolution: Grid resolution in meters/cell (default: 0.05) - min_height: Minimum height threshold for including points (default: 0.1) - max_height: Maximum height threshold for including points (default: 2.0) - frame_id: Reference frame for the grid (default: uses cloud's frame_id) - mark_free_radius: Radius in meters around obstacles to mark as free space (default: 0.0) - If 0, only immediate neighbors are marked free. - Set to preserve unknown areas for exploration. - - Returns: - OccupancyGrid with occupied cells where points were projected - """ - - # Get points as numpy array - points = cloud.as_numpy() - - if len(points) == 0: - # Return empty grid - return cls( - width=1, height=1, resolution=resolution, frame_id=frame_id or cloud.frame_id - ) - - # Filter points by height for obstacles - obstacle_mask = (points[:, 2] >= min_height) & (points[:, 2] <= max_height) - obstacle_points = points[obstacle_mask] - - # Get points below min_height for marking as free space - ground_mask = points[:, 2] < min_height - ground_points = points[ground_mask] - - # Find bounds of the point cloud in X-Y plane (use all points) - if len(points) > 0: - min_x = np.min(points[:, 0]) - max_x = np.max(points[:, 0]) - min_y = np.min(points[:, 1]) - max_y = np.max(points[:, 1]) - else: - # Return empty grid if no points at all - return cls( - width=1, height=1, resolution=resolution, frame_id=frame_id or cloud.frame_id - ) - - # Add some padding around the bounds - padding = 1.0 # 1 meter padding - min_x -= padding - max_x += padding - min_y -= padding - max_y += padding - - # Calculate grid dimensions - width = int(np.ceil((max_x - min_x) / resolution)) - height = int(np.ceil((max_y - min_y) / resolution)) - - # Create origin pose (bottom-left corner of the grid) - origin = Pose() - origin.position.x = min_x - origin.position.y = min_y - origin.position.z = 0.0 - origin.orientation.w = 1.0 # No rotation - - # Initialize grid (all unknown) - grid = np.full((height, width), -1, dtype=np.int8) - - # First, mark ground points as free space - if len(ground_points) > 0: - ground_x = ((ground_points[:, 0] - min_x) / resolution).astype(np.int32) - ground_y = ((ground_points[:, 1] - min_y) / resolution).astype(np.int32) - - # Clip indices to grid bounds - ground_x = np.clip(ground_x, 0, width - 1) - ground_y = np.clip(ground_y, 0, height - 1) - - # Mark ground cells as free - grid[ground_y, ground_x] = 0 # Free space - - # Then mark obstacle points (will override ground if at same location) - if len(obstacle_points) > 0: - obs_x = ((obstacle_points[:, 0] - min_x) / resolution).astype(np.int32) - obs_y = ((obstacle_points[:, 1] - min_y) / resolution).astype(np.int32) - - # Clip indices to grid bounds - obs_x = np.clip(obs_x, 0, width - 1) - obs_y = np.clip(obs_y, 0, height - 1) - - # Mark cells as occupied - grid[obs_y, obs_x] = 100 # Lethal obstacle - - # Apply mark_free_radius to expand free space areas - if mark_free_radius > 0: - # Expand existing free space areas by the specified radius - # This will NOT expand from obstacles, only from free space - - free_mask = grid == 0 # Current free space - free_radius_cells = int(np.ceil(mark_free_radius / resolution)) - - # Create circular kernel - y, x = np.ogrid[ - -free_radius_cells : free_radius_cells + 1, - -free_radius_cells : free_radius_cells + 1, - ] - kernel = x**2 + y**2 <= free_radius_cells**2 - - # Dilate free space areas - expanded_free = ndimage.binary_dilation(free_mask, structure=kernel, iterations=1) - - # Mark expanded areas as free, but don't override obstacles - grid[expanded_free & (grid != 100)] = 0 - - # Create and return OccupancyGrid - # Get timestamp from cloud if available - ts = cloud.ts if hasattr(cloud, "ts") and cloud.ts is not None else 0.0 - - occupancy_grid = cls( - grid=grid, - resolution=resolution, - origin=origin, - frame_id=frame_id or cloud.frame_id, - ts=ts, - ) - - return occupancy_grid - - def gradient(self, obstacle_threshold: int = 50, max_distance: float = 2.0) -> OccupancyGrid: - """Create a gradient OccupancyGrid for path planning. - - Creates a gradient where free space has value 0 and values increase near obstacles. - This can be used as a cost map for path planning algorithms like A*. - - Args: - obstacle_threshold: Cell values >= this are considered obstacles (default: 50) - max_distance: Maximum distance to compute gradient in meters (default: 2.0) - - Returns: - New OccupancyGrid with gradient values: - - -1: Unknown cells (preserved as-is) - - 0: Free space far from obstacles - - 1-99: Increasing cost as you approach obstacles - - 100: At obstacles - - Note: Unknown cells remain as unknown (-1) and do not receive gradient values. - """ - - # Remember which cells are unknown - unknown_mask = self.grid == CostValues.UNKNOWN - - # Create binary obstacle map - # Consider cells >= threshold as obstacles (1), everything else as free (0) - # Unknown cells are not considered obstacles for distance calculation - obstacle_map = (self.grid >= obstacle_threshold).astype(np.float32) - - # Compute distance transform (distance to nearest obstacle in cells) - # Unknown cells are treated as if they don't exist for distance calculation - distance_cells = ndimage.distance_transform_edt(1 - obstacle_map) - - # Convert to meters and clip to max distance - distance_meters = np.clip(distance_cells * self.resolution, 0, max_distance) - - # Invert and scale to 0-100 range - # Far from obstacles (max_distance) -> 0 - # At obstacles (0 distance) -> 100 - gradient_values = (1 - distance_meters / max_distance) * 100 - - # Ensure obstacles are exactly 100 - gradient_values[obstacle_map > 0] = CostValues.OCCUPIED - - # Convert to int8 for OccupancyGrid - gradient_data = gradient_values.astype(np.int8) - - # Preserve unknown cells as unknown (don't apply gradient to them) - gradient_data[unknown_mask] = CostValues.UNKNOWN - - # Create new OccupancyGrid with gradient - gradient_grid = OccupancyGrid( - grid=gradient_data, - resolution=self.resolution, - origin=self.origin, - frame_id=self.frame_id, - ts=self.ts, - ) - - return gradient_grid - def filter_above(self, threshold: int) -> OccupancyGrid: """Create a new OccupancyGrid with only values above threshold. @@ -606,3 +402,281 @@ def max(self) -> OccupancyGrid: ) return maxed + + def copy(self) -> OccupancyGrid: + """Create a deep copy of the OccupancyGrid. + + Returns: + A new OccupancyGrid instance with copied data. + """ + return OccupancyGrid( + grid=self.grid.copy(), + resolution=self.resolution, + origin=self.origin, + frame_id=self.frame_id, + ts=self.ts, + ) + + def cell_value(self, world_position: Vector3) -> int: + grid_position = self.world_to_grid(world_position) + x = int(grid_position.x) + y = int(grid_position.y) + + if not (0 <= x < self.width and 0 <= y < self.height): + return CostValues.UNKNOWN + + return int(self.grid[y, x]) + + def to_rerun( # type: ignore[no-untyped-def] + self, + colormap: str | None = None, + mode: str = "image", + z_offset: float = 0.01, + **kwargs: Any, + ): # type: ignore[no-untyped-def] + """Convert to Rerun visualization format. + + Args: + colormap: Optional colormap name (e.g., "RdBu_r" for blue=free, red=occupied). + If None, uses grayscale for image mode or default colors for 3D modes. + mode: Visualization mode: + - "image": 2D grayscale/colored image (default) + - "mesh": 3D textured plane overlay on floor + - "points": 3D points for occupied cells only + z_offset: Height offset for 3D modes (default 0.01m above floor) + **kwargs: Additional args (ignored for compatibility) + + Returns: + Rerun archetype for logging (rr.Image, rr.Mesh3D, or rr.Points3D) + + The visualization uses: + - Free space (value 0): white/blue + - Unknown space (value -1): gray/transparent + - Occupied space (value > 0): black/red with gradient + """ + if self.grid.size == 0: + if mode == "image": + return rr.Image(np.zeros((1, 1), dtype=np.uint8), color_model="L") + elif mode == "mesh": + return rr.Mesh3D(vertex_positions=[]) + else: + return rr.Points3D([]) + + if mode == "points": + return self._to_rerun_points(colormap, z_offset) + elif mode == "mesh": + return self._to_rerun_mesh(colormap, z_offset) + else: + return self._to_rerun_image(colormap) + + def _to_rerun_image(self, colormap: str | None = None): # type: ignore[no-untyped-def] + """Convert to 2D image visualization.""" + # Use existing cached visualization functions for supported palettes + if colormap in ("turbo", "rainbow"): + from dimos.mapping.occupancy.visualizations import rainbow_image, turbo_image + + if colormap == "turbo": + bgr_image = turbo_image(self.grid) + else: + bgr_image = rainbow_image(self.grid) + + # Convert BGR to RGB and flip for world coordinates + rgb_image = np.flipud(bgr_image[:, :, ::-1]) + return rr.Image(rgb_image, color_model="RGB") + + if colormap is not None: + # Use matplotlib colormap (cached for performance) + cmap = _get_matplotlib_cmap(colormap) + + grid_float = self.grid.astype(np.float32) + + # Create RGBA image + vis = np.zeros((self.height, self.width, 4), dtype=np.uint8) + + # Free space: low cost (blue in RdBu_r) + free_mask = self.grid == 0 + # Occupied: high cost (red in RdBu_r) + occupied_mask = self.grid > 0 + # Unknown: transparent gray + unknown_mask = self.grid == -1 + + # Map free to 0, costs to normalized value + if np.any(free_mask): + colors_free = (cmap(0.0)[:3] * np.array([255, 255, 255])).astype(np.uint8) + vis[free_mask, :3] = colors_free + vis[free_mask, 3] = 255 + + if np.any(occupied_mask): + # Normalize costs 1-100 to 0.5-1.0 range + costs = grid_float[occupied_mask] + cost_norm = 0.5 + (costs / 100) * 0.5 + colors_occ = (cmap(cost_norm)[:, :3] * 255).astype(np.uint8) + vis[occupied_mask, :3] = colors_occ + vis[occupied_mask, 3] = 255 + + if np.any(unknown_mask): + vis[unknown_mask] = [128, 128, 128, 100] # Semi-transparent gray + + # Flip vertically to match world coordinates (y=0 at bottom) + return rr.Image(np.flipud(vis), color_model="RGBA") + + # Grayscale visualization (no colormap) + vis_gray = np.zeros((self.height, self.width), dtype=np.uint8) + + # Free space = white + vis_gray[self.grid == 0] = 255 + + # Unknown = gray + vis_gray[self.grid == -1] = 128 + + # Occupied (100) = black, costs (1-99) = gradient + occupied_mask = self.grid > 0 + if np.any(occupied_mask): + # Map 1-100 to 127-0 (darker = more occupied) + costs = self.grid[occupied_mask].astype(np.float32) + vis_gray[occupied_mask] = (127 * (1 - costs / 100)).astype(np.uint8) + + # Flip vertically to match world coordinates (y=0 at bottom) + return rr.Image(np.flipud(vis_gray), color_model="L") + + def _to_rerun_points(self, colormap: str | None = None, z_offset: float = 0.01): # type: ignore[no-untyped-def] + """Convert to 3D points for occupied cells.""" + # Find occupied cells (cost > 0) + occupied_mask = self.grid > 0 + if not np.any(occupied_mask): + return rr.Points3D([]) + + # Get grid coordinates of occupied cells + gy, gx = np.where(occupied_mask) + costs = self.grid[occupied_mask].astype(np.float32) + + # Convert to world coordinates + ox = self.origin.position.x + oy = self.origin.position.y + wx = ox + (gx + 0.5) * self.resolution + wy = oy + (gy + 0.5) * self.resolution + wz = np.full_like(wx, z_offset) + + points = np.column_stack([wx, wy, wz]) + + # Determine colors + if colormap is not None: + # Normalize costs to 0-1 range + cost_norm = costs / 100.0 + cmap = _get_matplotlib_cmap(colormap) + point_colors = (cmap(cost_norm)[:, :3] * 255).astype(np.uint8) + else: + # Default: red gradient based on cost + intensity = (costs / 100.0 * 255).astype(np.uint8) + point_colors = np.column_stack( + [intensity, np.zeros_like(intensity), np.zeros_like(intensity)] + ) + + return rr.Points3D( + positions=points, + radii=self.resolution / 2, + colors=point_colors, + ) + + def _to_rerun_mesh(self, colormap: str | None = None, z_offset: float = 0.01): # type: ignore[no-untyped-def] + """Convert to 3D mesh overlay on floor plane. + + Only renders known cells (free or occupied), skipping unknown cells. + Uses per-vertex colors for proper alpha blending. + Fully vectorized for performance (~100x faster than loop version). + """ + # Only render known cells (not unknown = -1) + known_mask = self.grid != -1 + if not np.any(known_mask): + return rr.Mesh3D(vertex_positions=[]) + + # Get grid coordinates of known cells + gy, gx = np.where(known_mask) + n_cells = len(gy) + + ox = self.origin.position.x + oy = self.origin.position.y + r = self.resolution + + # === VECTORIZED VERTEX GENERATION === + # World positions of cell corners (bottom-left of each cell) + wx = ox + gx.astype(np.float32) * r + wy = oy + gy.astype(np.float32) * r + + # Each cell has 4 vertices: (wx,wy), (wx+r,wy), (wx+r,wy+r), (wx,wy+r) + # Shape: (n_cells, 4, 3) + vertices = np.zeros((n_cells, 4, 3), dtype=np.float32) + vertices[:, 0, 0] = wx + vertices[:, 0, 1] = wy + vertices[:, 0, 2] = z_offset + vertices[:, 1, 0] = wx + r + vertices[:, 1, 1] = wy + vertices[:, 1, 2] = z_offset + vertices[:, 2, 0] = wx + r + vertices[:, 2, 1] = wy + r + vertices[:, 2, 2] = z_offset + vertices[:, 3, 0] = wx + vertices[:, 3, 1] = wy + r + vertices[:, 3, 2] = z_offset + # Flatten to (n_cells*4, 3) + flat_vertices = vertices.reshape(-1, 3) + + # === VECTORIZED INDEX GENERATION === + # Base vertex indices for each cell: [0, 4, 8, 12, ...] + base_v = np.arange(n_cells, dtype=np.uint32) * 4 + # Two triangles per cell: (0,1,2) and (0,2,3) relative to base + indices = np.zeros((n_cells, 2, 3), dtype=np.uint32) + indices[:, 0, 0] = base_v + indices[:, 0, 1] = base_v + 1 + indices[:, 0, 2] = base_v + 2 + indices[:, 1, 0] = base_v + indices[:, 1, 1] = base_v + 2 + indices[:, 1, 2] = base_v + 3 + # Flatten to (n_cells*2, 3) + flat_indices = indices.reshape(-1, 3) + + # === VECTORIZED COLOR GENERATION === + cell_values = self.grid[gy, gx] # Get all cell values at once + + if colormap: + cmap = _get_matplotlib_cmap(colormap) + # Normalize costs: free(0) -> 0.0, cost(1-100) -> 0.5-1.0 + cost_norm = np.where(cell_values == 0, 0.0, 0.5 + (cell_values / 100) * 0.5) + # Sample colormap for all cells at once (returns Nx4 RGBA float) + rgba_float = cmap(cost_norm)[:, :3] # Drop alpha, we set our own + rgb = (rgba_float * 255).astype(np.uint8) + # Alpha: 180 for free, 220 for occupied + alpha = np.where(cell_values == 0, 180, 220).astype(np.uint8) + else: + # Foxglove-style coloring: blue-purple for free, black for occupied + # Free (0): #484981 = RGB(72, 73, 129) + # Occupied (100): #000000 = RGB(0, 0, 0) + rgb = np.zeros((n_cells, 3), dtype=np.uint8) + is_free = cell_values == 0 + is_occupied = ~is_free + + # Free space: blue-purple #484981 + rgb[is_free] = [72, 73, 129] + + # Occupied: gradient from blue-purple to black based on cost + # cost 1 -> mostly blue-purple, cost 100 -> black + if np.any(is_occupied): + costs = cell_values[is_occupied].astype(np.float32) + # Linear interpolation: (1 - cost/100) * blue-purple + factor = (1 - costs / 100).clip(0, 1) + rgb[is_occupied, 0] = (72 * factor).astype(np.uint8) + rgb[is_occupied, 1] = (73 * factor).astype(np.uint8) + rgb[is_occupied, 2] = (129 * factor).astype(np.uint8) + + alpha = np.where(is_free, 180, 220).astype(np.uint8) + + # Combine RGB and alpha into RGBA + colors_per_cell = np.column_stack([rgb, alpha]) # (n_cells, 4) + # Repeat each color 4 times (one per vertex) + colors = np.repeat(colors_per_cell, 4, axis=0) # (n_cells*4, 4) + + return rr.Mesh3D( + vertex_positions=flat_vertices, + triangle_indices=flat_indices, + vertex_colors=colors, + ) diff --git a/dimos/msgs/nav_msgs/Odometry.py b/dimos/msgs/nav_msgs/Odometry.py index 3a640b242d..3cdd631aa7 100644 --- a/dimos/msgs/nav_msgs/Odometry.py +++ b/dimos/msgs/nav_msgs/Odometry.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,9 +22,9 @@ from plum import dispatch try: - from nav_msgs.msg import Odometry as ROSOdometry + from nav_msgs.msg import Odometry as ROSOdometry # type: ignore[attr-defined] except ImportError: - ROSOdometry = None + ROSOdometry = None # type: ignore[assignment, misc] from dimos.msgs.geometry_msgs.Pose import Pose from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance @@ -41,12 +41,12 @@ ) -def sec_nsec(ts): +def sec_nsec(ts): # type: ignore[no-untyped-def] s = int(ts) return [s, int((ts - s) * 1_000_000_000)] -class Odometry(LCMOdometry, Timestamped): +class Odometry(LCMOdometry, Timestamped): # type: ignore[misc] pose: PoseWithCovariance twist: TwistWithCovariance msg_name = "nav_msgs.Odometry" @@ -96,7 +96,7 @@ def __init__( else: self.twist = TwistWithCovariance(Twist(twist)) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, odometry: Odometry) -> None: """Initialize from another Odometry (copy constructor).""" self.ts = odometry.ts @@ -105,7 +105,7 @@ def __init__(self, odometry: Odometry) -> None: self.pose = PoseWithCovariance(odometry.pose) self.twist = TwistWithCovariance(odometry.twist) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, lcm_odometry: LCMOdometry) -> None: """Initialize from an LCM Odometry.""" self.ts = lcm_odometry.header.stamp.sec + (lcm_odometry.header.stamp.nsec / 1_000_000_000) @@ -114,7 +114,7 @@ def __init__(self, lcm_odometry: LCMOdometry) -> None: self.pose = PoseWithCovariance(lcm_odometry.pose) self.twist = TwistWithCovariance(lcm_odometry.twist) - @dispatch + @dispatch # type: ignore[no-redef] def __init__( self, odometry_dict: dict[ @@ -154,7 +154,7 @@ def position(self) -> Vector3: return self.pose.position @property - def orientation(self): + def orientation(self): # type: ignore[no-untyped-def] """Get orientation from pose.""" return self.pose.orientation @@ -245,7 +245,7 @@ def __str__(self) -> str: f" Angular Velocity: [{self.wx:.3f}, {self.wy:.3f}, {self.wz:.3f}]" ) - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two Odometry messages are equal.""" if not isinstance(other, Odometry): return False @@ -262,25 +262,25 @@ def lcm_encode(self) -> bytes: lcm_msg = LCMOdometry() # Set header - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] lcm_msg.header.frame_id = self.frame_id lcm_msg.child_frame_id = self.child_frame_id # Set pose with covariance lcm_msg.pose.pose = self.pose.pose - if isinstance(self.pose.covariance, np.ndarray): - lcm_msg.pose.covariance = self.pose.covariance.tolist() + if isinstance(self.pose.covariance, np.ndarray): # type: ignore[has-type] + lcm_msg.pose.covariance = self.pose.covariance.tolist() # type: ignore[has-type] else: - lcm_msg.pose.covariance = list(self.pose.covariance) + lcm_msg.pose.covariance = list(self.pose.covariance) # type: ignore[has-type] # Set twist with covariance lcm_msg.twist.twist = self.twist.twist - if isinstance(self.twist.covariance, np.ndarray): - lcm_msg.twist.covariance = self.twist.covariance.tolist() + if isinstance(self.twist.covariance, np.ndarray): # type: ignore[has-type] + lcm_msg.twist.covariance = self.twist.covariance.tolist() # type: ignore[has-type] else: - lcm_msg.twist.covariance = list(self.twist.covariance) + lcm_msg.twist.covariance = list(self.twist.covariance) # type: ignore[has-type] - return lcm_msg.lcm_encode() + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes) -> Odometry: @@ -362,7 +362,7 @@ def to_ros_msg(self) -> ROSOdometry: ROS Odometry message """ - ros_msg = ROSOdometry() + ros_msg = ROSOdometry() # type: ignore[no-untyped-call] # Set header ros_msg.header.frame_id = self.frame_id diff --git a/dimos/msgs/nav_msgs/Path.py b/dimos/msgs/nav_msgs/Path.py index fa05ae4d6f..e92eab17a4 100644 --- a/dimos/msgs/nav_msgs/Path.py +++ b/dimos/msgs/nav_msgs/Path.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,9 +27,10 @@ from dimos_lcm.std_msgs import Header as LCMHeader, Time as LCMTime try: - from nav_msgs.msg import Path as ROSPath + from nav_msgs.msg import Path as ROSPath # type: ignore[attr-defined] except ImportError: - ROSPath = None + ROSPath = None # type: ignore[assignment, misc] +import rerun as rr from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped from dimos.types.timestamped import Timestamped @@ -38,7 +39,7 @@ from collections.abc import Iterator -def sec_nsec(ts): +def sec_nsec(ts): # type: ignore[no-untyped-def] s = int(ts) return [s, int((ts - s) * 1_000_000_000)] @@ -49,7 +50,7 @@ class Path(Timestamped): frame_id: str poses: list[PoseStamped] - def __init__( + def __init__( # type: ignore[no-untyped-def] self, ts: float = 0.0, frame_id: str = "world", @@ -116,16 +117,16 @@ def lcm_encode(self) -> bytes: lcm_pose.header.stamp = LCMTime() # Set the header with pose timestamp but path's frame_id - [lcm_pose.header.stamp.sec, lcm_pose.header.stamp.nsec] = sec_nsec(pose.ts) + [lcm_pose.header.stamp.sec, lcm_pose.header.stamp.nsec] = sec_nsec(pose.ts) # type: ignore[no-untyped-call] lcm_pose.header.frame_id = self.frame_id # All poses use path's frame_id lcm_poses.append(lcm_pose) lcm_msg.poses = lcm_poses # Set header with path's own timestamp - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] lcm_msg.header.frame_id = self.frame_id - return lcm_msg.lcm_encode() + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes | BinaryIO) -> Path: @@ -167,7 +168,7 @@ def __getitem__(self, index: int | slice) -> PoseStamped | list[PoseStamped]: """Allow indexing and slicing of poses.""" return self.poses[index] - def __iter__(self) -> Iterator: + def __iter__(self) -> Iterator: # type: ignore[type-arg] """Allow iteration over poses.""" return iter(self.poses) @@ -219,7 +220,7 @@ def to_ros_msg(self) -> ROSPath: ROS Path message """ - ros_msg = ROSPath() + ros_msg = ROSPath() # type: ignore[no-untyped-call] # Set header ros_msg.header.frame_id = self.frame_id @@ -231,3 +232,26 @@ def to_ros_msg(self) -> ROSPath: ros_msg.poses.append(pose.to_ros_msg()) return ros_msg + + def to_rerun( # type: ignore[no-untyped-def] + self, + color: tuple[int, int, int] = (0, 255, 128), + z_offset: float = 0.2, + radii: float = 0.05, + ): + """Convert to rerun LineStrips3D format. + + Args: + color: RGB color tuple for the path line + z_offset: Height above floor to render path (default 0.2m to avoid costmap occlusion) + radii: Thickness of the path line (default 0.05m = 5cm) + + Returns: + rr.LineStrips3D archetype for logging to rerun + """ + if not self.poses: + return rr.LineStrips3D([]) + + # Lift path above floor so it's visible over costmap + points = [[p.x, p.y, p.z + z_offset] for p in self.poses] + return rr.LineStrips3D([points], colors=[color], radii=radii) diff --git a/dimos/msgs/nav_msgs/__init__.py b/dimos/msgs/nav_msgs/__init__.py index 9df397c57c..9d099068ad 100644 --- a/dimos/msgs/nav_msgs/__init__.py +++ b/dimos/msgs/nav_msgs/__init__.py @@ -1,4 +1,8 @@ -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, MapMetaData, OccupancyGrid +from dimos.msgs.nav_msgs.OccupancyGrid import ( # type: ignore[attr-defined] + CostValues, + MapMetaData, + OccupancyGrid, +) from dimos.msgs.nav_msgs.Odometry import Odometry from dimos.msgs.nav_msgs.Path import Path diff --git a/dimos/msgs/nav_msgs/test_OccupancyGrid.py b/dimos/msgs/nav_msgs/test_OccupancyGrid.py index a4cd36f9c0..262a872c68 100644 --- a/dimos/msgs/nav_msgs/test_OccupancyGrid.py +++ b/dimos/msgs/nav_msgs/test_OccupancyGrid.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,11 +20,14 @@ import numpy as np import pytest +from dimos.mapping.occupancy.gradient import gradient +from dimos.mapping.occupancy.inflation import simple_inflate +from dimos.mapping.pointclouds.occupancy import general_occupancy from dimos.msgs.geometry_msgs import Pose from dimos.msgs.nav_msgs import OccupancyGrid from dimos.msgs.sensor_msgs import PointCloud2 from dimos.protocol.pubsub.lcmpubsub import LCM, Topic -from dimos.utils.testing import get_data +from dimos.utils.data import get_data def test_empty_grid() -> None: @@ -177,11 +180,9 @@ def test_from_pointcloud() -> None: pointcloud = PointCloud2.lcm_decode(lcm_msg) # Convert pointcloud to occupancy grid - occupancygrid = OccupancyGrid.from_pointcloud( - pointcloud, resolution=0.05, min_height=0.1, max_height=2.0 - ) + occupancygrid = general_occupancy(pointcloud, resolution=0.05, min_height=0.1, max_height=2.0) # Apply inflation separately if needed - occupancygrid = occupancygrid.inflate(0.1) + occupancygrid = simple_inflate(occupancygrid, 0.1) # Check that grid was created with reasonable properties assert occupancygrid.width > 0 @@ -200,7 +201,7 @@ def test_gradient() -> None: grid = OccupancyGrid(grid=data, resolution=0.1) # 0.1m per cell # Convert to gradient - gradient_grid = grid.gradient(obstacle_threshold=50, max_distance=1.0) + gradient_grid = gradient(grid, obstacle_threshold=50, max_distance=1.0) # Check that we get an OccupancyGrid back assert isinstance(gradient_grid, OccupancyGrid) @@ -229,7 +230,7 @@ def test_gradient() -> None: data_with_unknown[8:10, 8:10] = -1 # Add unknown area (far from obstacle) grid_with_unknown = OccupancyGrid(data_with_unknown, resolution=0.1) - gradient_with_unknown = grid_with_unknown.gradient(max_distance=1.0) # 1m max distance + gradient_with_unknown = gradient(grid_with_unknown, max_distance=1.0) # 1m max distance # Unknown cells should remain unknown (new behavior - unknowns are preserved) assert gradient_with_unknown.grid[0, 0] == -1 # Should remain unknown @@ -375,14 +376,12 @@ def test_lcm_broadcast() -> None: pointcloud = PointCloud2.lcm_decode(lcm_msg) # Create occupancy grid from pointcloud - occupancygrid = OccupancyGrid.from_pointcloud( - pointcloud, resolution=0.05, min_height=0.1, max_height=2.0 - ) + occupancygrid = general_occupancy(pointcloud, resolution=0.05, min_height=0.1, max_height=2.0) # Apply inflation separately if needed - occupancygrid = occupancygrid.inflate(0.1) + occupancygrid = simple_inflate(occupancygrid, 0.1) # Create gradient field with larger max_distance for better visualization - gradient_grid = occupancygrid.gradient(obstacle_threshold=70, max_distance=2.0) + gradient_grid = gradient(occupancygrid, obstacle_threshold=70, max_distance=2.0) # Debug: Print actual values to see the difference print("\n=== DEBUG: Comparing grids ===") diff --git a/dimos/msgs/nav_msgs/test_Odometry.py b/dimos/msgs/nav_msgs/test_Odometry.py index e61bb8e8da..ecdc83c6b4 100644 --- a/dimos/msgs/nav_msgs/test_Odometry.py +++ b/dimos/msgs/nav_msgs/test_Odometry.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/nav_msgs/test_Path.py b/dimos/msgs/nav_msgs/test_Path.py index 9f4c39b8a0..d933123b2b 100644 --- a/dimos/msgs/nav_msgs/test_Path.py +++ b/dimos/msgs/nav_msgs/test_Path.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/sensor_msgs/CameraInfo.py b/dimos/msgs/sensor_msgs/CameraInfo.py index 3d2a118b0d..b6f85dbaca 100644 --- a/dimos/msgs/sensor_msgs/CameraInfo.py +++ b/dimos/msgs/sensor_msgs/CameraInfo.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,11 +20,15 @@ from dimos_lcm.sensor_msgs import CameraInfo as LCMCameraInfo from dimos_lcm.std_msgs.Header import Header import numpy as np +import rerun as rr # Import ROS types try: - from sensor_msgs.msg import CameraInfo as ROSCameraInfo, RegionOfInterest as ROSRegionOfInterest - from std_msgs.msg import Header as ROSHeader + from sensor_msgs.msg import ( # type: ignore[attr-defined] + CameraInfo as ROSCameraInfo, + RegionOfInterest as ROSRegionOfInterest, + ) + from std_msgs.msg import Header as ROSHeader # type: ignore[attr-defined] ROS_AVAILABLE = True except ImportError: @@ -95,6 +99,29 @@ def __init__( self.roi_width = 0 self.roi_do_rectify = False + def with_ts(self, ts: float) -> CameraInfo: + """Return a copy of this CameraInfo with the given timestamp. + + Args: + ts: New timestamp + + Returns: + New CameraInfo instance with updated timestamp + """ + return CameraInfo( + height=self.height, + width=self.width, + distortion_model=self.distortion_model, + D=self.D.copy(), + K=self.K.copy(), + R=self.R.copy(), + P=self.P.copy(), + binning_x=self.binning_x, + binning_y=self.binning_y, + frame_id=self.frame_id, + ts=ts, + ) + @classmethod def from_yaml(cls, yaml_file: str) -> CameraInfo: """Create CameraInfo from YAML file. @@ -105,7 +132,7 @@ def from_yaml(cls, yaml_file: str) -> CameraInfo: Returns: CameraInfo instance with loaded calibration data """ - import yaml + import yaml # type: ignore[import-untyped] with open(yaml_file) as f: data = yaml.safe_load(f) @@ -140,41 +167,41 @@ def from_yaml(cls, yaml_file: str) -> CameraInfo: frame_id="camera_optical", ) - def get_K_matrix(self) -> np.ndarray: + def get_K_matrix(self) -> np.ndarray: # type: ignore[type-arg] """Get intrinsic matrix as numpy array.""" return np.array(self.K, dtype=np.float64).reshape(3, 3) - def get_P_matrix(self) -> np.ndarray: + def get_P_matrix(self) -> np.ndarray: # type: ignore[type-arg] """Get projection matrix as numpy array.""" return np.array(self.P, dtype=np.float64).reshape(3, 4) - def get_R_matrix(self) -> np.ndarray: + def get_R_matrix(self) -> np.ndarray: # type: ignore[type-arg] """Get rectification matrix as numpy array.""" return np.array(self.R, dtype=np.float64).reshape(3, 3) - def get_D_coeffs(self) -> np.ndarray: + def get_D_coeffs(self) -> np.ndarray: # type: ignore[type-arg] """Get distortion coefficients as numpy array.""" return np.array(self.D, dtype=np.float64) - def set_K_matrix(self, K: np.ndarray): + def set_K_matrix(self, K: np.ndarray): # type: ignore[no-untyped-def, type-arg] """Set intrinsic matrix from numpy array.""" if K.shape != (3, 3): raise ValueError(f"K matrix must be 3x3, got {K.shape}") self.K = K.flatten().tolist() - def set_P_matrix(self, P: np.ndarray): + def set_P_matrix(self, P: np.ndarray): # type: ignore[no-untyped-def, type-arg] """Set projection matrix from numpy array.""" if P.shape != (3, 4): raise ValueError(f"P matrix must be 3x4, got {P.shape}") self.P = P.flatten().tolist() - def set_R_matrix(self, R: np.ndarray): + def set_R_matrix(self, R: np.ndarray): # type: ignore[no-untyped-def, type-arg] """Set rectification matrix from numpy array.""" if R.shape != (3, 3): raise ValueError(f"R matrix must be 3x3, got {R.shape}") self.R = R.flatten().tolist() - def set_D_coeffs(self, D: np.ndarray) -> None: + def set_D_coeffs(self, D: np.ndarray) -> None: # type: ignore[type-arg] """Set distortion coefficients from numpy array.""" self.D = D.flatten().tolist() @@ -216,7 +243,7 @@ def lcm_encode(self) -> bytes: msg.roi.width = self.roi_width msg.roi.do_rectify = self.roi_do_rectify - return msg.lcm_encode() + return msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes) -> CameraInfo: @@ -298,10 +325,10 @@ def to_ros_msg(self) -> ROSCameraInfo: if not ROS_AVAILABLE: raise ImportError("ROS packages not available. Cannot convert to ROS message.") - ros_msg = ROSCameraInfo() + ros_msg = ROSCameraInfo() # type: ignore[no-untyped-call] # Set header - ros_msg.header = ROSHeader() + ros_msg.header = ROSHeader() # type: ignore[no-untyped-call] ros_msg.header.frame_id = self.frame_id ros_msg.header.stamp.sec = int(self.ts) ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1e9) @@ -324,7 +351,7 @@ def to_ros_msg(self) -> ROSCameraInfo: ros_msg.binning_y = self.binning_y # ROI - ros_msg.roi = ROSRegionOfInterest() + ros_msg.roi = ROSRegionOfInterest() # type: ignore[no-untyped-call] ros_msg.roi.x_offset = self.roi_x_offset ros_msg.roi.y_offset = self.roi_y_offset ros_msg.roi.height = self.roi_height @@ -351,7 +378,7 @@ def __str__(self) -> str: f" Binning: {self.binning_x}x{self.binning_y}" ) - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two CameraInfo messages are equal.""" if not isinstance(other, CameraInfo): return False @@ -369,11 +396,33 @@ def __eq__(self, other) -> bool: and self.frame_id == other.frame_id ) + def to_rerun(self, image_plane_distance: float = 0.5): # type: ignore[no-untyped-def] + """Convert to Rerun Pinhole archetype for camera frustum visualization. + + Args: + image_plane_distance: Distance to draw the image plane in the frustum + + Returns: + rr.Pinhole archetype for logging to Rerun + """ + # Extract intrinsics from K matrix + # K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] + fx, fy = self.K[0], self.K[4] + cx, cy = self.K[2], self.K[5] + + return rr.Pinhole( + focal_length=[fx, fy], + principal_point=[cx, cy], + width=self.width, + height=self.height, + image_plane_distance=image_plane_distance, + ) + class CalibrationProvider: """Provides lazy-loaded access to camera calibration YAML files in a directory.""" - def __init__(self, calibration_dir) -> None: + def __init__(self, calibration_dir) -> None: # type: ignore[no-untyped-def] """Initialize with a directory containing calibration YAML files. Args: @@ -382,7 +431,7 @@ def __init__(self, calibration_dir) -> None: from pathlib import Path self._calibration_dir = Path(calibration_dir) - self._cache = {} + self._cache = {} # type: ignore[var-annotated] def _to_snake_case(self, name: str) -> str: """Convert PascalCase to snake_case.""" @@ -393,7 +442,7 @@ def _to_snake_case(self, name: str) -> str: # Insert underscore before capital letter followed by lowercase return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() - def _find_yaml_file(self, name: str): + def _find_yaml_file(self, name: str): # type: ignore[no-untyped-def] """Find YAML file matching the given name (tries both snake_case and exact match). Args: @@ -433,12 +482,12 @@ def __getattr__(self, name: str) -> CameraInfo: """ # Check cache first if name in self._cache: - return self._cache[name] + return self._cache[name] # type: ignore[no-any-return] # Also check if the snake_case version is cached (for PascalCase access) snake_name = self._to_snake_case(name) if snake_name != name and snake_name in self._cache: - return self._cache[snake_name] + return self._cache[snake_name] # type: ignore[no-any-return] # Find matching YAML file yaml_file = self._find_yaml_file(name) @@ -455,7 +504,7 @@ def __getattr__(self, name: str) -> CameraInfo: return camera_info - def __dir__(self): + def __dir__(self): # type: ignore[no-untyped-def] """List available calibrations in both snake_case and PascalCase.""" calibrations = [] if self._calibration_dir.exists() and self._calibration_dir.is_dir(): diff --git a/dimos/msgs/sensor_msgs/Image.py b/dimos/msgs/sensor_msgs/Image.py index 051169d6a9..26ed141867 100644 --- a/dimos/msgs/sensor_msgs/Image.py +++ b/dimos/msgs/sensor_msgs/Image.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import base64 import time -from typing import TYPE_CHECKING, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Literal, TypedDict import cv2 from dimos_lcm.sensor_msgs.Image import Image as LCMImage @@ -24,7 +24,7 @@ import numpy as np import reactivex as rx from reactivex import operators as ops -from turbojpeg import TurboJPEG +from turbojpeg import TurboJPEG # type: ignore[import-untyped] from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ( HAS_CUDA, @@ -38,6 +38,8 @@ from dimos.utils.reactive import quality_barrier if TYPE_CHECKING: + import os + from reactivex.observable import Observable from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ( @@ -45,14 +47,14 @@ ) try: - import cupy as cp # type: ignore + import cupy as cp # type: ignore[import-not-found] except Exception: - cp = None # type: ignore + cp = None try: - from sensor_msgs.msg import Image as ROSImage + from sensor_msgs.msg import Image as ROSImage # type: ignore[attr-defined] except ImportError: - ROSImage = None + ROSImage = None # type: ignore[assignment, misc] class AgentImageMessage(TypedDict): @@ -67,7 +69,7 @@ class AgentImageMessage(TypedDict): class Image(Timestamped): msg_name = "sensor_msgs.Image" - def __init__( + def __init__( # type: ignore[no-untyped-def] self, impl: AbstractImage | None = None, *, @@ -106,14 +108,14 @@ def __init__( # Detect CuPy array without a hard dependency is_cu = False try: - import cupy as _cp # type: ignore + import cupy as _cp is_cu = isinstance(data, _cp.ndarray) except Exception: is_cu = False if is_cu and HAS_CUDA: - self._impl = CudaImage(data, fmt, fid, tstamp) # type: ignore + self._impl = CudaImage(data, fmt, fid, tstamp) else: self._impl = NumpyImage(np.asarray(data), fmt, fid, tstamp) @@ -129,9 +131,9 @@ def from_impl(cls, impl: AbstractImage) -> Image: return cls(impl) @classmethod - def from_numpy( + def from_numpy( # type: ignore[no-untyped-def] cls, - np_image: np.ndarray, + np_image: np.ndarray, # type: ignore[type-arg] format: ImageFormat = ImageFormat.BGR, to_cuda: bool = False, **kwargs, @@ -146,7 +148,7 @@ def from_numpy( kwargs.get("frame_id", ""), kwargs.get("ts", time.time()), ) - ) # type: ignore + ) return cls( NumpyImage( np.asarray(np_image), @@ -157,12 +159,16 @@ def from_numpy( ) @classmethod - def from_file( - cls, filepath: str, format: ImageFormat = ImageFormat.RGB, to_cuda: bool = False, **kwargs + def from_file( # type: ignore[no-untyped-def] + cls, + filepath: str | os.PathLike[str], + format: ImageFormat = ImageFormat.RGB, + to_cuda: bool = False, + **kwargs, ) -> Image: if kwargs.pop("to_gpu", False): to_cuda = True - arr = cv2.imread(filepath, cv2.IMREAD_UNCHANGED) + arr = cv2.imread(str(filepath), cv2.IMREAD_UNCHANGED) if arr is None: raise ValueError(f"Could not load image from {filepath}") if arr.ndim == 2: @@ -173,11 +179,14 @@ def from_file( detected = ImageFormat.BGRA # OpenCV default else: detected = format - return cls(CudaImage(arr, detected) if to_cuda and HAS_CUDA else NumpyImage(arr, detected)) # type: ignore + return cls(CudaImage(arr, detected) if to_cuda and HAS_CUDA else NumpyImage(arr, detected)) @classmethod - def from_opencv( - cls, cv_image: np.ndarray, format: ImageFormat = ImageFormat.BGR, **kwargs + def from_opencv( # type: ignore[no-untyped-def] + cls, + cv_image: np.ndarray, # type: ignore[type-arg] + format: ImageFormat = ImageFormat.BGR, + **kwargs, ) -> Image: """Construct from an OpenCV image (NumPy array).""" return cls( @@ -185,7 +194,7 @@ def from_opencv( ) @classmethod - def from_depth( + def from_depth( # type: ignore[no-untyped-def] cls, depth_data, frame_id: str = "", ts: float | None = None, to_cuda: bool = False ) -> Image: arr = np.asarray(depth_data) @@ -195,7 +204,7 @@ def from_depth( CudaImage(arr, ImageFormat.DEPTH, frame_id, time.time() if ts is None else ts) if to_cuda and HAS_CUDA else NumpyImage(arr, ImageFormat.DEPTH, frame_id, time.time() if ts is None else ts) - ) # type: ignore + ) return cls(impl) # Delegation @@ -204,18 +213,18 @@ def is_cuda(self) -> bool: return self._impl.is_cuda @property - def data(self): + def data(self): # type: ignore[no-untyped-def] return self._impl.data @data.setter - def data(self, value) -> None: + def data(self, value) -> None: # type: ignore[no-untyped-def] # Preserve backend semantics: ensure array type matches implementation if isinstance(self._impl, NumpyImage): self._impl.data = np.asarray(value) - elif isinstance(self._impl, CudaImage): # type: ignore + elif isinstance(self._impl, CudaImage): if cp is None: raise RuntimeError("CuPy not available to set CUDA image data") - self._impl.data = cp.asarray(value) # type: ignore + self._impl.data = cp.asarray(value) else: self._impl.data = value @@ -224,7 +233,7 @@ def format(self) -> ImageFormat: return self._impl.format @format.setter - def format(self, value) -> None: + def format(self, value) -> None: # type: ignore[no-untyped-def] if isinstance(value, ImageFormat): self._impl.format = value elif isinstance(value, str): @@ -264,11 +273,11 @@ def channels(self) -> int: return self._impl.channels @property - def shape(self): + def shape(self): # type: ignore[no-untyped-def] return self._impl.shape @property - def dtype(self): + def dtype(self): # type: ignore[no-untyped-def] return self._impl.dtype def copy(self) -> Image: @@ -296,9 +305,9 @@ def to_cupy(self) -> Image: CudaImage( np.asarray(self._impl.data), self._impl.format, self._impl.frame_id, self._impl.ts ) - ) # type: ignore + ) - def to_opencv(self) -> np.ndarray: + def to_opencv(self) -> np.ndarray: # type: ignore[type-arg] return self._impl.to_opencv() def to_rgb(self) -> Image: @@ -310,11 +319,33 @@ def to_bgr(self) -> Image: def to_grayscale(self) -> Image: return Image(self._impl.to_grayscale()) + def to_rerun(self) -> Any: + """Convert to rerun Image format.""" + return self._impl.to_rerun() + def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> Image: return Image(self._impl.resize(width, height, interpolation)) + def resize_to_fit( + self, max_width: int, max_height: int, interpolation: int = cv2.INTER_LINEAR + ) -> tuple[Image, float]: + """Resize image to fit within max dimensions while preserving aspect ratio. + + Only scales down if image exceeds max dimensions. Returns self if already fits. + + Returns: + Tuple of (resized_image, scale_factor). Scale factor is 1.0 if no resize needed. + """ + if self.width <= max_width and self.height <= max_height: + return self, 1.0 + + scale = min(max_width / self.width, max_height / self.height) + new_width = int(self.width * scale) + new_height = int(self.height * scale) + return self.resize(new_width, new_height, interpolation), scale + def crop(self, x: int, y: int, width: int, height: int) -> Image: - return Image(self._impl.crop(x, y, width, height)) + return Image(self._impl.crop(x, y, width, height)) # type: ignore[attr-defined] @property def sharpness(self) -> float: @@ -363,7 +394,7 @@ def to_base64( return base64.b64encode(buffer.tobytes()).decode("utf-8") def agent_encode(self) -> AgentImageMessage: - return [ + return [ # type: ignore[return-value] { "type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{self.to_base64()}"}, @@ -399,15 +430,14 @@ def lcm_encode(self, frame_id: str | None = None) -> bytes: channels = 1 if self.data.ndim == 2 else self.data.shape[2] msg.step = self.width * self.dtype.itemsize * channels - # Image data - use raw data to preserve format - image_bytes = self.data.tobytes() - msg.data_length = len(image_bytes) - msg.data = image_bytes + view = memoryview(np.ascontiguousarray(self.data)).cast("B") + msg.data_length = len(view) + msg.data = view - return msg.lcm_encode() + return msg.lcm_encode() # type: ignore[no-any-return] @classmethod - def lcm_decode(cls, data: bytes, **kwargs) -> Image: + def lcm_decode(cls, data: bytes, **kwargs) -> Image: # type: ignore[no-untyped-def] msg = LCMImage.lcm_decode(data) fmt, dtype, channels = _parse_lcm_encoding(msg.encoding) arr = np.frombuffer(msg.data, dtype=dtype) @@ -430,7 +460,7 @@ def lcm_decode(cls, data: bytes, **kwargs) -> Image: ) ) - def lcm_jpeg_encode(self, quality: int = 75, frame_id: Optional[str] = None) -> bytes: + def lcm_jpeg_encode(self, quality: int = 75, frame_id: str | None = None) -> bytes: """Convert to LCM Image message with JPEG-compressed data. Args: @@ -473,10 +503,10 @@ def lcm_jpeg_encode(self, quality: int = 75, frame_id: Optional[str] = None) -> msg.data_length = len(jpeg_data) msg.data = jpeg_data - return msg.lcm_encode() + return msg.lcm_encode() # type: ignore[no-any-return] @classmethod - def lcm_jpeg_decode(cls, data: bytes, **kwargs) -> Image: + def lcm_jpeg_decode(cls, data: bytes, **kwargs) -> Image: # type: ignore[no-untyped-def] """Decode an LCM Image message with JPEG-compressed data. Args: @@ -510,20 +540,20 @@ def lcm_jpeg_decode(cls, data: bytes, **kwargs) -> Image: ) # PnP wrappers - def solve_pnp(self, *args, **kwargs): - return self._impl.solve_pnp(*args, **kwargs) # type: ignore + def solve_pnp(self, *args, **kwargs): # type: ignore[no-untyped-def] + return self._impl.solve_pnp(*args, **kwargs) # type: ignore[attr-defined] - def solve_pnp_ransac(self, *args, **kwargs): - return self._impl.solve_pnp_ransac(*args, **kwargs) # type: ignore + def solve_pnp_ransac(self, *args, **kwargs): # type: ignore[no-untyped-def] + return self._impl.solve_pnp_ransac(*args, **kwargs) # type: ignore[attr-defined] - def solve_pnp_batch(self, *args, **kwargs): - return self._impl.solve_pnp_batch(*args, **kwargs) # type: ignore + def solve_pnp_batch(self, *args, **kwargs): # type: ignore[no-untyped-def] + return self._impl.solve_pnp_batch(*args, **kwargs) # type: ignore[attr-defined] - def create_csrt_tracker(self, *args, **kwargs): - return self._impl.create_csrt_tracker(*args, **kwargs) # type: ignore + def create_csrt_tracker(self, *args, **kwargs): # type: ignore[no-untyped-def] + return self._impl.create_csrt_tracker(*args, **kwargs) # type: ignore[attr-defined] - def csrt_update(self, *args, **kwargs): - return self._impl.csrt_update(*args, **kwargs) # type: ignore + def csrt_update(self, *args, **kwargs): # type: ignore[no-untyped-def] + return self._impl.csrt_update(*args, **kwargs) # type: ignore[attr-defined] @classmethod def from_ros_msg(cls, ros_msg: ROSImage) -> Image: @@ -587,7 +617,7 @@ def from_ros_msg(cls, ros_msg: ROSImage) -> Image: ) @staticmethod - def _parse_encoding(encoding: str) -> dict: + def _parse_encoding(encoding: str) -> dict: # type: ignore[type-arg] """Translate ROS encoding strings into format metadata.""" encoding_map = { "mono8": {"format": ImageFormat.GRAY, "dtype": np.uint8, "channels": 1}, @@ -614,7 +644,7 @@ def __repr__(self) -> str: dev = "cuda" if self.is_cuda else "cpu" return f"Image(shape={self.shape}, format={self.format.value}, dtype={self.dtype}, dev={dev}, frame_id='{self.frame_id}', ts={self.ts})" - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] if not isinstance(other, Image): return False return ( @@ -627,11 +657,11 @@ def __eq__(self, other) -> bool: def __len__(self) -> int: return int(self.height * self.width) - def __getstate__(self): + def __getstate__(self): # type: ignore[no-untyped-def] return {"data": self.data, "format": self.format, "frame_id": self.frame_id, "ts": self.ts} - def __setstate__(self, state) -> None: - self.__init__( + def __setstate__(self, state) -> None: # type: ignore[no-untyped-def] + self.__init__( # type: ignore[misc] data=state.get("data"), format=state.get("format"), frame_id=state.get("frame_id"), @@ -659,12 +689,12 @@ def sharpness_window(target_frequency: float, source: Observable[Image]) -> Obse if target_frequency <= 0: raise ValueError("target_frequency must be positive") - window = TimestampedBufferCollection(1.0 / target_frequency) + window = TimestampedBufferCollection(1.0 / target_frequency) # type: ignore[var-annotated] source.subscribe(window.add) - thread_scheduler = ThreadPoolScheduler(max_workers=1) + thread_scheduler = ThreadPoolScheduler(max_workers=1) # type: ignore[name-defined] - def find_best(*_args): + def find_best(*_args): # type: ignore[no-untyped-def] if not window._items: return None return max(window._items, key=lambda img: img.sharpness) @@ -676,14 +706,14 @@ def find_best(*_args): ) -def sharpness_barrier(target_frequency: float): +def sharpness_barrier(target_frequency: float): # type: ignore[no-untyped-def] """Select the sharpest Image within each time window.""" if target_frequency <= 0: raise ValueError("target_frequency must be positive") - return quality_barrier(lambda image: image.sharpness, target_frequency) + return quality_barrier(lambda image: image.sharpness, target_frequency) # type: ignore[attr-defined] -def _get_lcm_encoding(fmt: ImageFormat, dtype: np.dtype) -> str: +def _get_lcm_encoding(fmt: ImageFormat, dtype: np.dtype) -> str: # type: ignore[type-arg] if fmt == ImageFormat.GRAY: if dtype == np.uint8: return "mono8" @@ -712,7 +742,7 @@ def _get_lcm_encoding(fmt: ImageFormat, dtype: np.dtype) -> str: raise ValueError(f"Unsupported LCM encoding for fmt={fmt}, dtype={dtype}") -def _parse_lcm_encoding(enc: str): +def _parse_lcm_encoding(enc: str): # type: ignore[no-untyped-def] m = { "mono8": (ImageFormat.GRAY, np.uint8, 1), "mono16": (ImageFormat.GRAY16, np.uint16, 1), diff --git a/dimos/msgs/sensor_msgs/JointCommand.py b/dimos/msgs/sensor_msgs/JointCommand.py new file mode 100644 index 0000000000..78c541c50e --- /dev/null +++ b/dimos/msgs/sensor_msgs/JointCommand.py @@ -0,0 +1,143 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""LCM type definitions +This file automatically generated by lcm. +DO NOT MODIFY BY HAND!!!! +""" + +from io import BytesIO +import struct +import time + + +class JointCommand: + """ + Joint command message for robotic manipulators. + + Supports variable number of joints (DOF) with float64 values. + Can be used for position commands or velocity commands. + Includes timestamp for synchronization. + """ + + msg_name = "sensor_msgs.JointCommand" + + __slots__ = ["num_joints", "positions", "timestamp"] + + __typenames__ = ["double", "int32_t", "double"] + + __dimensions__ = [None, None, ["num_joints"]] + + def __init__( + self, positions: list[float] | None = None, timestamp: float | None = None + ) -> None: + """ + Initialize JointCommand. + + Args: + positions: List of joint values (positions or velocities) + timestamp: Unix timestamp (seconds since epoch). If None, uses current time. + """ + if positions is None: + positions = [] + + if timestamp is None: + timestamp = time.time() + + # LCM Type: double (timestamp) + self.timestamp = timestamp + # LCM Type: int32_t + self.num_joints = len(positions) + # LCM Type: double[num_joints] + self.positions = list(positions) + + def lcm_encode(self): # type: ignore[no-untyped-def] + """Encode for LCM transport (dimos uses lcm_encode method name).""" + return self.encode() # type: ignore[no-untyped-call] + + def encode(self): # type: ignore[no-untyped-def] + buf = BytesIO() + buf.write(JointCommand._get_packed_fingerprint()) # type: ignore[no-untyped-call] + self._encode_one(buf) + return buf.getvalue() + + def _encode_one(self, buf) -> None: # type: ignore[no-untyped-def] + # Encode timestamp + buf.write(struct.pack(">d", self.timestamp)) + + # Encode num_joints + buf.write(struct.pack(">i", self.num_joints)) + + # Encode positions array + for i in range(self.num_joints): + buf.write(struct.pack(">d", self.positions[i])) + + @classmethod + def lcm_decode(cls, data: bytes): # type: ignore[no-untyped-def] + """Decode from LCM transport (dimos uses lcm_decode method name).""" + return cls.decode(data) + + @classmethod + def decode(cls, data: bytes): # type: ignore[no-untyped-def] + if hasattr(data, "read"): + buf = data + else: + buf = BytesIO(data) # type: ignore[assignment] + if buf.read(8) != cls._get_packed_fingerprint(): # type: ignore[no-untyped-call] + raise ValueError("Decode error") + return cls._decode_one(buf) # type: ignore[no-untyped-call] + + @classmethod + def _decode_one(cls, buf): # type: ignore[no-untyped-def] + self = JointCommand.__new__(JointCommand) + + # Decode timestamp + self.timestamp = struct.unpack(">d", buf.read(8))[0] + + # Decode num_joints + self.num_joints = struct.unpack(">i", buf.read(4))[0] + + # Decode positions array + self.positions = [] + for _i in range(self.num_joints): + self.positions.append(struct.unpack(">d", buf.read(8))[0]) + + return self + + @classmethod + def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] + if cls in parents: + return 0 + # Hash for variable-length double array message + tmphash = (0x8A3D2E1C5F4B6A9D) & 0xFFFFFFFFFFFFFFFF + tmphash = (((tmphash << 1) & 0xFFFFFFFFFFFFFFFF) + (tmphash >> 63)) & 0xFFFFFFFFFFFFFFFF + return tmphash + + _packed_fingerprint = None + + @classmethod + def _get_packed_fingerprint(cls): # type: ignore[no-untyped-def] + if cls._packed_fingerprint is None: + cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] + return cls._packed_fingerprint + + def get_hash(self): # type: ignore[no-untyped-def] + """Get the LCM hash of the struct""" + return struct.unpack(">Q", JointCommand._get_packed_fingerprint())[0] # type: ignore[no-untyped-call] + + def __str__(self) -> str: + return f"JointCommand(timestamp={self.timestamp:.6f}, num_joints={self.num_joints}, positions={self.positions})" + + def __repr__(self) -> str: + return f"JointCommand(positions={self.positions}, timestamp={self.timestamp})" diff --git a/dimos/msgs/sensor_msgs/JointState.py b/dimos/msgs/sensor_msgs/JointState.py new file mode 100644 index 0000000000..2936012bcc --- /dev/null +++ b/dimos/msgs/sensor_msgs/JointState.py @@ -0,0 +1,195 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import TypeAlias + +from dimos_lcm.sensor_msgs import JointState as LCMJointState + +try: + from sensor_msgs.msg import JointState as ROSJointState # type: ignore[attr-defined] +except ImportError: + ROSJointState = None # type: ignore[assignment, misc] + +from plum import dispatch + +from dimos.types.timestamped import Timestamped + +# Types that can be converted to/from JointState +JointStateConvertable: TypeAlias = dict[str, list[str] | list[float]] | LCMJointState + + +def sec_nsec(ts): # type: ignore[no-untyped-def] + s = int(ts) + return [s, int((ts - s) * 1_000_000_000)] + + +class JointState(Timestamped): + msg_name = "sensor_msgs.JointState" + ts: float + frame_id: str + name: list[str] + position: list[float] + velocity: list[float] + effort: list[float] + + @dispatch + def __init__( + self, + ts: float = 0.0, + frame_id: str = "", + name: list[str] | None = None, + position: list[float] | None = None, + velocity: list[float] | None = None, + effort: list[float] | None = None, + ) -> None: + """Initialize a JointState message. + + Args: + ts: Timestamp in seconds + frame_id: Frame ID for the message + name: List of joint names + position: List of joint positions (rad or m) + velocity: List of joint velocities (rad/s or m/s) + effort: List of joint efforts (Nm or N) + """ + self.ts = ts if ts != 0 else time.time() + self.frame_id = frame_id + self.name = name if name is not None else [] + self.position = position if position is not None else [] + self.velocity = velocity if velocity is not None else [] + self.effort = effort if effort is not None else [] + + @dispatch # type: ignore[no-redef] + def __init__(self, joint_dict: dict[str, list[str] | list[float]]) -> None: + """Initialize from a dictionary.""" + self.ts = joint_dict.get("ts", time.time()) + self.frame_id = joint_dict.get("frame_id", "") + self.name = list(joint_dict.get("name", [])) + self.position = list(joint_dict.get("position", [])) + self.velocity = list(joint_dict.get("velocity", [])) + self.effort = list(joint_dict.get("effort", [])) + + @dispatch # type: ignore[no-redef] + def __init__(self, joint: JointState) -> None: + """Initialize from another JointState (copy constructor).""" + self.ts = joint.ts + self.frame_id = joint.frame_id + self.name = list(joint.name) + self.position = list(joint.position) + self.velocity = list(joint.velocity) + self.effort = list(joint.effort) + + @dispatch # type: ignore[no-redef] + def __init__(self, lcm_joint: LCMJointState) -> None: + """Initialize from an LCM JointState message.""" + self.ts = lcm_joint.header.stamp.sec + (lcm_joint.header.stamp.nsec / 1_000_000_000) + self.frame_id = lcm_joint.header.frame_id + self.name = list(lcm_joint.name) if lcm_joint.name else [] + self.position = list(lcm_joint.position) if lcm_joint.position else [] + self.velocity = list(lcm_joint.velocity) if lcm_joint.velocity else [] + self.effort = list(lcm_joint.effort) if lcm_joint.effort else [] + + def lcm_encode(self) -> bytes: + lcm_msg = LCMJointState() + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] + lcm_msg.header.frame_id = self.frame_id + lcm_msg.name_length = len(self.name) + lcm_msg.name = self.name + lcm_msg.position_length = len(self.position) + lcm_msg.position = self.position + lcm_msg.velocity_length = len(self.velocity) + lcm_msg.velocity = self.velocity + lcm_msg.effort_length = len(self.effort) + lcm_msg.effort = self.effort + return lcm_msg.lcm_encode() # type: ignore[no-any-return] + + @classmethod + def lcm_decode(cls, data: bytes) -> JointState: + lcm_msg = LCMJointState.lcm_decode(data) + return cls( + ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), + frame_id=lcm_msg.header.frame_id, + name=list(lcm_msg.name) if lcm_msg.name else [], + position=list(lcm_msg.position) if lcm_msg.position else [], + velocity=list(lcm_msg.velocity) if lcm_msg.velocity else [], + effort=list(lcm_msg.effort) if lcm_msg.effort else [], + ) + + def __str__(self) -> str: + return f"JointState({len(self.name)} joints, frame_id='{self.frame_id}')" + + def __repr__(self) -> str: + return ( + f"JointState(ts={self.ts}, frame_id='{self.frame_id}', " + f"name={self.name}, position={self.position}, " + f"velocity={self.velocity}, effort={self.effort})" + ) + + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] + """Check if two JointState messages are equal.""" + if not isinstance(other, JointState): + return False + return ( + self.name == other.name + and self.position == other.position + and self.velocity == other.velocity + and self.effort == other.effort + and self.frame_id == other.frame_id + ) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSJointState) -> JointState: + """Create a JointState from a ROS sensor_msgs/JointState message. + + Args: + ros_msg: ROS JointState message + + Returns: + JointState instance + """ + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + return cls( + ts=ts, + frame_id=ros_msg.header.frame_id, + name=list(ros_msg.name), + position=list(ros_msg.position), + velocity=list(ros_msg.velocity), + effort=list(ros_msg.effort), + ) + + def to_ros_msg(self) -> ROSJointState: + """Convert to a ROS sensor_msgs/JointState message. + + Returns: + ROS JointState message + """ + ros_msg = ROSJointState() # type: ignore[no-untyped-call] + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Set joint data + ros_msg.name = self.name + ros_msg.position = self.position + ros_msg.velocity = self.velocity + ros_msg.effort = self.effort + + return ros_msg diff --git a/dimos/msgs/sensor_msgs/Joy.py b/dimos/msgs/sensor_msgs/Joy.py index aa8611655a..c8c2fbcd3e 100644 --- a/dimos/msgs/sensor_msgs/Joy.py +++ b/dimos/msgs/sensor_msgs/Joy.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,9 +20,9 @@ from dimos_lcm.sensor_msgs import Joy as LCMJoy try: - from sensor_msgs.msg import Joy as ROSJoy + from sensor_msgs.msg import Joy as ROSJoy # type: ignore[attr-defined] except ImportError: - ROSJoy = None + ROSJoy = None # type: ignore[assignment, misc] from plum import dispatch @@ -34,7 +34,7 @@ ) -def sec_nsec(ts): +def sec_nsec(ts): # type: ignore[no-untyped-def] s = int(ts) return [s, int((ts - s) * 1_000_000_000)] @@ -67,7 +67,7 @@ def __init__( self.axes = axes if axes is not None else [] self.buttons = buttons if buttons is not None else [] - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, joy_tuple: tuple[list[float], list[int]]) -> None: """Initialize from a tuple of (axes, buttons).""" self.ts = time.time() @@ -75,7 +75,7 @@ def __init__(self, joy_tuple: tuple[list[float], list[int]]) -> None: self.axes = list(joy_tuple[0]) self.buttons = list(joy_tuple[1]) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, joy_dict: dict[str, list[float] | list[int]]) -> None: """Initialize from a dictionary with 'axes' and 'buttons' keys.""" self.ts = joy_dict.get("ts", time.time()) @@ -83,7 +83,7 @@ def __init__(self, joy_dict: dict[str, list[float] | list[int]]) -> None: self.axes = list(joy_dict.get("axes", [])) self.buttons = list(joy_dict.get("buttons", [])) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, joy: Joy) -> None: """Initialize from another Joy (copy constructor).""" self.ts = joy.ts @@ -91,7 +91,7 @@ def __init__(self, joy: Joy) -> None: self.axes = list(joy.axes) self.buttons = list(joy.buttons) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, lcm_joy: LCMJoy) -> None: """Initialize from an LCM Joy message.""" self.ts = lcm_joy.header.stamp.sec + (lcm_joy.header.stamp.nsec / 1_000_000_000) @@ -101,13 +101,13 @@ def __init__(self, lcm_joy: LCMJoy) -> None: def lcm_encode(self) -> bytes: lcm_msg = LCMJoy() - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] lcm_msg.header.frame_id = self.frame_id lcm_msg.axes_length = len(self.axes) lcm_msg.axes = self.axes lcm_msg.buttons_length = len(self.buttons) lcm_msg.buttons = self.buttons - return lcm_msg.lcm_encode() + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes) -> Joy: @@ -131,7 +131,7 @@ def __repr__(self) -> str: f"axes={self.axes}, buttons={self.buttons})" ) - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two Joy messages are equal.""" if not isinstance(other, Joy): return False @@ -167,7 +167,7 @@ def to_ros_msg(self) -> ROSJoy: Returns: ROS Joy message """ - ros_msg = ROSJoy() + ros_msg = ROSJoy() # type: ignore[no-untyped-call] # Set header ros_msg.header.frame_id = self.frame_id diff --git a/dimos/msgs/sensor_msgs/PointCloud2.py b/dimos/msgs/sensor_msgs/PointCloud2.py index b8de431fa0..1f048d5eaf 100644 --- a/dimos/msgs/sensor_msgs/PointCloud2.py +++ b/dimos/msgs/sensor_msgs/PointCloud2.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,24 +21,42 @@ from dimos_lcm.sensor_msgs.PointCloud2 import ( PointCloud2 as LCMPointCloud2, ) -from dimos_lcm.sensor_msgs.PointField import PointField -from dimos_lcm.std_msgs.Header import Header +from dimos_lcm.sensor_msgs.PointField import PointField # type: ignore[import-untyped] +from dimos_lcm.std_msgs.Header import Header # type: ignore[import-untyped] +import matplotlib.pyplot as plt import numpy as np -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] +import open3d.core as o3c # type: ignore[import-untyped] +import rerun as rr -from dimos.msgs.geometry_msgs import Vector3 +from dimos.msgs.geometry_msgs import Transform, Vector3 # Import ROS types try: - from sensor_msgs.msg import PointCloud2 as ROSPointCloud2, PointField as ROSPointField - from std_msgs.msg import Header as ROSHeader + from sensor_msgs.msg import ( # type: ignore[attr-defined] + PointCloud2 as ROSPointCloud2, + PointField as ROSPointField, + ) + from std_msgs.msg import Header as ROSHeader # type: ignore[attr-defined] ROS_AVAILABLE = True except ImportError: ROS_AVAILABLE = False +from typing import TYPE_CHECKING, Any + from dimos.types.timestamped import Timestamped +if TYPE_CHECKING: + from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo + from dimos.msgs.sensor_msgs.Image import Image + + +@functools.lru_cache(maxsize=16) +def _get_matplotlib_cmap(name: str): # type: ignore[no-untyped-def] + """Get a matplotlib colormap by name (cached for performance).""" + return plt.get_cmap(name) + # TODO: encode/decode need to be updated to work with full spectrum of pointcloud2 fields class PointCloud2(Timestamped): @@ -46,17 +64,95 @@ class PointCloud2(Timestamped): def __init__( self, - pointcloud: o3d.geometry.PointCloud = None, + pointcloud: o3d.geometry.PointCloud | o3d.t.geometry.PointCloud | None = None, frame_id: str = "world", ts: float | None = None, ) -> None: - self.ts = ts - self.pointcloud = pointcloud if pointcloud is not None else o3d.geometry.PointCloud() + self.ts = ts # type: ignore[assignment] self.frame_id = frame_id + # Store internally as tensor pointcloud for speed + if pointcloud is None: + self._pcd_tensor: o3d.t.geometry.PointCloud = o3d.t.geometry.PointCloud() + elif isinstance(pointcloud, o3d.t.geometry.PointCloud): + self._pcd_tensor = pointcloud + else: + # Convert legacy to tensor + self._pcd_tensor = o3d.t.geometry.PointCloud.from_legacy(pointcloud) + self._pcd_legacy_cache: o3d.geometry.PointCloud | None = None + + def _ensure_tensor_initialized(self) -> None: + """Ensure _pcd_tensor and _pcd_legacy_cache exist (handles unpickled old objects).""" + # Always ensure _pcd_legacy_cache exists + if not hasattr(self, "_pcd_legacy_cache"): + self._pcd_legacy_cache = None + + # Check for old pickled format: 'pointcloud' directly in __dict__ + # This takes priority even if _pcd_tensor exists (it might be empty) + old_pcd = self.__dict__.get("pointcloud") + if old_pcd is not None and isinstance(old_pcd, o3d.geometry.PointCloud): + self._pcd_tensor = o3d.t.geometry.PointCloud.from_legacy(old_pcd) + self._pcd_legacy_cache = old_pcd # reuse it + del self.__dict__["pointcloud"] + return + + if not hasattr(self, "_pcd_tensor"): + self._pcd_tensor = o3d.t.geometry.PointCloud() + + def __getstate__(self) -> dict[str, object]: + """Serialize to numpy for pickling (tensors don't pickle well).""" + self._ensure_tensor_initialized() + state = self.__dict__.copy() + # Convert tensor to numpy for serialization + if "positions" in self._pcd_tensor.point: + state["_pcd_numpy"] = self._pcd_tensor.point["positions"].numpy() + else: + state["_pcd_numpy"] = np.zeros((0, 3), dtype=np.float32) + # Remove non-picklable objects + del state["_pcd_tensor"] + state["_pcd_legacy_cache"] = None + return state + + def __setstate__(self, state: dict[str, object]) -> None: + """Restore from pickled state.""" + points_obj = state.pop("_pcd_numpy", None) + points: np.ndarray[tuple[int, int], np.dtype[np.float32]] = ( + points_obj if isinstance(points_obj, np.ndarray) else np.zeros((0, 3), dtype=np.float32) + ) + self.__dict__.update(state) + # Recreate tensor from numpy + self._pcd_tensor = o3d.t.geometry.PointCloud() + if len(points) > 0: + self._pcd_tensor.point["positions"] = o3c.Tensor(points, dtype=o3c.float32) + + @property + def pointcloud(self) -> o3d.geometry.PointCloud: + """Legacy pointcloud property for backwards compatibility. Cached.""" + self._ensure_tensor_initialized() + if self._pcd_legacy_cache is None: + self._pcd_legacy_cache = self._pcd_tensor.to_legacy() + return self._pcd_legacy_cache + + @pointcloud.setter + def pointcloud(self, value: o3d.geometry.PointCloud | o3d.t.geometry.PointCloud) -> None: + if isinstance(value, o3d.t.geometry.PointCloud): + self._pcd_tensor = value + else: + self._pcd_tensor = o3d.t.geometry.PointCloud.from_legacy(value) + self._pcd_legacy_cache = None + + @property + def pointcloud_tensor(self) -> o3d.t.geometry.PointCloud: + """Direct access to tensor pointcloud (faster, no conversion).""" + self._ensure_tensor_initialized() + return self._pcd_tensor + @classmethod def from_numpy( - cls, points: np.ndarray, frame_id: str = "world", timestamp: float | None = None + cls, + points: np.ndarray, # type: ignore[type-arg] + frame_id: str = "world", + timestamp: float | None = None, ) -> PointCloud2: """Create PointCloud2 from numpy array of shape (N, 3). @@ -68,12 +164,111 @@ def from_numpy( Returns: PointCloud2 instance """ - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(points) - return cls(pointcloud=pcd, ts=timestamp, frame_id=frame_id) + pcd_t = o3d.t.geometry.PointCloud() + pcd_t.point["positions"] = o3c.Tensor(points.astype(np.float32), dtype=o3c.float32) + return cls(pointcloud=pcd_t, ts=timestamp, frame_id=frame_id) + + @classmethod + def from_rgbd( + cls, + color_image: Image, + depth_image: Image, + camera_info: CameraInfo, + depth_scale: float = 1.0, + depth_trunc: float = 5.0, + ) -> PointCloud2: + """Create PointCloud2 from RGB and depth Image messages. + + Uses frame_id and timestamp from the depth image. + + Args: + color_image: RGB/BGR color Image message + depth_image: Depth Image message (float32 meters or uint16 mm) + camera_info: CameraInfo message with intrinsics + depth_scale: Scale factor to convert depth to meters (default 1.0 for float32) + depth_trunc: Maximum depth in meters to include + + Returns: + PointCloud2 instance with colored points + """ + # Get color as RGB numpy array + color_data = color_image.to_rgb().data + if hasattr(color_data, "get"): # CuPy array + color_data = color_data.get() + color_data = np.ascontiguousarray(color_data) + + # Get depth numpy array + depth_data = depth_image.data + if hasattr(depth_data, "get"): # CuPy array + depth_data = depth_data.get() + + # Convert depth to float32 meters if needed + if depth_data.dtype == np.uint16: + depth_data = depth_data.astype(np.float32) * depth_scale + elif depth_data.dtype != np.float32: + depth_data = depth_data.astype(np.float32) + depth_data = np.ascontiguousarray(depth_data) + + # Verify dimensions match + color_h, color_w = color_data.shape[:2] + depth_h, depth_w = depth_data.shape[:2] + if (color_h, color_w) != (depth_h, depth_w): + raise ValueError( + f"Color {color_w}x{color_h} and depth {depth_w}x{depth_h} dimensions don't match" + ) + + # Get intrinsics from camera_info + intrinsic = camera_info.get_K_matrix() + fx, fy = intrinsic[0, 0], intrinsic[1, 1] + cx, cy = intrinsic[0, 2], intrinsic[1, 2] + + # Verify intrinsics match image dimensions + if camera_info.width != color_w or camera_info.height != color_h: + # Scale intrinsics if resolution differs + scale_x = color_w / camera_info.width + scale_y = color_h / camera_info.height + fx *= scale_x + fy *= scale_y + cx *= scale_x + cy *= scale_y + + # Create Open3D images + color_o3d = o3d.geometry.Image(color_data.astype(np.uint8)) + + # Filter invalid depth values + depth_filtered = depth_data.copy() + valid_mask = np.isfinite(depth_filtered) & (depth_filtered > 0) + depth_filtered[~valid_mask] = 0.0 + depth_o3d = o3d.geometry.Image(depth_filtered.astype(np.float32)) + + o3d_intrinsic = o3d.camera.PinholeCameraIntrinsic( + width=color_w, + height=color_h, + fx=fx, + fy=fy, + cx=cx, + cy=cy, + ) + + # Create RGBD image and point cloud + rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth( + color_o3d, + depth_o3d, + depth_scale=1.0, # Already scaled + depth_trunc=depth_trunc, + convert_rgb_to_intensity=False, + ) + + pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, o3d_intrinsic) + + return cls( + pointcloud=pcd, + frame_id=depth_image.frame_id, + ts=depth_image.ts, + ) def __str__(self) -> str: - return f"PointCloud2(frame_id='{self.frame_id}', num_points={len(self.pointcloud.points)})" + return f"PointCloud2(frame_id='{self.frame_id}', num_points={len(self)})" @functools.cached_property def center(self) -> Vector3: @@ -81,8 +276,12 @@ def center(self) -> Vector3: center = np.asarray(self.pointcloud.points).mean(axis=0) return Vector3(*center) - def points(self): - return self.pointcloud.points + def points(self): # type: ignore[no-untyped-def] + """Get points (returns tensor positions, use as_numpy() for numpy array).""" + self._ensure_tensor_initialized() + if "positions" not in self._pcd_tensor.point: + return o3c.Tensor(np.zeros((0, 3), dtype=np.float32)) + return self._pcd_tensor.point["positions"] def __add__(self, other: PointCloud2) -> PointCloud2: """Combine two PointCloud2 instances into one. @@ -105,10 +304,74 @@ def __add__(self, other: PointCloud2) -> PointCloud2: ts=max(self.ts, other.ts), ) - # TODO what's the usual storage here? is it already numpy? - def as_numpy(self) -> np.ndarray: - """Get points as numpy array.""" - return np.asarray(self.pointcloud.points) + def transform(self, tf: Transform) -> PointCloud2: + """Transform the pointcloud using a Transform object. + + Applies the rotation and translation from the transform to all points, + converting them into the transform's frame_id. + + Args: + tf: Transform object containing rotation and translation + + Returns: + New PointCloud2 instance with transformed points in the new frame + """ + points, _ = self.as_numpy() + + if len(points) == 0: + return PointCloud2( + pointcloud=o3d.geometry.PointCloud(), + frame_id=tf.frame_id, + ts=self.ts, + ) + + # Build 4x4 transformation matrix from Transform + transform_matrix = tf.to_matrix() + + # Convert points to homogeneous coordinates (N, 4) + ones = np.ones((len(points), 1)) + points_homogeneous = np.hstack([points, ones]) + + # Apply transformation: (4, 4) @ (4, N) -> (4, N) -> transpose to (N, 4) + transformed_points = (transform_matrix @ points_homogeneous.T).T + + # Extract xyz coordinates (drop homogeneous coordinate) + transformed_xyz = transformed_points[:, :3].astype(np.float64) + + # Create new Open3D point cloud + new_pcd = o3d.geometry.PointCloud() + new_pcd.points = o3d.utility.Vector3dVector(transformed_xyz) + + # Copy colors if available + if self.pointcloud.has_colors(): + new_pcd.colors = self.pointcloud.colors + + return PointCloud2( + pointcloud=new_pcd, + frame_id=tf.frame_id, + ts=self.ts, + ) + + def voxel_downsample(self, voxel_size: float = 0.025) -> PointCloud2: + """Downsample the pointcloud with a voxel grid.""" + if len(self.pointcloud.points) < 20: + return self + downsampled = self._pcd_tensor.voxel_down_sample(voxel_size) + return PointCloud2(pointcloud=downsampled, frame_id=self.frame_id, ts=self.ts) + + def as_numpy( + self, + ) -> tuple[np.ndarray[Any, Any], np.ndarray[Any, Any] | None]: + """Get points and colors as numpy arrays. + + Returns: + Tuple of (points, colors) where: + - points: Nx3 numpy array of 3D points + - colors: Nx3 array in [0, 1] range, or None if no colors + """ + points = np.asarray(self.pointcloud.points) + colors = np.asarray(self.pointcloud.colors) if self.pointcloud.has_colors() else None + return points, colors @functools.cache def get_axis_aligned_bounding_box(self) -> o3d.geometry.AxisAlignedBoundingBox: @@ -140,7 +403,7 @@ def bounding_box_intersects(self, other: PointCloud2) -> bool: # Check overlap in all three dimensions # Boxes intersect if they overlap in ALL dimensions - return ( + return ( # type: ignore[no-any-return] min1[0] <= max2[0] and max1[0] >= min2[0] and min1[1] <= max2[1] @@ -150,68 +413,88 @@ def bounding_box_intersects(self, other: PointCloud2) -> bool: ) def lcm_encode(self, frame_id: str | None = None) -> bytes: - """Convert to LCM PointCloud2 message.""" + """Convert to LCM PointCloud2 message with optional RGB colors.""" msg = LCMPointCloud2() # Header msg.header = Header() - msg.header.seq = 0 # Initialize sequence number + msg.header.seq = 0 msg.header.frame_id = frame_id or self.frame_id msg.header.stamp.sec = int(self.ts) msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) - points = self.as_numpy() + points, _ = self.as_numpy() + + # Check if pointcloud has colors + self._ensure_tensor_initialized() + has_colors = "colors" in self._pcd_tensor.point + if len(points) == 0: - # Empty point cloud msg.height = 0 msg.width = 0 - msg.point_step = 16 # 4 floats * 4 bytes (x, y, z, intensity) + msg.point_step = 16 msg.row_step = 0 msg.data_length = 0 msg.data = b"" msg.is_dense = True msg.is_bigendian = False - msg.fields_length = 4 # x, y, z, intensity - msg.fields = self._create_xyz_field() - return msg.lcm_encode() + msg.fields_length = 4 + msg.fields = self._create_xyzrgb_fields() if has_colors else self._create_xyz_fields() + return msg.lcm_encode() # type: ignore[no-any-return] - # Point cloud dimensions - msg.height = 1 # Unorganized point cloud + msg.height = 1 msg.width = len(points) - # Define fields (X, Y, Z, intensity as float32) - msg.fields_length = 4 # x, y, z, intensity - msg.fields = self._create_xyz_field() + if has_colors: + # Get colors (0-1 range) and convert to uint8 + colors = self._pcd_tensor.point["colors"].numpy() + if colors.max() <= 1.0: + colors = (colors * 255).astype(np.uint8) + else: + colors = colors.astype(np.uint8) + + # Pack RGB into float32 (ROS convention: bytes are [padding, r, g, b]) + rgb_packed = np.zeros(len(points), dtype=np.float32) + rgb_uint32 = ( + (colors[:, 0].astype(np.uint32) << 16) + | (colors[:, 1].astype(np.uint32) << 8) + | colors[:, 2].astype(np.uint32) + ) + rgb_packed = rgb_uint32.view(np.float32) - # Point step and row step - msg.point_step = 16 # 4 floats * 4 bytes each (x, y, z, intensity) - msg.row_step = msg.point_step * msg.width + msg.fields = self._create_xyzrgb_fields() + msg.fields_length = 4 + msg.point_step = 16 # x, y, z, rgb (4 floats) - # Convert points to bytes with intensity padding (little endian float32) - # Add intensity column (zeros) to make it 4 columns: x, y, z, intensity - points_with_intensity = np.column_stack( - [ - points, # x, y, z columns - np.zeros(len(points), dtype=np.float32), # intensity column (padding) - ] - ) - data_bytes = points_with_intensity.astype(np.float32).tobytes() + point_data = np.column_stack([points, rgb_packed]).astype(np.float32) + else: + msg.fields = self._create_xyz_fields() + msg.fields_length = 4 + msg.point_step = 16 # x, y, z, intensity + + point_data = np.column_stack( + [ + points, + np.zeros(len(points), dtype=np.float32), + ] + ).astype(np.float32) + + msg.row_step = msg.point_step * msg.width + data_bytes = point_data.tobytes() msg.data_length = len(data_bytes) msg.data = data_bytes - # Properties - msg.is_dense = True # No invalid points - msg.is_bigendian = False # Little endian + msg.is_dense = True + msg.is_bigendian = False - return msg.lcm_encode() + return msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes) -> PointCloud2: msg = LCMPointCloud2.lcm_decode(data) if msg.width == 0 or msg.height == 0: - # Empty point cloud pc = o3d.geometry.PointCloud() return cls( pointcloud=pc, @@ -221,8 +504,8 @@ def lcm_decode(cls, data: bytes) -> PointCloud2: else None, ) - # Parse field information to find X, Y, Z offsets - x_offset = y_offset = z_offset = None + # Parse field offsets + x_offset = y_offset = z_offset = rgb_offset = None for msgfield in msg.fields: if msgfield.name == "x": x_offset = msgfield.offset @@ -230,82 +513,164 @@ def lcm_decode(cls, data: bytes) -> PointCloud2: y_offset = msgfield.offset elif msgfield.name == "z": z_offset = msgfield.offset + elif msgfield.name == "rgb": + rgb_offset = msgfield.offset if any(offset is None for offset in [x_offset, y_offset, z_offset]): raise ValueError("PointCloud2 message missing X, Y, or Z msgfields") - # Extract points from binary data num_points = msg.width * msg.height - points = np.zeros((num_points, 3), dtype=np.float32) - - data = msg.data + raw_data = msg.data point_step = msg.point_step - for i in range(num_points): - base_offset = i * point_step - - # Extract X, Y, Z (assuming float32, little endian) - x_bytes = data[base_offset + x_offset : base_offset + x_offset + 4] - y_bytes = data[base_offset + y_offset : base_offset + y_offset + 4] - z_bytes = data[base_offset + z_offset : base_offset + z_offset + 4] - - points[i, 0] = struct.unpack("= 12: + if point_step == 12: + points = np.frombuffer(raw_data, dtype=np.float32).reshape(-1, 3) + else: + dt = np.dtype( + [("x", "> 16) & 0xFF).astype(np.float32) / 255.0 + g = ((rgb_packed >> 8) & 0xFF).astype(np.float32) / 255.0 + b = (rgb_packed & 0xFF).astype(np.float32) / 255.0 + colors = np.column_stack([r, g, b]) + pcd_t.point["colors"] = o3c.Tensor(colors, dtype=o3c.float32) return cls( - pointcloud=pc, + pointcloud=pcd_t, frame_id=msg.header.frame_id if hasattr(msg, "header") else "", ts=msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 if hasattr(msg, "header") and msg.header.stamp.sec > 0 else None, ) - def _create_xyz_field(self) -> list: - """Create standard X, Y, Z field definitions for LCM PointCloud2.""" + def _create_xyz_fields(self) -> list: # type: ignore[type-arg] + """Create X, Y, Z, intensity field definitions.""" fields = [] + for i, name in enumerate(["x", "y", "z", "intensity"]): + field = PointField() + field.name = name + field.offset = i * 4 + field.datatype = 7 # FLOAT32 + field.count = 1 + fields.append(field) + return fields - # X field - x_field = PointField() - x_field.name = "x" - x_field.offset = 0 - x_field.datatype = 7 # FLOAT32 - x_field.count = 1 - fields.append(x_field) - - # Y field - y_field = PointField() - y_field.name = "y" - y_field.offset = 4 - y_field.datatype = 7 # FLOAT32 - y_field.count = 1 - fields.append(y_field) - - # Z field - z_field = PointField() - z_field.name = "z" - z_field.offset = 8 - z_field.datatype = 7 # FLOAT32 - z_field.count = 1 - fields.append(z_field) - - # I field - i_field = PointField() - i_field.name = "intensity" - i_field.offset = 12 - i_field.datatype = 7 # FLOAT32 - i_field.count = 1 - fields.append(i_field) + def _create_xyzrgb_fields(self) -> list: # type: ignore[type-arg] + """Create X, Y, Z, RGB field definitions for colored pointclouds.""" + fields = [] + for i, name in enumerate(["x", "y", "z"]): + field = PointField() + field.name = name + field.offset = i * 4 + field.datatype = 7 # FLOAT32 + field.count = 1 + fields.append(field) + + # RGB field (packed as float32, ROS convention) + rgb_field = PointField() + rgb_field.name = "rgb" + rgb_field.offset = 12 + rgb_field.datatype = 7 # FLOAT32 (contains packed RGB) + rgb_field.count = 1 + fields.append(rgb_field) return fields def __len__(self) -> int: """Return number of points.""" - return len(self.pointcloud.points) + self._ensure_tensor_initialized() + if "positions" not in self._pcd_tensor.point: + return 0 + return int(self._pcd_tensor.point["positions"].shape[0]) + + def to_rerun( # type: ignore[no-untyped-def] + self, + radii: float = 0.02, + colormap: str | None = None, + colors: list[int] | None = None, + mode: str = "boxes", + size: float | None = None, + fill_mode: str = "solid", + **kwargs, # type: ignore[no-untyped-def] + ): # type: ignore[no-untyped-def] + """Convert to Rerun Points3D or Boxes3D archetype. + + Args: + radii: Point radius for visualization (only for mode="points") + colormap: Optional colormap name (e.g., "turbo", "viridis") to color by height + colors: Optional RGB color [r, g, b] for all points (0-255) + mode: Visualization mode - "points" for spheres, "boxes" for cubes (default) + size: Box size for mode="boxes" (e.g., voxel_size). Defaults to radii*2. + fill_mode: Fill mode for boxes - "solid", "majorwireframe", or "densewireframe" + **kwargs: Additional args (ignored for compatibility) + + Returns: + rr.Points3D or rr.Boxes3D archetype for logging to Rerun + """ + points, _ = self.as_numpy() + if len(points) == 0: + return rr.Points3D([]) if mode == "points" else rr.Boxes3D(centers=[]) + + # Determine colors + point_colors = None + if colormap is not None: + # Color by height (z-coordinate) + z = points[:, 2] + z_norm = (z - z.min()) / (z.max() - z.min() + 1e-8) + cmap = _get_matplotlib_cmap(colormap) + point_colors = (cmap(z_norm)[:, :3] * 255).astype(np.uint8) + elif colors is not None: + point_colors = colors + + if mode == "boxes": + # Use boxes for voxel visualization + box_size = size if size is not None else radii * 2 + half = box_size / 2 + return rr.Boxes3D( + centers=points, + half_sizes=[half, half, half], + colors=point_colors, + fill_mode=fill_mode, # type: ignore[arg-type] + ) + else: + return rr.Points3D( + positions=points, + radii=radii, + colors=point_colors, + ) def filter_by_height( self, @@ -344,7 +709,7 @@ def filter_by_height( raise ValueError("At least one of min_height or max_height must be specified") # Get points as numpy array - points = self.as_numpy() + points, _ = self.as_numpy() if len(points) == 0: # Empty pointcloud - return a copy @@ -452,18 +817,18 @@ def from_ros_msg(cls, ros_msg: ROSPointCloud2) -> PointCloud2: dt_fields = [] # Add padding before x if needed - if x_offset > 0: + if x_offset > 0: # type: ignore[operator] dt_fields.append(("_pad_x", f"V{x_offset}")) dt_fields.append(("x", f"{byte_order}f4")) # Add padding between x and y if needed - gap_xy = y_offset - x_offset - 4 + gap_xy = y_offset - x_offset - 4 # type: ignore[operator] if gap_xy > 0: dt_fields.append(("_pad_xy", f"V{gap_xy}")) dt_fields.append(("y", f"{byte_order}f4")) # Add padding between y and z if needed - gap_yz = z_offset - y_offset - 4 + gap_yz = z_offset - y_offset - 4 # type: ignore[operator] if gap_yz > 0: dt_fields.append(("_pad_yz", f"V{gap_yz}")) dt_fields.append(("z", f"{byte_order}f4")) @@ -480,7 +845,7 @@ def from_ros_msg(cls, ros_msg: ROSPointCloud2) -> PointCloud2: # Filter out NaN and Inf values if not dense if not ros_msg.is_dense: mask = np.isfinite(points).all(axis=1) - points = points[mask] + points = points[mask] # type: ignore[assignment] # Create Open3D point cloud pc = o3d.geometry.PointCloud() @@ -498,21 +863,23 @@ def from_ros_msg(cls, ros_msg: ROSPointCloud2) -> PointCloud2: def to_ros_msg(self) -> ROSPointCloud2: """Convert to ROS sensor_msgs/PointCloud2 message. + Includes RGB color data if the pointcloud has colors. + Returns: ROS PointCloud2 message """ if not ROS_AVAILABLE: raise ImportError("ROS packages not available. Cannot convert to ROS message.") - ros_msg = ROSPointCloud2() + ros_msg = ROSPointCloud2() # type: ignore[no-untyped-call] # Set header - ros_msg.header = ROSHeader() + ros_msg.header = ROSHeader() # type: ignore[no-untyped-call] ros_msg.header.frame_id = self.frame_id ros_msg.header.stamp.sec = int(self.ts) ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1e9) - points = self.as_numpy() + points, _ = self.as_numpy() if len(points) == 0: # Empty point cloud @@ -530,19 +897,51 @@ def to_ros_msg(self) -> ROSPointCloud2: ros_msg.height = 1 # Unorganized point cloud ros_msg.width = len(points) - # Define fields (X, Y, Z as float32) - ros_msg.fields = [ - ROSPointField(name="x", offset=0, datatype=ROSPointField.FLOAT32, count=1), - ROSPointField(name="y", offset=4, datatype=ROSPointField.FLOAT32, count=1), - ROSPointField(name="z", offset=8, datatype=ROSPointField.FLOAT32, count=1), - ] + # Check if pointcloud has colors + has_colors = self.pointcloud.has_colors() - # Set point step and row step - ros_msg.point_step = 12 # 3 floats * 4 bytes each - ros_msg.row_step = ros_msg.point_step * ros_msg.width + if has_colors: + # Include RGB field - pack as XYZRGB + ros_msg.fields = [ + ROSPointField(name="x", offset=0, datatype=ROSPointField.FLOAT32, count=1), # type: ignore[no-untyped-call] + ROSPointField(name="y", offset=4, datatype=ROSPointField.FLOAT32, count=1), # type: ignore[no-untyped-call] + ROSPointField(name="z", offset=8, datatype=ROSPointField.FLOAT32, count=1), # type: ignore[no-untyped-call] + ROSPointField(name="rgb", offset=12, datatype=ROSPointField.UINT32, count=1), # type: ignore[no-untyped-call] + ] + ros_msg.point_step = 16 # 3 floats + 1 uint32 + + # Get colors and convert to packed RGB uint32 + colors = np.asarray(self.pointcloud.colors) # (N, 3) in [0, 1] + colors_uint8 = (colors * 255).astype(np.uint8) + rgb_packed = ( + (colors_uint8[:, 0].astype(np.uint32) << 16) + | (colors_uint8[:, 1].astype(np.uint32) << 8) + | colors_uint8[:, 2].astype(np.uint32) + ) - # Convert points to bytes (little endian float32) - ros_msg.data = points.astype(np.float32).tobytes() + # Create structured array with x, y, z, rgb + cloud_data = np.zeros( + len(points), + dtype=[("x", np.float32), ("y", np.float32), ("z", np.float32), ("rgb", np.uint32)], + ) + cloud_data["x"] = points[:, 0] + cloud_data["y"] = points[:, 1] + cloud_data["z"] = points[:, 2] + cloud_data["rgb"] = rgb_packed + + ros_msg.data = cloud_data.tobytes() + else: + # No colors - just XYZ + ros_msg.fields = [ + ROSPointField(name="x", offset=0, datatype=ROSPointField.FLOAT32, count=1), # type: ignore[no-untyped-call] + ROSPointField(name="y", offset=4, datatype=ROSPointField.FLOAT32, count=1), # type: ignore[no-untyped-call] + ROSPointField(name="z", offset=8, datatype=ROSPointField.FLOAT32, count=1), # type: ignore[no-untyped-call] + ] + ros_msg.point_step = 12 # 3 floats * 4 bytes each + + ros_msg.data = points.astype(np.float32).tobytes() + + ros_msg.row_step = ros_msg.point_step * ros_msg.width # Set properties ros_msg.is_bigendian = False # Little endian diff --git a/dimos/msgs/sensor_msgs/RobotState.py b/dimos/msgs/sensor_msgs/RobotState.py new file mode 100644 index 0000000000..20e41e7d24 --- /dev/null +++ b/dimos/msgs/sensor_msgs/RobotState.py @@ -0,0 +1,188 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""LCM type definitions +This file automatically generated by lcm. +DO NOT MODIFY BY HAND!!!! +""" + +from io import BytesIO +import struct + + +class RobotState: + msg_name = "sensor_msgs.RobotState" + + __slots__ = [ + "cmdnum", + "error_code", + "joints", + "mode", + "mt_able", + "mt_brake", + "state", + "tcp_offset", + "tcp_pose", + "warn_code", + ] + + __typenames__ = [ + "int32_t", + "int32_t", + "int32_t", + "int32_t", + "int32_t", + "int32_t", + "int32_t", + "float", + "float", + "float", + ] + + __dimensions__ = [None, None, None, None, None, None, None, None, None, None] + + def __init__( # type: ignore[no-untyped-def] + self, + state: int = 0, + mode: int = 0, + error_code: int = 0, + warn_code: int = 0, + cmdnum: int = 0, + mt_brake: int = 0, + mt_able: int = 0, + tcp_pose=None, + tcp_offset=None, + joints=None, + ) -> None: + # LCM Type: int32_t + self.state = state + # LCM Type: int32_t + self.mode = mode + # LCM Type: int32_t + self.error_code = error_code + # LCM Type: int32_t + self.warn_code = warn_code + # LCM Type: int32_t + self.cmdnum = cmdnum + # LCM Type: int32_t + self.mt_brake = mt_brake + # LCM Type: int32_t + self.mt_able = mt_able + # LCM Type: float[] - TCP pose [x, y, z, roll, pitch, yaw] + self.tcp_pose = tcp_pose if tcp_pose is not None else [] + # LCM Type: float[] - TCP offset [x, y, z, roll, pitch, yaw] + self.tcp_offset = tcp_offset if tcp_offset is not None else [] + # LCM Type: float[] - Joint positions (variable length based on robot DOF) + self.joints = joints if joints is not None else [] + + def lcm_encode(self): # type: ignore[no-untyped-def] + """Encode for LCM transport (dimos uses lcm_encode method name).""" + return self.encode() # type: ignore[no-untyped-call] + + def encode(self): # type: ignore[no-untyped-def] + buf = BytesIO() + buf.write(RobotState._get_packed_fingerprint()) # type: ignore[no-untyped-call] + self._encode_one(buf) + return buf.getvalue() + + def _encode_one(self, buf) -> None: # type: ignore[no-untyped-def] + buf.write( + struct.pack( + ">iiiiiii", + self.state, + self.mode, + self.error_code, + self.warn_code, + self.cmdnum, + self.mt_brake, + self.mt_able, + ) + ) + # Encode tcp_pose array + buf.write(struct.pack(">i", len(self.tcp_pose))) + for val in self.tcp_pose: + buf.write(struct.pack(">f", val)) + # Encode tcp_offset array + buf.write(struct.pack(">i", len(self.tcp_offset))) + for val in self.tcp_offset: + buf.write(struct.pack(">f", val)) + # Encode joints array + buf.write(struct.pack(">i", len(self.joints))) + for val in self.joints: + buf.write(struct.pack(">f", val)) + + @classmethod + def lcm_decode(cls, data: bytes): # type: ignore[no-untyped-def] + """Decode from LCM transport (dimos uses lcm_decode method name).""" + return cls.decode(data) + + @classmethod + def decode(cls, data: bytes): # type: ignore[no-untyped-def] + if hasattr(data, "read"): + buf = data + else: + buf = BytesIO(data) # type: ignore[assignment] + if buf.read(8) != cls._get_packed_fingerprint(): # type: ignore[no-untyped-call] + raise ValueError("Decode error") + return cls._decode_one(buf) # type: ignore[no-untyped-call] + + @classmethod + def _decode_one(cls, buf): # type: ignore[no-untyped-def] + self = RobotState() + ( + self.state, + self.mode, + self.error_code, + self.warn_code, + self.cmdnum, + self.mt_brake, + self.mt_able, + ) = struct.unpack(">iiiiiii", buf.read(28)) + # Decode tcp_pose array + tcp_pose_len = struct.unpack(">i", buf.read(4))[0] + self.tcp_pose = [] + for _ in range(tcp_pose_len): + self.tcp_pose.append(struct.unpack(">f", buf.read(4))[0]) + # Decode tcp_offset array + tcp_offset_len = struct.unpack(">i", buf.read(4))[0] + self.tcp_offset = [] + for _ in range(tcp_offset_len): + self.tcp_offset.append(struct.unpack(">f", buf.read(4))[0]) + # Decode joints array + joints_len = struct.unpack(">i", buf.read(4))[0] + self.joints = [] + for _ in range(joints_len): + self.joints.append(struct.unpack(">f", buf.read(4))[0]) + return self + + @classmethod + def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] + if cls in parents: + return 0 + # Updated hash to reflect new fields: tcp_pose, tcp_offset, joints + tmphash = (0x8C3B9A1FE7D24E6A) & 0xFFFFFFFFFFFFFFFF + tmphash = (((tmphash << 1) & 0xFFFFFFFFFFFFFFFF) + (tmphash >> 63)) & 0xFFFFFFFFFFFFFFFF + return tmphash + + _packed_fingerprint = None + + @classmethod + def _get_packed_fingerprint(cls): # type: ignore[no-untyped-def] + if cls._packed_fingerprint is None: + cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] + return cls._packed_fingerprint + + def get_hash(self): # type: ignore[no-untyped-def] + """Get the LCM hash of the struct""" + return struct.unpack(">Q", RobotState._get_packed_fingerprint())[0] # type: ignore[no-untyped-call] diff --git a/dimos/msgs/sensor_msgs/__init__.py b/dimos/msgs/sensor_msgs/__init__.py index 56574e448d..b58dda8db5 100644 --- a/dimos/msgs/sensor_msgs/__init__.py +++ b/dimos/msgs/sensor_msgs/__init__.py @@ -1,4 +1,18 @@ from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo from dimos.msgs.sensor_msgs.Image import Image, ImageFormat +from dimos.msgs.sensor_msgs.JointCommand import JointCommand +from dimos.msgs.sensor_msgs.JointState import JointState from dimos.msgs.sensor_msgs.Joy import Joy from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 +from dimos.msgs.sensor_msgs.RobotState import RobotState + +__all__ = [ + "CameraInfo", + "Image", + "ImageFormat", + "JointCommand", + "JointState", + "Joy", + "PointCloud2", + "RobotState", +] diff --git a/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py b/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py index 9dd0c647d2..f5d92a3bc6 100644 --- a/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py +++ b/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,13 +22,14 @@ import cv2 import numpy as np +import rerun as rr try: - import cupy as cp # type: ignore + import cupy as cp # type: ignore[import-not-found] HAS_CUDA = True except Exception: # pragma: no cover - optional dependency - cp = None # type: ignore + cp = None HAS_CUDA = False # Optional nvImageCodec (preferred GPU codec) @@ -36,19 +37,19 @@ NVIMGCODEC_LAST_USED = False try: # pragma: no cover - optional dependency if HAS_CUDA and USE_NVIMGCODEC: - from nvidia import nvimgcodec # type: ignore + from nvidia import nvimgcodec # type: ignore[import-untyped] try: - _enc_probe = nvimgcodec.Encoder() # type: ignore[attr-defined] + _enc_probe = nvimgcodec.Encoder() HAS_NVIMGCODEC = True except Exception: - nvimgcodec = None # type: ignore + nvimgcodec = None HAS_NVIMGCODEC = False else: - nvimgcodec = None # type: ignore + nvimgcodec = None HAS_NVIMGCODEC = False except Exception: # pragma: no cover - optional dependency - nvimgcodec = None # type: ignore + nvimgcodec = None HAS_NVIMGCODEC = False @@ -63,42 +64,42 @@ class ImageFormat(Enum): DEPTH16 = "DEPTH16" -def _is_cu(x) -> bool: - return HAS_CUDA and cp is not None and isinstance(x, cp.ndarray) # type: ignore +def _is_cu(x) -> bool: # type: ignore[no-untyped-def] + return HAS_CUDA and cp is not None and isinstance(x, cp.ndarray) -def _ascontig(x): +def _ascontig(x): # type: ignore[no-untyped-def] if _is_cu(x): - return x if x.flags["C_CONTIGUOUS"] else cp.ascontiguousarray(x) # type: ignore + return x if x.flags["C_CONTIGUOUS"] else cp.ascontiguousarray(x) return x if x.flags["C_CONTIGUOUS"] else np.ascontiguousarray(x) -def _to_cpu(x): - return cp.asnumpy(x) if _is_cu(x) else x # type: ignore +def _to_cpu(x): # type: ignore[no-untyped-def] + return cp.asnumpy(x) if _is_cu(x) else x -def _to_cu(x): - if HAS_CUDA and cp is not None and isinstance(x, np.ndarray): # type: ignore - return cp.asarray(x) # type: ignore +def _to_cu(x): # type: ignore[no-untyped-def] + if HAS_CUDA and cp is not None and isinstance(x, np.ndarray): + return cp.asarray(x) return x -def _encode_nvimgcodec_cuda(bgr_cu, quality: int = 80) -> bytes: # pragma: no cover - optional +def _encode_nvimgcodec_cuda(bgr_cu, quality: int = 80) -> bytes: # type: ignore[no-untyped-def] # pragma: no cover - optional if not HAS_NVIMGCODEC or nvimgcodec is None: raise RuntimeError("nvimgcodec not available") if bgr_cu.ndim != 3 or bgr_cu.shape[2] != 3: raise RuntimeError("nvimgcodec expects HxWx3 image") - if bgr_cu.dtype != cp.uint8: # type: ignore[attr-defined] + if bgr_cu.dtype != cp.uint8: raise RuntimeError("nvimgcodec requires uint8 input") if not bgr_cu.flags["C_CONTIGUOUS"]: - bgr_cu = cp.ascontiguousarray(bgr_cu) # type: ignore[attr-defined] - encoder = nvimgcodec.Encoder() # type: ignore[attr-defined] + bgr_cu = cp.ascontiguousarray(bgr_cu) + encoder = nvimgcodec.Encoder() try: - img = nvimgcodec.Image(bgr_cu, nvimgcodec.PixelFormat.BGR) # type: ignore[attr-defined] + img = nvimgcodec.Image(bgr_cu, nvimgcodec.PixelFormat.BGR) except Exception: - img = nvimgcodec.Image(cp.asnumpy(bgr_cu), nvimgcodec.PixelFormat.BGR) # type: ignore[attr-defined] + img = nvimgcodec.Image(cp.asnumpy(bgr_cu), nvimgcodec.PixelFormat.BGR) if hasattr(nvimgcodec, "EncodeParams"): - params = nvimgcodec.EncodeParams(quality=quality) # type: ignore[attr-defined] + params = nvimgcodec.EncodeParams(quality=quality) bitstreams = encoder.encode([img], [params]) else: bitstreams = encoder.encode([img]) @@ -108,6 +109,37 @@ def _encode_nvimgcodec_cuda(bgr_cu, quality: int = 80) -> bytes: # pragma: no c return bytes(bs0) +def format_to_rerun(data, fmt: ImageFormat): # type: ignore[no-untyped-def] + """Convert image data to Rerun archetype based on format. + + Args: + data: Image data (numpy array or cupy array on CPU) + fmt: ImageFormat enum value + + Returns: + Rerun archetype (rr.Image or rr.DepthImage) + """ + match fmt: + case ImageFormat.RGB: + return rr.Image(data, color_model="RGB") + case ImageFormat.RGBA: + return rr.Image(data, color_model="RGBA") + case ImageFormat.BGR: + return rr.Image(data, color_model="BGR") + case ImageFormat.BGRA: + return rr.Image(data, color_model="BGRA") + case ImageFormat.GRAY: + return rr.Image(data, color_model="L") + case ImageFormat.GRAY16: + return rr.Image(data, color_model="L") + case ImageFormat.DEPTH: + return rr.DepthImage(data) + case ImageFormat.DEPTH16: + return rr.DepthImage(data) + case _: + raise ValueError(f"Unsupported format for Rerun: {fmt}") + + class AbstractImage(ABC): data: Any format: ImageFormat @@ -136,15 +168,15 @@ def channels(self) -> int: raise ValueError("Invalid image dimensions") @property - def shape(self): + def shape(self): # type: ignore[no-untyped-def] return tuple(self.data.shape) @property - def dtype(self): + def dtype(self): # type: ignore[no-untyped-def] return self.data.dtype @abstractmethod - def to_opencv(self) -> np.ndarray: # pragma: no cover - abstract + def to_opencv(self) -> np.ndarray: # type: ignore[type-arg] # pragma: no cover - abstract ... @abstractmethod @@ -165,14 +197,18 @@ def resize( ) -> AbstractImage: # pragma: no cover - abstract ... + @abstractmethod + def to_rerun(self) -> Any: # pragma: no cover - abstract + ... + @abstractmethod def sharpness(self) -> float: # pragma: no cover - abstract ... def copy(self) -> AbstractImage: - return self.__class__( + return self.__class__( # type: ignore[call-arg] data=self.data.copy(), format=self.format, frame_id=self.frame_id, ts=self.ts - ) # type: ignore + ) def save(self, filepath: str) -> bool: global NVIMGCODEC_LAST_USED @@ -203,7 +239,9 @@ def to_base64(self, quality: int = 80) -> str: NVIMGCODEC_LAST_USED = False bgr = self.to_bgr() success, buffer = cv2.imencode( - ".jpg", _to_cpu(bgr.data), [int(cv2.IMWRITE_JPEG_QUALITY), int(quality)] + ".jpg", + _to_cpu(bgr.data), # type: ignore[no-untyped-call] + [int(cv2.IMWRITE_JPEG_QUALITY), int(quality)], ) if not success: raise ValueError("Failed to encode image as JPEG") diff --git a/dimos/msgs/sensor_msgs/image_impls/CudaImage.py b/dimos/msgs/sensor_msgs/image_impls/CudaImage.py index 3067138d36..8230daae29 100644 --- a/dimos/msgs/sensor_msgs/image_impls/CudaImage.py +++ b/dimos/msgs/sensor_msgs/image_impls/CudaImage.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,15 +30,15 @@ ) try: - import cupy as cp # type: ignore - from cupyx.scipy import ( - ndimage as cndimage, # type: ignore - signal as csignal, # type: ignore + import cupy as cp # type: ignore[import-not-found] + from cupyx.scipy import ( # type: ignore[import-not-found] + ndimage as cndimage, + signal as csignal, ) except Exception: # pragma: no cover - cp = None # type: ignore - cndimage = None # type: ignore - csignal = None # type: ignore + cp = None + cndimage = None + csignal = None _CUDA_SRC = r""" @@ -267,7 +267,7 @@ _pnp_kernel = _mod.get_function("pnp_gn_batch") -def _solve_pnp_cuda_kernel(obj, img, K, iterations: int = 15, damping: float = 1e-6): +def _solve_pnp_cuda_kernel(obj, img, K, iterations: int = 15, damping: float = 1e-6): # type: ignore[no-untyped-def] if cp is None: raise RuntimeError("CuPy/CUDA not available") @@ -291,7 +291,7 @@ def _solve_pnp_cuda_kernel(obj, img, K, iterations: int = 15, damping: float = 1 obj_cu = cp.ascontiguousarray(obj_cu) img_cu = cp.ascontiguousarray(img_cu) - K_np = np.asarray(_to_cpu(K), dtype=np.float32) + K_np = np.asarray(_to_cpu(K), dtype=np.float32) # type: ignore[no-untyped-call] np_intri = np.empty((B, 4), dtype=np.float32) if K_np.ndim == 2: if K_np.shape != (3, 3): @@ -344,42 +344,42 @@ def _solve_pnp_cuda_kernel(obj, img, K, iterations: int = 15, damping: float = 1 return r_host, t_host -def _bgr_to_rgb_cuda(img): +def _bgr_to_rgb_cuda(img): # type: ignore[no-untyped-def] return img[..., ::-1] -def _rgb_to_bgr_cuda(img): +def _rgb_to_bgr_cuda(img): # type: ignore[no-untyped-def] return img[..., ::-1] -def _bgra_to_rgba_cuda(img): +def _bgra_to_rgba_cuda(img): # type: ignore[no-untyped-def] out = img.copy() out[..., 0], out[..., 2] = img[..., 2], img[..., 0] return out -def _rgba_to_bgra_cuda(img): +def _rgba_to_bgra_cuda(img): # type: ignore[no-untyped-def] out = img.copy() out[..., 0], out[..., 2] = img[..., 2], img[..., 0] return out -def _gray_to_rgb_cuda(gray): - return cp.stack([gray, gray, gray], axis=-1) # type: ignore +def _gray_to_rgb_cuda(gray): # type: ignore[no-untyped-def] + return cp.stack([gray, gray, gray], axis=-1) -def _rgb_to_gray_cuda(rgb): - r = rgb[..., 0].astype(cp.float32) # type: ignore - g = rgb[..., 1].astype(cp.float32) # type: ignore - b = rgb[..., 2].astype(cp.float32) # type: ignore +def _rgb_to_gray_cuda(rgb): # type: ignore[no-untyped-def] + r = rgb[..., 0].astype(cp.float32) + g = rgb[..., 1].astype(cp.float32) + b = rgb[..., 2].astype(cp.float32) # These come from the Rec.601 conversion for YUV. R = 0.299, G = 0.587, B = 0.114 y = 0.299 * r + 0.587 * g + 0.114 * b - if rgb.dtype == cp.uint8: # type: ignore - y = cp.clip(y, 0, 255).astype(cp.uint8) # type: ignore + if rgb.dtype == cp.uint8: + y = cp.clip(y, 0, 255).astype(cp.uint8) return y -def _resize_bilinear_hwc_cuda(img, out_h: int, out_w: int): +def _resize_bilinear_hwc_cuda(img, out_h: int, out_w: int): # type: ignore[no-untyped-def] if cp is None or cndimage is None: raise RuntimeError("CuPy/CUDA not available") if img.ndim not in (2, 3): @@ -410,12 +410,11 @@ def _resize_bilinear_hwc_cuda(img, out_h: int, out_w: int): return out -def _rodrigues(x, inverse: bool = False): +def _rodrigues(x, inverse: bool = False): # type: ignore[no-untyped-def] """Unified Rodrigues transform (vector<->matrix) for NumPy/CuPy arrays.""" if cp is not None and ( - isinstance(x, cp.ndarray) # type: ignore[arg-type] - or getattr(x, "__cuda_array_interface__", None) is not None + isinstance(x, cp.ndarray) or getattr(x, "__cuda_array_interface__", None) is not None ): xp = cp else: @@ -437,7 +436,7 @@ def _rodrigues(x, inverse: bool = False): theta = xp.linalg.norm(vec, axis=1) small = theta < 1e-12 - def _skew(v): + def _skew(v): # type: ignore[no-untyped-def] vx, vy, vz = v[:, 0], v[:, 1], v[:, 2] O = xp.zeros_like(vx) return xp.stack( @@ -449,7 +448,7 @@ def _skew(v): axis=-2, ) - K = _skew(vec) + K = _skew(vec) # type: ignore[no-untyped-call] theta2 = theta * theta theta4 = theta2 * theta2 theta_safe = xp.where(small, 1.0, theta) @@ -543,12 +542,12 @@ def _undistort_points_cuda( @dataclass class CudaImage(AbstractImage): - data: any # cupy.ndarray + data: any # type: ignore[valid-type] # cupy.ndarray format: ImageFormat = field(default=ImageFormat.BGR) frame_id: str = field(default="") ts: float = field(default_factory=time.time) - def __post_init__(self): + def __post_init__(self): # type: ignore[no-untyped-def] if not HAS_CUDA or cp is None: raise RuntimeError("CuPy/CUDA not available") if not _is_cu(self.data): @@ -557,81 +556,90 @@ def __post_init__(self): self.data = cp.asarray(self.data) except Exception as e: raise ValueError("CudaImage requires a CuPy array") from e - if self.data.ndim < 2: + if self.data.ndim < 2: # type: ignore[attr-defined] raise ValueError("Image data must be at least 2D") - self.data = _ascontig(self.data) + self.data = _ascontig(self.data) # type: ignore[no-untyped-call] @property def is_cuda(self) -> bool: return True - def to_opencv(self) -> np.ndarray: + def to_opencv(self) -> np.ndarray: # type: ignore[type-arg] if self.format in (ImageFormat.BGR, ImageFormat.RGB, ImageFormat.RGBA, ImageFormat.BGRA): - return _to_cpu(self.to_bgr().data) - return _to_cpu(self.data) + return _to_cpu(self.to_bgr().data) # type: ignore[no-any-return, no-untyped-call] + return _to_cpu(self.data) # type: ignore[no-any-return, no-untyped-call] def to_rgb(self) -> CudaImage: if self.format == ImageFormat.RGB: - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] if self.format == ImageFormat.BGR: - return CudaImage(_bgr_to_rgb_cuda(self.data), ImageFormat.RGB, self.frame_id, self.ts) + return CudaImage(_bgr_to_rgb_cuda(self.data), ImageFormat.RGB, self.frame_id, self.ts) # type: ignore[no-untyped-call] if self.format == ImageFormat.RGBA: - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] if self.format == ImageFormat.BGRA: return CudaImage( - _bgra_to_rgba_cuda(self.data), ImageFormat.RGBA, self.frame_id, self.ts + _bgra_to_rgba_cuda(self.data), # type: ignore[no-untyped-call] + ImageFormat.RGBA, + self.frame_id, + self.ts, ) if self.format == ImageFormat.GRAY: - return CudaImage(_gray_to_rgb_cuda(self.data), ImageFormat.RGB, self.frame_id, self.ts) + return CudaImage(_gray_to_rgb_cuda(self.data), ImageFormat.RGB, self.frame_id, self.ts) # type: ignore[no-untyped-call] if self.format in (ImageFormat.GRAY16, ImageFormat.DEPTH16): - gray8 = (self.data.astype(cp.float32) / 256.0).clip(0, 255).astype(cp.uint8) # type: ignore - return CudaImage(_gray_to_rgb_cuda(gray8), ImageFormat.RGB, self.frame_id, self.ts) - return self.copy() # type: ignore + gray8 = (self.data.astype(cp.float32) / 256.0).clip(0, 255).astype(cp.uint8) # type: ignore[attr-defined] + return CudaImage(_gray_to_rgb_cuda(gray8), ImageFormat.RGB, self.frame_id, self.ts) # type: ignore[no-untyped-call] + return self.copy() # type: ignore[return-value] def to_bgr(self) -> CudaImage: if self.format == ImageFormat.BGR: - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] if self.format == ImageFormat.RGB: - return CudaImage(_rgb_to_bgr_cuda(self.data), ImageFormat.BGR, self.frame_id, self.ts) + return CudaImage(_rgb_to_bgr_cuda(self.data), ImageFormat.BGR, self.frame_id, self.ts) # type: ignore[no-untyped-call] if self.format == ImageFormat.RGBA: return CudaImage( - _rgba_to_bgra_cuda(self.data)[..., :3], ImageFormat.BGR, self.frame_id, self.ts + _rgba_to_bgra_cuda(self.data)[..., :3], # type: ignore[no-untyped-call] + ImageFormat.BGR, + self.frame_id, + self.ts, ) if self.format == ImageFormat.BGRA: - return CudaImage(self.data[..., :3], ImageFormat.BGR, self.frame_id, self.ts) + return CudaImage(self.data[..., :3], ImageFormat.BGR, self.frame_id, self.ts) # type: ignore[index] if self.format in (ImageFormat.GRAY, ImageFormat.DEPTH): return CudaImage( - _rgb_to_bgr_cuda(_gray_to_rgb_cuda(self.data)), + _rgb_to_bgr_cuda(_gray_to_rgb_cuda(self.data)), # type: ignore[no-untyped-call] ImageFormat.BGR, self.frame_id, self.ts, ) if self.format in (ImageFormat.GRAY16, ImageFormat.DEPTH16): - gray8 = (self.data.astype(cp.float32) / 256.0).clip(0, 255).astype(cp.uint8) # type: ignore + gray8 = (self.data.astype(cp.float32) / 256.0).clip(0, 255).astype(cp.uint8) # type: ignore[attr-defined] return CudaImage( - _rgb_to_bgr_cuda(_gray_to_rgb_cuda(gray8)), ImageFormat.BGR, self.frame_id, self.ts + _rgb_to_bgr_cuda(_gray_to_rgb_cuda(gray8)), # type: ignore[no-untyped-call] + ImageFormat.BGR, + self.frame_id, + self.ts, ) - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] def to_grayscale(self) -> CudaImage: if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH): - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] if self.format == ImageFormat.BGR: return CudaImage( - _rgb_to_gray_cuda(_bgr_to_rgb_cuda(self.data)), + _rgb_to_gray_cuda(_bgr_to_rgb_cuda(self.data)), # type: ignore[no-untyped-call] ImageFormat.GRAY, self.frame_id, self.ts, ) if self.format == ImageFormat.RGB: - return CudaImage(_rgb_to_gray_cuda(self.data), ImageFormat.GRAY, self.frame_id, self.ts) + return CudaImage(_rgb_to_gray_cuda(self.data), ImageFormat.GRAY, self.frame_id, self.ts) # type: ignore[no-untyped-call] if self.format in (ImageFormat.RGBA, ImageFormat.BGRA): rgb = ( - self.data[..., :3] + self.data[..., :3] # type: ignore[index] if self.format == ImageFormat.RGBA - else _bgra_to_rgba_cuda(self.data)[..., :3] + else _bgra_to_rgba_cuda(self.data)[..., :3] # type: ignore[no-untyped-call] ) - return CudaImage(_rgb_to_gray_cuda(rgb), ImageFormat.GRAY, self.frame_id, self.ts) + return CudaImage(_rgb_to_gray_cuda(rgb), ImageFormat.GRAY, self.frame_id, self.ts) # type: ignore[no-untyped-call] raise ValueError(f"Unsupported format: {self.format}") def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> CudaImage: @@ -639,6 +647,20 @@ def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) _resize_bilinear_hwc_cuda(self.data, height, width), self.format, self.frame_id, self.ts ) + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to rerun Image format. + + Transfers data from GPU to CPU and converts to appropriate format. + + Returns: + rr.Image or rr.DepthImage archetype for logging to rerun + """ + from dimos.msgs.sensor_msgs.image_impls.AbstractImage import format_to_rerun + + # Transfer to CPU + cpu_data = cp.asnumpy(self.data) + return format_to_rerun(cpu_data, self.format) + def crop(self, x: int, y: int, width: int, height: int) -> CudaImage: """Crop the image to the specified region. @@ -652,7 +674,7 @@ def crop(self, x: int, y: int, width: int, height: int) -> CudaImage: A new CudaImage containing the cropped region """ # Get current image dimensions - img_height, img_width = self.data.shape[:2] + img_height, img_width = self.data.shape[:2] # type: ignore[attr-defined] # Clamp the crop region to image bounds x = max(0, min(x, img_width)) @@ -661,12 +683,12 @@ def crop(self, x: int, y: int, width: int, height: int) -> CudaImage: y_end = min(y + height, img_height) # Perform the crop using array slicing - if self.data.ndim == 2: + if self.data.ndim == 2: # type: ignore[attr-defined] # Grayscale image - cropped_data = self.data[y:y_end, x:x_end] + cropped_data = self.data[y:y_end, x:x_end] # type: ignore[index] else: # Color image (HxWxC) - cropped_data = self.data[y:y_end, x:x_end, :] + cropped_data = self.data[y:y_end, x:x_end, :] # type: ignore[index] # Return a new CudaImage with the cropped data return CudaImage(cropped_data, self.format, self.frame_id, self.ts) @@ -675,17 +697,17 @@ def sharpness(self) -> float: if cp is None: return 0.0 try: - from cupyx.scipy import ndimage as cndimage # type: ignore + from cupyx.scipy import ndimage as cndimage - gray = self.to_grayscale().data.astype(cp.float32) + gray = self.to_grayscale().data.astype(cp.float32) # type: ignore[attr-defined] deriv5 = cp.asarray([1, 2, 0, -2, -1], dtype=cp.float32) smooth5 = cp.asarray([1, 4, 6, 4, 1], dtype=cp.float32) - gx = cndimage.convolve1d(gray, deriv5, axis=1, mode="reflect") # type: ignore - gx = cndimage.convolve1d(gx, smooth5, axis=0, mode="reflect") # type: ignore - gy = cndimage.convolve1d(gray, deriv5, axis=0, mode="reflect") # type: ignore - gy = cndimage.convolve1d(gy, smooth5, axis=1, mode="reflect") # type: ignore - magnitude = cp.hypot(gx, gy) # type: ignore - mean_mag = float(cp.asnumpy(magnitude.mean())) # type: ignore + gx = cndimage.convolve1d(gray, deriv5, axis=1, mode="reflect") + gx = cndimage.convolve1d(gx, smooth5, axis=0, mode="reflect") + gy = cndimage.convolve1d(gray, deriv5, axis=0, mode="reflect") + gy = cndimage.convolve1d(gy, smooth5, axis=1, mode="reflect") + magnitude = cp.hypot(gx, gy) + mean_mag = float(cp.asnumpy(magnitude.mean())) except Exception: return 0.0 if mean_mag <= 0: @@ -700,38 +722,38 @@ class BBox: w: int h: int - def create_csrt_tracker(self, bbox: BBox): + def create_csrt_tracker(self, bbox: BBox): # type: ignore[no-untyped-def] if csignal is None: raise RuntimeError("cupyx.scipy.signal not available for CUDA tracker") - x, y, w, h = map(int, bbox) - gray = self.to_grayscale().data.astype(cp.float32) + x, y, w, h = map(int, bbox) # type: ignore[call-overload] + gray = self.to_grayscale().data.astype(cp.float32) # type: ignore[attr-defined] tmpl = gray[y : y + h, x : x + w] if tmpl.size == 0: raise ValueError("Invalid bbox for CUDA tracker") return _CudaTemplateTracker(tmpl, x0=x, y0=y) - def csrt_update(self, tracker) -> tuple[bool, tuple[int, int, int, int]]: + def csrt_update(self, tracker) -> tuple[bool, tuple[int, int, int, int]]: # type: ignore[no-untyped-def] if not isinstance(tracker, _CudaTemplateTracker): raise TypeError("Expected CUDA tracker instance") - gray = self.to_grayscale().data.astype(cp.float32) + gray = self.to_grayscale().data.astype(cp.float32) # type: ignore[attr-defined] x, y, w, h = tracker.update(gray) return True, (int(x), int(y), int(w), int(h)) # PnP – Gauss–Newton (no distortion in batch), iterative per-instance def solve_pnp( self, - object_points: np.ndarray, - image_points: np.ndarray, - camera_matrix: np.ndarray, - dist_coeffs: np.ndarray | None = None, + object_points: np.ndarray, # type: ignore[type-arg] + image_points: np.ndarray, # type: ignore[type-arg] + camera_matrix: np.ndarray, # type: ignore[type-arg] + dist_coeffs: np.ndarray | None = None, # type: ignore[type-arg] flags: int = cv2.SOLVEPNP_ITERATIVE, - ) -> tuple[bool, np.ndarray, np.ndarray]: + ) -> tuple[bool, np.ndarray, np.ndarray]: # type: ignore[type-arg] if not HAS_CUDA or cp is None or (dist_coeffs is not None and np.any(dist_coeffs)): obj = np.asarray(object_points, dtype=np.float32).reshape(-1, 3) img = np.asarray(image_points, dtype=np.float32).reshape(-1, 2) K = np.asarray(camera_matrix, dtype=np.float64) dist = None if dist_coeffs is None else np.asarray(dist_coeffs, dtype=np.float64) - ok, rvec, tvec = cv2.solvePnP(obj, img, K, dist, flags=flags) + ok, rvec, tvec = cv2.solvePnP(obj, img, K, dist, flags=flags) # type: ignore[arg-type] return bool(ok), rvec.astype(np.float64), tvec.astype(np.float64) rvec, tvec = _solve_pnp_cuda_kernel(object_points, image_points, camera_matrix) @@ -740,13 +762,13 @@ def solve_pnp( def solve_pnp_batch( self, - object_points_batch: np.ndarray, - image_points_batch: np.ndarray, - camera_matrix: np.ndarray, - dist_coeffs: np.ndarray | None = None, + object_points_batch: np.ndarray, # type: ignore[type-arg] + image_points_batch: np.ndarray, # type: ignore[type-arg] + camera_matrix: np.ndarray, # type: ignore[type-arg] + dist_coeffs: np.ndarray | None = None, # type: ignore[type-arg] iterations: int = 15, damping: float = 1e-6, - ) -> tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: # type: ignore[type-arg] """Batched PnP (each block = one instance).""" if not HAS_CUDA or cp is None or (dist_coeffs is not None and np.any(dist_coeffs)): obj = np.asarray(object_points_batch, dtype=np.float32) @@ -771,7 +793,11 @@ def solve_pnp_batch( else: raise ValueError("dist_coeffs must be 1D or batched 2D") ok, rvec, tvec = cv2.solvePnP( - obj[b], img[b], K_b, dist_b, flags=cv2.SOLVEPNP_ITERATIVE + obj[b], + img[b], + K_b, + dist_b, # type: ignore[arg-type] + flags=cv2.SOLVEPNP_ITERATIVE, ) if not ok: raise RuntimeError(f"cv2.solvePnP failed for batch index {b}") @@ -779,7 +805,7 @@ def solve_pnp_batch( t_list[b] = tvec.astype(np.float64) return r_list, t_list - return _solve_pnp_cuda_kernel( + return _solve_pnp_cuda_kernel( # type: ignore[no-any-return] object_points_batch, image_points_batch, camera_matrix, @@ -789,15 +815,15 @@ def solve_pnp_batch( def solve_pnp_ransac( self, - object_points: np.ndarray, - image_points: np.ndarray, - camera_matrix: np.ndarray, - dist_coeffs: np.ndarray | None = None, + object_points: np.ndarray, # type: ignore[type-arg] + image_points: np.ndarray, # type: ignore[type-arg] + camera_matrix: np.ndarray, # type: ignore[type-arg] + dist_coeffs: np.ndarray | None = None, # type: ignore[type-arg] iterations_count: int = 100, reprojection_error: float = 3.0, confidence: float = 0.99, min_sample: int = 6, - ) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]: + ) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]: # type: ignore[type-arg] """RANSAC with CUDA PnP solver.""" if not HAS_CUDA or cp is None or (dist_coeffs is not None and np.any(dist_coeffs)): obj = np.asarray(object_points, dtype=np.float32) @@ -808,7 +834,7 @@ def solve_pnp_ransac( obj, img, K, - dist, + dist, # type: ignore[arg-type] iterationsCount=int(iterations_count), reprojectionError=float(reprojection_error), confidence=float(confidence), @@ -821,7 +847,7 @@ def solve_pnp_ransac( obj = cp.asarray(object_points, dtype=cp.float32) img = cp.asarray(image_points, dtype=cp.float32) - camera_matrix_np = np.asarray(_to_cpu(camera_matrix), dtype=np.float32) + camera_matrix_np = np.asarray(_to_cpu(camera_matrix), dtype=np.float32) # type: ignore[no-untyped-call] fx = float(camera_matrix_np[0, 0]) fy = float(camera_matrix_np[1, 1]) cx = float(camera_matrix_np[0, 2]) @@ -877,7 +903,7 @@ def __init__( self.y = int(y0) self.x = int(x0) - def update(self, gray: cp.ndarray): + def update(self, gray: cp.ndarray): # type: ignore[no-untyped-def] H, W = int(gray.shape[0]), int(gray.shape[1]) r = self.search_radius x0 = max(0, self.x - r) @@ -901,17 +927,17 @@ def update(self, gray: cp.ndarray): tmpl_energy = cp.sqrt(cp.sum(tmpl_zm * tmpl_zm)) + 1e-6 # NCC via correlate2d and local std ones = cp.ones((th, tw), dtype=cp.float32) - num = csignal.correlate2d(search, tmpl_zm, mode="valid") # type: ignore - sumS = csignal.correlate2d(search, ones, mode="valid") # type: ignore - sumS2 = csignal.correlate2d(search * search, ones, mode="valid") # type: ignore + num = csignal.correlate2d(search, tmpl_zm, mode="valid") + sumS = csignal.correlate2d(search, ones, mode="valid") + sumS2 = csignal.correlate2d(search * search, ones, mode="valid") n = float(th * tw) meanS = sumS / n varS = cp.clip(sumS2 - n * meanS * meanS, 0.0, None) stdS = cp.sqrt(varS) + 1e-6 res = num / (stdS * tmpl_energy) ij = cp.unravel_index(cp.argmax(res), res.shape) - dy, dx = int(ij[0].get()), int(ij[1].get()) # type: ignore - score = float(res[ij].get()) # type: ignore + dy, dx = int(ij[0].get()), int(ij[1].get()) + score = float(res[ij].get()) if score > best_score: best_score = score best = (x0 + dx, y0 + dy, tw, th) diff --git a/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py b/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py index d75adc66ea..250b951371 100644 --- a/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py +++ b/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,12 +28,12 @@ @dataclass class NumpyImage(AbstractImage): - data: np.ndarray + data: np.ndarray # type: ignore[type-arg] format: ImageFormat = field(default=ImageFormat.BGR) frame_id: str = field(default="") ts: float = field(default_factory=time.time) - def __post_init__(self): + def __post_init__(self): # type: ignore[no-untyped-def] if not isinstance(self.data, np.ndarray) or self.data.ndim < 2: raise ValueError("NumpyImage requires a 2D/3D NumPy array") @@ -41,7 +41,7 @@ def __post_init__(self): def is_cuda(self) -> bool: return False - def to_opencv(self) -> np.ndarray: + def to_opencv(self) -> np.ndarray: # type: ignore[type-arg] arr = self.data if self.format == ImageFormat.BGR: return arr @@ -62,14 +62,14 @@ def to_opencv(self) -> np.ndarray: def to_rgb(self) -> NumpyImage: if self.format == ImageFormat.RGB: - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] arr = self.data if self.format == ImageFormat.BGR: return NumpyImage( cv2.cvtColor(arr, cv2.COLOR_BGR2RGB), ImageFormat.RGB, self.frame_id, self.ts ) if self.format == ImageFormat.RGBA: - return self.copy() # RGBA contains RGB + alpha + return self.copy() # type: ignore[return-value] # RGBA contains RGB + alpha if self.format == ImageFormat.BGRA: rgba = cv2.cvtColor(arr, cv2.COLOR_BGRA2RGBA) return NumpyImage(rgba, ImageFormat.RGBA, self.frame_id, self.ts) @@ -77,11 +77,11 @@ def to_rgb(self) -> NumpyImage: gray8 = (arr / 256).astype(np.uint8) if self.format != ImageFormat.GRAY else arr rgb = cv2.cvtColor(gray8, cv2.COLOR_GRAY2RGB) return NumpyImage(rgb, ImageFormat.RGB, self.frame_id, self.ts) - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] def to_bgr(self) -> NumpyImage: if self.format == ImageFormat.BGR: - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] arr = self.data if self.format == ImageFormat.RGB: return NumpyImage( @@ -100,11 +100,11 @@ def to_bgr(self) -> NumpyImage: return NumpyImage( cv2.cvtColor(gray8, cv2.COLOR_GRAY2BGR), ImageFormat.BGR, self.frame_id, self.ts ) - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] def to_grayscale(self) -> NumpyImage: if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH): - return self.copy() # type: ignore + return self.copy() # type: ignore[return-value] if self.format == ImageFormat.BGR: return NumpyImage( cv2.cvtColor(self.data, cv2.COLOR_BGR2GRAY), @@ -126,6 +126,12 @@ def to_grayscale(self) -> NumpyImage: ) raise ValueError(f"Unsupported format: {self.format}") + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to rerun Image format.""" + from dimos.msgs.sensor_msgs.image_impls.AbstractImage import format_to_rerun + + return format_to_rerun(self.data, self.format) + def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> NumpyImage: return NumpyImage( cv2.resize(self.data, (width, height), interpolation=interpolation), @@ -179,20 +185,20 @@ def sharpness(self) -> float: # PnP wrappers def solve_pnp( self, - object_points: np.ndarray, - image_points: np.ndarray, - camera_matrix: np.ndarray, - dist_coeffs: np.ndarray | None = None, + object_points: np.ndarray, # type: ignore[type-arg] + image_points: np.ndarray, # type: ignore[type-arg] + camera_matrix: np.ndarray, # type: ignore[type-arg] + dist_coeffs: np.ndarray | None = None, # type: ignore[type-arg] flags: int = cv2.SOLVEPNP_ITERATIVE, - ) -> tuple[bool, np.ndarray, np.ndarray]: + ) -> tuple[bool, np.ndarray, np.ndarray]: # type: ignore[type-arg] obj = np.asarray(object_points, dtype=np.float32).reshape(-1, 3) img = np.asarray(image_points, dtype=np.float32).reshape(-1, 2) K = np.asarray(camera_matrix, dtype=np.float64) dist = None if dist_coeffs is None else np.asarray(dist_coeffs, dtype=np.float64) - ok, rvec, tvec = cv2.solvePnP(obj, img, K, dist, flags=flags) + ok, rvec, tvec = cv2.solvePnP(obj, img, K, dist, flags=flags) # type: ignore[arg-type] return bool(ok), rvec.astype(np.float64), tvec.astype(np.float64) - def create_csrt_tracker(self, bbox: tuple[int, int, int, int]): + def create_csrt_tracker(self, bbox: tuple[int, int, int, int]): # type: ignore[no-untyped-def] tracker = None if hasattr(cv2, "legacy") and hasattr(cv2.legacy, "TrackerCSRT_create"): tracker = cv2.legacy.TrackerCSRT_create() @@ -205,7 +211,7 @@ def create_csrt_tracker(self, bbox: tuple[int, int, int, int]): raise RuntimeError("Failed to initialize CSRT tracker") return tracker - def csrt_update(self, tracker) -> tuple[bool, tuple[int, int, int, int]]: + def csrt_update(self, tracker) -> tuple[bool, tuple[int, int, int, int]]: # type: ignore[no-untyped-def] ok, box = tracker.update(self.to_bgr().to_opencv()) if not ok: return False, (0, 0, 0, 0) @@ -214,15 +220,15 @@ def csrt_update(self, tracker) -> tuple[bool, tuple[int, int, int, int]]: def solve_pnp_ransac( self, - object_points: np.ndarray, - image_points: np.ndarray, - camera_matrix: np.ndarray, - dist_coeffs: np.ndarray | None = None, + object_points: np.ndarray, # type: ignore[type-arg] + image_points: np.ndarray, # type: ignore[type-arg] + camera_matrix: np.ndarray, # type: ignore[type-arg] + dist_coeffs: np.ndarray | None = None, # type: ignore[type-arg] iterations_count: int = 100, reprojection_error: float = 3.0, confidence: float = 0.99, min_sample: int = 6, - ) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]: + ) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]: # type: ignore[type-arg] obj = np.asarray(object_points, dtype=np.float32).reshape(-1, 3) img = np.asarray(image_points, dtype=np.float32).reshape(-1, 2) K = np.asarray(camera_matrix, dtype=np.float64) @@ -231,7 +237,7 @@ def solve_pnp_ransac( obj, img, K, - dist, + dist, # type: ignore[arg-type] iterationsCount=int(iterations_count), reprojectionError=float(reprojection_error), confidence=float(confidence), diff --git a/dimos/msgs/sensor_msgs/image_impls/test_image_backend_utils.py b/dimos/msgs/sensor_msgs/image_impls/test_image_backend_utils.py index c226e36bf0..9ddc15fe85 100644 --- a/dimos/msgs/sensor_msgs/image_impls/test_image_backend_utils.py +++ b/dimos/msgs/sensor_msgs/image_impls/test_image_backend_utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -37,10 +37,10 @@ def _has_cupy() -> bool: try: - import cupy as cp # type: ignore + import cupy as cp try: - ndev = cp.cuda.runtime.getDeviceCount() # type: ignore[attr-defined] + ndev = cp.cuda.runtime.getDeviceCount() if ndev <= 0: return False x = cp.array([1, 2, 3]) @@ -78,7 +78,7 @@ def test_rectify_image_cpu(shape, fmt) -> None: "shape,fmt", [((32, 32, 3), ImageFormat.BGR), ((32, 32), ImageFormat.GRAY)] ) def test_rectify_image_gpu_parity(shape, fmt) -> None: - import cupy as cp # type: ignore + import cupy as cp arr_np = (np.random.rand(*shape) * (255 if fmt != ImageFormat.GRAY else 65535)).astype( np.uint8 if fmt != ImageFormat.GRAY else np.uint16 @@ -101,7 +101,7 @@ def test_rectify_image_gpu_parity(shape, fmt) -> None: @pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") def test_rectify_image_gpu_nonzero_dist_close() -> None: - import cupy as cp # type: ignore + import cupy as cp H, W = 64, 96 # Structured pattern to make interpolation deterministic enough @@ -148,7 +148,7 @@ def test_project_roundtrip_cpu() -> None: @pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") def test_project_parity_gpu_cpu() -> None: - import cupy as cp # type: ignore + import cupy as cp pts3d_np = np.array([[0.1, 0.2, 1.0], [0.0, 0.0, 2.0], [0.5, -0.3, 3.0]], dtype=np.float32) fx, fy, cx, cy = 200.0, 220.0, 64.0, 48.0 @@ -167,7 +167,7 @@ def test_project_parity_gpu_cpu() -> None: @pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") def test_project_parity_gpu_cpu_random() -> None: - import cupy as cp # type: ignore + import cupy as cp rng = np.random.RandomState(0) N = 1000 @@ -205,7 +205,7 @@ def test_colorize_depth_cpu() -> None: @pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") def test_colorize_depth_gpu_parity() -> None: - import cupy as cp # type: ignore + import cupy as cp depth_np = np.zeros((16, 20), dtype=np.float32) depth_np[4:8, 5:15] = 2.0 @@ -224,7 +224,7 @@ def test_draw_bounding_box_cpu() -> None: @pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") def test_draw_bounding_box_gpu_parity() -> None: - import cupy as cp # type: ignore + import cupy as cp img_np = np.zeros((20, 30, 3), dtype=np.uint8) out_cpu = draw_bounding_box(img_np.copy(), [2, 3, 10, 12], color=(0, 255, 0), thickness=2) @@ -243,7 +243,7 @@ def test_draw_segmentation_mask_cpu() -> None: @pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") def test_draw_segmentation_mask_gpu_parity() -> None: - import cupy as cp # type: ignore + import cupy as cp img_np = np.zeros((20, 30, 3), dtype=np.uint8) mask_np = np.zeros((20, 30), dtype=np.uint8) @@ -271,7 +271,7 @@ def test_draw_object_detection_visualization_cpu() -> None: @pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") def test_draw_object_detection_visualization_gpu_parity() -> None: - import cupy as cp # type: ignore + import cupy as cp img_np = np.zeros((30, 40, 3), dtype=np.uint8) objects = [ diff --git a/dimos/msgs/sensor_msgs/image_impls/test_image_backends.py b/dimos/msgs/sensor_msgs/image_impls/test_image_backends.py index d8012a8f53..b1de0ac777 100644 --- a/dimos/msgs/sensor_msgs/image_impls/test_image_backends.py +++ b/dimos/msgs/sensor_msgs/image_impls/test_image_backends.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/sensor_msgs/test_CameraInfo.py b/dimos/msgs/sensor_msgs/test_CameraInfo.py index c35145255b..d66a39727f 100644 --- a/dimos/msgs/sensor_msgs/test_CameraInfo.py +++ b/dimos/msgs/sensor_msgs/test_CameraInfo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -394,7 +394,15 @@ def test_camera_info_from_yaml() -> None: """Test loading CameraInfo from YAML file.""" # Get path to the single webcam YAML file - yaml_path = get_project_root() / "dimos" / "hardware" / "camera" / "zed" / "single_webcam.yaml" + yaml_path = ( + get_project_root() + / "dimos" + / "hardware" + / "sensors" + / "camera" + / "zed" + / "single_webcam.yaml" + ) # Load CameraInfo from YAML camera_info = CameraInfo.from_yaml(str(yaml_path)) @@ -429,7 +437,7 @@ def test_camera_info_from_yaml() -> None: def test_calibration_provider() -> None: """Test CalibrationProvider lazy loading of YAML files.""" # Get the directory containing calibration files (not the file itself) - calibration_dir = get_project_root() / "dimos" / "hardware" / "camera" / "zed" + calibration_dir = get_project_root() / "dimos" / "hardware" / "sensors" / "camera" / "zed" # Create CalibrationProvider instance Calibrations = CalibrationProvider(calibration_dir) diff --git a/dimos/msgs/sensor_msgs/test_Joy.py b/dimos/msgs/sensor_msgs/test_Joy.py index ae1b4a6379..77b47f4983 100644 --- a/dimos/msgs/sensor_msgs/test_Joy.py +++ b/dimos/msgs/sensor_msgs/test_Joy.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/sensor_msgs/test_PointCloud2.py b/dimos/msgs/sensor_msgs/test_PointCloud2.py index d51b827fa7..e5cd11da8c 100644 --- a/dimos/msgs/sensor_msgs/test_PointCloud2.py +++ b/dimos/msgs/sensor_msgs/test_PointCloud2.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -45,8 +45,8 @@ def test_lcm_encode_decode() -> None: decoded = PointCloud2.lcm_decode(binary_msg) # 1. Check number of points - original_points = lidar_msg.as_numpy() - decoded_points = decoded.as_numpy() + original_points, _ = lidar_msg.as_numpy() + decoded_points, _ = decoded.as_numpy() print(f"Original points: {len(original_points)}") print(f"Decoded points: {len(decoded_points)}") @@ -134,8 +134,8 @@ def test_ros_conversion() -> None: converted = PointCloud2.from_ros_msg(ros_msg) # Check points are preserved - original_points = original.as_numpy() - converted_points = converted.as_numpy() + original_points, _ = original.as_numpy() + converted_points, _ = converted.as_numpy() assert len(original_points) == len(converted_points), ( f"Point count mismatch: {len(original_points)} vs {len(converted_points)}" @@ -199,7 +199,7 @@ def test_ros_conversion() -> None: f"Frame ID not preserved: expected 'ros_test_frame', got '{dimos_pc.frame_id}'" ) - decoded_points = dimos_pc.as_numpy() + decoded_points, _ = dimos_pc.as_numpy() assert len(decoded_points) == 3, ( f"Wrong number of points: expected 3, got {len(decoded_points)}" ) diff --git a/dimos/msgs/sensor_msgs/test_image.py b/dimos/msgs/sensor_msgs/test_image.py index 65237e4a6c..24375139b3 100644 --- a/dimos/msgs/sensor_msgs/test_image.py +++ b/dimos/msgs/sensor_msgs/test_image.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/std_msgs/Bool.py b/dimos/msgs/std_msgs/Bool.py index 55751a41eb..c11743573f 100644 --- a/dimos/msgs/std_msgs/Bool.py +++ b/dimos/msgs/std_msgs/Bool.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,12 +18,12 @@ from dimos_lcm.std_msgs import Bool as LCMBool try: - from std_msgs.msg import Bool as ROSBool + from std_msgs.msg import Bool as ROSBool # type: ignore[attr-defined] except ImportError: - ROSBool = None + ROSBool = None # type: ignore[assignment, misc] -class Bool(LCMBool): +class Bool(LCMBool): # type: ignore[misc] """ROS-compatible Bool message.""" msg_name = "std_msgs.Bool" @@ -52,6 +52,6 @@ def to_ros_msg(self) -> ROSBool: """ if ROSBool is None: raise ImportError("ROS std_msgs not available") - ros_msg = ROSBool() + ros_msg = ROSBool() # type: ignore[no-untyped-call] ros_msg.data = bool(self.data) return ros_msg diff --git a/dimos/msgs/std_msgs/Header.py b/dimos/msgs/std_msgs/Header.py index 1d17913941..5c54200497 100644 --- a/dimos/msgs/std_msgs/Header.py +++ b/dimos/msgs/std_msgs/Header.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,12 +22,14 @@ # Import the actual LCM header type that's returned from decoding try: - from lcm_msgs.std_msgs.Header import Header as DecodedLCMHeader + from lcm_msgs.std_msgs.Header import ( # type: ignore[import-not-found] + Header as DecodedLCMHeader, + ) except ImportError: DecodedLCMHeader = None -class Header(LCMHeader): +class Header(LCMHeader): # type: ignore[misc] msg_name = "std_msgs.Header" ts: float @@ -39,7 +41,7 @@ def __init__(self) -> None: nsec = int((self.ts - sec) * 1_000_000_000) super().__init__(seq=0, stamp=LCMTime(sec=sec, nsec=nsec), frame_id="") - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, frame_id: str) -> None: """Initialize a Header with current time and specified frame_id.""" self.ts = time.time() @@ -47,14 +49,14 @@ def __init__(self, frame_id: str) -> None: nsec = int((self.ts - sec) * 1_000_000_000) super().__init__(seq=1, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, timestamp: float, frame_id: str = "", seq: int = 1) -> None: """Initialize a Header with Unix timestamp, frame_id, and optional seq.""" sec = int(timestamp) nsec = int((timestamp - sec) * 1_000_000_000) super().__init__(seq=seq, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, timestamp: datetime, frame_id: str = "") -> None: """Initialize a Header with datetime object and frame_id.""" self.ts = timestamp.timestamp() @@ -62,17 +64,17 @@ def __init__(self, timestamp: datetime, frame_id: str = "") -> None: nsec = int((self.ts - sec) * 1_000_000_000) super().__init__(seq=1, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, seq: int, stamp: LCMTime, frame_id: str) -> None: """Initialize with explicit seq, stamp, and frame_id (LCM compatibility).""" super().__init__(seq=seq, stamp=stamp, frame_id=frame_id) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, header: LCMHeader) -> None: """Initialize from another Header (copy constructor).""" super().__init__(seq=header.seq, stamp=header.stamp, frame_id=header.frame_id) - @dispatch + @dispatch # type: ignore[no-redef] def __init__(self, header: object) -> None: """Initialize from a decoded LCM header object.""" # Handle the case where we get an lcm_msgs.std_msgs.Header.Header object @@ -90,7 +92,7 @@ def now(cls, frame_id: str = "", seq: int = 1) -> Header: @property def timestamp(self) -> float: """Get timestamp as Unix time (float).""" - return self.stamp.sec + (self.stamp.nsec / 1_000_000_000) + return self.stamp.sec + (self.stamp.nsec / 1_000_000_000) # type: ignore[no-any-return] @property def datetime(self) -> datetime: diff --git a/dimos/msgs/std_msgs/Int32.py b/dimos/msgs/std_msgs/Int32.py index 0ce2f03f60..ba4906f485 100644 --- a/dimos/msgs/std_msgs/Int32.py +++ b/dimos/msgs/std_msgs/Int32.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. """Int32 message type.""" @@ -22,7 +22,7 @@ from dimos_lcm.std_msgs import Int32 as LCMInt32 -class Int32(LCMInt32): +class Int32(LCMInt32): # type: ignore[misc] """ROS-compatible Int32 message.""" msg_name: ClassVar[str] = "std_msgs.Int32" diff --git a/dimos/msgs/std_msgs/Int8.py b/dimos/msgs/std_msgs/Int8.py index d76b479d41..b07e965e3f 100644 --- a/dimos/msgs/std_msgs/Int8.py +++ b/dimos/msgs/std_msgs/Int8.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. """Int32 message type.""" @@ -22,12 +22,12 @@ from dimos_lcm.std_msgs import Int8 as LCMInt8 try: - from std_msgs.msg import Int8 as ROSInt8 + from std_msgs.msg import Int8 as ROSInt8 # type: ignore[attr-defined] except ImportError: - ROSInt8 = None + ROSInt8 = None # type: ignore[assignment, misc] -class Int8(LCMInt8): +class Int8(LCMInt8): # type: ignore[misc] """ROS-compatible Int32 message.""" msg_name: ClassVar[str] = "std_msgs.Int8" @@ -56,6 +56,6 @@ def to_ros_msg(self) -> ROSInt8: """ if ROSInt8 is None: raise ImportError("ROS std_msgs not available") - ros_msg = ROSInt8() + ros_msg = ROSInt8() # type: ignore[no-untyped-call] ros_msg.data = self.data return ros_msg diff --git a/dimos/msgs/std_msgs/__init__.py b/dimos/msgs/std_msgs/__init__.py index e517ea1864..9002b8c4ef 100644 --- a/dimos/msgs/std_msgs/__init__.py +++ b/dimos/msgs/std_msgs/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/std_msgs/test_header.py b/dimos/msgs/std_msgs/test_header.py index 314ee5cd37..93f20da283 100644 --- a/dimos/msgs/std_msgs/test_header.py +++ b/dimos/msgs/std_msgs/test_header.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/tf2_msgs/TFMessage.py b/dimos/msgs/tf2_msgs/TFMessage.py index 5aabfa4b23..29e890de47 100644 --- a/dimos/msgs/tf2_msgs/TFMessage.py +++ b/dimos/msgs/tf2_msgs/TFMessage.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License.# You may obtain a copy of the License at @@ -32,11 +32,13 @@ from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage try: - from geometry_msgs.msg import TransformStamped as ROSTransformStamped - from tf2_msgs.msg import TFMessage as ROSTFMessage + from geometry_msgs.msg import ( # type: ignore[attr-defined] + TransformStamped as ROSTransformStamped, + ) + from tf2_msgs.msg import TFMessage as ROSTFMessage # type: ignore[attr-defined] except ImportError: - ROSTFMessage = None - ROSTransformStamped = None + ROSTFMessage = None # type: ignore[assignment, misc] + ROSTransformStamped = None # type: ignore[assignment, misc] from dimos.msgs.geometry_msgs.Quaternion import Quaternion from dimos.msgs.geometry_msgs.Transform import Transform @@ -75,7 +77,7 @@ def lcm_encode(self) -> bytes: transforms=res, ) - return lcm_msg.lcm_encode() + return lcm_msg.lcm_encode() # type: ignore[no-any-return] @classmethod def lcm_decode(cls, data: bytes | BinaryIO) -> TFMessage: @@ -113,7 +115,7 @@ def __getitem__(self, index: int) -> Transform: """Get transform by index.""" return self.transforms[index] - def __iter__(self) -> Iterator: + def __iter__(self) -> Iterator: # type: ignore[type-arg] """Iterate over transforms.""" return iter(self.transforms) @@ -150,10 +152,29 @@ def to_ros_msg(self) -> ROSTFMessage: Returns: ROS TFMessage message """ - ros_msg = ROSTFMessage() + ros_msg = ROSTFMessage() # type: ignore[no-untyped-call] # Convert each Transform to ROS TransformStamped for transform in self.transforms: ros_msg.transforms.append(transform.to_ros_transform_stamped()) return ros_msg + + def to_rerun(self): # type: ignore[no-untyped-def] + """Convert to a list of rerun Transform3D archetypes. + + Returns a list of tuples (entity_path, Transform3D) for each transform + in the message. The entity_path is derived from the child_frame_id. + + Returns: + List of (entity_path, rr.Transform3D) tuples + + Example: + for path, transform in tf_msg.to_rerun(): + rr.log(path, transform) + """ + results = [] + for transform in self.transforms: + entity_path = f"world/{transform.child_frame_id}" + results.append((entity_path, transform.to_rerun())) # type: ignore[no-untyped-call] + return results diff --git a/dimos/msgs/tf2_msgs/__init__.py b/dimos/msgs/tf2_msgs/__init__.py index 683e4ec61b..69d4e0137e 100644 --- a/dimos/msgs/tf2_msgs/__init__.py +++ b/dimos/msgs/tf2_msgs/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/tf2_msgs/test_TFMessage.py b/dimos/msgs/tf2_msgs/test_TFMessage.py index 26c0bac570..783692fb35 100644 --- a/dimos/msgs/tf2_msgs/test_TFMessage.py +++ b/dimos/msgs/tf2_msgs/test_TFMessage.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py b/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py index 9471673821..0846f91ee6 100644 --- a/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py +++ b/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/msgs/trajectory_msgs/JointTrajectory.py b/dimos/msgs/trajectory_msgs/JointTrajectory.py new file mode 100644 index 0000000000..ae2ad55fd1 --- /dev/null +++ b/dimos/msgs/trajectory_msgs/JointTrajectory.py @@ -0,0 +1,211 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +JointTrajectory message type. + +A sequence of joint trajectory points representing a full trajectory. +Similar to ROS trajectory_msgs/JointTrajectory. +""" + +from io import BytesIO +import struct +import time + +from dimos.msgs.trajectory_msgs.TrajectoryPoint import TrajectoryPoint + + +class JointTrajectory: + """ + A joint-space trajectory consisting of timestamped waypoints. + + Attributes: + timestamp: When trajectory was created (seconds since epoch) + joint_names: Names of joints (optional) + points: Sequence of TrajectoryPoints + duration: Total trajectory duration (seconds) + """ + + msg_name = "trajectory_msgs.JointTrajectory" + + __slots__ = ["duration", "joint_names", "num_joints", "num_points", "points", "timestamp"] + + def __init__( + self, + points: list[TrajectoryPoint] | None = None, + joint_names: list[str] | None = None, + timestamp: float | None = None, + ) -> None: + """ + Initialize JointTrajectory. + + Args: + points: List of TrajectoryPoints + joint_names: Names of joints (optional) + timestamp: Creation timestamp (defaults to now) + """ + self.timestamp = timestamp if timestamp is not None else time.time() + self.points = list(points) if points else [] + self.num_points = len(self.points) + self.joint_names = list(joint_names) if joint_names else [] + self.num_joints = ( + len(self.joint_names) + if self.joint_names + else (self.points[0].num_joints if self.points else 0) + ) + + # Compute duration from last point + if self.points: + self.duration = max(p.time_from_start for p in self.points) + else: + self.duration = 0.0 + + def sample(self, t: float) -> tuple[list[float], list[float]]: + """ + Sample the trajectory at time t using linear interpolation. + + Args: + t: Time from trajectory start (seconds) + + Returns: + Tuple of (positions, velocities) at time t + """ + if not self.points: + return [], [] + + # Clamp t to valid range + t = max(0.0, min(t, self.duration)) + + # Find bracketing points + if t <= self.points[0].time_from_start: + return list(self.points[0].positions), list(self.points[0].velocities) + + if t >= self.points[-1].time_from_start: + return list(self.points[-1].positions), list(self.points[-1].velocities) + + # Find interval + for i in range(len(self.points) - 1): + t0 = self.points[i].time_from_start + t1 = self.points[i + 1].time_from_start + + if t0 <= t <= t1: + # Linear interpolation + alpha = (t - t0) / (t1 - t0) if t1 > t0 else 0.0 + p0 = self.points[i] + p1 = self.points[i + 1] + + positions = [ + p0.positions[j] + alpha * (p1.positions[j] - p0.positions[j]) + for j in range(len(p0.positions)) + ] + velocities = [ + p0.velocities[j] + alpha * (p1.velocities[j] - p0.velocities[j]) + for j in range(len(p0.velocities)) + ] + return positions, velocities + + # Fallback + return list(self.points[-1].positions), list(self.points[-1].velocities) + + def lcm_encode(self) -> bytes: + """Encode for LCM transport.""" + return self.encode() + + def encode(self) -> bytes: + buf = BytesIO() + buf.write(JointTrajectory._get_packed_fingerprint()) + self._encode_one(buf) + return buf.getvalue() + + def _encode_one(self, buf: BytesIO) -> None: + # timestamp (double) + buf.write(struct.pack(">d", self.timestamp)) + # duration (double) + buf.write(struct.pack(">d", self.duration)) + # num_joint_names (int32) - actual count of joint names + buf.write(struct.pack(">i", len(self.joint_names))) + # joint_names (string[num_joint_names]) + for name in self.joint_names: + name_bytes = name.encode("utf-8") + buf.write(struct.pack(">i", len(name_bytes))) + buf.write(name_bytes) + # num_points (int32) + buf.write(struct.pack(">i", self.num_points)) + # points (TrajectoryPoint[num_points]) + for point in self.points: + point._encode_one(buf) + + @classmethod + def lcm_decode(cls, data: bytes) -> "JointTrajectory": + """Decode from LCM transport.""" + return cls.decode(data) + + @classmethod + def decode(cls, data: bytes) -> "JointTrajectory": + buf = BytesIO(data) if not hasattr(data, "read") else data + if buf.read(8) != cls._get_packed_fingerprint(): + raise ValueError("Decode error: fingerprint mismatch") + return cls._decode_one(buf) # type: ignore[arg-type] + + @classmethod + def _decode_one(cls, buf: BytesIO) -> "JointTrajectory": + self = cls.__new__(cls) + self.timestamp = struct.unpack(">d", buf.read(8))[0] + self.duration = struct.unpack(">d", buf.read(8))[0] + + # Read joint names + num_joint_names = struct.unpack(">i", buf.read(4))[0] + self.joint_names = [] + for _ in range(num_joint_names): + name_len = struct.unpack(">i", buf.read(4))[0] + self.joint_names.append(buf.read(name_len).decode("utf-8")) + + # Read points + self.num_points = struct.unpack(">i", buf.read(4))[0] + self.points = [TrajectoryPoint._decode_one(buf) for _ in range(self.num_points)] + + # Set num_joints from joint_names or points + self.num_joints = ( + len(self.joint_names) + if self.joint_names + else (self.points[0].num_joints if self.points else 0) + ) + + return self + + _packed_fingerprint = None + + @classmethod + def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] + if cls in parents: + return 0 + return 0x2B3C4D5E6F708192 & 0xFFFFFFFFFFFFFFFF + + @classmethod + def _get_packed_fingerprint(cls) -> bytes: + if cls._packed_fingerprint is None: + cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] + return cls._packed_fingerprint + + def __str__(self) -> str: + return f"JointTrajectory({self.num_points} points, duration={self.duration:.3f}s)" + + def __repr__(self) -> str: + return ( + f"JointTrajectory(points={self.points}, joint_names={self.joint_names}, " + f"timestamp={self.timestamp})" + ) + + def __len__(self) -> int: + return self.num_points diff --git a/dimos/msgs/trajectory_msgs/TrajectoryPoint.py b/dimos/msgs/trajectory_msgs/TrajectoryPoint.py new file mode 100644 index 0000000000..b2b9ab8406 --- /dev/null +++ b/dimos/msgs/trajectory_msgs/TrajectoryPoint.py @@ -0,0 +1,136 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +TrajectoryPoint message type. + +A single point in a joint trajectory with positions, velocities, and time. +Similar to ROS trajectory_msgs/JointTrajectoryPoint. +""" + +from io import BytesIO +import struct + + +class TrajectoryPoint: + """ + A single point in a joint trajectory. + + Attributes: + time_from_start: Time from trajectory start (seconds) + positions: Joint positions (radians) + velocities: Joint velocities (rad/s) + """ + + msg_name = "trajectory_msgs.TrajectoryPoint" + + __slots__ = ["num_joints", "positions", "time_from_start", "velocities"] + + def __init__( + self, + time_from_start: float = 0.0, + positions: list[float] | None = None, + velocities: list[float] | None = None, + ) -> None: + """ + Initialize TrajectoryPoint. + + Args: + time_from_start: Time from trajectory start (seconds) + positions: Joint positions (radians) + velocities: Joint velocities (rad/s), defaults to zeros if None + """ + self.time_from_start = time_from_start + self.positions = list(positions) if positions else [] + self.num_joints = len(self.positions) + + if velocities is not None: + self.velocities = list(velocities) + else: + self.velocities = [0.0] * self.num_joints + + def lcm_encode(self) -> bytes: + """Encode for LCM transport.""" + return self.encode() + + def encode(self) -> bytes: + buf = BytesIO() + buf.write(TrajectoryPoint._get_packed_fingerprint()) + self._encode_one(buf) + return buf.getvalue() + + def _encode_one(self, buf: BytesIO) -> None: + # time_from_start (double) + buf.write(struct.pack(">d", self.time_from_start)) + # num_joints (int32) + buf.write(struct.pack(">i", self.num_joints)) + # positions (double[num_joints]) + for p in self.positions: + buf.write(struct.pack(">d", p)) + # velocities (double[num_joints]) + for v in self.velocities: + buf.write(struct.pack(">d", v)) + + @classmethod + def lcm_decode(cls, data: bytes) -> "TrajectoryPoint": + """Decode from LCM transport.""" + return cls.decode(data) + + @classmethod + def decode(cls, data: bytes) -> "TrajectoryPoint": + buf = BytesIO(data) if not hasattr(data, "read") else data + if buf.read(8) != cls._get_packed_fingerprint(): + raise ValueError("Decode error: fingerprint mismatch") + return cls._decode_one(buf) # type: ignore[arg-type] + + @classmethod + def _decode_one(cls, buf: BytesIO) -> "TrajectoryPoint": + self = cls.__new__(cls) + self.time_from_start = struct.unpack(">d", buf.read(8))[0] + self.num_joints = struct.unpack(">i", buf.read(4))[0] + self.positions = [struct.unpack(">d", buf.read(8))[0] for _ in range(self.num_joints)] + self.velocities = [struct.unpack(">d", buf.read(8))[0] for _ in range(self.num_joints)] + return self + + _packed_fingerprint = None + + @classmethod + def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] + if cls in parents: + return 0 + return 0x1A2B3C4D5E6F7081 & 0xFFFFFFFFFFFFFFFF + + @classmethod + def _get_packed_fingerprint(cls) -> bytes: + if cls._packed_fingerprint is None: + cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] + return cls._packed_fingerprint + + def __str__(self) -> str: + return f"TrajectoryPoint(t={self.time_from_start:.3f}s, {self.num_joints} joints)" + + def __repr__(self) -> str: + return ( + f"TrajectoryPoint(time_from_start={self.time_from_start}, " + f"positions={self.positions}, velocities={self.velocities})" + ) + + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] + if not isinstance(other, TrajectoryPoint): + return False + return ( + self.time_from_start == other.time_from_start + and self.positions == other.positions + and self.velocities == other.velocities + ) diff --git a/dimos/msgs/trajectory_msgs/TrajectoryStatus.py b/dimos/msgs/trajectory_msgs/TrajectoryStatus.py new file mode 100644 index 0000000000..0a3c117e68 --- /dev/null +++ b/dimos/msgs/trajectory_msgs/TrajectoryStatus.py @@ -0,0 +1,170 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +TrajectoryStatus message type. + +Status feedback for trajectory execution. +""" + +from enum import IntEnum +from io import BytesIO +import struct +import time + + +class TrajectoryState(IntEnum): + """States for trajectory execution.""" + + IDLE = 0 # No trajectory, ready to accept + EXECUTING = 1 # Currently executing trajectory + COMPLETED = 2 # Trajectory finished successfully + ABORTED = 3 # Trajectory was cancelled + FAULT = 4 # Error occurred, requires reset() + + +class TrajectoryStatus: + """ + Status of trajectory execution. + + Attributes: + timestamp: When status was generated + state: Current TrajectoryState + progress: Progress 0.0 to 1.0 + time_elapsed: Seconds since trajectory start + time_remaining: Estimated seconds remaining + error: Error message if FAULT state (empty string otherwise) + """ + + msg_name = "trajectory_msgs.TrajectoryStatus" + + __slots__ = ["error", "progress", "state", "time_elapsed", "time_remaining", "timestamp"] + + def __init__( + self, + state: TrajectoryState = TrajectoryState.IDLE, + progress: float = 0.0, + time_elapsed: float = 0.0, + time_remaining: float = 0.0, + error: str = "", + timestamp: float | None = None, + ) -> None: + """ + Initialize TrajectoryStatus. + + Args: + state: Current execution state + progress: Progress through trajectory (0.0 to 1.0) + time_elapsed: Time since trajectory start (seconds) + time_remaining: Estimated time remaining (seconds) + error: Error message if in FAULT state + timestamp: When status was generated (defaults to now) + """ + self.timestamp = timestamp if timestamp is not None else time.time() + self.state = state + self.progress = progress + self.time_elapsed = time_elapsed + self.time_remaining = time_remaining + self.error = error + + @property + def state_name(self) -> str: + """Get human-readable state name.""" + return self.state.name + + def is_done(self) -> bool: + """Check if trajectory execution is finished (completed, aborted, or fault).""" + return self.state in ( + TrajectoryState.COMPLETED, + TrajectoryState.ABORTED, + TrajectoryState.FAULT, + ) + + def is_active(self) -> bool: + """Check if trajectory is currently executing.""" + return self.state == TrajectoryState.EXECUTING + + def lcm_encode(self) -> bytes: + """Encode for LCM transport.""" + return self.encode() + + def encode(self) -> bytes: + buf = BytesIO() + buf.write(TrajectoryStatus._get_packed_fingerprint()) + self._encode_one(buf) + return buf.getvalue() + + def _encode_one(self, buf: BytesIO) -> None: + # timestamp (double) + buf.write(struct.pack(">d", self.timestamp)) + # state (int32) + buf.write(struct.pack(">i", int(self.state))) + # progress (double) + buf.write(struct.pack(">d", self.progress)) + # time_elapsed (double) + buf.write(struct.pack(">d", self.time_elapsed)) + # time_remaining (double) + buf.write(struct.pack(">d", self.time_remaining)) + # error (string) + error_bytes = self.error.encode("utf-8") + buf.write(struct.pack(">i", len(error_bytes))) + buf.write(error_bytes) + + @classmethod + def lcm_decode(cls, data: bytes) -> "TrajectoryStatus": + """Decode from LCM transport.""" + return cls.decode(data) + + @classmethod + def decode(cls, data: bytes) -> "TrajectoryStatus": + buf = BytesIO(data) if not hasattr(data, "read") else data + if buf.read(8) != cls._get_packed_fingerprint(): + raise ValueError("Decode error: fingerprint mismatch") + return cls._decode_one(buf) # type: ignore[arg-type] + + @classmethod + def _decode_one(cls, buf: BytesIO) -> "TrajectoryStatus": + self = cls.__new__(cls) + self.timestamp = struct.unpack(">d", buf.read(8))[0] + self.state = TrajectoryState(struct.unpack(">i", buf.read(4))[0]) + self.progress = struct.unpack(">d", buf.read(8))[0] + self.time_elapsed = struct.unpack(">d", buf.read(8))[0] + self.time_remaining = struct.unpack(">d", buf.read(8))[0] + error_len = struct.unpack(">i", buf.read(4))[0] + self.error = buf.read(error_len).decode("utf-8") + return self + + _packed_fingerprint = None + + @classmethod + def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] + if cls in parents: + return 0 + return 0x3C4D5E6F708192A3 & 0xFFFFFFFFFFFFFFFF + + @classmethod + def _get_packed_fingerprint(cls) -> bytes: + if cls._packed_fingerprint is None: + cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] + return cls._packed_fingerprint + + def __str__(self) -> str: + return f"TrajectoryStatus({self.state_name}, progress={self.progress:.1%})" + + def __repr__(self) -> str: + return ( + f"TrajectoryStatus(state={self.state_name}, progress={self.progress}, " + f"time_elapsed={self.time_elapsed}, time_remaining={self.time_remaining}, " + f"error='{self.error}')" + ) diff --git a/dimos/msgs/trajectory_msgs/__init__.py b/dimos/msgs/trajectory_msgs/__init__.py new file mode 100644 index 0000000000..44039e594e --- /dev/null +++ b/dimos/msgs/trajectory_msgs/__init__.py @@ -0,0 +1,30 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Trajectory message types. + +Similar to ROS trajectory_msgs package. +""" + +from dimos.msgs.trajectory_msgs.JointTrajectory import JointTrajectory +from dimos.msgs.trajectory_msgs.TrajectoryPoint import TrajectoryPoint +from dimos.msgs.trajectory_msgs.TrajectoryStatus import TrajectoryState, TrajectoryStatus + +__all__ = [ + "JointTrajectory", + "TrajectoryPoint", + "TrajectoryState", + "TrajectoryStatus", +] diff --git a/dimos/msgs/vision_msgs/BoundingBox2DArray.py b/dimos/msgs/vision_msgs/BoundingBox2DArray.py index 6568656884..f376de6372 100644 --- a/dimos/msgs/vision_msgs/BoundingBox2DArray.py +++ b/dimos/msgs/vision_msgs/BoundingBox2DArray.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos_lcm.vision_msgs.BoundingBox2DArray import BoundingBox2DArray as LCMBoundingBox2DArray +from dimos_lcm.vision_msgs.BoundingBox2DArray import ( + BoundingBox2DArray as LCMBoundingBox2DArray, +) -class BoundingBox2DArray(LCMBoundingBox2DArray): +class BoundingBox2DArray(LCMBoundingBox2DArray): # type: ignore[misc] msg_name = "vision_msgs.BoundingBox2DArray" diff --git a/dimos/msgs/vision_msgs/BoundingBox3DArray.py b/dimos/msgs/vision_msgs/BoundingBox3DArray.py index afa3d793f9..d8d7775f91 100644 --- a/dimos/msgs/vision_msgs/BoundingBox3DArray.py +++ b/dimos/msgs/vision_msgs/BoundingBox3DArray.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos_lcm.vision_msgs.BoundingBox3DArray import BoundingBox3DArray as LCMBoundingBox3DArray +from dimos_lcm.vision_msgs.BoundingBox3DArray import ( + BoundingBox3DArray as LCMBoundingBox3DArray, +) -class BoundingBox3DArray(LCMBoundingBox3DArray): +class BoundingBox3DArray(LCMBoundingBox3DArray): # type: ignore[misc] msg_name = "vision_msgs.BoundingBox3DArray" diff --git a/dimos/msgs/vision_msgs/Detection2DArray.py b/dimos/msgs/vision_msgs/Detection2DArray.py index 79c84f7609..f33cc4cc2a 100644 --- a/dimos/msgs/vision_msgs/Detection2DArray.py +++ b/dimos/msgs/vision_msgs/Detection2DArray.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,12 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from dimos_lcm.vision_msgs.Detection2DArray import Detection2DArray as LCMDetection2DArray +from dimos_lcm.vision_msgs.Detection2DArray import ( + Detection2DArray as LCMDetection2DArray, +) from dimos.types.timestamped import to_timestamp -class Detection2DArray(LCMDetection2DArray): +class Detection2DArray(LCMDetection2DArray): # type: ignore[misc] msg_name = "vision_msgs.Detection2DArray" # for _get_field_type() to work when decoding in _decode_one() diff --git a/dimos/msgs/vision_msgs/Detection3DArray.py b/dimos/msgs/vision_msgs/Detection3DArray.py index 21dabb8057..59905cad4c 100644 --- a/dimos/msgs/vision_msgs/Detection3DArray.py +++ b/dimos/msgs/vision_msgs/Detection3DArray.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos_lcm.vision_msgs.Detection3DArray import Detection3DArray as LCMDetection3DArray +from dimos_lcm.vision_msgs.Detection3DArray import ( + Detection3DArray as LCMDetection3DArray, +) -class Detection3DArray(LCMDetection3DArray): +class Detection3DArray(LCMDetection3DArray): # type: ignore[misc] msg_name = "vision_msgs.Detection3DArray" diff --git a/dimos/navigation/base.py b/dimos/navigation/base.py new file mode 100644 index 0000000000..347c4ad124 --- /dev/null +++ b/dimos/navigation/base.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from enum import Enum + +from dimos.msgs.geometry_msgs import PoseStamped + + +class NavigationState(Enum): + IDLE = "idle" + FOLLOWING_PATH = "following_path" + RECOVERY = "recovery" + + +class NavigationInterface(ABC): + @abstractmethod + def set_goal(self, goal: PoseStamped) -> bool: + """ + Set a new navigation goal (non-blocking). + + Args: + goal: Target pose to navigate to + + Returns: + True if goal was accepted, False otherwise + """ + pass + + @abstractmethod + def get_state(self) -> NavigationState: + """ + Get the current state of the navigator. + + Returns: + Current navigation state + """ + pass + + @abstractmethod + def is_goal_reached(self) -> bool: + """ + Check if the current goal has been reached. + + Returns: + True if goal was reached, False otherwise + """ + pass + + @abstractmethod + def cancel_goal(self) -> bool: + """ + Cancel the current navigation goal. + + Returns: + True if goal was cancelled, False if no goal was active + """ + pass + + +__all__ = ["NavigationInterface", "NavigationState"] diff --git a/dimos/navigation/bbox_navigation.py b/dimos/navigation/bbox_navigation.py index db66ab8349..4f4aff3d16 100644 --- a/dimos/navigation/bbox_navigation.py +++ b/dimos/navigation/bbox_navigation.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,15 +22,15 @@ from dimos.msgs.vision_msgs import Detection2DArray from dimos.utils.logging_config import setup_logger -logger = setup_logger(__name__, level=logging.DEBUG) +logger = setup_logger(level=logging.DEBUG) class BBoxNavigationModule(Module): """Minimal module that converts 2D bbox center to navigation goals.""" - detection2d: In[Detection2DArray] = None - camera_info: In[CameraInfo] = None - goal_request: Out[PoseStamped] = None + detection2d: In[Detection2DArray] + camera_info: In[CameraInfo] + goal_request: Out[PoseStamped] def __init__(self, goal_distance: float = 1.0) -> None: super().__init__() diff --git a/dimos/navigation/bt_navigator/__init__.py b/dimos/navigation/bt_navigator/__init__.py deleted file mode 100644 index cfd252ff6a..0000000000 --- a/dimos/navigation/bt_navigator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .navigator import BehaviorTreeNavigator diff --git a/dimos/navigation/bt_navigator/goal_validator.py b/dimos/navigation/bt_navigator/goal_validator.py deleted file mode 100644 index f0c4a9ce37..0000000000 --- a/dimos/navigation/bt_navigator/goal_validator.py +++ /dev/null @@ -1,443 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import deque - -import numpy as np - -from dimos.msgs.geometry_msgs import Vector3, VectorLike -from dimos.msgs.nav_msgs import CostValues, OccupancyGrid - - -def find_safe_goal( - costmap: OccupancyGrid, - goal: VectorLike, - algorithm: str = "bfs", - cost_threshold: int = 50, - min_clearance: float = 0.3, - max_search_distance: float = 5.0, - connectivity_check_radius: int = 3, -) -> Vector3 | None: - """ - Find a safe goal position when the original goal is in collision or too close to obstacles. - - Args: - costmap: The occupancy grid/costmap - goal: Original goal position in world coordinates - algorithm: Algorithm to use ("bfs", "spiral", "voronoi", "gradient_descent") - cost_threshold: Maximum acceptable cost for a safe position (default: 50) - min_clearance: Minimum clearance from obstacles in meters (default: 0.3m) - max_search_distance: Maximum distance to search from original goal in meters (default: 5.0m) - connectivity_check_radius: Radius in cells to check for connectivity (default: 3) - - Returns: - Safe goal position in world coordinates, or None if no safe position found - """ - - if algorithm == "bfs": - return _find_safe_goal_bfs( - costmap, - goal, - cost_threshold, - min_clearance, - max_search_distance, - connectivity_check_radius, - ) - elif algorithm == "spiral": - return _find_safe_goal_spiral( - costmap, - goal, - cost_threshold, - min_clearance, - max_search_distance, - connectivity_check_radius, - ) - elif algorithm == "voronoi": - return _find_safe_goal_voronoi( - costmap, goal, cost_threshold, min_clearance, max_search_distance - ) - elif algorithm == "gradient_descent": - return _find_safe_goal_gradient( - costmap, - goal, - cost_threshold, - min_clearance, - max_search_distance, - connectivity_check_radius, - ) - else: - raise ValueError(f"Unknown algorithm: {algorithm}") - - -def _find_safe_goal_bfs( - costmap: OccupancyGrid, - goal: VectorLike, - cost_threshold: int, - min_clearance: float, - max_search_distance: float, - connectivity_check_radius: int, -) -> Vector3 | None: - """ - BFS-based search for nearest safe goal position. - This guarantees finding the closest valid position. - - Pros: - - Guarantees finding the closest safe position - - Can check connectivity to avoid isolated spots - - Efficient for small to medium search areas - - Cons: - - Can be slower for large search areas - - Memory usage scales with search area - """ - - # Convert goal to grid coordinates - goal_grid = costmap.world_to_grid(goal) - gx, gy = int(goal_grid.x), int(goal_grid.y) - - # Convert distances to grid cells - clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) - max_search_cells = int(np.ceil(max_search_distance / costmap.resolution)) - - # BFS queue and visited set - queue = deque([(gx, gy, 0)]) - visited = set([(gx, gy)]) - - # 8-connected neighbors - neighbors = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)] - - while queue: - x, y, dist = queue.popleft() - - # Check if we've exceeded max search distance - if dist > max_search_cells: - break - - # Check if position is valid - if _is_position_safe( - costmap, x, y, cost_threshold, clearance_cells, connectivity_check_radius - ): - # Convert back to world coordinates - return costmap.grid_to_world((x, y)) - - # Add neighbors to queue - for dx, dy in neighbors: - nx, ny = x + dx, y + dy - - # Check bounds - if 0 <= nx < costmap.width and 0 <= ny < costmap.height: - if (nx, ny) not in visited: - visited.add((nx, ny)) - queue.append((nx, ny, dist + 1)) - - return None - - -def _find_safe_goal_spiral( - costmap: OccupancyGrid, - goal: VectorLike, - cost_threshold: int, - min_clearance: float, - max_search_distance: float, - connectivity_check_radius: int, -) -> Vector3 | None: - """ - Spiral search pattern from goal outward. - - Pros: - - Simple and predictable pattern - - Memory efficient - - Good for uniformly distributed obstacles - - Cons: - - May not find the absolute closest safe position - - Can miss nearby safe spots due to spiral pattern - """ - - # Convert goal to grid coordinates - goal_grid = costmap.world_to_grid(goal) - cx, cy = int(goal_grid.x), int(goal_grid.y) - - # Convert distances to grid cells - clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) - max_radius = int(np.ceil(max_search_distance / costmap.resolution)) - - # Spiral outward - for radius in range(0, max_radius + 1): - if radius == 0: - # Check center point - if _is_position_safe( - costmap, cx, cy, cost_threshold, clearance_cells, connectivity_check_radius - ): - return costmap.grid_to_world((cx, cy)) - else: - # Check points on the square perimeter at this radius - points = [] - - # Top and bottom edges - for x in range(cx - radius, cx + radius + 1): - points.append((x, cy - radius)) # Top - points.append((x, cy + radius)) # Bottom - - # Left and right edges (excluding corners to avoid duplicates) - for y in range(cy - radius + 1, cy + radius): - points.append((cx - radius, y)) # Left - points.append((cx + radius, y)) # Right - - # Check each point - for x, y in points: - if 0 <= x < costmap.width and 0 <= y < costmap.height: - if _is_position_safe( - costmap, x, y, cost_threshold, clearance_cells, connectivity_check_radius - ): - return costmap.grid_to_world((x, y)) - - return None - - -def _find_safe_goal_voronoi( - costmap: OccupancyGrid, - goal: VectorLike, - cost_threshold: int, - min_clearance: float, - max_search_distance: float, -) -> Vector3 | None: - """ - Find safe position using Voronoi diagram (ridge points equidistant from obstacles). - - Pros: - - Finds positions maximally far from obstacles - - Good for narrow passages - - Natural safety margin - - Cons: - - More computationally expensive - - May find positions unnecessarily far from obstacles - - Requires scipy for efficient implementation - """ - - from scipy import ndimage - from skimage.morphology import skeletonize - - # Convert goal to grid coordinates - goal_grid = costmap.world_to_grid(goal) - gx, gy = int(goal_grid.x), int(goal_grid.y) - - # Create binary obstacle map - free_map = (costmap.grid < cost_threshold) & (costmap.grid != CostValues.UNKNOWN) - - # Compute distance transform - distance_field = ndimage.distance_transform_edt(free_map) - - # Find skeleton/medial axis (approximation of Voronoi diagram) - skeleton = skeletonize(free_map) - - # Filter skeleton points by minimum clearance - clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) - valid_skeleton = skeleton & (distance_field >= clearance_cells) - - if not np.any(valid_skeleton): - # Fall back to BFS if no valid skeleton points - return _find_safe_goal_bfs( - costmap, goal, cost_threshold, min_clearance, max_search_distance, 3 - ) - - # Find nearest valid skeleton point to goal - skeleton_points = np.argwhere(valid_skeleton) - if len(skeleton_points) == 0: - return None - - # Calculate distances from goal to all skeleton points - distances = np.sqrt((skeleton_points[:, 1] - gx) ** 2 + (skeleton_points[:, 0] - gy) ** 2) - - # Filter by max search distance - max_search_cells = max_search_distance / costmap.resolution - valid_indices = distances <= max_search_cells - - if not np.any(valid_indices): - return None - - # Find closest valid point - valid_distances = distances[valid_indices] - valid_points = skeleton_points[valid_indices] - closest_idx = np.argmin(valid_distances) - best_y, best_x = valid_points[closest_idx] - - return costmap.grid_to_world((best_x, best_y)) - - -def _find_safe_goal_gradient( - costmap: OccupancyGrid, - goal: VectorLike, - cost_threshold: int, - min_clearance: float, - max_search_distance: float, - connectivity_check_radius: int, -) -> Vector3 | None: - """ - Use gradient descent on the costmap to find a safe position. - - Pros: - - Naturally flows away from obstacles - - Works well with gradient costmaps - - Can handle complex cost distributions - - Cons: - - Can get stuck in local minima - - Requires a gradient costmap - - May not find globally optimal position - """ - - # Convert goal to grid coordinates - goal_grid = costmap.world_to_grid(goal) - x, y = goal_grid.x, goal_grid.y - - # Convert distances to grid cells - clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) - max_search_cells = int(np.ceil(max_search_distance / costmap.resolution)) - - # Create gradient if needed (assuming costmap might already be a gradient) - if np.all((costmap.grid == 0) | (costmap.grid == 100) | (costmap.grid == -1)): - # Binary map, create gradient - gradient_map = costmap.gradient( - obstacle_threshold=cost_threshold, max_distance=min_clearance * 2 - ) - grid = gradient_map.grid - else: - grid = costmap.grid - - # Gradient descent with momentum - momentum = 0.9 - learning_rate = 1.0 - vx, vy = 0.0, 0.0 - - best_x, best_y = None, None - best_cost = float("inf") - - for iteration in range(100): # Max iterations - ix, iy = int(x), int(y) - - # Check if current position is valid - if 0 <= ix < costmap.width and 0 <= iy < costmap.height: - current_cost = grid[iy, ix] - - # Check distance from original goal - dist = np.sqrt((x - goal_grid.x) ** 2 + (y - goal_grid.y) ** 2) - if dist > max_search_cells: - break - - # Check if position is safe - if _is_position_safe( - costmap, ix, iy, cost_threshold, clearance_cells, connectivity_check_radius - ): - if current_cost < best_cost: - best_x, best_y = ix, iy - best_cost = current_cost - - # If cost is very low, we found a good spot - if current_cost < 10: - break - - # Compute gradient using finite differences - gx, gy = 0.0, 0.0 - - if 0 < ix < costmap.width - 1: - gx = (grid[iy, min(ix + 1, costmap.width - 1)] - grid[iy, max(ix - 1, 0)]) / 2.0 - - if 0 < iy < costmap.height - 1: - gy = (grid[min(iy + 1, costmap.height - 1), ix] - grid[max(iy - 1, 0), ix]) / 2.0 - - # Update with momentum - vx = momentum * vx - learning_rate * gx - vy = momentum * vy - learning_rate * gy - - # Update position - x += vx - y += vy - - # Add small random noise to escape local minima - if iteration % 20 == 0: - x += np.random.randn() * 0.5 - y += np.random.randn() * 0.5 - - if best_x is not None and best_y is not None: - return costmap.grid_to_world((best_x, best_y)) - - return None - - -def _is_position_safe( - costmap: OccupancyGrid, - x: int, - y: int, - cost_threshold: int, - clearance_cells: int, - connectivity_check_radius: int, -) -> bool: - """ - Check if a position is safe based on multiple criteria. - - Args: - costmap: The occupancy grid - x, y: Grid coordinates to check - cost_threshold: Maximum acceptable cost - clearance_cells: Minimum clearance in cells - connectivity_check_radius: Radius to check for connectivity - - Returns: - True if position is safe, False otherwise - """ - - # Check bounds first - if not (0 <= x < costmap.width and 0 <= y < costmap.height): - return False - - # Check if position itself is free - if costmap.grid[y, x] >= cost_threshold or costmap.grid[y, x] == CostValues.UNKNOWN: - return False - - # Check clearance around position - for dy in range(-clearance_cells, clearance_cells + 1): - for dx in range(-clearance_cells, clearance_cells + 1): - nx, ny = x + dx, y + dy - if 0 <= nx < costmap.width and 0 <= ny < costmap.height: - # Check if within circular clearance - if dx * dx + dy * dy <= clearance_cells * clearance_cells: - if costmap.grid[ny, nx] >= cost_threshold: - return False - - # Check connectivity (not surrounded by obstacles) - # Count free neighbors in a larger radius - free_count = 0 - total_count = 0 - - for dy in range(-connectivity_check_radius, connectivity_check_radius + 1): - for dx in range(-connectivity_check_radius, connectivity_check_radius + 1): - if dx == 0 and dy == 0: - continue - - nx, ny = x + dx, y + dy - if 0 <= nx < costmap.width and 0 <= ny < costmap.height: - total_count += 1 - if ( - costmap.grid[ny, nx] < cost_threshold - and costmap.grid[ny, nx] != CostValues.UNKNOWN - ): - free_count += 1 - - # Require at least 50% of neighbors to be free (not surrounded) - if total_count > 0 and free_count < total_count * 0.5: - return False - - return True diff --git a/dimos/navigation/bt_navigator/navigator.py b/dimos/navigation/bt_navigator/navigator.py deleted file mode 100644 index 782e815bb3..0000000000 --- a/dimos/navigation/bt_navigator/navigator.py +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Navigator module for coordinating global and local planning. -""" - -from collections.abc import Callable -from enum import Enum -import threading -import time - -from dimos_lcm.std_msgs import Bool, String -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.core.rpc_client import RpcCall -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.navigation.bt_navigator.goal_validator import find_safe_goal -from dimos.navigation.bt_navigator.recovery_server import RecoveryServer -from dimos.protocol.tf import TF -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import apply_transform - -logger = setup_logger("dimos.navigation.bt_navigator") - - -class NavigatorState(Enum): - """Navigator state machine states.""" - - IDLE = "idle" - FOLLOWING_PATH = "following_path" - RECOVERY = "recovery" - - -class BehaviorTreeNavigator(Module): - """ - Navigator module for coordinating navigation tasks. - - Manages the state machine for navigation, coordinates between global - and local planners, and monitors goal completion. - - Inputs: - - odom: Current robot odometry - - Outputs: - - goal: Goal pose for global planner - """ - - # LCM inputs - odom: In[PoseStamped] = None - goal_request: In[PoseStamped] = None # Input for receiving goal requests - global_costmap: In[OccupancyGrid] = None - - # LCM outputs - target: Out[PoseStamped] = None - goal_reached: Out[Bool] = None - navigation_state: Out[String] = None - - def __init__( - self, - publishing_frequency: float = 1.0, - reset_local_planner: Callable[[], None] | None = None, - check_goal_reached: Callable[[], bool] | None = None, - **kwargs, - ) -> None: - """Initialize the Navigator. - - Args: - publishing_frequency: Frequency to publish goals to global planner (Hz) - goal_tolerance: Distance threshold to consider goal reached (meters) - """ - super().__init__(**kwargs) - - # Parameters - self.publishing_frequency = publishing_frequency - self.publishing_period = 1.0 / publishing_frequency - - # State machine - self.state = NavigatorState.IDLE - self.state_lock = threading.Lock() - - # Current goal - self.current_goal: PoseStamped | None = None - self.original_goal: PoseStamped | None = None - self.goal_lock = threading.Lock() - - # Goal reached state - self._goal_reached = False - - # Latest data - self.latest_odom: PoseStamped | None = None - self.latest_costmap: OccupancyGrid | None = None - - # Control thread - self.control_thread: threading.Thread | None = None - self.stop_event = threading.Event() - - # TF listener - self.tf = TF() - - # Local planner - self.reset_local_planner = reset_local_planner - self.check_goal_reached = check_goal_reached - - # Recovery server for stuck detection - self.recovery_server = RecoveryServer(stuck_duration=5.0) - - logger.info("Navigator initialized with stuck detection") - - @rpc - def set_HolonomicLocalPlanner_reset(self, callable: RpcCall) -> None: - self.reset_local_planner = callable - self.reset_local_planner.set_rpc(self.rpc) - - @rpc - def set_HolonomicLocalPlanner_is_goal_reached(self, callable: RpcCall) -> None: - self.check_goal_reached = callable - self.check_goal_reached.set_rpc(self.rpc) - - @rpc - def start(self) -> None: - super().start() - - # Subscribe to inputs - unsub = self.odom.subscribe(self._on_odom) - self._disposables.add(Disposable(unsub)) - - unsub = self.goal_request.subscribe(self._on_goal_request) - self._disposables.add(Disposable(unsub)) - - unsub = self.global_costmap.subscribe(self._on_costmap) - self._disposables.add(Disposable(unsub)) - - # Start control thread - self.stop_event.clear() - self.control_thread = threading.Thread(target=self._control_loop, daemon=True) - self.control_thread.start() - - logger.info("Navigator started") - - @rpc - def stop(self) -> None: - """Clean up resources including stopping the control thread.""" - - self.stop_navigation() - - self.stop_event.set() - if self.control_thread and self.control_thread.is_alive(): - self.control_thread.join(timeout=2.0) - - super().stop() - - @rpc - def cancel_goal(self) -> bool: - """ - Cancel the current navigation goal. - - Returns: - True if goal was cancelled, False if no goal was active - """ - self.stop_navigation() - return True - - @rpc - def set_goal(self, goal: PoseStamped) -> bool: - """ - Set a new navigation goal. - - Args: - goal: Target pose to navigate to - - Returns: - non-blocking: True if goal was accepted, False otherwise - blocking: True if goal was reached, False otherwise - """ - transformed_goal = self._transform_goal_to_odom_frame(goal) - if not transformed_goal: - logger.error("Failed to transform goal to odometry frame") - return False - - with self.goal_lock: - self.current_goal = transformed_goal - self.original_goal = transformed_goal - - self._goal_reached = False - - with self.state_lock: - self.state = NavigatorState.FOLLOWING_PATH - - return True - - @rpc - def get_state(self) -> NavigatorState: - """Get the current state of the navigator.""" - return self.state - - def _on_odom(self, msg: PoseStamped) -> None: - """Handle incoming odometry messages.""" - self.latest_odom = msg - - if self.state == NavigatorState.FOLLOWING_PATH: - self.recovery_server.update_odom(msg) - - def _on_goal_request(self, msg: PoseStamped) -> None: - """Handle incoming goal requests.""" - self.set_goal(msg) - - def _on_costmap(self, msg: OccupancyGrid) -> None: - """Handle incoming costmap messages.""" - self.latest_costmap = msg - - def _transform_goal_to_odom_frame(self, goal: PoseStamped) -> PoseStamped | None: - """Transform goal pose to the odometry frame.""" - if not goal.frame_id: - return goal - - odom_frame = self.latest_odom.frame_id - if goal.frame_id == odom_frame: - return goal - - try: - transform = None - max_retries = 3 - - for attempt in range(max_retries): - transform = self.tf.get( - parent_frame=odom_frame, - child_frame=goal.frame_id, - ) - - if transform: - break - - if attempt < max_retries - 1: - logger.warning( - f"Transform attempt {attempt + 1}/{max_retries} failed, retrying..." - ) - time.sleep(1.0) - else: - logger.error( - f"Could not find transform from '{goal.frame_id}' to '{odom_frame}' after {max_retries} attempts" - ) - return None - - pose = apply_transform(goal, transform) - transformed_goal = PoseStamped( - position=pose.position, - orientation=pose.orientation, - frame_id=odom_frame, - ts=goal.ts, - ) - return transformed_goal - - except Exception as e: - logger.error(f"Failed to transform goal: {e}") - return None - - def _control_loop(self) -> None: - """Main control loop running in separate thread.""" - while not self.stop_event.is_set(): - with self.state_lock: - current_state = self.state - self.navigation_state.publish(String(data=current_state.value)) - - if current_state == NavigatorState.FOLLOWING_PATH: - with self.goal_lock: - goal = self.current_goal - original_goal = self.original_goal - - if goal is not None and self.latest_costmap is not None: - # Check if robot is stuck - if self.recovery_server.check_stuck(): - logger.warning("Robot is stuck! Cancelling goal and resetting.") - self.cancel_goal() - continue - - costmap = self.latest_costmap.inflate(0.1).gradient(max_distance=1.0) - - # Find safe goal position - safe_goal_pos = find_safe_goal( - costmap, - original_goal.position, - algorithm="bfs", - cost_threshold=60, - min_clearance=0.25, - max_search_distance=5.0, - ) - - # Create new goal with safe position - if safe_goal_pos: - safe_goal = PoseStamped( - position=safe_goal_pos, - orientation=goal.orientation, - frame_id=goal.frame_id, - ts=goal.ts, - ) - self.target.publish(safe_goal) - self.current_goal = safe_goal - else: - logger.warning("Could not find safe goal position, cancelling goal") - self.cancel_goal() - - # Check if goal is reached - if self.check_goal_reached(): - reached_msg = Bool() - reached_msg.data = True - self.goal_reached.publish(reached_msg) - self.stop_navigation() - self._goal_reached = True - logger.info("Goal reached, resetting local planner") - - elif current_state == NavigatorState.RECOVERY: - with self.state_lock: - self.state = NavigatorState.IDLE - - time.sleep(self.publishing_period) - - @rpc - def is_goal_reached(self) -> bool: - """Check if the current goal has been reached. - - Returns: - True if goal was reached, False otherwise - """ - return self._goal_reached - - def stop_navigation(self) -> None: - """Stop navigation and return to IDLE state.""" - with self.goal_lock: - self.current_goal = None - - self._goal_reached = False - - with self.state_lock: - self.state = NavigatorState.IDLE - - self.reset_local_planner() - self.recovery_server.reset() # Reset recovery server when stopping - - logger.info("Navigator stopped") - - -behavior_tree_navigator = BehaviorTreeNavigator.blueprint - -__all__ = ["BehaviorTreeNavigator", "behavior_tree_navigator"] diff --git a/dimos/navigation/bt_navigator/recovery_server.py b/dimos/navigation/bt_navigator/recovery_server.py deleted file mode 100644 index 5b05d35de5..0000000000 --- a/dimos/navigation/bt_navigator/recovery_server.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Recovery server for handling stuck detection and recovery behaviors. -""" - -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import get_distance - -logger = setup_logger("dimos.navigation.bt_navigator.recovery_server") - - -class RecoveryServer: - """ - Recovery server for detecting stuck situations and executing recovery behaviors. - - Currently implements stuck detection based on time without significant movement. - Will be extended with actual recovery behaviors in the future. - """ - - def __init__( - self, - position_threshold: float = 0.2, - stuck_duration: float = 3.0, - ) -> None: - """Initialize the recovery server. - - Args: - position_threshold: Minimum distance to travel to reset stuck timer (meters) - stuck_duration: Time duration without significant movement to consider stuck (seconds) - """ - self.position_threshold = position_threshold - self.stuck_duration = stuck_duration - - # Store last position that exceeded threshold - self.last_moved_pose = None - self.last_moved_time = None - self.current_odom = None - - logger.info( - f"RecoveryServer initialized with position_threshold={position_threshold}, " - f"stuck_duration={stuck_duration}" - ) - - def update_odom(self, odom: PoseStamped) -> None: - """Update the odometry data for stuck detection. - - Args: - odom: Current robot odometry with timestamp - """ - if odom is None: - return - - # Store current odom for checking stuck - self.current_odom = odom - - # Initialize on first update - if self.last_moved_pose is None: - self.last_moved_pose = odom - self.last_moved_time = odom.ts - return - - # Calculate distance from the reference position (last significant movement) - distance = get_distance(odom, self.last_moved_pose) - - # If robot has moved significantly from the reference, update reference - if distance > self.position_threshold: - self.last_moved_pose = odom - self.last_moved_time = odom.ts - - def check_stuck(self) -> bool: - """Check if the robot is stuck based on time without movement. - - Returns: - True if robot appears to be stuck, False otherwise - """ - if self.last_moved_time is None: - return False - - # Need current odom to check - if self.current_odom is None: - return False - - # Calculate time since last significant movement - current_time = self.current_odom.ts - time_since_movement = current_time - self.last_moved_time - - # Check if stuck based on duration without movement - is_stuck = time_since_movement > self.stuck_duration - - if is_stuck: - logger.warning( - f"Robot appears stuck! No movement for {time_since_movement:.1f} seconds" - ) - - return is_stuck - - def reset(self) -> None: - """Reset the recovery server state.""" - self.last_moved_pose = None - self.last_moved_time = None - self.current_odom = None - logger.debug("RecoveryServer reset") diff --git a/dimos/navigation/demo_ros_navigation.py b/dimos/navigation/demo_ros_navigation.py new file mode 100644 index 0000000000..733f66c1b7 --- /dev/null +++ b/dimos/navigation/demo_ros_navigation.py @@ -0,0 +1,72 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import rclpy + +from dimos import core +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Twist, Vector3 +from dimos.msgs.nav_msgs import Path +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.navigation.rosnav import ROSNav +from dimos.protocol import pubsub +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +def main() -> None: + pubsub.lcm.autoconf() # type: ignore[attr-defined] + dimos = core.start(2) + + ros_nav = dimos.deploy(ROSNav) # type: ignore[attr-defined] + + ros_nav.goal_req.transport = core.LCMTransport("/goal", PoseStamped) + ros_nav.pointcloud.transport = core.LCMTransport("/pointcloud_map", PointCloud2) + ros_nav.global_pointcloud.transport = core.LCMTransport("/global_pointcloud", PointCloud2) + ros_nav.goal_active.transport = core.LCMTransport("/goal_active", PoseStamped) + ros_nav.path_active.transport = core.LCMTransport("/path_active", Path) + ros_nav.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + + ros_nav.start() + + logger.info("\nTesting navigation in 2 seconds...") + time.sleep(2) + + test_pose = PoseStamped( + ts=time.time(), + frame_id="map", + position=Vector3(2.0, 2.0, 0.0), + orientation=Quaternion(0.0, 0.0, 0.0, 1.0), + ) + + logger.info("Sending navigation goal to: (2.0, 2.0, 0.0)") + success = ros_nav.navigate_to(test_pose, timeout=30.0) + logger.info(f"Navigated successfully: {success}") + + try: + logger.info("\nNavBot running. Press Ctrl+C to stop.") + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("\nShutting down...") + ros_nav.stop() + + if rclpy.ok(): # type: ignore[attr-defined] + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/dimos/navigation/frontier_exploration/__init__.py b/dimos/navigation/frontier_exploration/__init__.py index 7236788842..24ce957ccf 100644 --- a/dimos/navigation/frontier_exploration/__init__.py +++ b/dimos/navigation/frontier_exploration/__init__.py @@ -1 +1,3 @@ from .wavefront_frontier_goal_selector import WavefrontFrontierExplorer, wavefront_frontier_explorer + +__all__ = ["WavefrontFrontierExplorer", "wavefront_frontier_explorer"] diff --git a/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py b/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py index ed5f364a74..aca154a6dd 100644 --- a/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py +++ b/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -450,7 +450,7 @@ def test_performance_timing() -> None: # Check that larger maps take more time (expected behavior) for result in results: - assert result["detect_time"] < 2.0, f"Detection too slow: {result['detect_time']}s" + assert result["detect_time"] < 3.0, f"Detection too slow: {result['detect_time']}s" assert result["goal_time"] < 1.5, f"Goal selection too slow: {result['goal_time']}s" print("\nPerformance test passed - all operations completed within time limits") diff --git a/dimos/navigation/frontier_exploration/utils.py b/dimos/navigation/frontier_exploration/utils.py index d307749531..28644cdd41 100644 --- a/dimos/navigation/frontier_exploration/utils.py +++ b/dimos/navigation/frontier_exploration/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ def costmap_to_pil_image(costmap: OccupancyGrid, scale_factor: int = 2) -> Image img_array[i, j] = [205, 205, 205] # Flip vertically to match ROS convention (origin at bottom-left) - img_array = np.flipud(img_array) + img_array = np.flipud(img_array) # type: ignore[assignment] # Create PIL image img = Image.fromarray(img_array, "RGB") @@ -59,7 +59,7 @@ def costmap_to_pil_image(costmap: OccupancyGrid, scale_factor: int = 2) -> Image # Scale up if requested if scale_factor > 1: new_size = (img.width * scale_factor, img.height * scale_factor) - img = img.resize(new_size, Image.NEAREST) # Use NEAREST to keep sharp pixels + img = img.resize(new_size, Image.NEAREST) # type: ignore[attr-defined] # Use NEAREST to keep sharp pixels return img diff --git a/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py b/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py index 71677635f5..c5d5ab2659 100644 --- a/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py +++ b/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,12 +29,13 @@ from reactivex.disposable import Disposable from dimos.core import In, Module, Out, rpc +from dimos.mapping.occupancy.inflation import simple_inflate from dimos.msgs.geometry_msgs import PoseStamped, Vector3 from dimos.msgs.nav_msgs import CostValues, OccupancyGrid from dimos.utils.logging_config import setup_logger from dimos.utils.transform_utils import get_distance -logger = setup_logger("dimos.robot.unitree.frontier_exploration") +logger = setup_logger() class PointClassification(IntFlag): @@ -60,14 +61,14 @@ class FrontierCache: """Cache for grid points to avoid duplicate point creation.""" def __init__(self) -> None: - self.points = {} + self.points = {} # type: ignore[var-annotated] def get_point(self, x: int, y: int) -> GridPoint: """Get or create a grid point at the given coordinates.""" key = (x, y) if key not in self.points: self.points[key] = GridPoint(x, y) - return self.points[key] + return self.points[key] # type: ignore[no-any-return] def clear(self) -> None: """Clear the point cache.""" @@ -90,16 +91,16 @@ class WavefrontFrontierExplorer(Module): """ # LCM inputs - global_costmap: In[OccupancyGrid] = None - odom: In[PoseStamped] = None - goal_reached: In[Bool] = None - explore_cmd: In[Bool] = None - stop_explore_cmd: In[Bool] = None + global_costmap: In[OccupancyGrid] + odom: In[PoseStamped] + goal_reached: In[Bool] + explore_cmd: In[Bool] + stop_explore_cmd: In[Bool] # LCM outputs - goal_request: Out[PoseStamped] = None + goal_request: Out[PoseStamped] - def __init__( + def __init__( # type: ignore[no-untyped-def] self, min_frontier_perimeter: float = 0.5, occupancy_threshold: int = 99, @@ -130,7 +131,7 @@ def __init__( self.info_gain_threshold = info_gain_threshold self.num_no_gain_attempts = num_no_gain_attempts self._cache = FrontierCache() - self.explored_goals = [] # list of explored goals + self.explored_goals = [] # type: ignore[var-annotated] # list of explored goals self.exploration_direction = Vector3(0.0, 0.0, 0.0) # current exploration direction self.last_costmap = None # store last costmap for information comparison self.no_gain_counter = 0 # track consecutive no-gain attempts @@ -651,7 +652,7 @@ def get_exploration_goal(self, robot_pose: Vector3, costmap: OccupancyGrid) -> V if not frontiers: # Store current costmap before returning - self.last_costmap = costmap + self.last_costmap = costmap # type: ignore[assignment] self.reset_exploration_session() return None @@ -664,12 +665,12 @@ def get_exploration_goal(self, robot_pose: Vector3, costmap: OccupancyGrid) -> V self.mark_explored_goal(selected_goal) # Store current costmap for next comparison - self.last_costmap = costmap + self.last_costmap = costmap # type: ignore[assignment] return selected_goal # Store current costmap before returning - self.last_costmap = costmap + self.last_costmap = costmap # type: ignore[assignment] return None def mark_explored_goal(self, goal: Vector3) -> None: @@ -762,7 +763,7 @@ def _exploration_loop(self) -> None: ) # Get exploration goal - costmap = self.latest_costmap.inflate(0.25) + costmap = simple_inflate(self.latest_costmap, 0.25) goal = self.get_exploration_goal(robot_pose, costmap) if goal: diff --git a/dimos/navigation/global_planner/__init__.py b/dimos/navigation/global_planner/__init__.py deleted file mode 100644 index 275619659b..0000000000 --- a/dimos/navigation/global_planner/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from dimos.navigation.global_planner.algo import astar -from dimos.navigation.global_planner.planner import AstarPlanner, astar_planner - -__all__ = ["AstarPlanner", "astar", "astar_planner"] diff --git a/dimos/navigation/global_planner/algo.py b/dimos/navigation/global_planner/algo.py deleted file mode 100644 index 16f8dc3600..0000000000 --- a/dimos/navigation/global_planner/algo.py +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import heapq - -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, VectorLike -from dimos.msgs.nav_msgs import CostValues, OccupancyGrid, Path -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.robot.unitree.global_planner.astar") - - -def astar( - costmap: OccupancyGrid, - goal: VectorLike, - start: VectorLike = (0.0, 0.0), - cost_threshold: int = 90, - unknown_penalty: float = 0.8, -) -> Path | None: - """ - A* path planning algorithm from start to goal position. - - Args: - costmap: Costmap object containing the environment - goal: Goal position as any vector-like object - start: Start position as any vector-like object (default: origin [0,0]) - cost_threshold: Cost threshold above which a cell is considered an obstacle - - Returns: - Path object containing waypoints, or None if no path found - """ - - # Convert world coordinates to grid coordinates directly using vector-like inputs - start_vector = costmap.world_to_grid(start) - goal_vector = costmap.world_to_grid(goal) - logger.debug(f"ASTAR {costmap} {start_vector} -> {goal_vector}") - - # Store positions as tuples for dictionary keys - start_tuple = (int(start_vector.x), int(start_vector.y)) - goal_tuple = (int(goal_vector.x), int(goal_vector.y)) - - # Check if goal is out of bounds - if not (0 <= goal_tuple[0] < costmap.width and 0 <= goal_tuple[1] < costmap.height): - return None - - # Define possible movements (8-connected grid with diagonal movements) - directions = [ - (0, 1), - (1, 0), - (0, -1), - (-1, 0), - (1, 1), - (1, -1), - (-1, 1), - (-1, -1), - ] - - # Cost for each movement (straight vs diagonal) - sc = 1.0 # Straight cost - dc = 1.42 # Diagonal cost (approximately sqrt(2)) - movement_costs = [sc, sc, sc, sc, dc, dc, dc, dc] - - # A* algorithm implementation - open_set = [] # Priority queue for nodes to explore - closed_set = set() # Set of explored nodes - - # Dictionary to store cost from start and parents for each node - g_score = {start_tuple: 0} - parents = {} - - # Heuristic function (Octile distance for 8-connected grid) - def heuristic(x1, y1, x2, y2): - dx = abs(x2 - x1) - dy = abs(y2 - y1) - # Octile distance: optimal for 8-connected grids with diagonal movement - return (dx + dy) + (dc - 2 * sc) * min(dx, dy) - - # Start with the starting node - f_score = g_score[start_tuple] + heuristic( - start_tuple[0], start_tuple[1], goal_tuple[0], goal_tuple[1] - ) - heapq.heappush(open_set, (f_score, start_tuple)) - - # Track nodes already in open set to avoid duplicates - open_set_hash = {start_tuple} - - while open_set: - # Get the node with the lowest f_score - _current_f, current = heapq.heappop(open_set) - current_x, current_y = current - - # Remove from open set hash - if current in open_set_hash: - open_set_hash.remove(current) - - # Skip if already processed (can happen with duplicate entries) - if current in closed_set: - continue - - # Check if we've reached the goal - if current == goal_tuple: - # Reconstruct the path - waypoints = [] - while current in parents: - world_point = costmap.grid_to_world(current) - # Create PoseStamped with identity quaternion (no orientation) - pose = PoseStamped( - frame_id="world", - position=[world_point.x, world_point.y, 0.0], - orientation=Quaternion(0, 0, 0, 1), # Identity quaternion - ) - waypoints.append(pose) - current = parents[current] - - # Add the start position - start_world_point = costmap.grid_to_world(start_tuple) - start_pose = PoseStamped( - frame_id="world", - position=[start_world_point.x, start_world_point.y, 0.0], - orientation=Quaternion(0, 0, 0, 1), - ) - waypoints.append(start_pose) - - # Reverse the path (start to goal) - waypoints.reverse() - - # Add the goal position if it's not already included - goal_point = costmap.grid_to_world(goal_tuple) - - if ( - not waypoints - or (waypoints[-1].x - goal_point.x) ** 2 + (waypoints[-1].y - goal_point.y) ** 2 - > 1e-10 - ): - goal_pose = PoseStamped( - frame_id="world", - position=[goal_point.x, goal_point.y, 0.0], - orientation=Quaternion(0, 0, 0, 1), - ) - waypoints.append(goal_pose) - - return Path(frame_id="world", poses=waypoints) - - # Add current node to closed set - closed_set.add(current) - - # Explore neighbors - for i, (dx, dy) in enumerate(directions): - neighbor_x, neighbor_y = current_x + dx, current_y + dy - neighbor = (neighbor_x, neighbor_y) - - # Check if the neighbor is valid - if not (0 <= neighbor_x < costmap.width and 0 <= neighbor_y < costmap.height): - continue - - # Check if the neighbor is already explored - if neighbor in closed_set: - continue - - # Get the neighbor's cost value - neighbor_val = costmap.grid[neighbor_y, neighbor_x] - - # Skip if it's a hard obstacle - if neighbor_val >= cost_threshold: - continue - - # Calculate movement cost with penalties - # Unknown cells get half the penalty of obstacles - if neighbor_val == CostValues.UNKNOWN: # Unknown cell (-1) - # Unknown cells have a moderate traversal cost (half of obstacle threshold) - cell_cost = cost_threshold * unknown_penalty - elif neighbor_val == CostValues.FREE: # Free space (0) - # Free cells have minimal cost - cell_cost = 0.0 - else: - # Other cells use their actual cost value (1-99) - cell_cost = neighbor_val - - # Calculate cost penalty based on cell cost (higher cost = higher penalty) - # This encourages the planner to prefer lower-cost paths - cost_penalty = cell_cost / CostValues.OCCUPIED # Normalized penalty (divide by 100) - - tentative_g_score = g_score[current] + movement_costs[i] * (1.0 + cost_penalty) - - # Get the current g_score for the neighbor or set to infinity if not yet explored - neighbor_g_score = g_score.get(neighbor, float("inf")) - - # If this path to the neighbor is better than any previous one - if tentative_g_score < neighbor_g_score: - # Update the neighbor's scores and parent - parents[neighbor] = current - g_score[neighbor] = tentative_g_score - f_score = tentative_g_score + heuristic( - neighbor_x, neighbor_y, goal_tuple[0], goal_tuple[1] - ) - - # Add the neighbor to the open set with its f_score - # Only add if not already in open set to reduce duplicates - if neighbor not in open_set_hash: - heapq.heappush(open_set, (f_score, neighbor)) - open_set_hash.add(neighbor) - - # If we get here, no path was found - return None diff --git a/dimos/navigation/global_planner/planner.py b/dimos/navigation/global_planner/planner.py deleted file mode 100644 index 89ac134b08..0000000000 --- a/dimos/navigation/global_planner/planner.py +++ /dev/null @@ -1,224 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.msgs.geometry_msgs import Pose, PoseStamped -from dimos.msgs.nav_msgs import OccupancyGrid, Path -from dimos.navigation.global_planner.algo import astar -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import euler_to_quaternion - -logger = setup_logger(__file__) - -import math - -from dimos.msgs.geometry_msgs import Quaternion, Vector3 - - -def add_orientations_to_path(path: Path, goal_orientation: Quaternion = None) -> Path: - """Add orientations to path poses based on direction of movement. - - Args: - path: Path with poses to add orientations to - goal_orientation: Desired orientation for the final pose - - Returns: - Path with orientations added to all poses - """ - if not path.poses or len(path.poses) < 2: - return path - - # Calculate orientations for all poses except the last one - for i in range(len(path.poses) - 1): - current_pose = path.poses[i] - next_pose = path.poses[i + 1] - - # Calculate direction to next point - dx = next_pose.position.x - current_pose.position.x - dy = next_pose.position.y - current_pose.position.y - - # Calculate yaw angle - yaw = math.atan2(dy, dx) - - # Convert to quaternion (roll=0, pitch=0, yaw) - orientation = euler_to_quaternion(Vector3(0, 0, yaw)) - current_pose.orientation = orientation - - # Set last pose orientation - identity_quat = Quaternion(0, 0, 0, 1) - if goal_orientation is not None and goal_orientation != identity_quat: - # Use the provided goal orientation if it's not the identity - path.poses[-1].orientation = goal_orientation - elif len(path.poses) > 1: - # Use the previous pose's orientation - path.poses[-1].orientation = path.poses[-2].orientation - else: - # Single pose with identity goal orientation - path.poses[-1].orientation = identity_quat - - return path - - -def resample_path(path: Path, spacing: float) -> Path: - """Resample a path to have approximately uniform spacing between poses. - - Args: - path: The original Path - spacing: Desired distance between consecutive poses - - Returns: - A new Path with resampled poses - """ - if len(path) < 2 or spacing <= 0: - return path - - resampled = [] - resampled.append(path.poses[0]) - - accumulated_distance = 0.0 - - for i in range(1, len(path.poses)): - current = path.poses[i] - prev = path.poses[i - 1] - - # Calculate segment distance - dx = current.x - prev.x - dy = current.y - prev.y - segment_length = (dx**2 + dy**2) ** 0.5 - - if segment_length < 1e-10: - continue - - # Direction vector - dir_x = dx / segment_length - dir_y = dy / segment_length - - # Add points along this segment - while accumulated_distance + segment_length >= spacing: - # Distance along segment for next point - dist_along = spacing - accumulated_distance - if dist_along < 0: - break - - # Create new pose - new_x = prev.x + dir_x * dist_along - new_y = prev.y + dir_y * dist_along - new_pose = PoseStamped( - frame_id=path.frame_id, - position=[new_x, new_y, 0.0], - orientation=prev.orientation, # Keep same orientation - ) - resampled.append(new_pose) - - # Update for next iteration - accumulated_distance = 0 - segment_length -= dist_along - prev = new_pose - - accumulated_distance += segment_length - - # Add last pose if not already there - if len(path.poses) > 1: - last = path.poses[-1] - if not resampled or (resampled[-1].x != last.x or resampled[-1].y != last.y): - resampled.append(last) - - return Path(frame_id=path.frame_id, poses=resampled) - - -class AstarPlanner(Module): - # LCM inputs - target: In[PoseStamped] = None - global_costmap: In[OccupancyGrid] = None - odom: In[PoseStamped] = None - - # LCM outputs - path: Out[Path] = None - - def __init__(self) -> None: - super().__init__() - - # Latest data - self.latest_costmap: OccupancyGrid | None = None - self.latest_odom: PoseStamped | None = None - - @rpc - def start(self) -> None: - super().start() - - unsub = self.target.subscribe(self._on_target) - self._disposables.add(Disposable(unsub)) - - unsub = self.global_costmap.subscribe(self._on_costmap) - self._disposables.add(Disposable(unsub)) - - unsub = self.odom.subscribe(self._on_odom) - self._disposables.add(Disposable(unsub)) - - logger.info("A* planner started") - - @rpc - def stop(self) -> None: - super().stop() - - def _on_costmap(self, msg: OccupancyGrid) -> None: - """Handle incoming costmap messages.""" - self.latest_costmap = msg - - def _on_odom(self, msg: PoseStamped) -> None: - """Handle incoming odometry messages.""" - self.latest_odom = msg - - def _on_target(self, msg: PoseStamped) -> None: - """Handle incoming target messages and trigger planning.""" - if self.latest_costmap is None or self.latest_odom is None: - logger.warning("Cannot plan: missing costmap or odometry data") - return - - path = self.plan(msg) - if path: - # Add orientations to the path, using the goal's orientation for the final pose - path = add_orientations_to_path(path, msg.orientation) - self.path.publish(path) - - def plan(self, goal: Pose) -> Path | None: - """Plan a path from current position to goal.""" - if self.latest_costmap is None or self.latest_odom is None: - logger.warning("Cannot plan: missing costmap or odometry data") - return None - - logger.debug(f"Planning path to goal {goal}") - - # Get current position from odometry - robot_pos = self.latest_odom.position - costmap = self.latest_costmap.inflate(0.2).gradient(max_distance=1.5) - - # Run A* planning - path = astar(costmap, goal.position, robot_pos) - - if path: - path = resample_path(path, 0.1) - logger.debug(f"Path found with {len(path.poses)} waypoints") - return path - - logger.warning("No path found to the goal.") - return None - - -astar_planner = AstarPlanner.blueprint - -__all__ = ["AstarPlanner", "astar_planner"] diff --git a/dimos/navigation/local_planner/__init__.py b/dimos/navigation/local_planner/__init__.py deleted file mode 100644 index 9e0f62931a..0000000000 --- a/dimos/navigation/local_planner/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner -from dimos.navigation.local_planner.local_planner import BaseLocalPlanner diff --git a/dimos/navigation/local_planner/holonomic_local_planner.py b/dimos/navigation/local_planner/holonomic_local_planner.py deleted file mode 100644 index acb8dcec98..0000000000 --- a/dimos/navigation/local_planner/holonomic_local_planner.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Gradient-Augmented Look-Ahead Pursuit (GLAP) holonomic local planner. -""" - -import numpy as np - -from dimos.core import rpc -from dimos.msgs.geometry_msgs import Twist, Vector3 -from dimos.navigation.local_planner.local_planner import BaseLocalPlanner -from dimos.utils.transform_utils import get_distance, normalize_angle, quaternion_to_euler - - -class HolonomicLocalPlanner(BaseLocalPlanner): - """ - Gradient-Augmented Look-Ahead Pursuit (GLAP) holonomic local planner. - - This planner combines path following with obstacle avoidance using - costmap gradients to produce smooth holonomic velocity commands. - - Args: - lookahead_dist: Look-ahead distance in meters (default: 1.0) - k_rep: Repulsion gain for obstacle avoidance (default: 1.0) - alpha: Low-pass filter coefficient [0-1] (default: 0.5) - v_max: Maximum velocity per component in m/s (default: 0.8) - goal_tolerance: Distance threshold to consider goal reached (default: 0.5) - control_frequency: Control loop frequency in Hz (default: 10.0) - """ - - def __init__( - self, - lookahead_dist: float = 1.0, - k_rep: float = 0.5, - k_angular: float = 0.75, - alpha: float = 0.5, - v_max: float = 0.8, - goal_tolerance: float = 0.5, - orientation_tolerance: float = 0.2, - control_frequency: float = 10.0, - **kwargs, - ) -> None: - """Initialize the GLAP planner with specified parameters.""" - super().__init__( - goal_tolerance=goal_tolerance, - orientation_tolerance=orientation_tolerance, - control_frequency=control_frequency, - **kwargs, - ) - - # Algorithm parameters - self.lookahead_dist = lookahead_dist - self.k_rep = k_rep - self.alpha = alpha - self.v_max = v_max - self.k_angular = k_angular - - # Previous velocity for filtering (vx, vy, vtheta) - self.v_prev = np.array([0.0, 0.0, 0.0]) - - @rpc - def start(self) -> None: - super().start() - - @rpc - def stop(self) -> None: - super().stop() - - def compute_velocity(self) -> Twist | None: - """ - Compute velocity commands using GLAP algorithm. - - Returns: - Twist with linear and angular velocities in robot frame - """ - if self.latest_odom is None or self.latest_path is None or self.latest_costmap is None: - return None - - pose = np.array([self.latest_odom.position.x, self.latest_odom.position.y]) - - euler = quaternion_to_euler(self.latest_odom.orientation) - robot_yaw = euler.z - - path_points = [] - for pose_stamped in self.latest_path.poses: - path_points.append([pose_stamped.position.x, pose_stamped.position.y]) - - if len(path_points) == 0: - return None - - path = np.array(path_points) - - costmap = self.latest_costmap.grid - - v_follow_odom = self._compute_path_following(pose, path) - - v_rep_odom = self._compute_obstacle_repulsion(pose, costmap) - - v_odom = v_follow_odom + v_rep_odom - - # Transform velocity from odom frame to robot frame - cos_yaw = np.cos(robot_yaw) - sin_yaw = np.sin(robot_yaw) - - v_robot_x = cos_yaw * v_odom[0] + sin_yaw * v_odom[1] - v_robot_y = -sin_yaw * v_odom[0] + cos_yaw * v_odom[1] - - # Compute angular velocity - closest_idx, _ = self._find_closest_point_on_path(pose, path) - - # Check if we're near the final goal - goal_pose = self.latest_path.poses[-1] - distance_to_goal = get_distance(self.latest_odom, goal_pose) - - if distance_to_goal < self.goal_tolerance: - # Near goal - rotate to match final goal orientation - goal_euler = quaternion_to_euler(goal_pose.orientation) - desired_yaw = goal_euler.z - else: - # Not near goal - align with path direction - lookahead_point = self._find_lookahead_point(path, closest_idx) - dx = lookahead_point[0] - pose[0] - dy = lookahead_point[1] - pose[1] - desired_yaw = np.arctan2(dy, dx) - - yaw_error = normalize_angle(desired_yaw - robot_yaw) - k_angular = self.k_angular - v_theta = k_angular * yaw_error - - # Slow down linear velocity when turning - # Scale linear velocity based on angular velocity magnitude - angular_speed = abs(v_theta) - max_angular_speed = self.v_max - - # Calculate speed reduction factor (1.0 when not turning, 0.2 when at max turn rate) - turn_slowdown = 1.0 - 0.8 * min(angular_speed / max_angular_speed, 1.0) - - # Apply speed reduction to linear velocities - v_robot_x = np.clip(v_robot_x * turn_slowdown, -self.v_max, self.v_max) - v_robot_y = np.clip(v_robot_y * turn_slowdown, -self.v_max, self.v_max) - v_theta = np.clip(v_theta, -self.v_max, self.v_max) - - v_raw = np.array([v_robot_x, v_robot_y, v_theta]) - v_filtered = self.alpha * v_raw + (1 - self.alpha) * self.v_prev - self.v_prev = v_filtered - - return Twist( - linear=Vector3(v_filtered[0], v_filtered[1], 0.0), - angular=Vector3(0.0, 0.0, v_filtered[2]), - ) - - def _compute_path_following(self, pose: np.ndarray, path: np.ndarray) -> np.ndarray: - """ - Compute path following velocity using pure pursuit. - - Args: - pose: Current robot position [x, y] - path: Path waypoints as Nx2 array - - Returns: - Path following velocity vector [vx, vy] - """ - closest_idx, _ = self._find_closest_point_on_path(pose, path) - - carrot = self._find_lookahead_point(path, closest_idx) - - direction = carrot - pose - distance = np.linalg.norm(direction) - - if distance < 1e-6: - return np.zeros(2) - - v_follow = self.v_max * direction / distance - - return v_follow - - def _compute_obstacle_repulsion(self, pose: np.ndarray, costmap: np.ndarray) -> np.ndarray: - """ - Compute obstacle repulsion velocity from costmap gradient. - - Args: - pose: Current robot position [x, y] - costmap: 2D costmap array - - Returns: - Repulsion velocity vector [vx, vy] - """ - grid_point = self.latest_costmap.world_to_grid(pose) - grid_x = int(grid_point.x) - grid_y = int(grid_point.y) - - height, width = costmap.shape - if not (1 <= grid_x < width - 1 and 1 <= grid_y < height - 1): - return np.zeros(2) - - # Compute gradient using central differences - # Note: costmap is in row-major order (y, x) - gx = (costmap[grid_y, grid_x + 1] - costmap[grid_y, grid_x - 1]) / ( - 2.0 * self.latest_costmap.resolution - ) - gy = (costmap[grid_y + 1, grid_x] - costmap[grid_y - 1, grid_x]) / ( - 2.0 * self.latest_costmap.resolution - ) - - # Gradient points towards higher cost, so negate for repulsion - v_rep = -self.k_rep * np.array([gx, gy]) - - return v_rep - - def _find_closest_point_on_path( - self, pose: np.ndarray, path: np.ndarray - ) -> tuple[int, np.ndarray]: - """ - Find the closest point on the path to current pose. - - Args: - pose: Current position [x, y] - path: Path waypoints as Nx2 array - - Returns: - Tuple of (closest_index, closest_point) - """ - distances = np.linalg.norm(path - pose, axis=1) - closest_idx = np.argmin(distances) - return closest_idx, path[closest_idx] - - def _find_lookahead_point(self, path: np.ndarray, start_idx: int) -> np.ndarray: - """ - Find look-ahead point on path at specified distance. - - Args: - path: Path waypoints as Nx2 array - start_idx: Starting index for search - - Returns: - Look-ahead point [x, y] - """ - accumulated_dist = 0.0 - - for i in range(start_idx, len(path) - 1): - segment_dist = np.linalg.norm(path[i + 1] - path[i]) - - if accumulated_dist + segment_dist >= self.lookahead_dist: - remaining_dist = self.lookahead_dist - accumulated_dist - t = remaining_dist / segment_dist - carrot = path[i] + t * (path[i + 1] - path[i]) - return carrot - - accumulated_dist += segment_dist - - return path[-1] - - def _clip(self, v: np.ndarray) -> np.ndarray: - """Instance method to clip velocity with access to v_max.""" - return np.clip(v, -self.v_max, self.v_max) - - -holonomic_local_planner = HolonomicLocalPlanner.blueprint - -__all__ = ["HolonomicLocalPlanner", "holonomic_local_planner"] diff --git a/dimos/navigation/local_planner/local_planner.py b/dimos/navigation/local_planner/local_planner.py deleted file mode 100644 index 0a569f00ed..0000000000 --- a/dimos/navigation/local_planner/local_planner.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Base Local Planner Module for robot navigation. -Subscribes to local costmap, odometry, and path, publishes movement commands. -""" - -from abc import abstractmethod -import threading -import time - -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped, Twist -from dimos.msgs.nav_msgs import OccupancyGrid, Path -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import get_distance, normalize_angle, quaternion_to_euler - -logger = setup_logger(__file__) - - -class BaseLocalPlanner(Module): - """ - local planner module for obstacle avoidance and path following. - - Subscribes to: - - /local_costmap: Local occupancy grid for obstacle detection - - /odom: Robot odometry for current pose - - /path: Path to follow (continuously updated at ~1Hz) - - Publishes: - - /cmd_vel: Velocity commands for robot movement - """ - - # LCM inputs - local_costmap: In[OccupancyGrid] = None - odom: In[PoseStamped] = None - path: In[Path] = None - - # LCM outputs - cmd_vel: Out[Twist] = None - - def __init__( - self, - goal_tolerance: float = 0.5, - orientation_tolerance: float = 0.2, - control_frequency: float = 10.0, - **kwargs, - ) -> None: - """Initialize the local planner module. - - Args: - goal_tolerance: Distance threshold to consider goal reached (meters) - orientation_tolerance: Orientation threshold to consider goal reached (radians) - control_frequency: Frequency for control loop (Hz) - """ - super().__init__(**kwargs) - - # Parameters - self.goal_tolerance = goal_tolerance - self.orientation_tolerance = orientation_tolerance - self.control_frequency = control_frequency - self.control_period = 1.0 / control_frequency - - # Latest data - self.latest_costmap: OccupancyGrid | None = None - self.latest_odom: PoseStamped | None = None - self.latest_path: Path | None = None - - # Control thread - self.planning_thread: threading.Thread | None = None - self.stop_planning = threading.Event() - - logger.info("Local planner module initialized") - - @rpc - def start(self) -> None: - super().start() - - unsub = self.local_costmap.subscribe(self._on_costmap) - self._disposables.add(Disposable(unsub)) - - unsub = self.odom.subscribe(self._on_odom) - self._disposables.add(Disposable(unsub)) - - unsub = self.path.subscribe(self._on_path) - self._disposables.add(Disposable(unsub)) - - @rpc - def stop(self) -> None: - self.cancel_planning() - super().stop() - - def _on_costmap(self, msg: OccupancyGrid) -> None: - self.latest_costmap = msg - - def _on_odom(self, msg: PoseStamped) -> None: - self.latest_odom = msg - - def _on_path(self, msg: Path) -> None: - self.latest_path = msg - - if msg and len(msg.poses) > 0: - if self.planning_thread is None or not self.planning_thread.is_alive(): - self._start_planning_thread() - - def _start_planning_thread(self) -> None: - """Start the planning thread.""" - self.stop_planning.clear() - self.planning_thread = threading.Thread(target=self._follow_path_loop, daemon=True) - self.planning_thread.start() - logger.debug("Started follow path thread") - - def _follow_path_loop(self) -> None: - """Main planning loop that runs in a separate thread.""" - while not self.stop_planning.is_set(): - if self.is_goal_reached(): - self.stop_planning.set() - stop_cmd = Twist() - self.cmd_vel.publish(stop_cmd) - break - - # Compute and publish velocity - self._plan() - - time.sleep(self.control_period) - - def _plan(self) -> None: - """Compute and publish velocity command.""" - cmd_vel = self.compute_velocity() - - if cmd_vel is not None: - self.cmd_vel.publish(cmd_vel) - - @abstractmethod - def compute_velocity(self) -> Twist | None: - """ - Compute velocity commands based on current costmap, odometry, and path. - Must be implemented by derived classes. - - Returns: - Twist message with linear and angular velocity commands, or None if no command - """ - pass - - @rpc - def is_goal_reached(self) -> bool: - """ - Check if the robot has reached the goal position and orientation. - - Returns: - True if goal is reached within tolerance, False otherwise - """ - if self.latest_odom is None or self.latest_path is None: - return False - - if len(self.latest_path.poses) == 0: - return True - - goal_pose = self.latest_path.poses[-1] - distance = get_distance(self.latest_odom, goal_pose) - - # Check distance tolerance - if distance >= self.goal_tolerance: - return False - - # Check orientation tolerance - current_euler = quaternion_to_euler(self.latest_odom.orientation) - goal_euler = quaternion_to_euler(goal_pose.orientation) - - # Calculate yaw difference and normalize to [-pi, pi] - yaw_error = normalize_angle(goal_euler.z - current_euler.z) - - return abs(yaw_error) < self.orientation_tolerance - - @rpc - def reset(self) -> None: - """Reset the local planner state, clearing the current path.""" - # Clear the latest path - self.latest_path = None - self.latest_odom = None - self.latest_costmap = None - self.cancel_planning() - logger.info("Local planner reset") - - @rpc - def cancel_planning(self) -> None: - """Stop the local planner and any running threads.""" - if self.planning_thread and self.planning_thread.is_alive(): - self.stop_planning.set() - self.planning_thread.join(timeout=1.0) - self.planning_thread = None - stop_cmd = Twist() - self.cmd_vel.publish(stop_cmd) diff --git a/dimos/navigation/local_planner/test_base_local_planner.py b/dimos/navigation/local_planner/test_base_local_planner.py deleted file mode 100644 index 8786b1a925..0000000000 --- a/dimos/navigation/local_planner/test_base_local_planner.py +++ /dev/null @@ -1,406 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Unit tests for the GLAP (Gradient-Augmented Look-Ahead Pursuit) holonomic local planner. -""" - -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion -from dimos.msgs.nav_msgs import OccupancyGrid, Path -from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner - - -class TestHolonomicLocalPlanner: - """Test suite for HolonomicLocalPlanner.""" - - @pytest.fixture - def planner(self): - """Create a planner instance for testing.""" - planner = HolonomicLocalPlanner( - lookahead_dist=1.5, - k_rep=1.0, - alpha=1.0, # No filtering for deterministic tests - v_max=1.0, - goal_tolerance=0.5, - control_frequency=10.0, - ) - yield planner - # TODO: This should call `planner.stop()` but that causes errors. - # Calling just this for now to fix thread leaks. - planner._close_module() - - @pytest.fixture - def empty_costmap(self): - """Create an empty costmap (all free space).""" - costmap = OccupancyGrid( - grid=np.zeros((100, 100), dtype=np.int8), resolution=0.1, origin=Pose() - ) - costmap.origin.position.x = -5.0 - costmap.origin.position.y = -5.0 - return costmap - - def test_straight_path_no_obstacles(self, planner, empty_costmap) -> None: - """Test that planner follows straight path with no obstacles.""" - # Set current position at origin - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 0.0 - planner.latest_odom.position.y = 0.0 - - # Create straight path along +X - path = Path() - for i in range(10): - ps = PoseStamped() - ps.position.x = float(i) - ps.position.y = 0.0 - ps.orientation.w = 1.0 # Identity quaternion - path.poses.append(ps) - planner.latest_path = path - - # Set empty costmap - planner.latest_costmap = empty_costmap - - # Compute velocity - vel = planner.compute_velocity() - - # Should move along +X - assert vel is not None - assert vel.linear.x > 0.9 # Close to v_max - assert abs(vel.linear.y) < 0.1 # Near zero - assert abs(vel.angular.z) < 0.1 # Small angular velocity when aligned with path - - def test_obstacle_gradient_repulsion(self, planner) -> None: - """Test that obstacle gradients create repulsive forces.""" - # Set position at origin - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 0.0 - planner.latest_odom.position.y = 0.0 - - # Simple path forward - path = Path() - ps = PoseStamped() - ps.position.x = 5.0 - ps.position.y = 0.0 - ps.orientation.w = 1.0 - path.poses.append(ps) - planner.latest_path = path - - # Create costmap with gradient pointing south (higher cost north) - costmap_grid = np.zeros((100, 100), dtype=np.int8) - for i in range(100): - costmap_grid[i, :] = max(0, 50 - i) # Gradient from north to south - - planner.latest_costmap = OccupancyGrid(grid=costmap_grid, resolution=0.1, origin=Pose()) - planner.latest_costmap.origin.position.x = -5.0 - planner.latest_costmap.origin.position.y = -5.0 - - # Compute velocity - vel = planner.compute_velocity() - - # Should have positive Y component (pushed north by gradient) - assert vel is not None - assert vel.linear.y > 0.1 # Repulsion pushes north - - def test_lowpass_filter(self) -> None: - """Test that low-pass filter smooths velocity commands.""" - # Create planner with alpha=0.5 for filtering - planner = HolonomicLocalPlanner( - lookahead_dist=1.0, - k_rep=0.0, # No repulsion - alpha=0.5, # 50% filtering - v_max=1.0, - ) - - # Setup similar to straight path test - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 0.0 - planner.latest_odom.position.y = 0.0 - - path = Path() - ps = PoseStamped() - ps.position.x = 5.0 - ps.position.y = 0.0 - ps.orientation.w = 1.0 - path.poses.append(ps) - planner.latest_path = path - - planner.latest_costmap = OccupancyGrid( - grid=np.zeros((100, 100), dtype=np.int8), resolution=0.1, origin=Pose() - ) - planner.latest_costmap.origin.position.x = -5.0 - planner.latest_costmap.origin.position.y = -5.0 - - # First call - previous velocity is zero - vel1 = planner.compute_velocity() - assert vel1 is not None - - # Store first velocity - first_vx = vel1.linear.x - - # Second call - should be filtered - vel2 = planner.compute_velocity() - assert vel2 is not None - - # With alpha=0.5 and same conditions: - # v2 = 0.5 * v_raw + 0.5 * v1 - # The filtering effect should be visible - # v2 should be between v1 and the raw velocity - assert vel2.linear.x != first_vx # Should be different due to filtering - assert 0 < vel2.linear.x <= planner.v_max # Should still be positive and within limits - planner._close_module() - - def test_no_path(self, planner, empty_costmap) -> None: - """Test that planner returns None when no path is available.""" - planner.latest_odom = PoseStamped() - planner.latest_costmap = empty_costmap - planner.latest_path = Path() # Empty path - - vel = planner.compute_velocity() - assert vel is None - - def test_no_odometry(self, planner, empty_costmap) -> None: - """Test that planner returns None when no odometry is available.""" - planner.latest_odom = None - planner.latest_costmap = empty_costmap - - path = Path() - ps = PoseStamped() - ps.position.x = 1.0 - ps.position.y = 0.0 - path.poses.append(ps) - planner.latest_path = path - - vel = planner.compute_velocity() - assert vel is None - - def test_no_costmap(self, planner) -> None: - """Test that planner returns None when no costmap is available.""" - planner.latest_odom = PoseStamped() - planner.latest_costmap = None - - path = Path() - ps = PoseStamped() - ps.position.x = 1.0 - ps.position.y = 0.0 - path.poses.append(ps) - planner.latest_path = path - - vel = planner.compute_velocity() - assert vel is None - - def test_goal_reached(self, planner, empty_costmap) -> None: - """Test velocity when robot is at goal.""" - # Set robot at goal position - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 5.0 - planner.latest_odom.position.y = 0.0 - - # Path with single point at robot position - path = Path() - ps = PoseStamped() - ps.position.x = 5.0 - ps.position.y = 0.0 - ps.orientation.w = 1.0 - path.poses.append(ps) - planner.latest_path = path - - planner.latest_costmap = empty_costmap - - # Compute velocity - vel = planner.compute_velocity() - - # Should have near-zero velocity - assert vel is not None - assert abs(vel.linear.x) < 0.1 - assert abs(vel.linear.y) < 0.1 - - def test_velocity_saturation(self, planner, empty_costmap) -> None: - """Test that velocities are capped at v_max.""" - # Set robot far from goal to maximize commanded velocity - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 0.0 - planner.latest_odom.position.y = 0.0 - - # Create path far away - path = Path() - ps = PoseStamped() - ps.position.x = 100.0 # Very far - ps.position.y = 0.0 - ps.orientation.w = 1.0 - path.poses.append(ps) - planner.latest_path = path - - planner.latest_costmap = empty_costmap - - # Compute velocity - vel = planner.compute_velocity() - - # Velocity should be saturated at v_max - assert vel is not None - assert abs(vel.linear.x) <= planner.v_max + 0.01 # Small tolerance - assert abs(vel.linear.y) <= planner.v_max + 0.01 - assert abs(vel.angular.z) <= planner.v_max + 0.01 - - def test_lookahead_interpolation(self, planner, empty_costmap) -> None: - """Test that lookahead point is correctly interpolated on path.""" - # Set robot at origin - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 0.0 - planner.latest_odom.position.y = 0.0 - - # Create path with waypoints closer than lookahead distance - path = Path() - for i in range(5): - ps = PoseStamped() - ps.position.x = i * 0.5 # 0.5m spacing - ps.position.y = 0.0 - ps.orientation.w = 1.0 - path.poses.append(ps) - planner.latest_path = path - - planner.latest_costmap = empty_costmap - - # Compute velocity - vel = planner.compute_velocity() - - # Should move forward along path - assert vel is not None - assert vel.linear.x > 0.5 # Moving forward - assert abs(vel.linear.y) < 0.1 # Staying on path - - def test_curved_path_following(self, planner, empty_costmap) -> None: - """Test following a curved path.""" - # Set robot at origin - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 0.0 - planner.latest_odom.position.y = 0.0 - - # Create curved path (quarter circle) - path = Path() - for i in range(10): - angle = (np.pi / 2) * (i / 9.0) # 0 to 90 degrees - ps = PoseStamped() - ps.position.x = 2.0 * np.cos(angle) - ps.position.y = 2.0 * np.sin(angle) - ps.orientation.w = 1.0 - path.poses.append(ps) - planner.latest_path = path - - planner.latest_costmap = empty_costmap - - # Compute velocity - vel = planner.compute_velocity() - - # Should have both X and Y components for curved motion - assert vel is not None - # Test general behavior: should be moving (not exact values) - assert vel.linear.x > 0 # Moving forward (any positive value) - assert vel.linear.y > 0 # Turning left (any positive value) - # Ensure we have meaningful movement, not just noise - total_linear = np.sqrt(vel.linear.x**2 + vel.linear.y**2) - assert total_linear > 0.1 # Some reasonable movement - - def test_robot_frame_transformation(self, empty_costmap) -> None: - """Test that velocities are correctly transformed to robot frame.""" - # Create planner with no filtering for deterministic test - planner = HolonomicLocalPlanner( - lookahead_dist=1.0, - k_rep=0.0, # No repulsion - alpha=1.0, # No filtering - v_max=1.0, - ) - - # Set robot at origin but rotated 90 degrees (facing +Y in odom frame) - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 0.0 - planner.latest_odom.position.y = 0.0 - # Quaternion for 90 degree rotation around Z - planner.latest_odom.orientation = Quaternion(0.0, 0.0, 0.7071068, 0.7071068) - - # Create path along +X axis in odom frame - path = Path() - for i in range(5): - ps = PoseStamped() - ps.position.x = float(i) - ps.position.y = 0.0 - ps.orientation.w = 1.0 - path.poses.append(ps) - planner.latest_path = path - - planner.latest_costmap = empty_costmap - - # Compute velocity - vel = planner.compute_velocity() - - # Robot is facing +Y, path is along +X - # So in robot frame: forward is +Y direction, path is to the right - assert vel is not None - # Test relative magnitudes and signs rather than exact values - # Path is to the right, so Y velocity should be negative - assert vel.linear.y < 0 # Should move right (negative Y in robot frame) - # Should turn to align with path - assert vel.angular.z < 0 # Should turn right (negative angular velocity) - # X velocity should be relatively small compared to Y - assert abs(vel.linear.x) < abs(vel.linear.y) # Lateral movement dominates - planner._close_module() - - def test_angular_velocity_computation(self, empty_costmap) -> None: - """Test that angular velocity is computed to align with path.""" - planner = HolonomicLocalPlanner( - lookahead_dist=2.0, - k_rep=0.0, # No repulsion - alpha=1.0, # No filtering - v_max=1.0, - ) - - # Robot at origin facing +X - planner.latest_odom = PoseStamped() - planner.latest_odom.position.x = 0.0 - planner.latest_odom.position.y = 0.0 - planner.latest_odom.orientation.w = 1.0 # Identity quaternion - - # Create path at 45 degrees - path = Path() - for i in range(5): - ps = PoseStamped() - ps.position.x = float(i) - ps.position.y = float(i) # Diagonal path - ps.orientation.w = 1.0 - path.poses.append(ps) - planner.latest_path = path - - planner.latest_costmap = empty_costmap - - # Compute velocity - vel = planner.compute_velocity() - - # Path is at 45 degrees, robot facing 0 degrees - # Should have positive angular velocity to turn left - assert vel is not None - # Test general behavior without exact thresholds - assert vel.linear.x > 0 # Moving forward (any positive value) - assert vel.linear.y > 0 # Moving left (holonomic, any positive value) - assert vel.angular.z > 0 # Turning left (positive angular velocity) - # Verify the robot is actually moving with reasonable speed - total_linear = np.sqrt(vel.linear.x**2 + vel.linear.y**2) - assert total_linear > 0.1 # Some meaningful movement - # Since path is diagonal, X and Y should be similar magnitude - assert ( - abs(vel.linear.x - vel.linear.y) < max(vel.linear.x, vel.linear.y) * 0.5 - ) # Within 50% of each other - planner._close_module() diff --git a/dimos/navigation/replanning_a_star/controllers.py b/dimos/navigation/replanning_a_star/controllers.py new file mode 100644 index 0000000000..865aafb8be --- /dev/null +++ b/dimos/navigation/replanning_a_star/controllers.py @@ -0,0 +1,156 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import Protocol + +import numpy as np +from numpy.typing import NDArray + +from dimos.core.global_config import GlobalConfig +from dimos.msgs.geometry_msgs import Twist, Vector3 +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped +from dimos.utils.trigonometry import angle_diff + + +class Controller(Protocol): + def advance(self, lookahead_point: NDArray[np.float64], current_odom: PoseStamped) -> Twist: ... + + def rotate(self, yaw_error: float) -> Twist: ... + + def reset_errors(self) -> None: ... + + def reset_yaw_error(self, value: float) -> None: ... + + +class PController: + _global_config: GlobalConfig + _speed: float + _control_frequency: float + + _min_linear_velocity: float = 0.2 + _min_angular_velocity: float = 0.2 + _k_angular: float = 0.5 + _max_angular_accel: float = 2.0 + _rotation_threshold: float = 90 * (math.pi / 180) + + def __init__(self, global_config: GlobalConfig, speed: float, control_frequency: float): + self._global_config = global_config + self._speed = speed + self._control_frequency = control_frequency + + def advance(self, lookahead_point: NDArray[np.float64], current_odom: PoseStamped) -> Twist: + current_pos = np.array([current_odom.position.x, current_odom.position.y]) + direction = lookahead_point - current_pos + distance = np.linalg.norm(direction) + + if distance < 1e-6: + # Robot is coincidentally at the lookahead point; skip this cycle. + return Twist() + + robot_yaw = current_odom.orientation.euler[2] + desired_yaw = np.arctan2(direction[1], direction[0]) + yaw_error = angle_diff(desired_yaw, robot_yaw) + + angular_velocity = self._compute_angular_velocity(yaw_error) + + # Rotate-then-drive: if heading error is large, rotate in place first + if abs(yaw_error) > self._rotation_threshold: + return self._angular_twist(angular_velocity) + + # When aligned, drive forward with proportional angular correction + linear_velocity = self._speed * (1.0 - abs(yaw_error) / self._rotation_threshold) + linear_velocity = self._apply_min_velocity(linear_velocity, self._min_linear_velocity) + + return Twist( + linear=Vector3(linear_velocity, 0.0, 0.0), + angular=Vector3(0.0, 0.0, angular_velocity), + ) + + def rotate(self, yaw_error: float) -> Twist: + angular_velocity = self._compute_angular_velocity(yaw_error) + return self._angular_twist(angular_velocity) + + def _compute_angular_velocity(self, yaw_error: float) -> float: + angular_velocity = self._k_angular * yaw_error + angular_velocity = np.clip(angular_velocity, -self._speed, self._speed) + angular_velocity = self._apply_min_velocity(angular_velocity, self._min_angular_velocity) + return float(angular_velocity) + + def reset_errors(self) -> None: + pass + + def reset_yaw_error(self, value: float) -> None: + pass + + def _apply_min_velocity(self, velocity: float, min_velocity: float) -> float: + """Apply minimum velocity threshold, preserving sign. Returns 0 if velocity is 0.""" + if velocity == 0.0: + return 0.0 + if abs(velocity) < min_velocity: + return min_velocity if velocity > 0 else -min_velocity + return velocity + + def _angular_twist(self, angular_velocity: float) -> Twist: + # In simulation, add a small forward velocity to help the locomotion + # policy execute rotation (some policies don't handle pure in-place rotation). + linear_x = 0.18 if self._global_config.simulation else 0.0 + + return Twist( + linear=Vector3(linear_x, 0.0, 0.0), + angular=Vector3(0.0, 0.0, angular_velocity), + ) + + +class PdController(PController): + _k_derivative: float = 0.15 + + _prev_yaw_error: float + _prev_angular_velocity: float + + def __init__(self, global_config: GlobalConfig, speed: float, control_frequency: float): + super().__init__(global_config, speed, control_frequency) + + self._prev_yaw_error = 0.0 + self._prev_angular_velocity = 0.0 + + def reset_errors(self) -> None: + self._prev_yaw_error = 0.0 + self._prev_angular_velocity = 0.0 + + def reset_yaw_error(self, value: float) -> None: + self._prev_yaw_error = value + + def _compute_angular_velocity(self, yaw_error: float) -> float: + dt = 1.0 / self._control_frequency + + # PD control: proportional + derivative damping + yaw_error_derivative = (yaw_error - self._prev_yaw_error) / dt + angular_velocity = self._k_angular * yaw_error - self._k_derivative * yaw_error_derivative + + # Rate limiting: limit angular acceleration to prevent jerky corrections + max_delta = self._max_angular_accel * dt + angular_velocity = np.clip( + angular_velocity, + self._prev_angular_velocity - max_delta, + self._prev_angular_velocity + max_delta, + ) + + angular_velocity = np.clip(angular_velocity, -self._speed, self._speed) + angular_velocity = self._apply_min_velocity(angular_velocity, self._min_angular_velocity) + + self._prev_yaw_error = yaw_error + self._prev_angular_velocity = angular_velocity + + return float(angular_velocity) diff --git a/dimos/navigation/replanning_a_star/global_planner.py b/dimos/navigation/replanning_a_star/global_planner.py new file mode 100644 index 0000000000..8dc1a42ccf --- /dev/null +++ b/dimos/navigation/replanning_a_star/global_planner.py @@ -0,0 +1,349 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from threading import Event, RLock, Thread, current_thread +import time + +from dimos_lcm.std_msgs import Bool +from reactivex import Subject +from reactivex.disposable import CompositeDisposable + +from dimos.core.global_config import GlobalConfig +from dimos.core.resource import Resource +from dimos.mapping.occupancy.path_resampling import smooth_resample_path +from dimos.msgs.geometry_msgs import Twist +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid +from dimos.msgs.nav_msgs.Path import Path +from dimos.msgs.sensor_msgs import Image +from dimos.navigation.base import NavigationState +from dimos.navigation.replanning_a_star.goal_validator import find_safe_goal +from dimos.navigation.replanning_a_star.local_planner import LocalPlanner, StopMessage +from dimos.navigation.replanning_a_star.min_cost_astar import min_cost_astar +from dimos.navigation.replanning_a_star.navigation_map import NavigationMap +from dimos.navigation.replanning_a_star.position_tracker import PositionTracker +from dimos.navigation.replanning_a_star.replan_limiter import ReplanLimiter +from dimos.utils.logging_config import setup_logger +from dimos.utils.trigonometry import angle_diff + +logger = setup_logger() + + +class GlobalPlanner(Resource): + path: Subject[Path] + goal_reached: Subject[Bool] + + _current_odom: PoseStamped | None = None + _current_goal: PoseStamped | None = None + _goal_reached: bool = False + _thread: Thread | None = None + + _global_config: GlobalConfig + _navigation_map: NavigationMap + _local_planner: LocalPlanner + _position_tracker: PositionTracker + _replan_limiter: ReplanLimiter + _disposables: CompositeDisposable + _stop_planner: Event + _replan_event: Event + _replan_reason: StopMessage | None + _lock: RLock + + _safe_goal_tolerance: float = 4.0 + _goal_tolerance: float = 0.2 + _rotation_tolerance: float = math.radians(15) + _replan_goal_tolerance: float = 0.5 + _max_replan_attempts: int = 10 + _stuck_time_window: float = 8.0 + _max_path_deviation: float = 0.9 + + def __init__(self, global_config: GlobalConfig) -> None: + self.path = Subject() + self.goal_reached = Subject() + + self._global_config = global_config + self._navigation_map = NavigationMap(self._global_config) + self._local_planner = LocalPlanner( + self._global_config, self._navigation_map, self._goal_tolerance + ) + self._position_tracker = PositionTracker(self._stuck_time_window) + self._replan_limiter = ReplanLimiter() + self._disposables = CompositeDisposable() + self._stop_planner = Event() + self._replan_event = Event() + self._replan_reason = None + self._lock = RLock() + + def start(self) -> None: + self._local_planner.start() + self._disposables.add( + self._local_planner.stopped_navigating.subscribe(self._on_stopped_navigating) + ) + self._stop_planner.clear() + self._thread = Thread(target=self._thread_entrypoint, daemon=True) + self._thread.start() + + def stop(self) -> None: + self.cancel_goal() + self._local_planner.stop() + self._disposables.dispose() + self._stop_planner.set() + self._replan_event.set() + + if self._thread is not None and self._thread is not current_thread(): + self._thread.join(2) + if self._thread.is_alive(): + logger.error("GlobalPlanner thread did not stop in time.") + self._thread = None + + def handle_odom(self, msg: PoseStamped) -> None: + with self._lock: + self._current_odom = msg + + self._local_planner.handle_odom(msg) + self._position_tracker.add_position(msg) + + def handle_global_costmap(self, msg: OccupancyGrid) -> None: + self._navigation_map.update(msg) + + def handle_goal_request(self, goal: PoseStamped) -> None: + logger.info("Got new goal", goal=str(goal)) + with self._lock: + self._current_goal = goal + self._goal_reached = False + self._replan_limiter.reset() + self._plan_path() + + def cancel_goal(self, *, but_will_try_again: bool = False, arrived: bool = False) -> None: + logger.info("Cancelling goal.", but_will_try_again=but_will_try_again, arrived=arrived) + + with self._lock: + self._position_tracker.reset_data() + + if not but_will_try_again: + self._current_goal = None + self._goal_reached = arrived + self._replan_limiter.reset() + + self.path.on_next(Path()) + self._local_planner.stop_planning() + + if not but_will_try_again: + self.goal_reached.on_next(Bool(arrived)) + + def get_state(self) -> NavigationState: + return self._local_planner.get_state() + + def is_goal_reached(self) -> bool: + with self._lock: + return self._goal_reached + + @property + def cmd_vel(self) -> Subject[Twist]: + return self._local_planner.cmd_vel + + @property + def debug_navigation(self) -> Subject[Image]: + return self._local_planner.debug_navigation + + def _thread_entrypoint(self) -> None: + """Monitor if the robot is stuck, veers off track, or stopped navigating.""" + + last_id = -1 + last_stuck_check = time.perf_counter() + + while not self._stop_planner.is_set(): + # Wait for either timeout or replan signal from local planner. + replanning_wanted = self._replan_event.wait(timeout=0.1) + + if self._stop_planner.is_set(): + break + + # Handle stop message from local planner (priority) + if replanning_wanted: + self._replan_event.clear() + with self._lock: + reason = self._replan_reason + self._replan_reason = None + + if reason is not None: + self._handle_stop_message(reason) + last_stuck_check = time.perf_counter() + continue + + with self._lock: + current_goal = self._current_goal + current_odom = self._current_odom + + if not current_goal or not current_odom: + continue + + if ( + current_goal.position.distance(current_odom.position) < self._goal_tolerance + and abs( + angle_diff(current_goal.orientation.euler[2], current_odom.orientation.euler[2]) + ) + < self._rotation_tolerance + ): + logger.info("Close enough to goal. Accepting as arrived.") + self.cancel_goal(arrived=True) + continue + + # Check if robot has veered too far off the path + deviation = self._local_planner.get_distance_to_path() + if deviation is not None and deviation > self._max_path_deviation: + logger.info( + "Robot veered off track. Replanning.", + deviation=round(deviation, 2), + threshold=self._max_path_deviation, + ) + self._replan_path() + last_stuck_check = time.perf_counter() + continue + + _, new_id = self._local_planner.get_unique_state() + + if new_id != last_id: + last_id = new_id + last_stuck_check = time.perf_counter() + continue + + if ( + time.perf_counter() - last_stuck_check > self._stuck_time_window + and self._position_tracker.is_stuck() + ): + logger.info("Robot is stuck. Replanning.") + self._replan_path() + last_stuck_check = time.perf_counter() + + def _on_stopped_navigating(self, stop_message: StopMessage) -> None: + with self._lock: + self._replan_reason = stop_message + # Signal the monitoring thread to do the replanning. This is so we don't have two + # threads which could be replanning at the same time. + self._replan_event.set() + + def _handle_stop_message(self, stop_message: StopMessage) -> None: + # Note, this runs in the monitoring thread. + + self.path.on_next(Path()) + + if stop_message == "arrived": + logger.info("Arrived at goal.") + self.cancel_goal(arrived=True) + elif stop_message == "obstacle_found": + logger.info("Replanning path due to obstacle found.") + self._replan_path() + elif stop_message == "error": + logger.info("Failure in navigation.") + self._replan_path() + else: + logger.error(f"No code to handle '{stop_message}'.") + self.cancel_goal() + + def _replan_path(self) -> None: + with self._lock: + current_odom = self._current_odom + current_goal = self._current_goal + + logger.info("Replanning.", attempt=self._replan_limiter.get_attempt()) + + assert current_odom is not None + assert current_goal is not None + + if current_goal.position.distance(current_odom.position) < self._replan_goal_tolerance: + self.cancel_goal(arrived=True) + return + + if not self._replan_limiter.can_retry(current_odom.position): + self.cancel_goal() + return + + self._replan_limiter.will_retry() + + self._plan_path() + + def _plan_path(self) -> None: + self.cancel_goal(but_will_try_again=True) + + with self._lock: + current_odom = self._current_odom + current_goal = self._current_goal + + assert current_goal is not None + + if current_odom is None: + logger.warning("Cannot handle goal request: missing odometry.") + return + + safe_goal = self._find_safe_goal(current_goal.position) + + if not safe_goal: + return + + path = self._find_wide_path(safe_goal, current_odom.position) + + if not path: + logger.warning( + "No path found to the goal.", x=round(safe_goal.x, 3), y=round(safe_goal.y, 3) + ) + return + + resampled_path = smooth_resample_path(path, current_goal, 0.1) + + self.path.on_next(resampled_path) + + self._local_planner.start_planning(resampled_path) + + def _find_wide_path(self, goal: Vector3, robot_pos: Vector3) -> Path | None: + # sizes_to_try: list[float] = [2.2, 1.7, 1.3, 1] + sizes_to_try: list[float] = [1.1] + + for size in sizes_to_try: + costmap = self._navigation_map.make_gradient_costmap(size) + path = min_cost_astar(costmap, goal, robot_pos) + if path and path.poses: + logger.info(f"Found path {size}x robot width.") + return path + + return None + + def _find_safe_goal(self, goal: Vector3) -> Vector3 | None: + costmap = self._navigation_map.binary_costmap + + if costmap.cell_value(goal) == CostValues.UNKNOWN: + return goal + + safe_goal = find_safe_goal( + costmap, + goal, + algorithm="bfs_contiguous", + cost_threshold=CostValues.OCCUPIED, + min_clearance=self._global_config.robot_rotation_diameter / 2, + max_search_distance=self._safe_goal_tolerance, + ) + + if safe_goal is None: + logger.warning("No safe goal found near requested target.") + return None + + goals_distance = safe_goal.distance(goal) + if goals_distance > 0.2: + logger.warning(f"Travelling to goal {goals_distance}m away from requested goal.") + + logger.info("Found safe goal.", x=round(safe_goal.x, 2), y=round(safe_goal.y, 2)) + + return safe_goal diff --git a/dimos/navigation/replanning_a_star/goal_validator.py b/dimos/navigation/replanning_a_star/goal_validator.py new file mode 100644 index 0000000000..5cd093e955 --- /dev/null +++ b/dimos/navigation/replanning_a_star/goal_validator.py @@ -0,0 +1,264 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import deque + +import numpy as np + +from dimos.msgs.geometry_msgs import Vector3, VectorLike +from dimos.msgs.nav_msgs import CostValues, OccupancyGrid + + +def find_safe_goal( + costmap: OccupancyGrid, + goal: VectorLike, + algorithm: str = "bfs", + cost_threshold: int = 50, + min_clearance: float = 0.3, + max_search_distance: float = 5.0, + connectivity_check_radius: int = 3, +) -> Vector3 | None: + """ + Find a safe goal position when the original goal is in collision or too close to obstacles. + + Args: + costmap: The occupancy grid/costmap + goal: Original goal position in world coordinates + algorithm: Algorithm to use ("bfs", "spiral", "voronoi", "gradient_descent") + cost_threshold: Maximum acceptable cost for a safe position (default: 50) + min_clearance: Minimum clearance from obstacles in meters (default: 0.3m) + max_search_distance: Maximum distance to search from original goal in meters (default: 5.0m) + connectivity_check_radius: Radius in cells to check for connectivity (default: 3) + + Returns: + Safe goal position in world coordinates, or None if no safe position found + """ + + if algorithm == "bfs": + return _find_safe_goal_bfs( + costmap, + goal, + cost_threshold, + min_clearance, + max_search_distance, + connectivity_check_radius, + ) + elif algorithm == "bfs_contiguous": + return _find_safe_goal_bfs_contiguous( + costmap, + goal, + cost_threshold, + min_clearance, + max_search_distance, + connectivity_check_radius, + ) + else: + raise ValueError(f"Unknown algorithm: {algorithm}") + + +def _find_safe_goal_bfs( + costmap: OccupancyGrid, + goal: VectorLike, + cost_threshold: int, + min_clearance: float, + max_search_distance: float, + connectivity_check_radius: int, +) -> Vector3 | None: + """ + BFS-based search for nearest safe goal position. + This guarantees finding the closest valid position. + + Pros: + - Guarantees finding the closest safe position + - Can check connectivity to avoid isolated spots + - Efficient for small to medium search areas + + Cons: + - Can be slower for large search areas + - Memory usage scales with search area + """ + + # Convert goal to grid coordinates + goal_grid = costmap.world_to_grid(goal) + gx, gy = int(goal_grid.x), int(goal_grid.y) + + # Convert distances to grid cells + clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) + max_search_cells = int(np.ceil(max_search_distance / costmap.resolution)) + + # BFS queue and visited set + queue = deque([(gx, gy, 0)]) + visited = set([(gx, gy)]) + + # 8-connected neighbors + neighbors = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)] + + while queue: + x, y, dist = queue.popleft() + + # Check if we've exceeded max search distance + if dist > max_search_cells: + break + + # Check if position is valid + if _is_position_safe( + costmap, x, y, cost_threshold, clearance_cells, connectivity_check_radius + ): + # Convert back to world coordinates + return costmap.grid_to_world((x, y)) + + # Add neighbors to queue + for dx, dy in neighbors: + nx, ny = x + dx, y + dy + + # Check bounds + if 0 <= nx < costmap.width and 0 <= ny < costmap.height: + if (nx, ny) not in visited: + visited.add((nx, ny)) + queue.append((nx, ny, dist + 1)) + + return None + + +def _find_safe_goal_bfs_contiguous( + costmap: OccupancyGrid, + goal: VectorLike, + cost_threshold: int, + min_clearance: float, + max_search_distance: float, + connectivity_check_radius: int, +) -> Vector3 | None: + """ + BFS-based search for nearest safe goal position, only following passable cells. + Unlike regular BFS, this only expands through cells with occupancy < 100, + ensuring the path doesn't cross through impassable obstacles. + + Pros: + - Guarantees finding the closest safe position reachable without crossing obstacles + - Ensures connectivity to the goal through passable space + - Good for finding safe positions in the same "room" or connected area + + Cons: + - May not find nearby safe spots if they're on the other side of a wall + - Slightly slower than regular BFS due to additional checks + """ + + # Convert goal to grid coordinates + goal_grid = costmap.world_to_grid(goal) + gx, gy = int(goal_grid.x), int(goal_grid.y) + + # Convert distances to grid cells + clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) + max_search_cells = int(np.ceil(max_search_distance / costmap.resolution)) + + # BFS queue and visited set + queue = deque([(gx, gy, 0)]) + visited = set([(gx, gy)]) + + # 8-connected neighbors + neighbors = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)] + + while queue: + x, y, dist = queue.popleft() + + # Check if we've exceeded max search distance + if dist > max_search_cells: + break + + # Check if position is valid + if _is_position_safe( + costmap, x, y, cost_threshold, clearance_cells, connectivity_check_radius + ): + # Convert back to world coordinates + return costmap.grid_to_world((x, y)) + + # Add neighbors to queue + for dx, dy in neighbors: + nx, ny = x + dx, y + dy + + # Check bounds + if 0 <= nx < costmap.width and 0 <= ny < costmap.height: + if (nx, ny) not in visited: + # Only expand through passable cells (occupancy < 100) + if costmap.grid[ny, nx] < 100: + visited.add((nx, ny)) + queue.append((nx, ny, dist + 1)) + + return None + + +def _is_position_safe( + costmap: OccupancyGrid, + x: int, + y: int, + cost_threshold: int, + clearance_cells: int, + connectivity_check_radius: int, +) -> bool: + """ + Check if a position is safe based on multiple criteria. + + Args: + costmap: The occupancy grid + x, y: Grid coordinates to check + cost_threshold: Maximum acceptable cost + clearance_cells: Minimum clearance in cells + connectivity_check_radius: Radius to check for connectivity + + Returns: + True if position is safe, False otherwise + """ + + # Check bounds first + if not (0 <= x < costmap.width and 0 <= y < costmap.height): + return False + + # Check if position itself is free + if costmap.grid[y, x] >= cost_threshold or costmap.grid[y, x] == CostValues.UNKNOWN: + return False + + # Check clearance around position + for dy in range(-clearance_cells, clearance_cells + 1): + for dx in range(-clearance_cells, clearance_cells + 1): + nx, ny = x + dx, y + dy + if 0 <= nx < costmap.width and 0 <= ny < costmap.height: + # Check if within circular clearance + if dx * dx + dy * dy <= clearance_cells * clearance_cells: + if costmap.grid[ny, nx] >= cost_threshold: + return False + + # Check connectivity (not surrounded by obstacles) + # Count free neighbors in a larger radius + free_count = 0 + total_count = 0 + + for dy in range(-connectivity_check_radius, connectivity_check_radius + 1): + for dx in range(-connectivity_check_radius, connectivity_check_radius + 1): + if dx == 0 and dy == 0: + continue + + nx, ny = x + dx, y + dy + if 0 <= nx < costmap.width and 0 <= ny < costmap.height: + total_count += 1 + if ( + costmap.grid[ny, nx] < cost_threshold + and costmap.grid[ny, nx] != CostValues.UNKNOWN + ): + free_count += 1 + + # Require at least 50% of neighbors to be free (not surrounded) + if total_count > 0 and free_count < total_count * 0.5: + return False + + return True diff --git a/dimos/navigation/replanning_a_star/local_planner.py b/dimos/navigation/replanning_a_star/local_planner.py new file mode 100644 index 0000000000..cc5f6164dc --- /dev/null +++ b/dimos/navigation/replanning_a_star/local_planner.py @@ -0,0 +1,365 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from threading import Event, RLock, Thread +import time +import traceback +from typing import Literal, TypeAlias + +import numpy as np +from reactivex import Subject + +from dimos.core.global_config import GlobalConfig +from dimos.core.resource import Resource +from dimos.mapping.occupancy.visualize_path import visualize_path +from dimos.msgs.geometry_msgs import Twist +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped +from dimos.msgs.nav_msgs import Path +from dimos.msgs.sensor_msgs import Image +from dimos.navigation.base import NavigationState +from dimos.navigation.replanning_a_star.controllers import Controller, PController, PdController +from dimos.navigation.replanning_a_star.navigation_map import NavigationMap +from dimos.navigation.replanning_a_star.path_clearance import PathClearance +from dimos.navigation.replanning_a_star.path_distancer import PathDistancer +from dimos.utils.logging_config import setup_logger +from dimos.utils.trigonometry import angle_diff + +PlannerState: TypeAlias = Literal[ + "idle", "initial_rotation", "path_following", "final_rotation", "arrived" +] +StopMessage: TypeAlias = Literal["arrived", "obstacle_found", "error"] + +logger = setup_logger() + + +class LocalPlanner(Resource): + cmd_vel: Subject[Twist] + stopped_navigating: Subject[StopMessage] + debug_navigation: Subject[Image] + + _thread: Thread | None = None + _path: Path | None = None + _path_clearance: PathClearance | None = None + _path_distancer: PathDistancer | None = None + _current_odom: PoseStamped | None = None + + _pose_index: int + _lock: RLock + _stop_planning_event: Event + _state: PlannerState + _state_unique_id: int + _global_config: GlobalConfig + _navigation_map: NavigationMap + _goal_tolerance: float + _controller: Controller + + _speed: float = 0.55 + _control_frequency: float = 10 + _orientation_tolerance: float = 0.35 + _debug_navigation_interval: float = 1.0 + _debug_navigation_last: float = 0.0 + + def __init__( + self, global_config: GlobalConfig, navigation_map: NavigationMap, goal_tolerance: float + ) -> None: + self.cmd_vel = Subject() + self.stopped_navigating = Subject() + self.debug_navigation = Subject() + + self._pose_index = 0 + self._lock = RLock() + self._stop_planning_event = Event() + self._state = "idle" + self._state_unique_id = 0 + self._global_config = global_config + self._navigation_map = navigation_map + self._goal_tolerance = goal_tolerance + + controller = PController if global_config.simulation else PdController + + self._controller = controller( + self._global_config, + self._speed, + self._control_frequency, + ) + + def start(self) -> None: + pass + + def stop(self) -> None: + self.stop_planning() + + def handle_odom(self, msg: PoseStamped) -> None: + with self._lock: + self._current_odom = msg + + def start_planning(self, path: Path) -> None: + self.stop_planning() + + self._stop_planning_event = Event() + + with self._lock: + self._path = path + self._path_clearance = PathClearance(self._global_config, self._path) + self._path_distancer = PathDistancer(self._path) + self._pose_index = 0 + self._thread = Thread(target=self._thread_entrypoint, daemon=True) + self._thread.start() + + def stop_planning(self) -> None: + self.cmd_vel.on_next(Twist()) + self._stop_planning_event.set() + + with self._lock: + self._thread = None + + self._reset_state() + + def get_state(self) -> NavigationState: + with self._lock: + state = self._state + + match state: + case "idle" | "arrived": + return NavigationState.IDLE + case "initial_rotation" | "path_following" | "final_rotation": + return NavigationState.FOLLOWING_PATH + case _: + raise ValueError(f"Unknown planner state: {state}") + + def get_unique_state(self) -> tuple[PlannerState, int]: + with self._lock: + return (self._state, self._state_unique_id) + + def _thread_entrypoint(self) -> None: + try: + self._loop() + except Exception as e: + traceback.print_exc() + logger.exception("Error in local planning", exc_info=e) + self.stopped_navigating.on_next("error") + finally: + self._reset_state() + self.cmd_vel.on_next(Twist()) + + def _change_state(self, new_state: PlannerState) -> None: + self._state = new_state + self._state_unique_id += 1 + logger.info("changed state", state=new_state) + + def _loop(self) -> None: + stop_event = self._stop_planning_event + + with self._lock: + path = self._path + path_clearance = self._path_clearance + current_odom = self._current_odom + + if path is None or path_clearance is None: + raise RuntimeError("No path set for local planner.") + + # Determine initial state: skip initial_rotation if already aligned. + new_state: PlannerState = "initial_rotation" + if current_odom is not None and len(path.poses) > 0: + first_yaw = path.poses[0].orientation.euler[2] + robot_yaw = current_odom.orientation.euler[2] + initial_yaw_error = angle_diff(first_yaw, robot_yaw) + self._controller.reset_yaw_error(initial_yaw_error) + angle_in_tolerance = abs(initial_yaw_error) < self._orientation_tolerance + if angle_in_tolerance: + position_in_tolerance = ( + path.poses[0].position.distance(current_odom.position) < 0.01 + ) + if position_in_tolerance: + new_state = "final_rotation" + else: + new_state = "path_following" + + with self._lock: + self._change_state(new_state) + + while not stop_event.is_set(): + start_time = time.perf_counter() + + with self._lock: + path_clearance.update_costmap(self._navigation_map.binary_costmap) + path_clearance.update_pose_index(self._pose_index) + + self._send_debug_navigation(path, path_clearance) + + if path_clearance.is_obstacle_ahead(): + logger.info("Obstacle detected ahead, stopping local planner.") + self.stopped_navigating.on_next("obstacle_found") + break + + with self._lock: + state: PlannerState = self._state + + if state == "initial_rotation": + cmd_vel = self._compute_initial_rotation() + elif state == "path_following": + cmd_vel = self._compute_path_following() + elif state == "final_rotation": + cmd_vel = self._compute_final_rotation() + elif state == "arrived": + self.stopped_navigating.on_next("arrived") + break + elif state == "idle": + cmd_vel = None + + if cmd_vel is not None: + self.cmd_vel.on_next(cmd_vel) + + elapsed = time.perf_counter() - start_time + sleep_time = max(0.0, (1.0 / self._control_frequency) - elapsed) + stop_event.wait(sleep_time) + + if stop_event.is_set(): + logger.info("Local planner loop exited due to stop event.") + + def _compute_initial_rotation(self) -> Twist: + with self._lock: + path = self._path + current_odom = self._current_odom + + assert path is not None + assert current_odom is not None + + first_pose = path.poses[0] + first_yaw = first_pose.orientation.euler[2] + robot_yaw = current_odom.orientation.euler[2] + yaw_error = angle_diff(first_yaw, robot_yaw) + + if abs(yaw_error) < self._orientation_tolerance: + with self._lock: + self._change_state("path_following") + return self._compute_path_following() + + return self._controller.rotate(yaw_error) + + def get_distance_to_path(self) -> float | None: + with self._lock: + path_distancer = self._path_distancer + current_odom = self._current_odom + + if path_distancer is None or current_odom is None: + return None + + current_pos = np.array([current_odom.position.x, current_odom.position.y]) + + return path_distancer.get_distance_to_path(current_pos) + + def _compute_path_following(self) -> Twist: + with self._lock: + path_distancer = self._path_distancer + current_odom = self._current_odom + + assert path_distancer is not None + assert current_odom is not None + + current_pos = np.array([current_odom.position.x, current_odom.position.y]) + + if path_distancer.distance_to_goal(current_pos) < self._goal_tolerance: + logger.info("Reached goal position, starting final rotation") + with self._lock: + self._change_state("final_rotation") + return self._compute_final_rotation() + + closest_index = path_distancer.find_closest_point_index(current_pos) + + with self._lock: + self._pose_index = closest_index + + lookahead_point = path_distancer.find_lookahead_point(closest_index) + + return self._controller.advance(lookahead_point, current_odom) + + def _compute_final_rotation(self) -> Twist: + with self._lock: + path = self._path + current_odom = self._current_odom + + assert path is not None + assert current_odom is not None + + goal_yaw = path.poses[-1].orientation.euler[2] + robot_yaw = current_odom.orientation.euler[2] + yaw_error = angle_diff(goal_yaw, robot_yaw) + + if abs(yaw_error) < self._orientation_tolerance: + logger.info("Final rotation complete, goal reached") + with self._lock: + self._change_state("arrived") + return Twist() + + return self._controller.rotate(yaw_error) + + def _reset_state(self) -> None: + with self._lock: + self._change_state("idle") + self._path = None + self._path_clearance = None + self._path_distancer = None + self._pose_index = 0 + self._controller.reset_errors() + + def _send_debug_navigation(self, path: Path, path_clearance: PathClearance) -> None: + if "DEBUG_NAVIGATION" not in os.environ: + return + + now = time.time() + if now - self._debug_navigation_last < self._debug_navigation_interval: + return + + self._debug_navigation_last = now + + self.debug_navigation.on_next(self._make_debug_navigation_image(path, path_clearance)) + + def _make_debug_navigation_image(self, path: Path, path_clearance: PathClearance) -> Image: + scale = 8 + image = visualize_path( + self._navigation_map.gradient_costmap, + path, + self._global_config.robot_width, + self._global_config.robot_rotation_diameter, + 2, + scale, + ) + image.data = np.flipud(image.data) + + # Add path mask. + mask = path_clearance.mask + scaled_mask = np.repeat(np.repeat(mask, scale, axis=0), scale, axis=1) + scaled_mask = np.flipud(scaled_mask) + white = np.array([255, 255, 255], dtype=np.int16) + image.data[scaled_mask] = (image.data[scaled_mask].astype(np.int16) * 3 + white * 7) // 10 + + with self._lock: + current_odom = self._current_odom + + # Draw robot position. + if current_odom is not None: + grid_pos = self._navigation_map.gradient_costmap.world_to_grid(current_odom.position) + x = int(grid_pos.x * scale) + y = image.data.shape[0] - 1 - int(grid_pos.y * scale) + radius = 8 + for dy in range(-radius, radius + 1): + for dx in range(-radius, radius + 1): + if dx * dx + dy * dy <= radius * radius: + py, px = y + dy, x + dx + if 0 <= py < image.data.shape[0] and 0 <= px < image.data.shape[1]: + image.data[py, px] = [255, 255, 255] + + return image diff --git a/dimos/navigation/replanning_a_star/min_cost_astar.py b/dimos/navigation/replanning_a_star/min_cost_astar.py new file mode 100644 index 0000000000..c3430e64d9 --- /dev/null +++ b/dimos/navigation/replanning_a_star/min_cost_astar.py @@ -0,0 +1,227 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import heapq + +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, VectorLike +from dimos.msgs.nav_msgs import CostValues, OccupancyGrid, Path +from dimos.utils.logging_config import setup_logger + +# Try to import C++ extension for faster pathfinding +try: + from dimos.navigation.replanning_a_star.min_cost_astar_ext import ( + min_cost_astar_cpp as _astar_cpp, + ) + + _USE_CPP = True +except ImportError: + _USE_CPP = False + +logger = setup_logger() + +# Define possible movements (8-connected grid with diagonal movements) +_directions = [ + (0, 1), + (1, 0), + (0, -1), + (-1, 0), + (1, 1), + (1, -1), + (-1, 1), + (-1, -1), +] + +# Cost for each movement (straight vs diagonal) +_sc = 1.0 # Straight cost +_dc = 1.42 # Diagonal cost (approximately sqrt(2)) +_movement_costs = [_sc, _sc, _sc, _sc, _dc, _dc, _dc, _dc] + + +# Heuristic function (Octile distance for 8-connected grid) +def _heuristic(x1: int, y1: int, x2: int, y2: int) -> float: + dx = abs(x2 - x1) + dy = abs(y2 - y1) + # Octile distance: optimal for 8-connected grids with diagonal movement + return (dx + dy) + (_dc - 2 * _sc) * min(dx, dy) + + +def _reconstruct_path( + parents: dict[tuple[int, int], tuple[int, int]], + current: tuple[int, int], + costmap: OccupancyGrid, + start_tuple: tuple[int, int], + goal_tuple: tuple[int, int], +) -> Path: + waypoints: list[PoseStamped] = [] + while current in parents: + world_point = costmap.grid_to_world(current) + pose = PoseStamped( + frame_id="world", + position=[world_point.x, world_point.y, 0.0], + orientation=Quaternion(0, 0, 0, 1), # Identity quaternion + ) + waypoints.append(pose) + current = parents[current] + + start_world_point = costmap.grid_to_world(start_tuple) + start_pose = PoseStamped( + frame_id="world", + position=[start_world_point.x, start_world_point.y, 0.0], + orientation=Quaternion(0, 0, 0, 1), + ) + waypoints.append(start_pose) + + waypoints.reverse() + + # Add the goal position if it's not already included + goal_point = costmap.grid_to_world(goal_tuple) + + if ( + not waypoints + or (waypoints[-1].x - goal_point.x) ** 2 + (waypoints[-1].y - goal_point.y) ** 2 > 1e-10 + ): + goal_pose = PoseStamped( + frame_id="world", + position=[goal_point.x, goal_point.y, 0.0], + orientation=Quaternion(0, 0, 0, 1), + ) + waypoints.append(goal_pose) + + return Path(frame_id="world", poses=waypoints) + + +def _reconstruct_path_from_coords( + path_coords: list[tuple[int, int]], + costmap: OccupancyGrid, +) -> Path: + waypoints: list[PoseStamped] = [] + + for gx, gy in path_coords: + world_point = costmap.grid_to_world((gx, gy)) + pose = PoseStamped( + frame_id="world", + position=[world_point.x, world_point.y, 0.0], + orientation=Quaternion(0, 0, 0, 1), + ) + waypoints.append(pose) + + return Path(frame_id="world", poses=waypoints) + + +def min_cost_astar( + costmap: OccupancyGrid, + goal: VectorLike, + start: VectorLike = (0.0, 0.0), + cost_threshold: int = 100, + unknown_penalty: float = 0.8, + use_cpp: bool = True, +) -> Path | None: + start_vector = costmap.world_to_grid(start) + goal_vector = costmap.world_to_grid(goal) + + start_tuple = (int(start_vector.x), int(start_vector.y)) + goal_tuple = (int(goal_vector.x), int(goal_vector.y)) + + if not (0 <= goal_tuple[0] < costmap.width and 0 <= goal_tuple[1] < costmap.height): + return None + + if use_cpp: + if _USE_CPP: + path_coords = _astar_cpp( + costmap.grid, + start_tuple[0], + start_tuple[1], + goal_tuple[0], + goal_tuple[1], + cost_threshold, + unknown_penalty, + ) + if not path_coords: + return None + return _reconstruct_path_from_coords(path_coords, costmap) + else: + logger.warning("C++ A* module could not be imported. Using Python.") + + open_set: list[tuple[float, float, tuple[int, int]]] = [] # Priority queue for nodes to explore + closed_set: set[tuple[int, int]] = set() # Set of explored nodes + + # Dictionary to store cost and distance from start, and parents for each node + # Track cumulative cell cost and path length separately + cost_score: dict[tuple[int, int], float] = {start_tuple: 0.0} # Cumulative cell cost + dist_score: dict[tuple[int, int], float] = {start_tuple: 0.0} # Cumulative path length + parents: dict[tuple[int, int], tuple[int, int]] = {} + + # Start with the starting node + # Priority: (total_cost + heuristic_cost, total_distance + heuristic_distance, node) + h_dist = _heuristic(start_tuple[0], start_tuple[1], goal_tuple[0], goal_tuple[1]) + heapq.heappush(open_set, (0.0, h_dist, start_tuple)) + + while open_set: + _, _, current = heapq.heappop(open_set) + current_x, current_y = current + + if current in closed_set: + continue + + if current == goal_tuple: + return _reconstruct_path(parents, current, costmap, start_tuple, goal_tuple) + + closed_set.add(current) + + for i, (dx, dy) in enumerate(_directions): + neighbor_x, neighbor_y = current_x + dx, current_y + dy + neighbor = (neighbor_x, neighbor_y) + + if not (0 <= neighbor_x < costmap.width and 0 <= neighbor_y < costmap.height): + continue + + if neighbor in closed_set: + continue + + neighbor_val = costmap.grid[neighbor_y, neighbor_x] + + if neighbor_val >= cost_threshold: + continue + + if neighbor_val == CostValues.UNKNOWN: + # Unknown cells have a moderate traversal cost + cell_cost = cost_threshold * unknown_penalty + elif neighbor_val == CostValues.FREE: + cell_cost = 0.0 + else: + cell_cost = neighbor_val + + tentative_cost = cost_score[current] + cell_cost + tentative_dist = dist_score[current] + _movement_costs[i] + + # Get the current scores for the neighbor or set to infinity if not yet explored + neighbor_cost = cost_score.get(neighbor, float("inf")) + neighbor_dist = dist_score.get(neighbor, float("inf")) + + # If this path to the neighbor is better (prioritize cost, then distance) + if (tentative_cost, tentative_dist) < (neighbor_cost, neighbor_dist): + # Update the neighbor's scores and parent + parents[neighbor] = current + cost_score[neighbor] = tentative_cost + dist_score[neighbor] = tentative_dist + + # Calculate priority: cost first, then distance (both with heuristic) + h_dist = _heuristic(neighbor_x, neighbor_y, goal_tuple[0], goal_tuple[1]) + priority_cost = tentative_cost + priority_dist = tentative_dist + h_dist + + # Add the neighbor to the open set with its priority + heapq.heappush(open_set, (priority_cost, priority_dist, neighbor)) + + return None diff --git a/dimos/navigation/replanning_a_star/min_cost_astar_cpp.cpp b/dimos/navigation/replanning_a_star/min_cost_astar_cpp.cpp new file mode 100644 index 0000000000..f19b3bf826 --- /dev/null +++ b/dimos/navigation/replanning_a_star/min_cost_astar_cpp.cpp @@ -0,0 +1,265 @@ +// Copyright 2025 Dimensional Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace py = pybind11; + +// Movement directions (8-connected grid) +// Order: right, down, left, up, down-right, down-left, up-right, up-left +constexpr int DX[8] = {0, 1, 0, -1, 1, 1, -1, -1}; +constexpr int DY[8] = {1, 0, -1, 0, 1, -1, 1, -1}; + +// Movement costs: straight = 1.0, diagonal = sqrt(2) ā‰ˆ 1.42 +constexpr double STRAIGHT_COST = 1.0; +constexpr double DIAGONAL_COST = 1.42; +constexpr double MOVE_COSTS[8] = { + STRAIGHT_COST, STRAIGHT_COST, STRAIGHT_COST, STRAIGHT_COST, + DIAGONAL_COST, DIAGONAL_COST, DIAGONAL_COST, DIAGONAL_COST +}; + +constexpr int8_t COST_UNKNOWN = -1; +constexpr int8_t COST_FREE = 0; + +// Pack coordinates into a single 64-bit key for fast hashing +inline uint64_t pack_coords(int x, int y) { + return (static_cast(static_cast(x)) << 32) | + static_cast(static_cast(y)); +} + +// Unpack coordinates from 64-bit key +inline std::pair unpack_coords(uint64_t key) { + return {static_cast(key >> 32), static_cast(key & 0xFFFFFFFF)}; +} + +// Octile distance heuristic - optimal for 8-connected grids with diagonal movement +inline double heuristic(int x1, int y1, int x2, int y2) { + int dx = std::abs(x2 - x1); + int dy = std::abs(y2 - y1); + // Octile distance: straight moves + diagonal adjustment + return (dx + dy) + (DIAGONAL_COST - 2 * STRAIGHT_COST) * std::min(dx, dy); +} + +// Reconstruct path from goal to start using parent map +inline std::vector> reconstruct_path( + const std::unordered_map& parents, + uint64_t goal_key, + int start_x, + int start_y +) { + std::vector> path; + uint64_t node = goal_key; + + while (parents.count(node)) { + auto [x, y] = unpack_coords(node); + path.emplace_back(x, y); + node = parents.at(node); + } + + path.emplace_back(start_x, start_y); + std::reverse(path.begin(), path.end()); + return path; +} + +// Priority queue node: (priority_cost, priority_dist, x, y) +struct Node { + double cost; + double dist; + int x; + int y; + + // Min-heap comparison: lower values have higher priority + bool operator>(const Node& other) const { + if (cost != other.cost) return cost > other.cost; + return dist > other.dist; + } +}; + +/** + * A* pathfinding algorithm optimized for costmap grids. + * + * @param grid 2D numpy array of int8 values (height x width) + * @param start_x Starting X coordinate in grid cells + * @param start_y Starting Y coordinate in grid cells + * @param goal_x Goal X coordinate in grid cells + * @param goal_y Goal Y coordinate in grid cells + * @param cost_threshold Cells with value >= this are obstacles (default: 100) + * @param unknown_penalty Cost multiplier for unknown cells (default: 0.8) + * @return Vector of (x, y) grid coordinates from start to goal, empty if no path + */ +std::vector> min_cost_astar_cpp( + py::array_t grid, + int start_x, + int start_y, + int goal_x, + int goal_y, + int cost_threshold = 100, + double unknown_penalty = 0.8 +) { + // Get buffer info for direct array access + auto buf = grid.unchecked<2>(); + const int height = static_cast(buf.shape(0)); + const int width = static_cast(buf.shape(1)); + + // Bounds check for goal + if (goal_x < 0 || goal_x >= width || goal_y < 0 || goal_y >= height) { + return {}; + } + + // Bounds check for start + if (start_x < 0 || start_x >= width || start_y < 0 || start_y >= height) { + return {}; + } + + const uint64_t start_key = pack_coords(start_x, start_y); + const uint64_t goal_key = pack_coords(goal_x, goal_y); + + std::priority_queue, std::greater> open_set; + + std::unordered_set closed_set; + closed_set.reserve(width * height / 4); // Pre-allocate + + // Parent tracking for path reconstruction + std::unordered_map parents; + parents.reserve(width * height / 4); + + // Score tracking (cost and distance) + std::unordered_map cost_score; + std::unordered_map dist_score; + cost_score.reserve(width * height / 4); + dist_score.reserve(width * height / 4); + + // Initialize start node + cost_score[start_key] = 0.0; + dist_score[start_key] = 0.0; + double h = heuristic(start_x, start_y, goal_x, goal_y); + open_set.push({0.0, h, start_x, start_y}); + + while (!open_set.empty()) { + Node current = open_set.top(); + open_set.pop(); + + const int cx = current.x; + const int cy = current.y; + const uint64_t current_key = pack_coords(cx, cy); + + if (closed_set.count(current_key)) { + continue; + } + + if (current_key == goal_key) { + return reconstruct_path(parents, current_key, start_x, start_y); + } + + closed_set.insert(current_key); + + const double current_cost = cost_score[current_key]; + const double current_dist = dist_score[current_key]; + + // Explore all 8 neighbors + for (int i = 0; i < 8; ++i) { + const int nx = cx + DX[i]; + const int ny = cy + DY[i]; + + if (nx < 0 || nx >= width || ny < 0 || ny >= height) { + continue; + } + + const uint64_t neighbor_key = pack_coords(nx, ny); + + if (closed_set.count(neighbor_key)) { + continue; + } + + // Get cell value (note: grid is [y, x] in row-major order) + const int8_t val = buf(ny, nx); + + if (val >= cost_threshold) { + continue; + } + + double cell_cost; + if (val == COST_UNKNOWN) { + // Unknown cells have a moderate traversal cost + cell_cost = cost_threshold * unknown_penalty; + } else if (val == COST_FREE) { + cell_cost = 0.0; + } else { + cell_cost = static_cast(val); + } + + const double tentative_cost = current_cost + cell_cost; + const double tentative_dist = current_dist + MOVE_COSTS[i]; + + // Get existing scores (infinity if not yet visited) + auto cost_it = cost_score.find(neighbor_key); + auto dist_it = dist_score.find(neighbor_key); + const double n_cost = (cost_it != cost_score.end()) ? cost_it->second : INFINITY; + const double n_dist = (dist_it != dist_score.end()) ? dist_it->second : INFINITY; + + // Check if this path is better (prioritize cost, then distance) + if (tentative_cost < n_cost || + (tentative_cost == n_cost && tentative_dist < n_dist)) { + + // Update parent and scores + parents[neighbor_key] = current_key; + cost_score[neighbor_key] = tentative_cost; + dist_score[neighbor_key] = tentative_dist; + + // Calculate priority with heuristic + const double h_dist = heuristic(nx, ny, goal_x, goal_y); + const double priority_cost = tentative_cost; + const double priority_dist = tentative_dist + h_dist; + + open_set.push({priority_cost, priority_dist, nx, ny}); + } + } + } + + return {}; +} + +PYBIND11_MODULE(min_cost_astar_ext, m) { + m.doc() = "C++ implementation of A* pathfinding for costmap grids"; + + m.def("min_cost_astar_cpp", &min_cost_astar_cpp, + "A* pathfinding on a costmap grid.\n\n" + "Args:\n" + " grid: 2D numpy array of int8 values (height x width)\n" + " start_x: Starting X coordinate in grid cells\n" + " start_y: Starting Y coordinate in grid cells\n" + " goal_x: Goal X coordinate in grid cells\n" + " goal_y: Goal Y coordinate in grid cells\n" + " cost_threshold: Cells >= this value are obstacles (default: 100)\n" + " unknown_penalty: Cost multiplier for unknown cells (default: 0.8)\n\n" + "Returns:\n" + " List of (x, y) grid coordinates from start to goal, or empty list if no path", + py::arg("grid"), + py::arg("start_x"), + py::arg("start_y"), + py::arg("goal_x"), + py::arg("goal_y"), + py::arg("cost_threshold") = 100, + py::arg("unknown_penalty") = 0.8); +} diff --git a/dimos/navigation/replanning_a_star/min_cost_astar_ext.pyi b/dimos/navigation/replanning_a_star/min_cost_astar_ext.pyi new file mode 100644 index 0000000000..558b010ce5 --- /dev/null +++ b/dimos/navigation/replanning_a_star/min_cost_astar_ext.pyi @@ -0,0 +1,26 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from numpy.typing import NDArray + +def min_cost_astar_cpp( + grid: NDArray[np.int8], + start_x: int, + start_y: int, + goal_x: int, + goal_y: int, + cost_threshold: int, + unknown_penalty: float, +) -> list[tuple[int, int]]: ... diff --git a/dimos/navigation/replanning_a_star/module.py b/dimos/navigation/replanning_a_star/module.py new file mode 100644 index 0000000000..6ba1ae0ba1 --- /dev/null +++ b/dimos/navigation/replanning_a_star/module.py @@ -0,0 +1,114 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from dimos_lcm.std_msgs import Bool, String +from reactivex.disposable import Disposable +import rerun as rr + +from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig +from dimos.dashboard.rerun_init import connect_rerun +from dimos.msgs.geometry_msgs import PoseStamped, Twist +from dimos.msgs.nav_msgs import OccupancyGrid, Path +from dimos.msgs.sensor_msgs import Image +from dimos.navigation.base import NavigationInterface, NavigationState +from dimos.navigation.replanning_a_star.global_planner import GlobalPlanner + + +class ReplanningAStarPlanner(Module, NavigationInterface): + odom: In[PoseStamped] # TODO: Use TF. + global_costmap: In[OccupancyGrid] + goal_request: In[PoseStamped] + target: In[PoseStamped] + + goal_reached: Out[Bool] + navigation_state: Out[String] # TODO: set it + cmd_vel: Out[Twist] + path: Out[Path] + debug_navigation: Out[Image] + + _planner: GlobalPlanner + _global_config: GlobalConfig + + def __init__(self, global_config: GlobalConfig | None = None) -> None: + super().__init__() + self._global_config = global_config or GlobalConfig() + self._planner = GlobalPlanner(self._global_config) + + @rpc + def start(self) -> None: + super().start() + + if self._global_config.viewer_backend.startswith("rerun"): + connect_rerun(global_config=self._global_config) + + # Manual Rerun logging for path + def _log_path_to_rerun(path: Path) -> None: + rr.log("world/nav/path", path.to_rerun()) # type: ignore[no-untyped-call] + + self._disposables.add(self._planner.path.subscribe(_log_path_to_rerun)) + + self._disposables.add(Disposable(self.odom.subscribe(self._planner.handle_odom))) + self._disposables.add( + Disposable(self.global_costmap.subscribe(self._planner.handle_global_costmap)) + ) + self._disposables.add( + Disposable(self.goal_request.subscribe(self._planner.handle_goal_request)) + ) + self._disposables.add(Disposable(self.target.subscribe(self._planner.handle_goal_request))) + + self._disposables.add(self._planner.path.subscribe(self.path.publish)) + + self._disposables.add(self._planner.cmd_vel.subscribe(self.cmd_vel.publish)) + + self._disposables.add(self._planner.goal_reached.subscribe(self.goal_reached.publish)) + + if "DEBUG_NAVIGATION" in os.environ: + self._disposables.add( + self._planner.debug_navigation.subscribe(self.debug_navigation.publish) + ) + + self._planner.start() + + @rpc + def stop(self) -> None: + self.cancel_goal() + self._planner.stop() + + super().stop() + + @rpc + def set_goal(self, goal: PoseStamped) -> bool: + self._planner.handle_goal_request(goal) + return True + + @rpc + def get_state(self) -> NavigationState: + return self._planner.get_state() + + @rpc + def is_goal_reached(self) -> bool: + return self._planner.is_goal_reached() + + @rpc + def cancel_goal(self) -> bool: + self._planner.cancel_goal() + return True + + +replanning_a_star_planner = ReplanningAStarPlanner.blueprint + +__all__ = ["ReplanningAStarPlanner", "replanning_a_star_planner"] diff --git a/dimos/navigation/replanning_a_star/navigation_map.py b/dimos/navigation/replanning_a_star/navigation_map.py new file mode 100644 index 0000000000..f1c149ded6 --- /dev/null +++ b/dimos/navigation/replanning_a_star/navigation_map.py @@ -0,0 +1,66 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import RLock + +from dimos.core.global_config import GlobalConfig +from dimos.mapping.occupancy.path_map import make_navigation_map +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid + + +class NavigationMap: + _global_config: GlobalConfig + _binary: OccupancyGrid | None = None + _lock: RLock + + def __init__(self, global_config: GlobalConfig) -> None: + self._global_config = global_config + self._lock = RLock() + + def update(self, occupancy_grid: OccupancyGrid) -> None: + with self._lock: + self._binary = occupancy_grid + + @property + def binary_costmap(self) -> OccupancyGrid: + """ + Get the latest binary costmap received from the global costmap source. + """ + + with self._lock: + if self._binary is None: + raise ValueError("No current global costmap available") + + return self._binary + + @property + def gradient_costmap(self) -> OccupancyGrid: + return self.make_gradient_costmap() + + def make_gradient_costmap(self, robot_increase: float = 1.0) -> OccupancyGrid: + """ + Get the latest navigation map created from inflating and applying a + gradient to the binary costmap. + """ + + with self._lock: + binary = self._binary + if binary is None: + raise ValueError("No current global costmap available") + + return make_navigation_map( + binary, + self._global_config.robot_width * robot_increase, + strategy=self._global_config.planner_strategy, + ) diff --git a/dimos/navigation/replanning_a_star/path_clearance.py b/dimos/navigation/replanning_a_star/path_clearance.py new file mode 100644 index 0000000000..e99fba26c3 --- /dev/null +++ b/dimos/navigation/replanning_a_star/path_clearance.py @@ -0,0 +1,94 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import RLock + +import numpy as np +from numpy.typing import NDArray + +from dimos.core.global_config import GlobalConfig +from dimos.mapping.occupancy.path_mask import make_path_mask +from dimos.msgs.nav_msgs import Path +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid + + +class PathClearance: + _costmap: OccupancyGrid | None = None + _last_costmap: OccupancyGrid | None = None + _path_lookup_distance: float = 3.0 + _max_distance_cache: float = 1.0 + _last_used_shape: tuple[int, ...] | None = None + _last_mask: NDArray[np.bool_] | None = None + _last_used_pose: int | None = None + _global_config: GlobalConfig + _lock: RLock + _path: Path + _pose_index: int + + def __init__(self, global_config: GlobalConfig, path: Path) -> None: + self._global_config = global_config + self._path = path + self._pose_index = 0 + self._lock = RLock() + + def update_costmap(self, costmap: OccupancyGrid) -> None: + with self._lock: + self._costmap = costmap + + def update_pose_index(self, index: int) -> None: + with self._lock: + self._pose_index = index + + @property + def mask(self) -> NDArray[np.bool_]: + with self._lock: + costmap = self._costmap + pose_index = self._pose_index + + assert costmap is not None + + if ( + self._last_mask is not None + and self._last_used_pose is not None + and costmap.grid.shape == self._last_used_shape + and self._pose_distance(self._last_used_pose, pose_index) < self._max_distance_cache + ): + return self._last_mask + + self._last_mask = make_path_mask( + occupancy_grid=costmap, + path=self._path, + robot_width=self._global_config.robot_width, + pose_index=pose_index, + max_length=self._path_lookup_distance, + ) + + self._last_used_shape = costmap.grid.shape + self._last_used_pose = pose_index + + return self._last_mask + + def is_obstacle_ahead(self) -> bool: + with self._lock: + costmap = self._costmap + + if costmap is None: + return True + + return bool(np.any(costmap.grid[self.mask] == CostValues.OCCUPIED)) + + def _pose_distance(self, index1: int, index2: int) -> float: + p1 = self._path.poses[index1].position + p2 = self._path.poses[index2].position + return p1.distance(p2) diff --git a/dimos/navigation/replanning_a_star/path_distancer.py b/dimos/navigation/replanning_a_star/path_distancer.py new file mode 100644 index 0000000000..04d844267f --- /dev/null +++ b/dimos/navigation/replanning_a_star/path_distancer.py @@ -0,0 +1,89 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import cast + +import numpy as np +from numpy.typing import NDArray + +from dimos.msgs.nav_msgs import Path + + +class PathDistancer: + _lookahead_dist: float = 0.5 + _path: NDArray[np.float64] + _cumulative_dists: NDArray[np.float64] + + def __init__(self, path: Path) -> None: + self._path = np.array([[p.position.x, p.position.y] for p in path.poses]) + self._cumulative_dists = _make_cumulative_distance_array(self._path) + + def find_lookahead_point(self, start_idx: int) -> NDArray[np.float64]: + """ + Given a path, and a precomputed array of cumulative distances, find the + point which is `lookahead_dist` ahead of the current point. + """ + + if start_idx >= len(self._path) - 1: + return cast("NDArray[np.float64]", self._path[-1]) + + # Distance from path[0] to path[start_idx]. + base_dist = self._cumulative_dists[start_idx - 1] if start_idx > 0 else 0.0 + target_dist = base_dist + self._lookahead_dist + + # Binary search: cumulative_dists[i] = distance from path[0] to path[i+1] + idx = int(np.searchsorted(self._cumulative_dists, target_dist)) + + if idx >= len(self._cumulative_dists): + return cast("NDArray[np.float64]", self._path[-1]) + + # Interpolate within segment from path[idx] to path[idx+1]. + prev_cum_dist = self._cumulative_dists[idx - 1] if idx > 0 else 0.0 + segment_dist = self._cumulative_dists[idx] - prev_cum_dist + remaining_dist = target_dist - prev_cum_dist + + if segment_dist > 0: + t = remaining_dist / segment_dist + return cast( + "NDArray[np.float64]", + self._path[idx] + t * (self._path[idx + 1] - self._path[idx]), + ) + + return cast("NDArray[np.float64]", self._path[idx]) + + def distance_to_goal(self, current_pos: NDArray[np.float64]) -> float: + return float(np.linalg.norm(self._path[-1] - current_pos)) + + def get_distance_to_path(self, pos: NDArray[np.float64]) -> float: + index = self.find_closest_point_index(pos) + return float(np.linalg.norm(self._path[index] - pos)) + + def find_closest_point_index(self, pos: NDArray[np.float64]) -> int: + """Find the index of the closest point on the path.""" + distances = np.linalg.norm(self._path - pos, axis=1) + return int(np.argmin(distances)) + + +def _make_cumulative_distance_array(array: NDArray[np.float64]) -> NDArray[np.float64]: + """ + For an array representing 2D points, create an array of all the distances + between the points. + """ + + if len(array) < 2: + return np.array([0.0]) + + segments = array[1:] - array[:-1] + segment_dists = np.linalg.norm(segments, axis=1) + return np.cumsum(segment_dists) diff --git a/dimos/navigation/replanning_a_star/position_tracker.py b/dimos/navigation/replanning_a_star/position_tracker.py new file mode 100644 index 0000000000..77b4df0dd0 --- /dev/null +++ b/dimos/navigation/replanning_a_star/position_tracker.py @@ -0,0 +1,83 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import RLock +import time +from typing import cast + +import numpy as np +from numpy.typing import NDArray + +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped + +_max_points_per_second = 1000 + + +class PositionTracker: + _lock: RLock + _time_window: float + _max_points: int + _threshold: float + _timestamps: NDArray[np.float32] + _positions: NDArray[np.float32] + _index: int + _size: int + + def __init__(self, time_window: float) -> None: + self._lock = RLock() + self._time_window = time_window + self._threshold = 0.4 + self._max_points = int(_max_points_per_second * self._time_window) + self.reset_data() + + def reset_data(self) -> None: + with self._lock: + self._timestamps = np.zeros(self._max_points, dtype=np.float32) + self._positions = np.zeros((self._max_points, 2), dtype=np.float32) + self._index = 0 + self._size = 0 + + def add_position(self, pose: PoseStamped) -> None: + with self._lock: + self._timestamps[self._index] = time.time() + self._positions[self._index] = (pose.position.x, pose.position.y) + self._index = (self._index + 1) % self._max_points + self._size = min(self._size + 1, self._max_points) + + def _get_recent_positions(self) -> NDArray[np.float32]: + cutoff = time.time() - self._time_window + + if self._size == 0: + return np.empty((0, 2), dtype=np.float32) + + if self._size < self._max_points: + mask = self._timestamps[: self._size] >= cutoff + return self._positions[: self._size][mask] + + ts = np.concatenate([self._timestamps[self._index :], self._timestamps[: self._index]]) + pos = np.concatenate([self._positions[self._index :], self._positions[: self._index]]) + mask = ts >= cutoff + return cast("NDArray[np.float32]", pos[mask]) + + def is_stuck(self) -> bool: + with self._lock: + recent = self._get_recent_positions() + + if len(recent) == 0: + return False + + centroid = recent.mean(axis=0) + distances = np.linalg.norm(recent - centroid, axis=1) + + return bool(np.all(distances < self._threshold)) diff --git a/dimos/navigation/replanning_a_star/replan_limiter.py b/dimos/navigation/replanning_a_star/replan_limiter.py new file mode 100644 index 0000000000..8cc630f3df --- /dev/null +++ b/dimos/navigation/replanning_a_star/replan_limiter.py @@ -0,0 +1,68 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import RLock + +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class ReplanLimiter: + """ + This class limits replanning too many times in the same area. But if we exit + the area, the number of attempts is reset. + """ + + _max_attempts: int = 6 + _reset_distance: float = 2.0 + _attempt_pos: Vector3 | None = None + _lock: RLock + + _attempt: int + + def __init__(self) -> None: + self._lock = RLock() + self._attempt = 0 + + def can_retry(self, position: Vector3) -> bool: + with self._lock: + if self._attempt == 0: + self._attempt_pos = position + + if self._attempt >= 1 and self._attempt_pos: + distance = self._attempt_pos.distance(position) + if distance >= self._reset_distance: + logger.info( + "Traveled enough to reset attempts", + attempts=self._attempt, + distance=distance, + ) + self._attempt = 0 + self._attempt_pos = position + + return self._attempt + 1 <= self._max_attempts + + def will_retry(self) -> None: + with self._lock: + self._attempt += 1 + + def reset(self) -> None: + with self._lock: + self._attempt = 0 + + def get_attempt(self) -> int: + with self._lock: + return self._attempt diff --git a/dimos/navigation/replanning_a_star/test_goal_validator.py b/dimos/navigation/replanning_a_star/test_goal_validator.py new file mode 100644 index 0000000000..4cda9de863 --- /dev/null +++ b/dimos/navigation/replanning_a_star/test_goal_validator.py @@ -0,0 +1,53 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +from dimos.msgs.geometry_msgs import Vector3 +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid +from dimos.navigation.replanning_a_star.goal_validator import find_safe_goal +from dimos.utils.data import get_data + + +@pytest.fixture +def costmap() -> OccupancyGrid: + return OccupancyGrid(np.load(get_data("occupancy_simple.npy"))) + + +@pytest.mark.parametrize( + "input_pos,expected_pos", + [ + # Identical. + ((6.15, 10.0), (6.15, 10.0)), + # Very slightly off. + ((6.0, 10.0), (6.05, 10.0)), + # Don't pick a spot that's the closest, but is actually on the other side of the wall. + ((5.0, 9.0), (5.85, 9.6)), + ], +) +def test_find_safe_goal(costmap, input_pos, expected_pos) -> None: + goal = Vector3(input_pos[0], input_pos[1], 0.0) + + safe_goal = find_safe_goal( + costmap, + goal, + algorithm="bfs_contiguous", + cost_threshold=CostValues.OCCUPIED, + min_clearance=0.3, + max_search_distance=5.0, + connectivity_check_radius=0, + ) + + assert safe_goal == Vector3(expected_pos[0], expected_pos[1], 0.0) diff --git a/dimos/navigation/replanning_a_star/test_min_cost_astar.py b/dimos/navigation/replanning_a_star/test_min_cost_astar.py new file mode 100644 index 0000000000..9cc0cad29a --- /dev/null +++ b/dimos/navigation/replanning_a_star/test_min_cost_astar.py @@ -0,0 +1,88 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import numpy as np +from open3d.geometry import PointCloud +import pytest + +from dimos.mapping.occupancy.gradient import gradient, voronoi_gradient +from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid +from dimos.msgs.sensor_msgs.Image import Image +from dimos.navigation.replanning_a_star.min_cost_astar import min_cost_astar +from dimos.utils.data import get_data + + +@pytest.fixture +def costmap() -> PointCloud: + return gradient(OccupancyGrid(np.load(get_data("occupancy_simple.npy"))), max_distance=1.5) + + +@pytest.fixture +def costmap_three_paths() -> PointCloud: + return voronoi_gradient(OccupancyGrid(np.load(get_data("three_paths.npy"))), max_distance=1.5) + + +def test_astar(costmap) -> None: + start = Vector3(4.0, 2.0) + goal = Vector3(6.15, 10.0) + expected = Image.from_file(get_data("astar_min_cost.png")) + + path = min_cost_astar(costmap, goal, start, use_cpp=False) + actual = visualize_occupancy_grid(costmap, "rainbow", path) + + np.testing.assert_array_equal(actual.data, expected.data) + + +def test_astar_corner(costmap_three_paths) -> None: + start = Vector3(2.8, 3.35) + goal = Vector3(6.35, 4.25) + expected = Image.from_file(get_data("astar_corner_min_cost.png")) + + path = min_cost_astar(costmap_three_paths, goal, start, use_cpp=False) + actual = visualize_occupancy_grid(costmap_three_paths, "rainbow", path) + + np.testing.assert_array_equal(actual.data, expected.data) + + +def test_astar_python_and_cpp(costmap) -> None: + start = Vector3(4.0, 2.0, 0) + goal = Vector3(6.15, 10.0) + + start_time = time.perf_counter() + path_python = min_cost_astar(costmap, goal, start, use_cpp=False) + elapsed_time_python = time.perf_counter() - start_time + print(f"\nastar Python took {elapsed_time_python:.6f} seconds") + assert path_python is not None + assert len(path_python.poses) > 0 + + start_time = time.perf_counter() + path_cpp = min_cost_astar(costmap, goal, start, use_cpp=True) + elapsed_time_cpp = time.perf_counter() - start_time + print(f"astar C++ took {elapsed_time_cpp:.6f} seconds") + assert path_cpp is not None + assert len(path_cpp.poses) > 0 + + times_better = elapsed_time_python / elapsed_time_cpp + print(f"astar C++ is {times_better:.2f} times faster than Python") + + # Assert that both implementations return almost identical points. + np.testing.assert_allclose( + [(p.position.x, p.position.y) for p in path_python.poses], + [(p.position.x, p.position.y) for p in path_cpp.poses], + atol=0.05001, + ) diff --git a/dimos/navigation/rosnav.py b/dimos/navigation/rosnav.py new file mode 100644 index 0000000000..88fa7985eb --- /dev/null +++ b/dimos/navigation/rosnav.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +NavBot class for navigation-related functionality. +Encapsulates ROS bridge and topic remapping for Unitree robots. +""" + +from collections.abc import Generator +from dataclasses import dataclass, field +import logging +import threading +import time + +from geometry_msgs.msg import ( # type: ignore[attr-defined] + PointStamped as ROSPointStamped, + PoseStamped as ROSPoseStamped, + TwistStamped as ROSTwistStamped, +) +from nav_msgs.msg import Path as ROSPath # type: ignore[attr-defined] +import rclpy +from rclpy.node import Node +from reactivex import operators as ops +from reactivex.subject import Subject +from sensor_msgs.msg import ( # type: ignore[attr-defined] + Joy as ROSJoy, + PointCloud2 as ROSPointCloud2, +) +from std_msgs.msg import ( # type: ignore[attr-defined] + Bool as ROSBool, + Int8 as ROSInt8, +) +from tf2_msgs.msg import TFMessage as ROSTFMessage # type: ignore[attr-defined] + +from dimos import spec +from dimos.agents import Reducer, Stream, skill # type: ignore[attr-defined] +from dimos.core import DimosCluster, In, LCMTransport, Module, Out, pSHMTransport, rpc +from dimos.core.module import ModuleConfig +from dimos.msgs.geometry_msgs import ( + PoseStamped, + Quaternion, + Transform, + Twist, + Vector3, +) +from dimos.msgs.nav_msgs import Path +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.msgs.std_msgs import Bool +from dimos.msgs.tf2_msgs.TFMessage import TFMessage +from dimos.navigation.base import NavigationInterface, NavigationState +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import euler_to_quaternion + +logger = setup_logger(level=logging.INFO) + + +@dataclass +class Config(ModuleConfig): + local_pointcloud_freq: float = 2.0 + global_pointcloud_freq: float = 1.0 + sensor_to_base_link_transform: Transform = field( + default_factory=lambda: Transform(frame_id="sensor", child_frame_id="base_link") + ) + + +class ROSNav( + Module, NavigationInterface, spec.Nav, spec.Global3DMap, spec.Pointcloud, spec.LocalPlanner +): + config: Config + default_config = Config + + goal_req: In[PoseStamped] + + pointcloud: Out[PointCloud2] + global_pointcloud: Out[PointCloud2] + + goal_active: Out[PoseStamped] + path_active: Out[Path] + cmd_vel: Out[Twist] + + # Using RxPY Subjects for reactive data flow instead of storing state + _local_pointcloud_subject: Subject # type: ignore[type-arg] + _global_pointcloud_subject: Subject # type: ignore[type-arg] + + _current_position_running: bool = False + _spin_thread: threading.Thread | None = None + _goal_reach: bool | None = None + + # Navigation state tracking for NavigationInterface + _navigation_state: NavigationState = NavigationState.IDLE + _state_lock: threading.Lock + _navigation_thread: threading.Thread | None = None + _current_goal: PoseStamped | None = None + _goal_reached: bool = False + + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(*args, **kwargs) + + # Initialize RxPY Subjects for streaming data + self._local_pointcloud_subject = Subject() + self._global_pointcloud_subject = Subject() + + # Initialize state tracking + self._state_lock = threading.Lock() + self._navigation_state = NavigationState.IDLE + self._goal_reached = False + + if not rclpy.ok(): # type: ignore[attr-defined] + rclpy.init() + + self._node = Node("navigation_module") + + # ROS2 Publishers + self.goal_pose_pub = self._node.create_publisher(ROSPoseStamped, "/goal_pose", 10) + self.cancel_goal_pub = self._node.create_publisher(ROSBool, "/cancel_goal", 10) + self.soft_stop_pub = self._node.create_publisher(ROSInt8, "/stop", 10) + self.joy_pub = self._node.create_publisher(ROSJoy, "/joy", 10) + + # ROS2 Subscribers + self.goal_reached_sub = self._node.create_subscription( + ROSBool, "/goal_reached", self._on_ros_goal_reached, 10 + ) + self.cmd_vel_sub = self._node.create_subscription( + ROSTwistStamped, "/cmd_vel", self._on_ros_cmd_vel, 10 + ) + self.goal_waypoint_sub = self._node.create_subscription( + ROSPointStamped, "/way_point", self._on_ros_goal_waypoint, 10 + ) + self.registered_scan_sub = self._node.create_subscription( + ROSPointCloud2, "/registered_scan", self._on_ros_registered_scan, 10 + ) + + self.global_pointcloud_sub = self._node.create_subscription( + ROSPointCloud2, "/terrain_map_ext", self._on_ros_global_pointcloud, 10 + ) + + self.path_sub = self._node.create_subscription(ROSPath, "/path", self._on_ros_path, 10) + self.tf_sub = self._node.create_subscription(ROSTFMessage, "/tf", self._on_ros_tf, 10) + + logger.info("NavigationModule initialized with ROS2 node") + + @rpc + def start(self) -> None: + self._running = True + + self._disposables.add( + self._local_pointcloud_subject.pipe( + ops.sample(1.0 / self.config.local_pointcloud_freq), # Sample at desired frequency + ops.map(lambda msg: PointCloud2.from_ros_msg(msg)), # type: ignore[arg-type] + ).subscribe( + on_next=self.pointcloud.publish, + on_error=lambda e: logger.error(f"Lidar stream error: {e}"), + ) + ) + + self._disposables.add( + self._global_pointcloud_subject.pipe( + ops.sample(1.0 / self.config.global_pointcloud_freq), # Sample at desired frequency + ops.map(lambda msg: PointCloud2.from_ros_msg(msg)), # type: ignore[arg-type] + ).subscribe( + on_next=self.global_pointcloud.publish, + on_error=lambda e: logger.error(f"Map stream error: {e}"), + ) + ) + + # Create and start the spin thread for ROS2 node spinning + self._spin_thread = threading.Thread( + target=self._spin_node, daemon=True, name="ROS2SpinThread" + ) + self._spin_thread.start() + + self.goal_req.subscribe(self._on_goal_pose) + logger.info("NavigationModule started with ROS2 spinning and RxPY streams") + + def _spin_node(self) -> None: + while self._running and rclpy.ok(): # type: ignore[attr-defined] + try: + rclpy.spin_once(self._node, timeout_sec=0.1) + except Exception as e: + if self._running: + logger.error(f"ROS2 spin error: {e}") + + def _on_ros_goal_reached(self, msg: ROSBool) -> None: + self._goal_reach = msg.data + if msg.data: + with self._state_lock: + self._goal_reached = True + self._navigation_state = NavigationState.IDLE + + def _on_ros_goal_waypoint(self, msg: ROSPointStamped) -> None: + dimos_pose = PoseStamped( + ts=time.time(), + frame_id=msg.header.frame_id, + position=Vector3(msg.point.x, msg.point.y, msg.point.z), + orientation=Quaternion(0.0, 0.0, 0.0, 1.0), + ) + self.goal_active.publish(dimos_pose) + + def _on_ros_cmd_vel(self, msg: ROSTwistStamped) -> None: + self.cmd_vel.publish(Twist.from_ros_msg(msg.twist)) + + def _on_ros_registered_scan(self, msg: ROSPointCloud2) -> None: + self._local_pointcloud_subject.on_next(msg) + + def _on_ros_global_pointcloud(self, msg: ROSPointCloud2) -> None: + self._global_pointcloud_subject.on_next(msg) + + def _on_ros_path(self, msg: ROSPath) -> None: + dimos_path = Path.from_ros_msg(msg) + dimos_path.frame_id = "base_link" + self.path_active.publish(dimos_path) + + def _on_ros_tf(self, msg: ROSTFMessage) -> None: + ros_tf = TFMessage.from_ros_msg(msg) + + map_to_world_tf = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=euler_to_quaternion(Vector3(0.0, 0.0, 0.0)), + frame_id="map", + child_frame_id="world", + ts=time.time(), + ) + + self.tf.publish( + self.config.sensor_to_base_link_transform.now(), + map_to_world_tf, + *ros_tf.transforms, + ) + + def _on_goal_pose(self, msg: PoseStamped) -> None: + self.navigate_to(msg) + + def _on_cancel_goal(self, msg: Bool) -> None: + if msg.data: + self.stop() + + def _set_autonomy_mode(self) -> None: + joy_msg = ROSJoy() # type: ignore[no-untyped-call] + joy_msg.axes = [ + 0.0, # axis 0 + 0.0, # axis 1 + -1.0, # axis 2 + 0.0, # axis 3 + 1.0, # axis 4 + 1.0, # axis 5 + 0.0, # axis 6 + 0.0, # axis 7 + ] + joy_msg.buttons = [ + 0, # button 0 + 0, # button 1 + 0, # button 2 + 0, # button 3 + 0, # button 4 + 0, # button 5 + 0, # button 6 + 1, # button 7 - controls autonomy mode + 0, # button 8 + 0, # button 9 + 0, # button 10 + ] + self.joy_pub.publish(joy_msg) + logger.info("Setting autonomy mode via Joy message") + + @skill(stream=Stream.passive, reducer=Reducer.latest) # type: ignore[arg-type] + def current_position(self): # type: ignore[no-untyped-def] + """passively stream the current position of the robot every second""" + if self._current_position_running: + return "already running" + while True: + self._current_position_running = True + time.sleep(1.0) + tf = self.tf.get("map", "base_link") + if not tf: + continue + yield f"current position {tf.translation.x}, {tf.translation.y}" + + @skill(stream=Stream.call_agent, reducer=Reducer.string) # type: ignore[arg-type] + def goto(self, x: float, y: float): # type: ignore[no-untyped-def] + """ + move the robot in relative coordinates + x is forward, y is left + + goto(1, 0) will move the robot forward by 1 meter + """ + pose_to = PoseStamped( + position=Vector3(x, y, 0), + orientation=Quaternion(0.0, 0.0, 0.0, 0.0), + frame_id="base_link", + ts=time.time(), + ) + + yield "moving, please wait..." + self.navigate_to(pose_to) + yield "arrived" + + @skill(stream=Stream.call_agent, reducer=Reducer.string) # type: ignore[arg-type] + def goto_global(self, x: float, y: float) -> Generator[str, None, None]: + """ + go to coordinates x,y in the map frame + 0,0 is your starting position + """ + target = PoseStamped( + ts=time.time(), + frame_id="map", + position=Vector3(x, y, 0.0), + orientation=Quaternion(0.0, 0.0, 0.0, 0.0), + ) + + pos = self.tf.get("base_link", "map").translation + + yield f"moving from {pos.x:.2f}, {pos.y:.2f} to {x:.2f}, {y:.2f}, please wait..." + + self.navigate_to(target) + + yield "arrived to {x:.2f}, {y:.2f}" + + @rpc + def navigate_to(self, pose: PoseStamped, timeout: float = 60.0) -> bool: + """ + Navigate to a target pose by publishing to ROS topics. + + Args: + pose: Target pose to navigate to + timeout: Maximum time to wait for goal (seconds) + + Returns: + True if navigation was successful + """ + logger.info( + f"Navigating to goal: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f} @ {pose.frame_id})" + ) + + self._goal_reach = None + self._set_autonomy_mode() + + # Enable soft stop (0 = enable) + soft_stop_msg = ROSInt8() # type: ignore[no-untyped-call] + soft_stop_msg.data = 0 + self.soft_stop_pub.publish(soft_stop_msg) + + ros_pose = pose.to_ros_msg() + self.goal_pose_pub.publish(ros_pose) + + # Wait for goal to be reached + start_time = time.time() + while time.time() - start_time < timeout: + if self._goal_reach is not None: + soft_stop_msg.data = 2 + self.soft_stop_pub.publish(soft_stop_msg) + return self._goal_reach + time.sleep(0.1) + + self.stop_navigation() + logger.warning(f"Navigation timed out after {timeout} seconds") + return False + + @rpc + def stop_navigation(self) -> bool: + """ + Stop current navigation by publishing to ROS topics. + + Returns: + True if stop command was sent successfully + """ + logger.info("Stopping navigation") + + cancel_msg = ROSBool() # type: ignore[no-untyped-call] + cancel_msg.data = True + self.cancel_goal_pub.publish(cancel_msg) + + soft_stop_msg = ROSInt8() # type: ignore[no-untyped-call] + soft_stop_msg.data = 2 + self.soft_stop_pub.publish(soft_stop_msg) + + with self._state_lock: + self._navigation_state = NavigationState.IDLE + self._current_goal = None + self._goal_reached = False + + return True + + @rpc + def set_goal(self, goal: PoseStamped) -> bool: + """Set a new navigation goal (non-blocking).""" + with self._state_lock: + self._current_goal = goal + self._goal_reached = False + self._navigation_state = NavigationState.FOLLOWING_PATH + + # Start navigation in a separate thread to make it non-blocking + if self._navigation_thread and self._navigation_thread.is_alive(): + logger.warning("Previous navigation still running, cancelling") + self.stop_navigation() + self._navigation_thread.join(timeout=1.0) + + self._navigation_thread = threading.Thread( + target=self._navigate_to_goal_async, + args=(goal,), + daemon=True, + name="ROSNavNavigationThread", + ) + self._navigation_thread.start() + + return True + + def _navigate_to_goal_async(self, goal: PoseStamped) -> None: + """Internal method to handle navigation in a separate thread.""" + try: + result = self.navigate_to(goal, timeout=60.0) + with self._state_lock: + self._goal_reached = result + self._navigation_state = NavigationState.IDLE + except Exception as e: + logger.error(f"Navigation failed: {e}") + with self._state_lock: + self._goal_reached = False + self._navigation_state = NavigationState.IDLE + + @rpc + def get_state(self) -> NavigationState: + """Get the current state of the navigator.""" + with self._state_lock: + return self._navigation_state + + @rpc + def is_goal_reached(self) -> bool: + """Check if the current goal has been reached.""" + with self._state_lock: + return self._goal_reached + + @rpc + def cancel_goal(self) -> bool: + """Cancel the current navigation goal.""" + + with self._state_lock: + had_goal = self._current_goal is not None + + if had_goal: + self.stop_navigation() + + return had_goal + + @rpc + def stop(self) -> None: + """Stop the navigation module and clean up resources.""" + self.stop_navigation() + try: + self._running = False + + self._local_pointcloud_subject.on_completed() + self._global_pointcloud_subject.on_completed() + + if self._spin_thread and self._spin_thread.is_alive(): + self._spin_thread.join(timeout=1.0) + + if hasattr(self, "_node") and self._node: + self._node.destroy_node() # type: ignore[no-untyped-call] + + except Exception as e: + logger.error(f"Error during shutdown: {e}") + finally: + super().stop() + + +ros_nav = ROSNav.blueprint + + +def deploy(dimos: DimosCluster): # type: ignore[no-untyped-def] + nav = dimos.deploy(ROSNav) # type: ignore[attr-defined] + + nav.pointcloud.transport = pSHMTransport("/lidar") + nav.global_pointcloud.transport = pSHMTransport("/map") + nav.goal_req.transport = LCMTransport("/goal_req", PoseStamped) + nav.goal_active.transport = LCMTransport("/goal_active", PoseStamped) + nav.path_active.transport = LCMTransport("/path_active", Path) + nav.cmd_vel.transport = LCMTransport("/cmd_vel", Twist) + + nav.start() + return nav + + +__all__ = ["ROSNav", "deploy", "ros_nav"] diff --git a/dimos/navigation/rosnav/__init__.py b/dimos/navigation/rosnav/__init__.py deleted file mode 100644 index 2ed1f51d04..0000000000 --- a/dimos/navigation/rosnav/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from dimos.navigation.rosnav.nav_bot import NavBot, ROSNavigationModule -from dimos.navigation.rosnav.rosnav import ROSNav diff --git a/dimos/navigation/rosnav/nav_bot.py b/dimos/navigation/rosnav/nav_bot.py deleted file mode 100644 index 0e3fc08cc8..0000000000 --- a/dimos/navigation/rosnav/nav_bot.py +++ /dev/null @@ -1,422 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -NavBot class for navigation-related functionality. -Encapsulates ROS bridge and topic remapping for Unitree robots. -""" - -import logging -import threading -import time - -# ROS2 message imports -from geometry_msgs.msg import ( - PointStamped as ROSPointStamped, - PoseStamped as ROSPoseStamped, - TwistStamped as ROSTwistStamped, -) -from nav_msgs.msg import Odometry as ROSOdometry, Path as ROSPath -import rclpy -from rclpy.node import Node -from sensor_msgs.msg import Joy as ROSJoy, PointCloud2 as ROSPointCloud2 -from std_msgs.msg import Bool as ROSBool, Int8 as ROSInt8 -from tf2_msgs.msg import TFMessage as ROSTFMessage - -from dimos import core -from dimos.core import In, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 -from dimos.msgs.nav_msgs import Odometry, Path -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.msgs.std_msgs import Bool -from dimos.msgs.tf2_msgs.TFMessage import TFMessage -from dimos.navigation.rosnav import ROSNav -from dimos.protocol import pubsub -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import euler_to_quaternion - -logger = setup_logger("dimos.robot.unitree_webrtc.nav_bot", level=logging.INFO) - - -class ROSNavigationModule(ROSNav): - """ - Handles navigation control and odometry remapping. - """ - - goal_req: In[PoseStamped] = None - cancel_goal: In[Bool] = None - - pointcloud: Out[PointCloud2] = None - global_pointcloud: Out[PointCloud2] = None - - goal_active: Out[PoseStamped] = None - path_active: Out[Path] = None - goal_reached: Out[Bool] = None - odom: Out[Odometry] = None - cmd_vel: Out[Twist] = None - odom_pose: Out[PoseStamped] = None - - def __init__(self, sensor_to_base_link_transform=None, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - if not rclpy.ok(): - rclpy.init() - self._node = Node("navigation_module") - - self.goal_reach = None - self.sensor_to_base_link_transform = sensor_to_base_link_transform or [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ] - self.spin_thread = None - - # ROS2 Publishers - self.goal_pose_pub = self._node.create_publisher(ROSPoseStamped, "/goal_pose", 10) - self.cancel_goal_pub = self._node.create_publisher(ROSBool, "/cancel_goal", 10) - self.soft_stop_pub = self._node.create_publisher(ROSInt8, "/soft_stop", 10) - self.joy_pub = self._node.create_publisher(ROSJoy, "/joy", 10) - - # ROS2 Subscribers - self.goal_reached_sub = self._node.create_subscription( - ROSBool, "/goal_reached", self._on_ros_goal_reached, 10 - ) - self.odom_sub = self._node.create_subscription( - ROSOdometry, "/state_estimation", self._on_ros_odom, 10 - ) - self.cmd_vel_sub = self._node.create_subscription( - ROSTwistStamped, "/cmd_vel", self._on_ros_cmd_vel, 10 - ) - self.goal_waypoint_sub = self._node.create_subscription( - ROSPointStamped, "/way_point", self._on_ros_goal_waypoint, 10 - ) - self.registered_scan_sub = self._node.create_subscription( - ROSPointCloud2, "/registered_scan", self._on_ros_registered_scan, 10 - ) - self.global_pointcloud_sub = self._node.create_subscription( - ROSPointCloud2, "/terrain_map_ext", self._on_ros_global_pointcloud, 10 - ) - self.path_sub = self._node.create_subscription(ROSPath, "/path", self._on_ros_path, 10) - self.tf_sub = self._node.create_subscription(ROSTFMessage, "/tf", self._on_ros_tf, 10) - - logger.info("NavigationModule initialized with ROS2 node") - - @rpc - def start(self) -> None: - self._running = True - self.spin_thread = threading.Thread(target=self._spin_node, daemon=True) - self.spin_thread.start() - - self.goal_req.subscribe(self._on_goal_pose) - self.cancel_goal.subscribe(self._on_cancel_goal) - - logger.info("NavigationModule started with ROS2 spinning") - - def _spin_node(self) -> None: - while self._running and rclpy.ok(): - try: - rclpy.spin_once(self._node, timeout_sec=0.1) - except Exception as e: - if self._running: - logger.error(f"ROS2 spin error: {e}") - - def _on_ros_goal_reached(self, msg: ROSBool) -> None: - self.goal_reach = msg.data - dimos_bool = Bool(data=msg.data) - self.goal_reached.publish(dimos_bool) - - def _on_ros_goal_waypoint(self, msg: ROSPointStamped) -> None: - dimos_pose = PoseStamped( - ts=time.time(), - frame_id=msg.header.frame_id, - position=Vector3(msg.point.x, msg.point.y, msg.point.z), - orientation=Quaternion(0.0, 0.0, 0.0, 1.0), - ) - self.goal_active.publish(dimos_pose) - - def _on_ros_cmd_vel(self, msg: ROSTwistStamped) -> None: - # Extract the twist from the stamped message - dimos_twist = Twist( - linear=Vector3(msg.twist.linear.x, msg.twist.linear.y, msg.twist.linear.z), - angular=Vector3(msg.twist.angular.x, msg.twist.angular.y, msg.twist.angular.z), - ) - self.cmd_vel.publish(dimos_twist) - - def _on_ros_odom(self, msg: ROSOdometry) -> None: - dimos_odom = Odometry.from_ros_msg(msg) - self.odom.publish(dimos_odom) - - dimos_pose = PoseStamped( - ts=dimos_odom.ts, - frame_id=dimos_odom.frame_id, - position=dimos_odom.pose.pose.position, - orientation=dimos_odom.pose.pose.orientation, - ) - self.odom_pose.publish(dimos_pose) - - def _on_ros_registered_scan(self, msg: ROSPointCloud2) -> None: - dimos_pointcloud = PointCloud2.from_ros_msg(msg) - self.pointcloud.publish(dimos_pointcloud) - - def _on_ros_global_pointcloud(self, msg: ROSPointCloud2) -> None: - dimos_pointcloud = PointCloud2.from_ros_msg(msg) - self.global_pointcloud.publish(dimos_pointcloud) - - def _on_ros_path(self, msg: ROSPath) -> None: - dimos_path = Path.from_ros_msg(msg) - self.path_active.publish(dimos_path) - - def _on_ros_tf(self, msg: ROSTFMessage) -> None: - ros_tf = TFMessage.from_ros_msg(msg) - - translation = Vector3( - self.sensor_to_base_link_transform[0], - self.sensor_to_base_link_transform[1], - self.sensor_to_base_link_transform[2], - ) - euler_angles = Vector3( - self.sensor_to_base_link_transform[3], - self.sensor_to_base_link_transform[4], - self.sensor_to_base_link_transform[5], - ) - rotation = euler_to_quaternion(euler_angles) - - sensor_to_base_link_tf = Transform( - translation=translation, - rotation=rotation, - frame_id="sensor", - child_frame_id="base_link", - ts=time.time(), - ) - - map_to_world_tf = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=euler_to_quaternion(Vector3(0.0, 0.0, 0.0)), - frame_id="map", - child_frame_id="world", - ts=time.time(), - ) - - self.tf.publish(sensor_to_base_link_tf, map_to_world_tf, *ros_tf.transforms) - - def _on_goal_pose(self, msg: PoseStamped) -> None: - self.navigate_to(msg) - - def _on_cancel_goal(self, msg: Bool) -> None: - if msg.data: - self.stop() - - def _set_autonomy_mode(self) -> None: - joy_msg = ROSJoy() - joy_msg.axes = [ - 0.0, # axis 0 - 0.0, # axis 1 - -1.0, # axis 2 - 0.0, # axis 3 - 1.0, # axis 4 - 1.0, # axis 5 - 0.0, # axis 6 - 0.0, # axis 7 - ] - joy_msg.buttons = [ - 0, # button 0 - 0, # button 1 - 0, # button 2 - 0, # button 3 - 0, # button 4 - 0, # button 5 - 0, # button 6 - 1, # button 7 - controls autonomy mode - 0, # button 8 - 0, # button 9 - 0, # button 10 - ] - self.joy_pub.publish(joy_msg) - logger.info("Setting autonomy mode via Joy message") - - @rpc - def navigate_to(self, pose: PoseStamped, timeout: float = 60.0) -> bool: - """ - Navigate to a target pose by publishing to ROS topics. - - Args: - pose: Target pose to navigate to - timeout: Maximum time to wait for goal (seconds) - - Returns: - True if navigation was successful - """ - logger.info( - f"Navigating to goal: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" - ) - - self.goal_reach = None - self._set_autonomy_mode() - - # Enable soft stop (0 = enable) - soft_stop_msg = ROSInt8() - soft_stop_msg.data = 0 - self.soft_stop_pub.publish(soft_stop_msg) - - ros_pose = pose.to_ros_msg() - self.goal_pose_pub.publish(ros_pose) - - # Wait for goal to be reached - start_time = time.time() - while time.time() - start_time < timeout: - if self.goal_reach is not None: - soft_stop_msg.data = 2 - self.soft_stop_pub.publish(soft_stop_msg) - return self.goal_reach - time.sleep(0.1) - - self.stop_navigation() - logger.warning(f"Navigation timed out after {timeout} seconds") - return False - - @rpc - def stop_navigation(self) -> bool: - """ - Stop current navigation by publishing to ROS topics. - - Returns: - True if stop command was sent successfully - """ - logger.info("Stopping navigation") - - cancel_msg = ROSBool() - cancel_msg.data = True - self.cancel_goal_pub.publish(cancel_msg) - - soft_stop_msg = ROSInt8() - soft_stop_msg.data = 2 - self.soft_stop_pub.publish(soft_stop_msg) - - return True - - @rpc - def stop(self) -> None: - try: - self._running = False - if self.spin_thread: - self.spin_thread.join(timeout=1) - self._node.destroy_node() - except Exception as e: - logger.error(f"Error during shutdown: {e}") - - -class NavBot: - """ - NavBot wrapper that deploys NavigationModule with proper DIMOS/ROS2 integration. - """ - - def __init__(self, dimos=None, sensor_to_base_link_transform=None) -> None: - """ - Initialize NavBot. - - Args: - dimos: DIMOS instance (creates new one if None) - sensor_to_base_link_transform: Optional [x, y, z, roll, pitch, yaw] transform - """ - if dimos is None: - self.dimos = core.start(2) - else: - self.dimos = dimos - - self.sensor_to_base_link_transform = sensor_to_base_link_transform or [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ] - self.navigation_module = None - - def start(self) -> None: - logger.info("Deploying navigation module...") - self.navigation_module = self.dimos.deploy( - ROSNavigationModule, sensor_to_base_link_transform=self.sensor_to_base_link_transform - ) - - self.navigation_module.goal_req.transport = core.LCMTransport("/goal", PoseStamped) - self.navigation_module.cancel_goal.transport = core.LCMTransport("/cancel_goal", Bool) - - self.navigation_module.pointcloud.transport = core.LCMTransport( - "/pointcloud_map", PointCloud2 - ) - self.navigation_module.global_pointcloud.transport = core.LCMTransport( - "/global_pointcloud", PointCloud2 - ) - self.navigation_module.goal_active.transport = core.LCMTransport( - "/goal_active", PoseStamped - ) - self.navigation_module.path_active.transport = core.LCMTransport("/path_active", Path) - self.navigation_module.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) - self.navigation_module.odom.transport = core.LCMTransport("/odom", Odometry) - self.navigation_module.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - self.navigation_module.odom_pose.transport = core.LCMTransport("/odom_pose", PoseStamped) - - self.navigation_module.start() - - def shutdown(self) -> None: - logger.info("Shutting down NavBot...") - - if self.navigation_module: - self.navigation_module.stop() - - if rclpy.ok(): - rclpy.shutdown() - - logger.info("NavBot shutdown complete") - - -def main() -> None: - pubsub.lcm.autoconf() - nav_bot = NavBot() - nav_bot.start() - - logger.info("\nTesting navigation in 2 seconds...") - time.sleep(2) - - test_pose = PoseStamped( - ts=time.time(), - frame_id="map", - position=Vector3(1.0, 1.0, 0.0), - orientation=Quaternion(0.0, 0.0, 0.0, 0.0), - ) - - logger.info("Sending navigation goal to: (1.0, 1.0, 0.0)") - - if nav_bot.navigation_module: - success = nav_bot.navigation_module.navigate_to(test_pose, timeout=30.0) - if success: - logger.info("āœ“ Navigation goal reached!") - else: - logger.warning("āœ— Navigation failed or timed out") - - try: - logger.info("\nNavBot running. Press Ctrl+C to stop.") - while True: - time.sleep(1) - except KeyboardInterrupt: - logger.info("\nShutting down...") - nav_bot.shutdown() - - -if __name__ == "__main__": - main() diff --git a/dimos/navigation/rosnav/rosnav.py b/dimos/navigation/rosnav/rosnav.py deleted file mode 100644 index 440a0f4269..0000000000 --- a/dimos/navigation/rosnav/rosnav.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core import In, Module, Out -from dimos.msgs.geometry_msgs import PoseStamped, Twist -from dimos.msgs.nav_msgs import Path -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.msgs.std_msgs import Bool - - -class ROSNav(Module): - goal_req: In[PoseStamped] = None # type: ignore - goal_active: Out[PoseStamped] = None # type: ignore - path_active: Out[Path] = None # type: ignore - cancel_goal: In[Bool] = None # type: ignore - cmd_vel: Out[Twist] = None # type: ignore - - # PointcloudPerception attributes - pointcloud: Out[PointCloud2] = None # type: ignore - - # Global3DMapSpec attributes - global_pointcloud: Out[PointCloud2] = None # type: ignore - - def start(self) -> None: - pass - - def stop(self) -> None: - pass - - def navigate_to(self, target: PoseStamped) -> None: - # TODO: Implement navigation logic - pass - - def stop_navigation(self) -> None: - # TODO: Implement stop logic - pass diff --git a/dimos/navigation/visual/query.py b/dimos/navigation/visual/query.py index 45a0ede40d..2e0951951e 100644 --- a/dimos/navigation/visual/query.py +++ b/dimos/navigation/visual/query.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/common/detection2d_tracker.py b/dimos/perception/common/detection2d_tracker.py index 7645acd380..9ff36be8a1 100644 --- a/dimos/perception/common/detection2d_tracker.py +++ b/dimos/perception/common/detection2d_tracker.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import numpy as np -def compute_iou(bbox1, bbox2): +def compute_iou(bbox1, bbox2): # type: ignore[no-untyped-def] """ Compute Intersection over Union (IoU) of two bounding boxes. Each bbox is [x1, y1, x2, y2]. @@ -38,7 +38,7 @@ def compute_iou(bbox1, bbox2): return inter_area / union_area -def get_tracked_results(tracked_targets): +def get_tracked_results(tracked_targets): # type: ignore[no-untyped-def] """ Extract tracked results from a list of target2d objects. @@ -77,7 +77,7 @@ class target2d: detection probabilities, and computed texture values. """ - def __init__( + def __init__( # type: ignore[no-untyped-def] self, initial_mask, initial_bbox, @@ -106,14 +106,14 @@ def __init__( self.score = 1.0 self.track_id = track_id - self.probs_history = deque(maxlen=history_size) - self.texture_history = deque(maxlen=history_size) + self.probs_history = deque(maxlen=history_size) # type: ignore[var-annotated] + self.texture_history = deque(maxlen=history_size) # type: ignore[var-annotated] - self.frame_count = deque(maxlen=history_size) # Total frames this target has been seen. + self.frame_count = deque(maxlen=history_size) # type: ignore[var-annotated] # Total frames this target has been seen. self.missed_frames = 0 # Consecutive frames when no detection was assigned. self.history_size = history_size - def update(self, mask, bbox, track_id, prob: float, name: str, texture_value) -> None: + def update(self, mask, bbox, track_id, prob: float, name: str, texture_value) -> None: # type: ignore[no-untyped-def] """ Update the target with a new detection. """ @@ -135,7 +135,7 @@ def mark_missed(self) -> None: self.missed_frames += 1 self.frame_count.append(0) - def compute_score( + def compute_score( # type: ignore[no-untyped-def] self, frame_shape, min_area_ratio, @@ -249,7 +249,7 @@ class target2dTracker: falls below the stop threshold or if they are missed for too many consecutive frames. """ - def __init__( + def __init__( # type: ignore[no-untyped-def] self, history_size: int = 10, score_threshold_start: float = 0.5, @@ -290,10 +290,10 @@ def __init__( weights = {"prob": 1.0, "temporal": 1.0, "texture": 1.0, "border": 1.0, "size": 1.0} self.weights = weights - self.targets = {} # Dictionary mapping target_id -> target2d instance. + self.targets = {} # type: ignore[var-annotated] # Dictionary mapping target_id -> target2d instance. self.next_target_id = 0 - def update( + def update( # type: ignore[no-untyped-def] self, frame, masks, @@ -339,7 +339,7 @@ def update( if matched_target is None: best_iou = 0 for target in self.targets.values(): - iou = compute_iou(bbox, target.latest_bbox) + iou = compute_iou(bbox, target.latest_bbox) # type: ignore[no-untyped-call] if iou > 0.5 and iou > best_iou: best_iou = iou matched_target = target diff --git a/dimos/perception/common/export_tensorrt.py b/dimos/perception/common/export_tensorrt.py index 9d73b4ae3f..ca671e36f2 100644 --- a/dimos/perception/common/export_tensorrt.py +++ b/dimos/perception/common/export_tensorrt.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ import argparse -from ultralytics import YOLO, FastSAM +from ultralytics import YOLO, FastSAM # type: ignore[attr-defined, import-not-found] -def parse_args(): +def parse_args(): # type: ignore[no-untyped-def] parser = argparse.ArgumentParser(description="Export YOLO/FastSAM models to different formats") parser.add_argument("--model_path", type=str, required=True, help="Path to the model weights") parser.add_argument( @@ -41,12 +41,12 @@ def parse_args(): def main() -> None: - args = parse_args() + args = parse_args() # type: ignore[no-untyped-call] half = args.precision == "fp16" int8 = args.precision == "int8" # Load the appropriate model if args.model_type == "yolo": - model = YOLO(args.model_path) + model: YOLO | FastSAM = YOLO(args.model_path) else: model = FastSAM(args.model_path) diff --git a/dimos/perception/common/ibvs.py b/dimos/perception/common/ibvs.py index 2978aff84f..e24819f432 100644 --- a/dimos/perception/common/ibvs.py +++ b/dimos/perception/common/ibvs.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ class PersonDistanceEstimator: - def __init__(self, K, camera_pitch, camera_height) -> None: + def __init__(self, K, camera_pitch, camera_height) -> None: # type: ignore[no-untyped-def] """ Initialize the distance estimator using ground plane constraint. @@ -49,7 +49,7 @@ def __init__(self, K, camera_pitch, camera_height) -> None: self.fx = K[0, 0] self.cx = K[0, 2] - def estimate_distance_angle(self, bbox: tuple, robot_pitch: float | None = None): + def estimate_distance_angle(self, bbox: tuple, robot_pitch: float | None = None): # type: ignore[no-untyped-def, type-arg] """ Estimate distance and angle to person using ground plane constraint. @@ -123,7 +123,7 @@ class ObjectDistanceEstimator: camera's intrinsic parameters to estimate the distance to a detected object. """ - def __init__(self, K, camera_pitch, camera_height) -> None: + def __init__(self, K, camera_pitch, camera_height) -> None: # type: ignore[no-untyped-def] """ Initialize the distance estimator using ground plane constraint. @@ -158,7 +158,7 @@ def __init__(self, K, camera_pitch, camera_height) -> None: self.cx = K[0, 2] self.estimated_object_size = None - def estimate_object_size(self, bbox: tuple, distance: float): + def estimate_object_size(self, bbox: tuple, distance: float): # type: ignore[no-untyped-def, type-arg] """ Estimate the physical size of an object based on its bbox and known distance. @@ -188,9 +188,9 @@ def set_estimated_object_size(self, size: float) -> None: Args: size: Estimated physical size of the object (in meters) """ - self.estimated_object_size = size + self.estimated_object_size = size # type: ignore[assignment] - def estimate_distance_angle(self, bbox: tuple): + def estimate_distance_angle(self, bbox: tuple): # type: ignore[no-untyped-def, type-arg] """ Estimate distance and angle to object using size-based estimation. diff --git a/dimos/perception/common/utils.py b/dimos/perception/common/utils.py index 2676206bd7..1144234d71 100644 --- a/dimos/perception/common/utils.py +++ b/dimos/perception/common/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ import cv2 from dimos_lcm.sensor_msgs import CameraInfo -from dimos_lcm.vision_msgs import BoundingBox2D, Detection2D, Detection3D +from dimos_lcm.vision_msgs import ( + BoundingBox2D, + Detection2D, + Detection3D, +) import numpy as np import torch -import yaml +import yaml # type: ignore[import-untyped] from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 from dimos.msgs.sensor_msgs import Image @@ -28,30 +32,30 @@ from dimos.types.vector import Vector from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.common.utils") +logger = setup_logger() # Optional CuPy support try: # pragma: no cover - optional dependency - import cupy as cp # type: ignore + import cupy as cp # type: ignore[import-not-found] _HAS_CUDA = True except Exception: # pragma: no cover - optional dependency - cp = None # type: ignore + cp = None _HAS_CUDA = False -def _is_cu_array(x) -> bool: - return _HAS_CUDA and cp is not None and isinstance(x, cp.ndarray) # type: ignore +def _is_cu_array(x) -> bool: # type: ignore[no-untyped-def] + return _HAS_CUDA and cp is not None and isinstance(x, cp.ndarray) -def _to_numpy(x): - return cp.asnumpy(x) if _is_cu_array(x) else x # type: ignore +def _to_numpy(x): # type: ignore[no-untyped-def] + return cp.asnumpy(x) if _is_cu_array(x) else x -def _to_cupy(x): - if _HAS_CUDA and cp is not None and isinstance(x, np.ndarray): # type: ignore +def _to_cupy(x): # type: ignore[no-untyped-def] + if _HAS_CUDA and cp is not None and isinstance(x, np.ndarray): try: - return cp.asarray(x) # type: ignore + return cp.asarray(x) except Exception: return x return x @@ -114,7 +118,7 @@ def load_camera_info(yaml_path: str, frame_id: str = "camera_link") -> CameraInf ) -def load_camera_info_opencv(yaml_path: str) -> tuple[np.ndarray, np.ndarray]: +def load_camera_info_opencv(yaml_path: str) -> tuple[np.ndarray, np.ndarray]: # type: ignore[type-arg] """ Load ROS-style camera_info YAML file and convert to OpenCV camera matrix and distortion coefficients. @@ -143,27 +147,27 @@ def load_camera_info_opencv(yaml_path: str) -> tuple[np.ndarray, np.ndarray]: return K, dist -def rectify_image_cpu(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: +def rectify_image_cpu(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: # type: ignore[type-arg] """CPU rectification using OpenCV. Preserves backend by caller. Returns an Image with numpy or cupy data depending on caller choice. """ - src = _to_numpy(image.data) + src = _to_numpy(image.data) # type: ignore[no-untyped-call] rect = cv2.undistort(src, camera_matrix, dist_coeffs) # Caller decides whether to convert back to GPU. return Image(data=rect, format=image.format, frame_id=image.frame_id, ts=image.ts) -def rectify_image_cuda(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: +def rectify_image_cuda(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: # type: ignore[type-arg] """GPU rectification using CuPy bilinear sampling. Generates an undistorted output grid and samples from the distorted source. Falls back to CPU if CUDA not available. """ - if not _HAS_CUDA or cp is None or not image.is_cuda: # type: ignore + if not _HAS_CUDA or cp is None or not image.is_cuda: return rectify_image_cpu(image, camera_matrix, dist_coeffs) - xp = cp # type: ignore + xp = cp # Source (distorted) image on device src = image.data @@ -205,7 +209,7 @@ def rectify_image_cuda(image: Image, camera_matrix: np.ndarray, dist_coeffs: np. vs = fy * yd + cy # Bilinear sample from src at (vs, us) - def _bilinear_sample_cuda(img, x_src, y_src): + def _bilinear_sample_cuda(img, x_src, y_src): # type: ignore[no-untyped-def] h, w = int(img.shape[0]), int(img.shape[1]) # Base integer corners (not clamped) x0i = xp.floor(x_src).astype(xp.int32) @@ -268,11 +272,11 @@ def _bilinear_sample_cuda(img, x_src, y_src): out = out.astype(img.dtype, copy=False) return out - rect = _bilinear_sample_cuda(src, us, vs) + rect = _bilinear_sample_cuda(src, us, vs) # type: ignore[no-untyped-call] return Image(data=rect, format=image.format, frame_id=image.frame_id, ts=image.ts) -def rectify_image(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: +def rectify_image(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: # type: ignore[type-arg] """ Rectify (undistort) an image using camera calibration parameters. @@ -292,7 +296,7 @@ def rectify_image(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarr def project_3d_points_to_2d_cuda( points_3d: "cp.ndarray", camera_intrinsics: Union[list[float], "cp.ndarray"] ) -> "cp.ndarray": - xp = cp # type: ignore + xp = cp pts = points_3d.astype(xp.float64, copy=False) mask = pts[:, 2] > 0 if not bool(xp.any(mask)): @@ -301,7 +305,7 @@ def project_3d_points_to_2d_cuda( if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: fx, fy, cx, cy = [xp.asarray(v, dtype=xp.float64) for v in camera_intrinsics] else: - K = camera_intrinsics.astype(xp.float64, copy=False) + K = camera_intrinsics.astype(xp.float64, copy=False) # type: ignore[union-attr] fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] u = (valid[:, 0] * fx / valid[:, 2]) + cx v = (valid[:, 1] * fy / valid[:, 2]) + cy @@ -309,8 +313,9 @@ def project_3d_points_to_2d_cuda( def project_3d_points_to_2d_cpu( - points_3d: np.ndarray, camera_intrinsics: list[float] | np.ndarray -) -> np.ndarray: + points_3d: np.ndarray, # type: ignore[type-arg] + camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] +) -> np.ndarray: # type: ignore[type-arg] pts = np.asarray(points_3d, dtype=np.float64) valid_mask = pts[:, 2] > 0 if not np.any(valid_mask): @@ -327,9 +332,9 @@ def project_3d_points_to_2d_cpu( def project_3d_points_to_2d( - points_3d: Union[np.ndarray, "cp.ndarray"], - camera_intrinsics: Union[list[float], np.ndarray, "cp.ndarray"], -) -> Union[np.ndarray, "cp.ndarray"]: + points_3d: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] + camera_intrinsics: Union[list[float], np.ndarray, "cp.ndarray"], # type: ignore[type-arg] +) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] """ Project 3D points to 2D image coordinates using camera intrinsics. @@ -349,10 +354,10 @@ def project_3d_points_to_2d( # Filter out points with zero or negative depth if _is_cu_array(points_3d) or _is_cu_array(camera_intrinsics): - xp = cp # type: ignore + xp = cp pts = points_3d if _is_cu_array(points_3d) else xp.asarray(points_3d) K = camera_intrinsics if _is_cu_array(camera_intrinsics) else camera_intrinsics - return project_3d_points_to_2d_cuda(pts, K) # type: ignore[arg-type] + return project_3d_points_to_2d_cuda(pts, K) return project_3d_points_to_2d_cpu(np.asarray(points_3d), np.asarray(camera_intrinsics)) @@ -361,7 +366,7 @@ def project_2d_points_to_3d_cuda( depth_values: "cp.ndarray", camera_intrinsics: Union[list[float], "cp.ndarray"], ) -> "cp.ndarray": - xp = cp # type: ignore + xp = cp pts = points_2d.astype(xp.float64, copy=False) depths = depth_values.astype(xp.float64, copy=False) valid = depths > 0 @@ -372,7 +377,7 @@ def project_2d_points_to_3d_cuda( if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: fx, fy, cx, cy = [xp.asarray(v, dtype=xp.float64) for v in camera_intrinsics] else: - K = camera_intrinsics.astype(xp.float64, copy=False) + K = camera_intrinsics.astype(xp.float64, copy=False) # type: ignore[union-attr] fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] X = (uv[:, 0] - cx) * Z / fx Y = (uv[:, 1] - cy) * Z / fy @@ -380,10 +385,10 @@ def project_2d_points_to_3d_cuda( def project_2d_points_to_3d_cpu( - points_2d: np.ndarray, - depth_values: np.ndarray, - camera_intrinsics: list[float] | np.ndarray, -) -> np.ndarray: + points_2d: np.ndarray, # type: ignore[type-arg] + depth_values: np.ndarray, # type: ignore[type-arg] + camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] +) -> np.ndarray: # type: ignore[type-arg] pts = np.asarray(points_2d, dtype=np.float64) depths = np.asarray(depth_values, dtype=np.float64) valid_mask = depths > 0 @@ -406,10 +411,10 @@ def project_2d_points_to_3d_cpu( def project_2d_points_to_3d( - points_2d: Union[np.ndarray, "cp.ndarray"], - depth_values: Union[np.ndarray, "cp.ndarray"], - camera_intrinsics: Union[list[float], np.ndarray, "cp.ndarray"], -) -> Union[np.ndarray, "cp.ndarray"]: + points_2d: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] + depth_values: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] + camera_intrinsics: Union[list[float], np.ndarray, "cp.ndarray"], # type: ignore[type-arg] +) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] """ Project 2D image points to 3D coordinates using depth values and camera intrinsics. @@ -430,19 +435,21 @@ def project_2d_points_to_3d( # Ensure depth_values is a numpy array if _is_cu_array(points_2d) or _is_cu_array(depth_values) or _is_cu_array(camera_intrinsics): - xp = cp # type: ignore + xp = cp pts = points_2d if _is_cu_array(points_2d) else xp.asarray(points_2d) depths = depth_values if _is_cu_array(depth_values) else xp.asarray(depth_values) K = camera_intrinsics if _is_cu_array(camera_intrinsics) else camera_intrinsics - return project_2d_points_to_3d_cuda(pts, depths, K) # type: ignore[arg-type] + return project_2d_points_to_3d_cuda(pts, depths, K) return project_2d_points_to_3d_cpu( np.asarray(points_2d), np.asarray(depth_values), np.asarray(camera_intrinsics) ) def colorize_depth( - depth_img: Union[np.ndarray, "cp.ndarray"], max_depth: float = 5.0, overlay_stats: bool = True -) -> Union[np.ndarray, "cp.ndarray"] | None: + depth_img: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] + max_depth: float = 5.0, + overlay_stats: bool = True, +) -> Union[np.ndarray, "cp.ndarray"] | None: # type: ignore[type-arg] """ Normalize and colorize depth image using COLORMAP_JET with optional statistics overlay. @@ -458,7 +465,7 @@ def colorize_depth( return None was_cu = _is_cu_array(depth_img) - xp = cp if was_cu else np # type: ignore + xp = cp if was_cu else np depth = depth_img if was_cu else np.asarray(depth_img) valid_mask = xp.isfinite(depth) & (depth > 0) @@ -467,26 +474,26 @@ def colorize_depth( depth_norm = xp.where(valid_mask, xp.clip(depth / max_depth, 0, 1), depth_norm) # Use CPU for colormap/text; convert back to GPU if needed - depth_norm_np = _to_numpy(depth_norm) + depth_norm_np = _to_numpy(depth_norm) # type: ignore[no-untyped-call] depth_colored = cv2.applyColorMap((depth_norm_np * 255).astype(np.uint8), cv2.COLORMAP_JET) depth_rgb_np = cv2.cvtColor(depth_colored, cv2.COLOR_BGR2RGB) depth_rgb_np = (depth_rgb_np * 0.6).astype(np.uint8) - if overlay_stats and (np.any(_to_numpy(valid_mask))): - valid_depths = _to_numpy(depth)[_to_numpy(valid_mask)] + if overlay_stats and (np.any(_to_numpy(valid_mask))): # type: ignore[no-untyped-call] + valid_depths = _to_numpy(depth)[_to_numpy(valid_mask)] # type: ignore[no-untyped-call] min_depth = float(np.min(valid_depths)) max_depth_actual = float(np.max(valid_depths)) h, w = depth_rgb_np.shape[:2] center_y, center_x = h // 2, w // 2 - center_region = _to_numpy(depth)[ - max(0, center_y - 2) : min(h, center_y + 3), max(0, center_x - 2) : min(w, center_x + 3) - ] + center_region = _to_numpy( # type: ignore[no-untyped-call] + depth + )[max(0, center_y - 2) : min(h, center_y + 3), max(0, center_x - 2) : min(w, center_x + 3)] center_mask = np.isfinite(center_region) & (center_region > 0) if center_mask.any(): center_depth = float(np.median(center_region[center_mask])) else: - depth_np = _to_numpy(depth) - vm_np = _to_numpy(valid_mask) + depth_np = _to_numpy(depth) # type: ignore[no-untyped-call] + vm_np = _to_numpy(valid_mask) # type: ignore[no-untyped-call] center_depth = float(depth_np[center_y, center_x]) if vm_np[center_y, center_x] else 0.0 font = cv2.FONT_HERSHEY_SIMPLEX @@ -576,11 +583,11 @@ def colorize_depth( line_type, ) - return _to_cupy(depth_rgb_np) if was_cu else depth_rgb_np + return _to_cupy(depth_rgb_np) if was_cu else depth_rgb_np # type: ignore[no-untyped-call] def draw_bounding_box( - image: Union[np.ndarray, "cp.ndarray"], + image: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] bbox: list[float], color: tuple[int, int, int] = (0, 255, 0), thickness: int = 2, @@ -588,7 +595,7 @@ def draw_bounding_box( confidence: float | None = None, object_id: int | None = None, font_scale: float = 0.6, -) -> Union[np.ndarray, "cp.ndarray"]: +) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] """ Draw a bounding box with optional label on an image. @@ -606,7 +613,7 @@ def draw_bounding_box( Image with bounding box drawn """ was_cu = _is_cu_array(image) - img_np = _to_numpy(image) + img_np = _to_numpy(image) # type: ignore[no-untyped-call] x1, y1, x2, y2 = map(int, bbox) cv2.rectangle(img_np, (x1, y1), (x2, y2), color, thickness) @@ -643,17 +650,17 @@ def draw_bounding_box( 1, ) - return _to_cupy(img_np) if was_cu else img_np + return _to_cupy(img_np) if was_cu else img_np # type: ignore[no-untyped-call] def draw_segmentation_mask( - image: Union[np.ndarray, "cp.ndarray"], - mask: Union[np.ndarray, "cp.ndarray"], + image: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] + mask: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] color: tuple[int, int, int] = (0, 200, 200), alpha: float = 0.5, draw_contours: bool = True, contour_thickness: int = 2, -) -> Union[np.ndarray, "cp.ndarray"]: +) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] """ Draw segmentation mask overlay on an image. @@ -672,8 +679,8 @@ def draw_segmentation_mask( return image was_cu = _is_cu_array(image) - img_np = _to_numpy(image) - mask_np = _to_numpy(mask) + img_np = _to_numpy(image) # type: ignore[no-untyped-call] + mask_np = _to_numpy(mask) # type: ignore[no-untyped-call] try: mask_np = mask_np.astype(np.uint8) @@ -689,17 +696,17 @@ def draw_segmentation_mask( except Exception as e: logger.warning(f"Error drawing segmentation mask: {e}") - return _to_cupy(img_np) if was_cu else img_np + return _to_cupy(img_np) if was_cu else img_np # type: ignore[no-untyped-call] def draw_object_detection_visualization( - image: Union[np.ndarray, "cp.ndarray"], + image: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] objects: list[ObjectData], draw_masks: bool = False, bbox_color: tuple[int, int, int] = (0, 255, 0), mask_color: tuple[int, int, int] = (0, 200, 200), font_scale: float = 0.6, -) -> Union[np.ndarray, "cp.ndarray"]: +) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] """ Create object detection visualization with bounding boxes and optional masks. @@ -715,7 +722,7 @@ def draw_object_detection_visualization( Image with detection visualization """ was_cu = _is_cu_array(image) - viz_image = _to_numpy(image).copy() + viz_image = _to_numpy(image).copy() # type: ignore[no-untyped-call] for obj in objects: try: @@ -732,7 +739,7 @@ def draw_object_detection_visualization( if "color" in obj and obj["color"] is not None: obj_color = obj["color"] if isinstance(obj_color, np.ndarray): - color = tuple(int(c) for c in obj_color) + color = tuple(int(c) for c in obj_color) # type: ignore[assignment] elif isinstance(obj_color, list | tuple): color = tuple(int(c) for c in obj_color[:3]) @@ -749,7 +756,7 @@ def draw_object_detection_visualization( except Exception as e: logger.warning(f"Error drawing object visualization: {e}") - return _to_cupy(viz_image) if was_cu else viz_image + return _to_cupy(viz_image) if was_cu else viz_image # type: ignore[no-untyped-call] def detection_results_to_object_data( @@ -758,7 +765,7 @@ def detection_results_to_object_data( class_ids: list[int], confidences: list[float], names: list[str], - masks: list[np.ndarray] | None = None, + masks: list[np.ndarray] | None = None, # type: ignore[type-arg] source: str = "detection", ) -> list[ObjectData]: """ @@ -795,14 +802,14 @@ def detection_results_to_object_data( "class_id": class_ids[i] if i < len(class_ids) else 0, "label": names[i] if i < len(names) else f"{source}_object", "movement_tolerance": 1.0, # Default to freely movable - "segmentation_mask": masks[i].cpu().numpy() + "segmentation_mask": masks[i].cpu().numpy() # type: ignore[attr-defined, typeddict-item] if masks and i < len(masks) and isinstance(masks[i], torch.Tensor) else masks[i] if masks and i < len(masks) else None, # Initialize 3D properties (will be populated by point cloud processing) - "position": Vector(0, 0, 0), - "rotation": Vector(0, 0, 0), + "position": Vector(0, 0, 0), # type: ignore[arg-type] + "rotation": Vector(0, 0, 0), # type: ignore[arg-type] "size": { "width": 0.0, "height": 0.0, @@ -835,7 +842,7 @@ def combine_object_data( # Check mask overlap mask2 = obj2.get("segmentation_mask") - m2 = _to_numpy(mask2) if mask2 is not None else None + m2 = _to_numpy(mask2) if mask2 is not None else None # type: ignore[no-untyped-call] if m2 is None or np.sum(m2 > 0) == 0: combined.append(obj_copy) continue @@ -848,7 +855,7 @@ def combine_object_data( if mask1 is None: continue - m1 = _to_numpy(mask1) + m1 = _to_numpy(mask1) # type: ignore[no-untyped-call] intersection = np.sum((m1 > 0) & (m2 > 0)) if intersection / mask2_area >= overlap_threshold: is_duplicate = True @@ -925,7 +932,7 @@ def find_clicked_detection( return None -def extract_pose_from_detection3d(detection3d: Detection3D): +def extract_pose_from_detection3d(detection3d: Detection3D): # type: ignore[no-untyped-def] """Extract PoseStamped from Detection3D message. Args: diff --git a/dimos/perception/detection/conftest.py b/dimos/perception/detection/conftest.py index dcc20e5b25..1c9c8ca05c 100644 --- a/dimos/perception/detection/conftest.py +++ b/dimos/perception/detection/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ ImageDetections3DPC, ) from dimos.protocol.tf import TF -from dimos.robot.unitree_webrtc.modular.connection_module import ConnectionModule +from dimos.robot.unitree.connection import go2 from dimos.robot.unitree_webrtc.type.lidar import LidarMessage from dimos.robot.unitree_webrtc.type.odometry import Odometry from dimos.utils.data import get_data @@ -101,19 +101,15 @@ def moment_provider(**kwargs) -> Moment: if odom_frame is None: raise ValueError("No odom frame found") - transforms = ConnectionModule._odom_to_tf(odom_frame) + transforms = go2.GO2Connection._odom_to_tf(odom_frame) tf.receive_transform(*transforms) - camera_info_out = ConnectionModule._camera_info() - # ConnectionModule._camera_info() returns Out[CameraInfo], extract the value - from typing import cast - camera_info = cast("CameraInfo", camera_info_out) return { "odom_frame": odom_frame, "lidar_frame": lidar_frame, "image_frame": image_frame, - "camera_info": camera_info, + "camera_info": go2._camera_info_static(), "transforms": transforms, "tf": tf, } @@ -265,11 +261,8 @@ def object_db_module(get_moment): from dimos.perception.detection.detectors import Yolo2DDetector module2d = Detection2DModule(detector=lambda: Yolo2DDetector(device="cpu")) - module3d = Detection3DModule(camera_info=ConnectionModule._camera_info()) - moduleDB = ObjectDBModule( - camera_info=ConnectionModule._camera_info(), - goto=lambda obj_id: None, # No-op for testing - ) + module3d = Detection3DModule(camera_info=go2._camera_info_static()) + moduleDB = ObjectDBModule(camera_info=go2._camera_info_static()) # Process 5 frames to build up object history for i in range(5): diff --git a/dimos/perception/detection/detectors/config/custom_tracker.yaml b/dimos/perception/detection/detectors/config/custom_tracker.yaml index 4386473086..7a6748ebf6 100644 --- a/dimos/perception/detection/detectors/config/custom_tracker.yaml +++ b/dimos/perception/detection/detectors/config/custom_tracker.yaml @@ -18,4 +18,4 @@ gmc_method: sparseOptFlow # method of global motion compensation # ReID model related thresh (not supported yet) proximity_thresh: 0.6 appearance_thresh: 0.35 -with_reid: False \ No newline at end of file +with_reid: False diff --git a/dimos/perception/detection/detectors/conftest.py b/dimos/perception/detection/detectors/conftest.py index 7caca818c9..9cb600aeff 100644 --- a/dimos/perception/detection/detectors/conftest.py +++ b/dimos/perception/detection/detectors/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/detectors/detic.py b/dimos/perception/detection/detectors/detic.py index 4432988f28..288a3e056d 100644 --- a/dimos/perception/detection/detectors/detic.py +++ b/dimos/perception/detection/detectors/detic.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -36,8 +36,8 @@ PIL.Image.LINEAR = PIL.Image.BILINEAR # type: ignore[attr-defined] # Detectron2 imports -from detectron2.config import get_cfg -from detectron2.data import MetadataCatalog +from detectron2.config import get_cfg # type: ignore[import-not-found] +from detectron2.data import MetadataCatalog # type: ignore[import-not-found] # Simple tracking implementation @@ -48,9 +48,9 @@ def __init__(self, iou_threshold: float = 0.3, max_age: int = 5) -> None: self.iou_threshold = iou_threshold self.max_age = max_age self.next_id = 1 - self.tracks = {} # id -> {bbox, class_id, age, mask, etc} + self.tracks = {} # type: ignore[var-annotated] # id -> {bbox, class_id, age, mask, etc} - def _calculate_iou(self, bbox1, bbox2): + def _calculate_iou(self, bbox1, bbox2): # type: ignore[no-untyped-def] """Calculate IoU between two bboxes in format [x1,y1,x2,y2]""" x1 = max(bbox1[0], bbox2[0]) y1 = max(bbox1[1], bbox2[1]) @@ -67,7 +67,7 @@ def _calculate_iou(self, bbox1, bbox2): return intersection / union if union > 0 else 0 - def update(self, detections, masks): + def update(self, detections, masks): # type: ignore[no-untyped-def] """Update tracker with new detections Args: @@ -113,7 +113,7 @@ def update(self, detections, masks): if det[5] != track["class_id"]: continue - iou = self._calculate_iou(track["bbox"], det[:4]) + iou = self._calculate_iou(track["bbox"], det[:4]) # type: ignore[no-untyped-call] if iou > best_iou: best_iou = iou best_idx = i @@ -161,7 +161,7 @@ def update(self, detections, masks): class Detic2DDetector(Detector): - def __init__( + def __init__( # type: ignore[no-untyped-def] self, model_path=None, device: str = "cuda", vocabulary=None, threshold: float = 0.5 ) -> None: """ @@ -179,10 +179,12 @@ def __init__( # Set up Detic paths - already added to sys.path at module level # Import Detic modules - from centernet.config import add_centernet_config - from detic.config import add_detic_config - from detic.modeling.text.text_encoder import build_text_encoder - from detic.modeling.utils import reset_cls_test + from centernet.config import add_centernet_config # type: ignore[import-not-found] + from detic.config import add_detic_config # type: ignore[import-not-found] + from detic.modeling.text.text_encoder import ( # type: ignore[import-not-found] + build_text_encoder, + ) + from detic.modeling.utils import reset_cls_test # type: ignore[import-not-found] # Keep reference to these functions for later use self.reset_cls_test = reset_cls_test @@ -249,12 +251,12 @@ def __init__( # Setup with initial vocabulary vocabulary = vocabulary or "lvis" - self.setup_vocabulary(vocabulary) + self.setup_vocabulary(vocabulary) # type: ignore[no-untyped-call] # Initialize our simple tracker self.tracker = SimpleTracker(iou_threshold=0.5, max_age=5) - def setup_vocabulary(self, vocabulary): + def setup_vocabulary(self, vocabulary): # type: ignore[no-untyped-def] """ Setup the model's vocabulary. @@ -264,7 +266,7 @@ def setup_vocabulary(self, vocabulary): """ if self.predictor is None: # Initialize the model - from detectron2.engine import DefaultPredictor + from detectron2.engine import DefaultPredictor # type: ignore[import-not-found] self.predictor = DefaultPredictor(self.cfg) @@ -285,7 +287,7 @@ def setup_vocabulary(self, vocabulary): except: # Default to LVIS if there's an issue print(f"Error loading vocabulary from {vocabulary}, using LVIS") - return self.setup_vocabulary("lvis") + return self.setup_vocabulary("lvis") # type: ignore[no-untyped-call] else: # Assume it's a list of class names class_names = vocabulary @@ -300,10 +302,10 @@ def setup_vocabulary(self, vocabulary): num_classes = len(class_names) # Reset model with new vocabulary - self.reset_cls_test(self.predictor.model, classifier, num_classes) + self.reset_cls_test(self.predictor.model, classifier, num_classes) # type: ignore[attr-defined] return self.class_names - def _get_clip_embeddings(self, vocabulary, prompt: str = "a "): + def _get_clip_embeddings(self, vocabulary, prompt: str = "a "): # type: ignore[no-untyped-def] """ Generate CLIP embeddings for a vocabulary list. @@ -320,7 +322,7 @@ def _get_clip_embeddings(self, vocabulary, prompt: str = "a "): emb = text_encoder(texts).detach().permute(1, 0).contiguous().cpu() return emb - def process_image(self, image: Image): + def process_image(self, image: Image): # type: ignore[no-untyped-def] """ Process an image and return detection results. @@ -337,7 +339,7 @@ def process_image(self, image: Image): - masks: list of segmentation masks (numpy arrays) """ # Run inference with Detic - outputs = self.predictor(image.to_opencv()) + outputs = self.predictor(image.to_opencv()) # type: ignore[misc] instances = outputs["instances"].to("cpu") # Extract bounding boxes, classes, scores, and masks @@ -371,7 +373,7 @@ def process_image(self, image: Image): return [], [], [], [], [] # , [] # Update tracker with detections and correctly aligned masks - track_results = self.tracker.update(detections, filtered_masks) + track_results = self.tracker.update(detections, filtered_masks) # type: ignore[no-untyped-call] # Process tracking results track_ids = [] @@ -398,7 +400,7 @@ def process_image(self, image: Image): # tracked_masks, ) - def visualize_results( + def visualize_results( # type: ignore[no-untyped-def] self, image, bboxes, track_ids, class_ids, confidences, names: Sequence[str] ): """ diff --git a/dimos/perception/detection/detectors/person/test_person_detectors.py b/dimos/perception/detection/detectors/person/test_person_detectors.py index d912bec3a0..2ed7cdc7dc 100644 --- a/dimos/perception/detection/detectors/person/test_person_detectors.py +++ b/dimos/perception/detection/detectors/person/test_person_detectors.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/detectors/person/yolo.py b/dimos/perception/detection/detectors/person/yolo.py index 6421ab7d1d..519f45f2f6 100644 --- a/dimos/perception/detection/detectors/person/yolo.py +++ b/dimos/perception/detection/detectors/person/yolo.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ultralytics import YOLO +from ultralytics import YOLO # type: ignore[attr-defined, import-not-found] from dimos.msgs.sensor_msgs import Image from dimos.perception.detection.detectors.types import Detector @@ -21,7 +21,7 @@ from dimos.utils.gpu_utils import is_cuda_available from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.detection.yolo.person") +logger = setup_logger() class YoloPersonDetector(Detector): @@ -39,7 +39,7 @@ def __init__( self.device = device return - if is_cuda_available(): + if is_cuda_available(): # type: ignore[no-untyped-call] self.device = "cuda" logger.info("Using CUDA for YOLO person detector") else: diff --git a/dimos/perception/detection/detectors/test_bbox_detectors.py b/dimos/perception/detection/detectors/test_bbox_detectors.py index a86690279f..bd9c1358b5 100644 --- a/dimos/perception/detection/detectors/test_bbox_detectors.py +++ b/dimos/perception/detection/detectors/test_bbox_detectors.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/detectors/types.py b/dimos/perception/detection/detectors/types.py index 1a3b0b5471..e85c5ae18e 100644 --- a/dimos/perception/detection/detectors/types.py +++ b/dimos/perception/detection/detectors/types.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/detectors/yolo.py b/dimos/perception/detection/detectors/yolo.py index 64e56ad456..c9a65a120e 100644 --- a/dimos/perception/detection/detectors/yolo.py +++ b/dimos/perception/detection/detectors/yolo.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ultralytics import YOLO +from ultralytics import YOLO # type: ignore[attr-defined, import-not-found] from dimos.msgs.sensor_msgs import Image from dimos.perception.detection.detectors.types import Detector @@ -21,7 +21,7 @@ from dimos.utils.gpu_utils import is_cuda_available from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.detection.yolo_2d_det") +logger = setup_logger() class Yolo2DDetector(Detector): @@ -40,7 +40,7 @@ def __init__( self.device = device return - if is_cuda_available(): + if is_cuda_available(): # type: ignore[no-untyped-call] self.device = "cuda" logger.debug("Using CUDA for YOLO 2d detector") else: diff --git a/dimos/perception/detection/module2D.py b/dimos/perception/detection/module2D.py index 4bc99bab28..cfca3b2192 100644 --- a/dimos/perception/detection/module2D.py +++ b/dimos/perception/detection/module2D.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,22 +18,20 @@ from dimos_lcm.foxglove_msgs.ImageAnnotations import ( ImageAnnotations, ) -from dimos_lcm.sensor_msgs import CameraInfo from reactivex import operators as ops from reactivex.observable import Observable from reactivex.subject import Subject -from dimos.core import In, Module, Out, rpc +from dimos import spec +from dimos.core import DimosCluster, In, Module, Out, rpc from dimos.core.module import ModuleConfig from dimos.msgs.geometry_msgs import Transform, Vector3 -from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs import CameraInfo, Image from dimos.msgs.sensor_msgs.Image import sharpness_barrier from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.detectors import Detector -from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector -from dimos.perception.detection.type import ( - ImageDetections2D, -) +from dimos.perception.detection.detectors import Detector # type: ignore[attr-defined] +from dimos.perception.detection.detectors.yolo import Yolo2DDetector +from dimos.perception.detection.type import Filter2D, ImageDetections2D from dimos.utils.decorators.decorators import simple_mcache from dimos.utils.reactive import backpressure @@ -41,8 +39,16 @@ @dataclass class Config(ModuleConfig): max_freq: float = 10 - detector: Callable[[Any], Detector] | None = YoloPersonDetector - camera_info: CameraInfo = CameraInfo() + detector: Callable[[Any], Detector] | None = Yolo2DDetector + publish_detection_images: bool = True + camera_info: CameraInfo = None # type: ignore[assignment] + filter: list[Filter2D] | Filter2D | None = None + + def __post_init__(self) -> None: + if self.filter is None: + self.filter = [] + elif not isinstance(self.filter, list): + self.filter = [self.filter] class Detection2DModule(Module): @@ -50,65 +56,40 @@ class Detection2DModule(Module): config: Config detector: Detector - image: In[Image] = None # type: ignore + color_image: In[Image] - detections: Out[Detection2DArray] = None # type: ignore - annotations: Out[ImageAnnotations] = None # type: ignore + detections: Out[Detection2DArray] + annotations: Out[ImageAnnotations] - detected_image_0: Out[Image] = None # type: ignore - detected_image_1: Out[Image] = None # type: ignore - detected_image_2: Out[Image] = None # type: ignore + detected_image_0: Out[Image] + detected_image_1: Out[Image] + detected_image_2: Out[Image] cnt: int = 0 - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) - self.config: Config = Config(**kwargs) - self.detector = self.config.detector() - self.vlm_detections_subject = Subject() + self.detector = self.config.detector() # type: ignore[call-arg, misc] + self.vlm_detections_subject = Subject() # type: ignore[var-annotated] self.previous_detection_count = 0 def process_image_frame(self, image: Image) -> ImageDetections2D: - return self.detector.process_image(image) + imageDetections = self.detector.process_image(image) + if not self.config.filter: + return imageDetections + return imageDetections.filter(*self.config.filter) # type: ignore[misc, return-value] @simple_mcache def sharp_image_stream(self) -> Observable[Image]: return backpressure( - self.image.pure_observable().pipe( + self.color_image.pure_observable().pipe( sharpness_barrier(self.config.max_freq), ) ) @simple_mcache def detection_stream_2d(self) -> Observable[ImageDetections2D]: - return backpressure(self.image.observable().pipe(ops.map(self.process_image_frame))) - - def pixel_to_3d( - self, - pixel: tuple[int, int], - camera_info: CameraInfo, - assumed_depth: float = 1.0, - ) -> Vector3: - """Unproject 2D pixel coordinates to 3D position in camera optical frame. - - Args: - camera_info: Camera calibration information - assumed_depth: Assumed depth in meters (default 1.0m from camera) - - Returns: - Vector3 position in camera optical frame coordinates - """ - # Extract camera intrinsics - fx, fy = camera_info.K[0], camera_info.K[4] - cx, cy = camera_info.K[2], camera_info.K[5] - - # Unproject pixel to normalized camera coordinates - x_norm = (pixel[0] - cx) / fx - y_norm = (pixel[1] - cy) / fy - - # Create 3D point at assumed depth in camera optical frame - # Camera optical frame: X right, Y down, Z forward - return Vector3(x_norm * assumed_depth, y_norm * assumed_depth, assumed_depth) + return backpressure(self.sharp_image_stream().pipe(ops.map(self.process_image_frame))) def track(self, detections: ImageDetections2D) -> None: sensor_frame = self.tf.get("sensor", "camera_optical", detections.image.ts, 5.0) @@ -130,8 +111,10 @@ def track(self, detections: ImageDetections2D) -> None: if index < current_count: # Active detection - compute real position detection = detections.detections[index] - position_3d = self.pixel_to_3d( - detection.center_bbox, self.config.camera_info, assumed_depth=1.0 + position_3d = self.pixel_to_3d( # type: ignore[attr-defined] + detection.center_bbox, + self.config.camera_info, + assumed_depth=1.0, ) else: # No detection at this index - publish zero transform @@ -151,7 +134,7 @@ def track(self, detections: ImageDetections2D) -> None: @rpc def start(self) -> None: - self.detection_stream_2d().subscribe(self.track) + # self.detection_stream_2d().subscribe(self.track) self.detection_stream_2d().subscribe( lambda det: self.detections.publish(det.to_ros_detection2d_array()) @@ -166,7 +149,31 @@ def publish_cropped_images(detections: ImageDetections2D) -> None: image_topic = getattr(self, "detected_image_" + str(index)) image_topic.publish(detection.cropped_image()) - self.detection_stream_2d().subscribe(publish_cropped_images) + if self.config.publish_detection_images: + self.detection_stream_2d().subscribe(publish_cropped_images) @rpc - def stop(self) -> None: ... + def stop(self) -> None: + return super().stop() # type: ignore[no-any-return] + + +def deploy( # type: ignore[no-untyped-def] + dimos: DimosCluster, + camera: spec.Camera, + prefix: str = "/detector2d", + **kwargs, +) -> Detection2DModule: + from dimos.core import LCMTransport + + detector = Detection2DModule(**kwargs) + detector.color_image.connect(camera.color_image) + + detector.annotations.transport = LCMTransport(f"{prefix}/annotations", ImageAnnotations) + detector.detections.transport = LCMTransport(f"{prefix}/detections", Detection2DArray) + + detector.detected_image_0.transport = LCMTransport(f"{prefix}/image/0", Image) + detector.detected_image_1.transport = LCMTransport(f"{prefix}/image/1", Image) + detector.detected_image_2.transport = LCMTransport(f"{prefix}/image/2", Image) + + detector.start() + return detector diff --git a/dimos/perception/detection/module3D.py b/dimos/perception/detection/module3D.py index 9016ae6006..037376f995 100644 --- a/dimos/perception/detection/module3D.py +++ b/dimos/perception/detection/module3D.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,17 +13,20 @@ # limitations under the License. -from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations -from lcm_msgs.foxglove_msgs import SceneUpdate +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + ImageAnnotations, +) +from lcm_msgs.foxglove_msgs import SceneUpdate # type: ignore[import-not-found] from reactivex import operators as ops from reactivex.observable import Observable -from dimos.agents2 import skill -from dimos.core import In, Out, rpc -from dimos.msgs.geometry_msgs import Transform +from dimos import spec +from dimos.agents import skill # type: ignore[attr-defined] +from dimos.core import DimosCluster, In, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 from dimos.msgs.sensor_msgs import Image, PointCloud2 from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.module2D import Config as Module2DConfig, Detection2DModule +from dimos.perception.detection.module2D import Detection2DModule from dimos.perception.detection.type import ( ImageDetections2D, ImageDetections3DPC, @@ -33,27 +36,24 @@ from dimos.utils.reactive import backpressure -class Config(Module2DConfig): ... - - class Detection3DModule(Detection2DModule): - image: In[Image] = None # type: ignore - pointcloud: In[PointCloud2] = None # type: ignore + color_image: In[Image] + pointcloud: In[PointCloud2] - detections: Out[Detection2DArray] = None # type: ignore - annotations: Out[ImageAnnotations] = None # type: ignore - scene_update: Out[SceneUpdate] = None # type: ignore + detections: Out[Detection2DArray] + annotations: Out[ImageAnnotations] + scene_update: Out[SceneUpdate] # just for visualization, # emits latest pointclouds of detected objects in a frame - detected_pointcloud_0: Out[PointCloud2] = None # type: ignore - detected_pointcloud_1: Out[PointCloud2] = None # type: ignore - detected_pointcloud_2: Out[PointCloud2] = None # type: ignore + detected_pointcloud_0: Out[PointCloud2] + detected_pointcloud_1: Out[PointCloud2] + detected_pointcloud_2: Out[PointCloud2] # just for visualization, emits latest top 3 detections in a frame - detected_image_0: Out[Image] = None # type: ignore - detected_image_1: Out[Image] = None # type: ignore - detected_image_2: Out[Image] = None # type: ignore + detected_image_0: Out[Image] + detected_image_1: Out[Image] + detected_image_2: Out[Image] detection_3d_stream: Observable[ImageDetections3DPC] | None = None @@ -79,8 +79,46 @@ def process_frame( return ImageDetections3DPC(detections.image, detection3d_list) - @skill # type: ignore[arg-type] - def ask_vlm(self, question: str) -> str | ImageDetections3DPC: + def pixel_to_3d( + self, + pixel: tuple[int, int], + assumed_depth: float = 1.0, + ) -> Vector3: + """Unproject 2D pixel coordinates to 3D position in camera optical frame. + + Args: + camera_info: Camera calibration information + assumed_depth: Assumed depth in meters (default 1.0m from camera) + + Returns: + Vector3 position in camera optical frame coordinates + """ + # Extract camera intrinsics + fx, fy = self.config.camera_info.K[0], self.config.camera_info.K[4] + cx, cy = self.config.camera_info.K[2], self.config.camera_info.K[5] + + # Unproject pixel to normalized camera coordinates + x_norm = (pixel[0] - cx) / fx + y_norm = (pixel[1] - cy) / fy + + # Create 3D point at assumed depth in camera optical frame + # Camera optical frame: X right, Y down, Z forward + return Vector3(x_norm * assumed_depth, y_norm * assumed_depth, assumed_depth) + + @skill() + def ask_vlm(self, question: str) -> str: + """asks a visual model about the view of the robot, for example + is the bannana in the trunk? + """ + from dimos.models.vl.qwen import QwenVlModel + + model = QwenVlModel() + image = self.color_image.get_next() + return model.query(image, question) + + # @skill + @rpc + def nav_vlm(self, question: str) -> str: """ query visual model about the view in front of the camera you can ask to mark objects like: @@ -92,28 +130,50 @@ def ask_vlm(self, question: str) -> str | ImageDetections3DPC: from dimos.models.vl.qwen import QwenVlModel model = QwenVlModel() - result = model.query(self.image.get_next(), question) + image = self.color_image.get_next() + result = model.query_detections(image, question) + + print("VLM result:", result, "for", image, "and question", question) if isinstance(result, str) or not result or not len(result): - return "No detections" + return None # type: ignore[return-value] detections: ImageDetections2D = result + + print(detections) + if not len(detections): + print("No 2d detections") + return None # type: ignore[return-value] + pc = self.pointcloud.get_next() transform = self.tf.get("camera_optical", pc.frame_id, detections.image.ts, 5.0) - return self.process_frame(detections, pc, transform) + + detections3d = self.process_frame(detections, pc, transform) + + if len(detections3d): + return detections3d[0].pose # type: ignore[no-any-return] + print("No 3d detections, projecting 2d") + + center = detections[0].get_bbox_center() + return PoseStamped( + ts=detections.image.ts, + frame_id="world", + position=self.pixel_to_3d(center, assumed_depth=1.5), + orientation=Quaternion(0.0, 0.0, 0.0, 1.0), + ) @rpc def start(self) -> None: super().start() - def detection2d_to_3d(args): + def detection2d_to_3d(args): # type: ignore[no-untyped-def] detections, pc = args transform = self.tf.get("camera_optical", pc.frame_id, detections.image.ts, 5.0) return self.process_frame(detections, pc, transform) self.detection_stream_3d = align_timestamped( backpressure(self.detection_stream_2d()), - self.pointcloud.observable(), + self.pointcloud.observable(), # type: ignore[no-untyped-call] match_tolerance=0.25, buffer_size=20.0, ).pipe(ops.map(detection2d_to_3d)) @@ -131,3 +191,41 @@ def _publish_detections(self, detections: ImageDetections3DPC) -> None: for index, detection in enumerate(detections[:3]): pointcloud_topic = getattr(self, "detected_pointcloud_" + str(index)) pointcloud_topic.publish(detection.pointcloud) + + self.scene_update.publish(detections.to_foxglove_scene_update()) + + +def deploy( # type: ignore[no-untyped-def] + dimos: DimosCluster, + lidar: spec.Pointcloud, + camera: spec.Camera, + prefix: str = "/detector3d", + **kwargs, +) -> Detection3DModule: + from dimos.core import LCMTransport + + detector = dimos.deploy(Detection3DModule, camera_info=camera.hardware_camera_info, **kwargs) # type: ignore[attr-defined] + + detector.image.connect(camera.color_image) + detector.pointcloud.connect(lidar.pointcloud) + + detector.annotations.transport = LCMTransport(f"{prefix}/annotations", ImageAnnotations) + detector.detections.transport = LCMTransport(f"{prefix}/detections", Detection2DArray) + detector.scene_update.transport = LCMTransport(f"{prefix}/scene_update", SceneUpdate) + + detector.detected_image_0.transport = LCMTransport(f"{prefix}/image/0", Image) + detector.detected_image_1.transport = LCMTransport(f"{prefix}/image/1", Image) + detector.detected_image_2.transport = LCMTransport(f"{prefix}/image/2", Image) + + detector.detected_pointcloud_0.transport = LCMTransport(f"{prefix}/pointcloud/0", PointCloud2) + detector.detected_pointcloud_1.transport = LCMTransport(f"{prefix}/pointcloud/1", PointCloud2) + detector.detected_pointcloud_2.transport = LCMTransport(f"{prefix}/pointcloud/2", PointCloud2) + + detector.start() + + return detector # type: ignore[no-any-return] + + +detection3d_module = Detection3DModule.blueprint + +__all__ = ["Detection3DModule", "deploy", "detection3d_module"] diff --git a/dimos/perception/detection/moduleDB.py b/dimos/perception/detection/moduleDB.py index d9cc5434ab..c37dff8dea 100644 --- a/dimos/perception/detection/moduleDB.py +++ b/dimos/perception/detection/moduleDB.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,8 +17,10 @@ import time from typing import Any -from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations -from lcm_msgs.foxglove_msgs import SceneUpdate +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + ImageAnnotations, +) +from lcm_msgs.foxglove_msgs import SceneUpdate # type: ignore[import-not-found] from reactivex.observable import Observable from dimos.core import In, Out, rpc @@ -32,9 +34,9 @@ # Represents an object in space, as collection of 3d detections over time class Object3D(Detection3DPC): - best_detection: Detection3DPC | None = None # type: ignore - center: Vector3 | None = None # type: ignore - track_id: str | None = None # type: ignore + best_detection: Detection3DPC | None = None + center: Vector3 | None = None # type: ignore[assignment] + track_id: str | None = None # type: ignore[assignment] detections: int = 0 def to_repr_dict(self) -> dict[str, Any]: @@ -50,7 +52,7 @@ def to_repr_dict(self) -> dict[str, Any]: "center": center_str, } - def __init__( + def __init__( # type: ignore[no-untyped-def] self, track_id: str, detection: Detection3DPC | None = None, *args, **kwargs ) -> None: if detection is None: @@ -97,7 +99,7 @@ def get_image(self) -> Image | None: def scene_entity_label(self) -> str: return f"{self.name} ({self.detections})" - def agent_encode(self): + def agent_encode(self): # type: ignore[no-untyped-def] return { "id": self.track_id, "name": self.name, @@ -140,29 +142,47 @@ class ObjectDBModule(Detection3DModule, TableStr): goto: Callable[[PoseStamped], Any] | None = None - image: In[Image] = None # type: ignore - pointcloud: In[PointCloud2] = None # type: ignore + color_image: In[Image] + pointcloud: In[PointCloud2] - detections: Out[Detection2DArray] = None # type: ignore - annotations: Out[ImageAnnotations] = None # type: ignore + detections: Out[Detection2DArray] + annotations: Out[ImageAnnotations] - detected_pointcloud_0: Out[PointCloud2] = None # type: ignore - detected_pointcloud_1: Out[PointCloud2] = None # type: ignore - detected_pointcloud_2: Out[PointCloud2] = None # type: ignore + detected_pointcloud_0: Out[PointCloud2] + detected_pointcloud_1: Out[PointCloud2] + detected_pointcloud_2: Out[PointCloud2] - detected_image_0: Out[Image] = None # type: ignore - detected_image_1: Out[Image] = None # type: ignore - detected_image_2: Out[Image] = None # type: ignore + detected_image_0: Out[Image] + detected_image_1: Out[Image] + detected_image_2: Out[Image] - scene_update: Out[SceneUpdate] = None # type: ignore + scene_update: Out[SceneUpdate] - target: Out[PoseStamped] = None # type: ignore + target: Out[PoseStamped] remembered_locations: dict[str, PoseStamped] - def __init__(self, goto: Callable[[PoseStamped], Any], *args, **kwargs) -> None: + @rpc + def start(self) -> None: + Detection3DModule.start(self) + + def update_objects(imageDetections: ImageDetections3DPC) -> None: + for detection in imageDetections.detections: + self.add_detection(detection) + + def scene_thread() -> None: + while True: + scene_update = self.to_foxglove_scene_update() + self.scene_update.publish(scene_update) + time.sleep(1.0) + + threading.Thread(target=scene_thread, daemon=True).start() + + self.detection_stream_3d.subscribe(update_objects) + + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) - self.goto = goto + self.goto = None self.objects = {} self.remembered_locations = {} @@ -183,7 +203,7 @@ def add_detections(self, detections: list[Detection3DPC]) -> list[Object3D]: detection for detection in map(self.add_detection, detections) if detection is not None ] - def add_detection(self, detection: Detection3DPC): + def add_detection(self, detection: Detection3DPC): # type: ignore[no-untyped-def] """Add detection to existing object or create new one.""" closest = self.closest_object(detection) if closest and closest.bounding_box_intersects(detection): @@ -191,13 +211,13 @@ def add_detection(self, detection: Detection3DPC): else: return self.create_new_object(detection) - def add_to_object(self, closest: Object3D, detection: Detection3DPC): + def add_to_object(self, closest: Object3D, detection: Detection3DPC): # type: ignore[no-untyped-def] new_object = closest + detection if closest.track_id is not None: self.objects[closest.track_id] = new_object return new_object - def create_new_object(self, detection: Detection3DPC): + def create_new_object(self, detection: Detection3DPC): # type: ignore[no-untyped-def] new_object = Object3D(f"obj_{self.cnt}", detection) if new_object.track_id is not None: self.objects[new_object.track_id] = new_object @@ -209,65 +229,51 @@ def agent_encode(self) -> str: for obj in copy(self.objects).values(): # we need at least 3 detectieons to consider it a valid object # for this to be serious we need a ratio of detections within the window of observations - # if len(obj.detections) < 3: - # continue - ret.append(str(obj.agent_encode())) + if len(obj.detections) < 4: # type: ignore[arg-type] + continue + ret.append(str(obj.agent_encode())) # type: ignore[no-untyped-call] if not ret: return "No objects detected yet." return "\n".join(ret) - def vlm_query(self, description: str) -> Object3D | None: # type: ignore[override] - imageDetections2D = super().ask_vlm(description) - print("VLM query found", imageDetections2D, "detections") - time.sleep(3) - - if not imageDetections2D.detections: - return None - - ret = [] - for obj in self.objects.values(): - if obj.ts != imageDetections2D.ts: - print( - "Skipping", - obj.track_id, - "ts", - obj.ts, - "!=", - imageDetections2D.ts, - ) - continue - if obj.class_id != -100: - continue - if obj.name != imageDetections2D.detections[0].name: - print("Skipping", obj.name, "!=", imageDetections2D.detections[0].name) - continue - ret.append(obj) - ret.sort(key=lambda x: x.ts) - - return ret[0] if ret else None + # @rpc + # def vlm_query(self, description: str) -> Object3D | None: + # imageDetections2D = super().ask_vlm(description) + # print("VLM query found", imageDetections2D, "detections") + # time.sleep(3) + + # if not imageDetections2D.detections: + # return None + + # ret = [] + # for obj in self.objects.values(): + # if obj.ts != imageDetections2D.ts: + # print( + # "Skipping", + # obj.track_id, + # "ts", + # obj.ts, + # "!=", + # imageDetections2D.ts, + # ) + # continue + # if obj.class_id != -100: + # continue + # if obj.name != imageDetections2D.detections[0].name: + # print("Skipping", obj.name, "!=", imageDetections2D.detections[0].name) + # continue + # ret.append(obj) + # ret.sort(key=lambda x: x.ts) + + # return ret[0] if ret else None def lookup(self, label: str) -> list[Detection3DPC]: """Look up a detection by label.""" return [] @rpc - def start(self) -> None: - Detection3DModule.start(self) - - def update_objects(imageDetections: ImageDetections3DPC): - for detection in imageDetections.detections: - # print(detection) - return self.add_detection(detection) - - def scene_thread() -> None: - while True: - scene_update = self.to_foxglove_scene_update() - self.scene_update.publish(scene_update) - time.sleep(1.0) - - threading.Thread(target=scene_thread, daemon=True).start() - - self.detection_stream_3d.subscribe(update_objects) + def stop(self): # type: ignore[no-untyped-def] + return super().stop() def goto_object(self, object_id: str) -> Object3D | None: """Go to object by id.""" @@ -286,25 +292,21 @@ def to_foxglove_scene_update(self) -> "SceneUpdate": scene_update.deletions = [] scene_update.entities = [] - for obj in copy(self.objects).values(): - # we need at least 3 detectieons to consider it a valid object - # for this to be serious we need a ratio of detections within the window of observations - # if obj.class_id != -100 and obj.detections < 2: - # continue - - # print( - # f"Object {obj.track_id}: {len(obj.detections)} detections, confidence {obj.confidence}" - # ) - # print(obj.to_pose()) - - scene_update.entities.append( - obj.to_foxglove_scene_entity( - entity_id=f"object_{obj.name}_{obj.track_id}_{obj.detections}" + for obj in self.objects: + try: + scene_update.entities.append( + obj.to_foxglove_scene_entity(entity_id=f"{obj.name}_{obj.track_id}") # type: ignore[attr-defined] ) - ) + except Exception: + pass scene_update.entities_length = len(scene_update.entities) return scene_update def __len__(self) -> int: return len(self.objects.values()) + + +detectionDB_module = ObjectDBModule.blueprint + +__all__ = ["ObjectDBModule", "detectionDB_module"] diff --git a/dimos/perception/detection/person_tracker.py b/dimos/perception/detection/person_tracker.py index 568214d972..6212080858 100644 --- a/dimos/perception/detection/person_tracker.py +++ b/dimos/perception/detection/person_tracker.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,6 +13,8 @@ # limitations under the License. +from typing import Any + from reactivex import operators as ops from reactivex.observable import Observable @@ -26,19 +28,19 @@ class PersonTracker(Module): - detections: In[Detection2DArray] = None # type: ignore - image: In[Image] = None # type: ignore - target: Out[PoseStamped] = None # type: ignore + detections: In[Detection2DArray] + color_image: In[Image] + target: Out[PoseStamped] camera_info: CameraInfo - def __init__(self, cameraInfo: CameraInfo, **kwargs) -> None: + def __init__(self, cameraInfo: CameraInfo, **kwargs: Any) -> None: super().__init__(**kwargs) self.camera_info = cameraInfo def center_to_3d( self, - pixel: tuple[int, int], + pixel: tuple[float, float], camera_info: CameraInfo, assumed_depth: float = 1.0, ) -> Vector3: @@ -74,13 +76,19 @@ def center_to_3d( def detections_stream(self) -> Observable[ImageDetections2D]: return backpressure( align_timestamped( - self.image.pure_observable(), + self.color_image.pure_observable(), self.detections.pure_observable().pipe( ops.filter(lambda d: d.detections_length > 0) # type: ignore[attr-defined] ), match_tolerance=0.0, buffer_size=2.0, - ).pipe(ops.map(lambda pair: ImageDetections2D.from_ros_detection2d_array(*pair))) + ).pipe( + ops.map( + lambda pair: ImageDetections2D.from_ros_detection2d_array( # type: ignore[misc] + *pair + ) + ) + ) ) @rpc @@ -113,3 +121,8 @@ def track(self, detections2D: ImageDetections2D) -> None: pose_in_world = tf_world_to_target.to_pose(ts=detections2D.ts) self.target.publish(pose_in_world) + + +person_tracker_module = PersonTracker.blueprint + +__all__ = ["PersonTracker", "person_tracker_module"] diff --git a/dimos/perception/detection/reid/embedding_id_system.py b/dimos/perception/detection/reid/embedding_id_system.py index c1c406fe56..9b57e1eb6c 100644 --- a/dimos/perception/detection/reid/embedding_id_system.py +++ b/dimos/perception/detection/reid/embedding_id_system.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -58,9 +58,9 @@ def __init__( # Call model factory (class or function) to get model instance self.model = model() - # Call warmup if available - if hasattr(self.model, "warmup"): - self.model.warmup() + # Call start if available (Resource interface) + if hasattr(self.model, "start"): + self.model.start() self.padding = padding self.similarity_threshold = similarity_threshold @@ -70,7 +70,7 @@ def __init__( self.min_embeddings_for_matching = min_embeddings_for_matching # Track embeddings (list of all embeddings as numpy arrays) - self.track_embeddings: dict[int, list[np.ndarray]] = {} + self.track_embeddings: dict[int, list[np.ndarray]] = {} # type: ignore[type-arg] # Negative constraints (track_ids that co-occurred = different objects) self.negative_pairs: dict[int, set[int]] = {} @@ -129,7 +129,9 @@ def update_embedding(self, track_id: int, new_embedding: Embedding) -> None: embeddings.pop(0) # Remove oldest def _compute_group_similarity( - self, query_embeddings: list[np.ndarray], candidate_embeddings: list[np.ndarray] + self, + query_embeddings: list[np.ndarray], # type: ignore[type-arg] + candidate_embeddings: list[np.ndarray], # type: ignore[type-arg] ) -> float: """Compute similarity between two groups of embeddings. diff --git a/dimos/perception/detection/reid/module.py b/dimos/perception/detection/reid/module.py index 3cef9f2ff2..4e239da39a 100644 --- a/dimos/perception/detection/reid/module.py +++ b/dimos/perception/detection/reid/module.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -38,17 +38,17 @@ class Config(ModuleConfig): class ReidModule(Module): default_config = Config - detections: In[Detection2DArray] = None # type: ignore - image: In[Image] = None # type: ignore - annotations: Out[ImageAnnotations] = None # type: ignore + detections: In[Detection2DArray] + image: In[Image] + annotations: Out[ImageAnnotations] - def __init__(self, idsystem: IDSystem | None = None, **kwargs) -> None: + def __init__(self, idsystem: IDSystem | None = None, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) if idsystem is None: try: from dimos.models.embedding import TorchReIDModel - idsystem = EmbeddingIDSystem(model=TorchReIDModel, padding=0) + idsystem = EmbeddingIDSystem(model=TorchReIDModel, padding=0) # type: ignore[arg-type] except Exception as e: raise RuntimeError( "TorchReIDModel not available. Please install with: pip install dimos[torchreid]" diff --git a/dimos/perception/detection/reid/test_embedding_id_system.py b/dimos/perception/detection/reid/test_embedding_id_system.py index 840ecb2fb8..3a0899c848 100644 --- a/dimos/perception/detection/reid/test_embedding_id_system.py +++ b/dimos/perception/detection/reid/test_embedding_id_system.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ def mobileclip_model(): model_path = get_data("models_mobileclip") / "mobileclip2_s0.pt" model = MobileCLIPModel(model_name="MobileCLIP2-S0", model_path=model_path) - model.warmup() + model.start() return model diff --git a/dimos/perception/detection/reid/test_module.py b/dimos/perception/detection/reid/test_module.py index cd580a1111..d962da6b6c 100644 --- a/dimos/perception/detection/reid/test_module.py +++ b/dimos/perception/detection/reid/test_module.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ def test_reid_ingress(imageDetections2d) -> None: # Create TorchReID-based IDSystem for testing reid_model = TorchReIDModel(model_name="osnet_x1_0") - reid_model.warmup() + reid_model.start() idsystem = EmbeddingIDSystem( model=lambda: reid_model, padding=20, diff --git a/dimos/perception/detection/reid/type.py b/dimos/perception/detection/reid/type.py index 0ef2da961c..28ea719f81 100644 --- a/dimos/perception/detection/reid/type.py +++ b/dimos/perception/detection/reid/type.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/test_moduleDB.py b/dimos/perception/detection/test_moduleDB.py index 62c72b7ded..e9815f1f3e 100644 --- a/dimos/perception/detection/test_moduleDB.py +++ b/dimos/perception/detection/test_moduleDB.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,17 +22,16 @@ from dimos.msgs.sensor_msgs import Image, PointCloud2 from dimos.msgs.vision_msgs import Detection2DArray from dimos.perception.detection.moduleDB import ObjectDBModule -from dimos.robot.unitree_webrtc.modular import deploy_connection -from dimos.robot.unitree_webrtc.modular.connection_module import ConnectionModule +from dimos.robot.unitree.connection import go2 @pytest.mark.module def test_moduleDB(dimos_cluster) -> None: - connection = deploy_connection(dimos_cluster) + connection = go2.deploy(dimos_cluster, "fake") moduleDB = dimos_cluster.deploy( ObjectDBModule, - camera_info=ConnectionModule._camera_info(), + camera_info=go2._camera_info_static(), goto=lambda obj_id: print(f"Going to {obj_id}"), ) moduleDB.image.connect(connection.video) @@ -56,6 +55,5 @@ def test_moduleDB(dimos_cluster) -> None: moduleDB.start() time.sleep(4) - print("STARTING QUERY!!") print("VLM RES", moduleDB.navigate_to_object_in_view("white floor")) time.sleep(30) diff --git a/dimos/perception/detection/type/__init__.py b/dimos/perception/detection/type/__init__.py index 04589441ec..d69d00ba97 100644 --- a/dimos/perception/detection/type/__init__.py +++ b/dimos/perception/detection/type/__init__.py @@ -1,7 +1,9 @@ -from dimos.perception.detection.type.detection2d import ( +from dimos.perception.detection.type.detection2d import ( # type: ignore[attr-defined] Detection2D, Detection2DBBox, Detection2DPerson, + Detection2DPoint, + Filter2D, ImageDetections2D, ) from dimos.perception.detection.type.detection3d import ( @@ -23,10 +25,12 @@ "Detection2D", "Detection2DBBox", "Detection2DPerson", + "Detection2DPoint", # 3D Detection types "Detection3D", "Detection3DBBox", "Detection3DPC", + "Filter2D", # Base types "ImageDetections", "ImageDetections2D", diff --git a/dimos/perception/detection/type/detection2d/__init__.py b/dimos/perception/detection/type/detection2d/__init__.py index 1db1a8c384..ad3b7fa62e 100644 --- a/dimos/perception/detection/type/detection2d/__init__.py +++ b/dimos/perception/detection/type/detection2d/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos.perception.detection.type.detection2d.base import Detection2D +from dimos.perception.detection.type.detection2d.base import Detection2D, Filter2D from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox from dimos.perception.detection.type.detection2d.imageDetections2D import ImageDetections2D from dimos.perception.detection.type.detection2d.person import Detection2DPerson +from dimos.perception.detection.type.detection2d.point import Detection2DPoint __all__ = [ "Detection2D", "Detection2DBBox", "Detection2DPerson", + "Detection2DPoint", "ImageDetections2D", ] diff --git a/dimos/perception/detection/type/detection2d/base.py b/dimos/perception/detection/type/detection2d/base.py index 5cba3d673f..ee9374af8c 100644 --- a/dimos/perception/detection/type/detection2d/base.py +++ b/dimos/perception/detection/type/detection2d/base.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,8 +13,8 @@ # limitations under the License. from abc import abstractmethod +from collections.abc import Callable -from dimos_lcm.foxglove_msgs.ImageAnnotations import PointsAnnotation, TextAnnotation from dimos_lcm.vision_msgs import Detection2D as ROSDetection2D from dimos.msgs.foxglove_msgs import ImageAnnotations @@ -36,16 +36,14 @@ def to_image_annotations(self) -> ImageAnnotations: ... @abstractmethod - def to_text_annotation(self) -> list[TextAnnotation]: - """Return text annotations for visualization.""" + def to_ros_detection2d(self) -> ROSDetection2D: + """Convert detection to ROS Detection2D message.""" ... @abstractmethod - def to_points_annotation(self) -> list[PointsAnnotation]: - """Return points/shape annotations for visualization.""" + def is_valid(self) -> bool: + """Check if the detection is valid.""" ... - @abstractmethod - def to_ros_detection2d(self) -> ROSDetection2D: - """Convert detection to ROS Detection2D message.""" - ... + +Filter2D = Callable[[Detection2D], bool] diff --git a/dimos/perception/detection/type/detection2d/bbox.py b/dimos/perception/detection/type/detection2d/bbox.py index 46e8fe2cc7..32109dffd3 100644 --- a/dimos/perception/detection/type/detection2d/bbox.py +++ b/dimos/perception/detection/type/detection2d/bbox.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,8 +18,10 @@ import hashlib from typing import TYPE_CHECKING, Any +from typing_extensions import Self + if TYPE_CHECKING: - from ultralytics.engine.results import Results + from ultralytics.engine.results import Results # type: ignore[import-not-found] from dimos.msgs.sensor_msgs import Image @@ -100,9 +102,9 @@ def to_repr_dict(self) -> dict[str, Any]: def center_to_3d( self, pixel: tuple[int, int], - camera_info: CameraInfo, + camera_info: CameraInfo, # type: ignore[name-defined] assumed_depth: float = 1.0, - ) -> PoseStamped: + ) -> PoseStamped: # type: ignore[name-defined] """Unproject 2D pixel coordinates to 3D position in camera optical frame. Args: @@ -122,7 +124,7 @@ def center_to_3d( # Create 3D point at assumed depth in camera optical frame # Camera optical frame: X right, Y down, Z forward - return Vector3(x_norm * assumed_depth, y_norm * assumed_depth, assumed_depth) + return Vector3(x_norm * assumed_depth, y_norm * assumed_depth, assumed_depth) # type: ignore[name-defined] # return focused image, only on the bbox def cropped_image(self, padding: int = 20) -> Image: @@ -269,7 +271,7 @@ def to_ros_bbox(self) -> BoundingBox2D: size_y=height, ) - def lcm_encode(self): + def lcm_encode(self): # type: ignore[no-untyped-def] return self.to_image_annotations().lcm_encode() def to_text_annotation(self) -> list[TextAnnotation]: @@ -349,7 +351,7 @@ def to_image_annotations(self) -> ImageAnnotations: ) @classmethod - def from_ros_detection2d(cls, ros_det: ROSDetection2D, **kwargs) -> Detection2D: + def from_ros_detection2d(cls, ros_det: ROSDetection2D, **kwargs) -> Self: # type: ignore[no-untyped-def] """Convert from ROS Detection2D message to Detection2D object.""" # Extract bbox from ROS format center_x = ros_det.bbox.center.position.x diff --git a/dimos/perception/detection/type/detection2d/imageDetections2D.py b/dimos/perception/detection/type/detection2d/imageDetections2D.py index 0c505ae2b5..680f9dd117 100644 --- a/dimos/perception/detection/type/detection2d/imageDetections2D.py +++ b/dimos/perception/detection/type/detection2d/imageDetections2D.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generic + +from typing_extensions import TypeVar from dimos.perception.detection.type.detection2d.base import Detection2D from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox @@ -22,29 +24,32 @@ if TYPE_CHECKING: from dimos_lcm.vision_msgs import Detection2DArray - from ultralytics.engine.results import Results + from ultralytics.engine.results import Results # type: ignore[import-not-found] from dimos.msgs.sensor_msgs import Image +# TypeVar with default - Detection2DBBox is the default when no type param given +T2D = TypeVar("T2D", bound=Detection2D, default=Detection2DBBox) + -class ImageDetections2D(ImageDetections[Detection2D]): +class ImageDetections2D(ImageDetections[T2D], Generic[T2D]): @classmethod - def from_ros_detection2d_array( + def from_ros_detection2d_array( # type: ignore[no-untyped-def] cls, image: Image, ros_detections: Detection2DArray, **kwargs - ) -> ImageDetections2D: + ) -> ImageDetections2D[Detection2DBBox]: """Convert from ROS Detection2DArray message to ImageDetections2D object.""" - detections: list[Detection2D] = [] + detections: list[Detection2DBBox] = [] for ros_det in ros_detections.detections: detection = Detection2DBBox.from_ros_detection2d(ros_det, image=image, **kwargs) - if detection.is_valid(): # type: ignore[attr-defined] + if detection.is_valid(): detections.append(detection) - return cls(image=image, detections=detections) + return ImageDetections2D(image=image, detections=detections) @classmethod - def from_ultralytics_result( + def from_ultralytics_result( # type: ignore[no-untyped-def] cls, image: Image, results: list[Results], **kwargs - ) -> ImageDetections2D: + ) -> ImageDetections2D[Detection2DBBox]: """Create ImageDetections2D from ultralytics Results. Dispatches to appropriate Detection2D subclass based on result type: @@ -61,14 +66,14 @@ def from_ultralytics_result( """ from dimos.perception.detection.type.detection2d.person import Detection2DPerson - detections: list[Detection2D] = [] + detections: list[Detection2DBBox] = [] for result in results: if result.boxes is None: continue num_detections = len(result.boxes.xyxy) for i in range(num_detections): - detection: Detection2D + detection: Detection2DBBox if result.keypoints is not None: # Pose detection with keypoints detection = Detection2DPerson.from_ultralytics_result(result, i, image) @@ -78,4 +83,4 @@ def from_ultralytics_result( if detection.is_valid(): detections.append(detection) - return cls(image=image, detections=detections) + return ImageDetections2D(image=image, detections=detections) diff --git a/dimos/perception/detection/type/detection2d/person.py b/dimos/perception/detection/type/detection2d/person.py index 1d84613051..efb12ebdbc 100644 --- a/dimos/perception/detection/type/detection2d/person.py +++ b/dimos/perception/detection/type/detection2d/person.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,10 @@ # Import for type checking only to avoid circular imports from typing import TYPE_CHECKING -from dimos_lcm.foxglove_msgs.ImageAnnotations import PointsAnnotation, TextAnnotation +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + PointsAnnotation, + TextAnnotation, +) from dimos_lcm.foxglove_msgs.Point2 import Point2 import numpy as np @@ -28,7 +31,7 @@ from dimos.utils.decorators.decorators import simple_mcache if TYPE_CHECKING: - from ultralytics.engine.results import Results + from ultralytics.engine.results import Results # type: ignore[import-not-found] @dataclass @@ -36,12 +39,12 @@ class Detection2DPerson(Detection2DBBox): """Represents a detected person with pose keypoints.""" # Pose keypoints - additional fields beyond Detection2DBBox - keypoints: np.ndarray # [17, 2] - x,y coordinates - keypoint_scores: np.ndarray # [17] - confidence scores + keypoints: np.ndarray # type: ignore[type-arg] # [17, 2] - x,y coordinates + keypoint_scores: np.ndarray # type: ignore[type-arg] # [17] - confidence scores # Optional normalized coordinates - bbox_normalized: np.ndarray | None = None # [x1, y1, x2, y2] in 0-1 range - keypoints_normalized: np.ndarray | None = None # [17, 2] in 0-1 range + bbox_normalized: np.ndarray | None = None # type: ignore[type-arg] # [x1, y1, x2, y2] in 0-1 range + keypoints_normalized: np.ndarray | None = None # type: ignore[type-arg] # [17, 2] in 0-1 range # Image dimensions for context image_width: int | None = None @@ -173,7 +176,7 @@ def from_yolo(cls, result: "Results", idx: int, image: Image) -> "Detection2DPer return cls.from_ultralytics_result(result, idx, image) @classmethod - def from_ros_detection2d(cls, *args, **kwargs) -> "Detection2DPerson": + def from_ros_detection2d(cls, *args, **kwargs) -> "Detection2DPerson": # type: ignore[no-untyped-def] """Conversion from ROS Detection2D is not supported for Detection2DPerson. The ROS Detection2D message format does not include keypoint data, @@ -191,7 +194,7 @@ def from_ros_detection2d(cls, *args, **kwargs) -> "Detection2DPerson": "message format that includes pose keypoints." ) - def get_keypoint(self, name: str) -> tuple[np.ndarray, float]: + def get_keypoint(self, name: str) -> tuple[np.ndarray, float]: # type: ignore[type-arg] """Get specific keypoint by name. Returns: Tuple of (xy_coordinates, confidence_score) @@ -202,7 +205,7 @@ def get_keypoint(self, name: str) -> tuple[np.ndarray, float]: idx = self.KEYPOINT_NAMES.index(name) return self.keypoints[idx], self.keypoint_scores[idx] - def get_visible_keypoints(self, threshold: float = 0.5) -> list[tuple[str, np.ndarray, float]]: + def get_visible_keypoints(self, threshold: float = 0.5) -> list[tuple[str, np.ndarray, float]]: # type: ignore[type-arg] """Get all keypoints above confidence threshold. Returns: List of tuples: (keypoint_name, xy_coordinates, confidence) diff --git a/dimos/perception/detection/type/detection2d/point.py b/dimos/perception/detection/type/detection2d/point.py new file mode 100644 index 0000000000..216ec57b82 --- /dev/null +++ b/dimos/perception/detection/type/detection2d/point.py @@ -0,0 +1,184 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + CircleAnnotation, + TextAnnotation, +) +from dimos_lcm.foxglove_msgs.Point2 import Point2 +from dimos_lcm.vision_msgs import ( + BoundingBox2D, + Detection2D as ROSDetection2D, + ObjectHypothesis, + ObjectHypothesisWithPose, + Point2D, + Pose2D, +) + +from dimos.msgs.foxglove_msgs import ImageAnnotations +from dimos.msgs.foxglove_msgs.Color import Color +from dimos.msgs.std_msgs import Header +from dimos.perception.detection.type.detection2d.base import Detection2D +from dimos.types.timestamped import to_ros_stamp + +if TYPE_CHECKING: + from dimos.msgs.sensor_msgs import Image + + +@dataclass +class Detection2DPoint(Detection2D): + """A 2D point detection, visualized as a circle.""" + + x: float + y: float + name: str + ts: float + image: Image + track_id: int = -1 + class_id: int = -1 + confidence: float = 1.0 + + def to_repr_dict(self) -> dict[str, str]: + """Return a dictionary representation for display purposes.""" + return { + "name": self.name, + "track": str(self.track_id), + "conf": f"{self.confidence:.2f}", + "point": f"({self.x:.0f},{self.y:.0f})", + } + + def cropped_image(self, padding: int = 20) -> Image: + """Return a cropped version of the image focused on the point. + + Args: + padding: Pixels to add around the point (default: 20) + + Returns: + Cropped Image containing the area around the point + """ + x, y = int(self.x), int(self.y) + return self.image.crop( + x - padding, + y - padding, + 2 * padding, + 2 * padding, + ) + + @property + def diameter(self) -> float: + return self.image.width / 40 + + def to_circle_annotation(self) -> list[CircleAnnotation]: + """Return circle annotations for visualization.""" + return [ + CircleAnnotation( + timestamp=to_ros_stamp(self.ts), + position=Point2(x=self.x, y=self.y), + diameter=self.diameter, + thickness=1.0, + fill_color=Color.from_string(self.name, alpha=0.3), + outline_color=Color.from_string(self.name, alpha=1.0, brightness=1.25), + ) + ] + + def to_text_annotation(self) -> list[TextAnnotation]: + """Return text annotations for visualization.""" + font_size = self.image.width / 80 + + # Build label text + if self.class_id == -1: + if self.track_id == -1: + label_text = self.name + else: + label_text = f"{self.name}_{self.track_id}" + else: + label_text = f"{self.name}_{self.class_id}_{self.track_id}" + + annotations = [ + TextAnnotation( + timestamp=to_ros_stamp(self.ts), + position=Point2(x=self.x + self.diameter / 2, y=self.y + self.diameter / 2), + text=label_text, + font_size=font_size, + text_color=Color(r=1.0, g=1.0, b=1.0, a=1), + background_color=Color(r=0, g=0, b=0, a=1), + ), + ] + + # Only show confidence if it's not 1.0 + if self.confidence != 1.0: + annotations.append( + TextAnnotation( + timestamp=to_ros_stamp(self.ts), + position=Point2(x=self.x + self.diameter / 2 + 2, y=self.y + font_size + 2), + text=f"{self.confidence:.2f}", + font_size=font_size, + text_color=Color(r=1.0, g=1.0, b=1.0, a=1), + background_color=Color(r=0, g=0, b=0, a=1), + ) + ) + + return annotations + + def to_image_annotations(self) -> ImageAnnotations: + """Convert detection to Foxglove ImageAnnotations for visualization.""" + texts = self.to_text_annotation() + circles = self.to_circle_annotation() + + return ImageAnnotations( + texts=texts, + texts_length=len(texts), + points=[], + points_length=0, + circles=circles, + circles_length=len(circles), + ) + + def to_ros_detection2d(self) -> ROSDetection2D: + """Convert point to ROS Detection2D message (as zero-size bbox at point).""" + return ROSDetection2D( + header=Header(self.ts, "camera_link"), + bbox=BoundingBox2D( + center=Pose2D( + position=Point2D(x=self.x, y=self.y), + theta=0.0, + ), + size_x=0.0, + size_y=0.0, + ), + results=[ + ObjectHypothesisWithPose( + ObjectHypothesis( + class_id=self.class_id, + score=self.confidence, + ) + ) + ], + id=str(self.track_id), + ) + + def is_valid(self) -> bool: + """Check if the point is within image bounds.""" + if self.image.shape: + h, w = self.image.shape[:2] + return bool(0 <= self.x <= w and 0 <= self.y <= h) + return True + + def lcm_encode(self): # type: ignore[no-untyped-def] + return self.to_image_annotations().lcm_encode() diff --git a/dimos/perception/detection/type/detection2d/test_bbox.py b/dimos/perception/detection/type/detection2d/test_bbox.py index a12e4e0d76..5a76b41601 100644 --- a/dimos/perception/detection/type/detection2d/test_bbox.py +++ b/dimos/perception/detection/type/detection2d/test_bbox.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/detection2d/test_imageDetections2D.py b/dimos/perception/detection/type/detection2d/test_imageDetections2D.py index 120072cfb6..83487d2c25 100644 --- a/dimos/perception/detection/type/detection2d/test_imageDetections2D.py +++ b/dimos/perception/detection/type/detection2d/test_imageDetections2D.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/detection2d/test_person.py b/dimos/perception/detection/type/detection2d/test_person.py index 2ff1e81237..06c5883ae2 100644 --- a/dimos/perception/detection/type/detection2d/test_person.py +++ b/dimos/perception/detection/type/detection2d/test_person.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/detection3d/__init__.py b/dimos/perception/detection/type/detection3d/__init__.py index 0e765b175f..53ab73259e 100644 --- a/dimos/perception/detection/type/detection3d/__init__.py +++ b/dimos/perception/detection/type/detection3d/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/detection3d/base.py b/dimos/perception/detection/type/detection3d/base.py index 7988c19a47..d8cc430c44 100644 --- a/dimos/perception/detection/type/detection3d/base.py +++ b/dimos/perception/detection/type/detection3d/base.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/detection3d/bbox.py b/dimos/perception/detection/type/detection3d/bbox.py index 30ca882d16..ac6f82a25e 100644 --- a/dimos/perception/detection/type/detection3d/bbox.py +++ b/dimos/perception/detection/type/detection3d/bbox.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/detection3d/imageDetections3DPC.py b/dimos/perception/detection/type/detection3d/imageDetections3DPC.py index f843fb96fd..0fbb1a7c59 100644 --- a/dimos/perception/detection/type/detection3d/imageDetections3DPC.py +++ b/dimos/perception/detection/type/detection3d/imageDetections3DPC.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ from __future__ import annotations -from lcm_msgs.foxglove_msgs import SceneUpdate +from lcm_msgs.foxglove_msgs import SceneUpdate # type: ignore[import-not-found] from dimos.perception.detection.type.detection3d.pointcloud import Detection3DPC from dimos.perception.detection.type.imageDetections import ImageDetections diff --git a/dimos/perception/detection/type/detection3d/pointcloud.py b/dimos/perception/detection/type/detection3d/pointcloud.py index 56423d2f29..fd924a6564 100644 --- a/dimos/perception/detection/type/detection3d/pointcloud.py +++ b/dimos/perception/detection/type/detection3d/pointcloud.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,9 +18,18 @@ import functools from typing import TYPE_CHECKING, Any -from lcm_msgs.builtin_interfaces import Duration -from lcm_msgs.foxglove_msgs import CubePrimitive, SceneEntity, TextPrimitive -from lcm_msgs.geometry_msgs import Point, Pose, Quaternion, Vector3 as LCMVector3 +from lcm_msgs.builtin_interfaces import Duration # type: ignore[import-not-found] +from lcm_msgs.foxglove_msgs import ( # type: ignore[import-not-found] + CubePrimitive, + SceneEntity, + TextPrimitive, +) +from lcm_msgs.geometry_msgs import ( # type: ignore[import-not-found] + Point, + Pose, + Quaternion, + Vector3 as LCMVector3, +) import numpy as np from dimos.msgs.foxglove_msgs.Color import Color @@ -63,11 +72,11 @@ def pose(self) -> PoseStamped: orientation=(0.0, 0.0, 0.0, 1.0), # Identity quaternion ) - def get_bounding_box(self): + def get_bounding_box(self): # type: ignore[no-untyped-def] """Get axis-aligned bounding box of the detection's pointcloud.""" return self.pointcloud.get_axis_aligned_bounding_box() - def get_oriented_bounding_box(self): + def get_oriented_bounding_box(self): # type: ignore[no-untyped-def] """Get oriented bounding box of the detection's pointcloud.""" return self.pointcloud.get_oriented_bounding_box() @@ -112,7 +121,7 @@ def to_foxglove_scene_entity(self, entity_id: str | None = None) -> SceneEntity: cube = CubePrimitive() # Get the axis-aligned bounding box - aabb = self.get_bounding_box() + aabb = self.get_bounding_box() # type: ignore[no-untyped-call] # Set pose from axis-aligned bounding box cube.pose = Pose() @@ -243,7 +252,7 @@ def from_2d( # type: ignore[override] camera_matrix = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) # Convert pointcloud to numpy array - world_points = world_pointcloud.as_numpy() + world_points, _ = world_pointcloud.as_numpy() # Project points to camera frame points_homogeneous = np.hstack([world_points, np.ones((world_points.shape[0], 1))]) diff --git a/dimos/perception/detection/type/detection3d/pointcloud_filters.py b/dimos/perception/detection/type/detection3d/pointcloud_filters.py index 1c6085b690..984e04bc99 100644 --- a/dimos/perception/detection/type/detection3d/pointcloud_filters.py +++ b/dimos/perception/detection/type/detection3d/pointcloud_filters.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py b/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py index 4ad2660738..cca8b862d4 100644 --- a/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py +++ b/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/detection3d/test_pointcloud.py b/dimos/perception/detection/type/detection3d/test_pointcloud.py index f616fe7f33..ad1c5cdf1b 100644 --- a/dimos/perception/detection/type/detection3d/test_pointcloud.py +++ b/dimos/perception/detection/type/detection3d/test_pointcloud.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,15 +57,12 @@ def test_detection3dpc(detection3dpc) -> None: # def test_point_cloud_properties(detection3dpc): """Test point cloud data and boundaries.""" - pc_points = detection3dpc.pointcloud.points() - assert len(pc_points) > 60 + points, _ = detection3dpc.pointcloud.as_numpy() + assert len(points) > 60 assert detection3dpc.pointcloud.frame_id == "world", ( f"Expected frame_id 'world', got '{detection3dpc.pointcloud.frame_id}'" ) - # Extract xyz coordinates from points - points = np.array([[pt[0], pt[1], pt[2]] for pt in pc_points]) - min_pt = np.min(points, axis=0) max_pt = np.max(points, axis=0) center = np.mean(points, axis=0) diff --git a/dimos/perception/detection/type/imageDetections.py b/dimos/perception/detection/type/imageDetections.py index 1a597595ea..12a1f4efb9 100644 --- a/dimos/perception/detection/type/imageDetections.py +++ b/dimos/perception/detection/type/imageDetections.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ from __future__ import annotations +from functools import reduce +from operator import add from typing import TYPE_CHECKING, Generic, TypeVar from dimos_lcm.vision_msgs import Detection2DArray @@ -23,7 +25,7 @@ from dimos.perception.detection.type.utils import TableStr if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator from dimos.msgs.sensor_msgs import Image from dimos.perception.detection.type.detection2d.base import Detection2D @@ -53,12 +55,28 @@ def __init__(self, image: Image, detections: list[T] | None = None) -> None: def __len__(self) -> int: return len(self.detections) - def __iter__(self) -> Iterator: + def __iter__(self) -> Iterator: # type: ignore[type-arg] return iter(self.detections) - def __getitem__(self, index): + def __getitem__(self, index): # type: ignore[no-untyped-def] return self.detections[index] + def filter(self, *predicates: Callable[[T], bool]) -> ImageDetections[T]: + """Filter detections using one or more predicate functions. + + Multiple predicates are applied in cascade (all must return True). + + Args: + *predicates: Functions that take a detection and return True to keep it + + Returns: + A new ImageDetections instance with filtered detections + """ + filtered_detections = self.detections + for predicate in predicates: + filtered_detections = [det for det in filtered_detections if predicate(det)] + return ImageDetections(self.image, filtered_detections) + def to_ros_detection2d_array(self) -> Detection2DArray: return Detection2DArray( detections_length=len(self.detections), @@ -67,15 +85,8 @@ def to_ros_detection2d_array(self) -> Detection2DArray: ) def to_foxglove_annotations(self) -> ImageAnnotations: - def flatten(xss): - return [x for xs in xss for x in xs] - - texts = flatten(det.to_text_annotation() for det in self.detections) - points = flatten(det.to_points_annotation() for det in self.detections) - - return ImageAnnotations( - texts=texts, - texts_length=len(texts), - points=points, - points_length=len(points), - ) + if not self.detections: + return ImageAnnotations( + texts=[], texts_length=0, points=[], points_length=0, circles=[], circles_length=0 + ) + return reduce(add, (det.to_image_annotations() for det in self.detections)) diff --git a/dimos/perception/detection/type/test_detection3d.py b/dimos/perception/detection/type/test_detection3d.py index 031623afe3..b467df7ffe 100644 --- a/dimos/perception/detection/type/test_detection3d.py +++ b/dimos/perception/detection/type/test_detection3d.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/test_object3d.py b/dimos/perception/detection/type/test_object3d.py index 4acd2f1afa..7057fbb9cb 100644 --- a/dimos/perception/detection/type/test_object3d.py +++ b/dimos/perception/detection/type/test_object3d.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/detection/type/utils.py b/dimos/perception/detection/type/utils.py index 89cf41b404..eb924cbd1a 100644 --- a/dimos/perception/detection/type/utils.py +++ b/dimos/perception/detection/type/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -55,19 +55,19 @@ def __str__(self) -> str: # Create a table for detections table = Table( - title=f"{self.__class__.__name__} [{len(self.detections)} detections @ {to_timestamp(self.image.ts):.3f}]", + title=f"{self.__class__.__name__} [{len(self.detections)} detections @ {to_timestamp(self.image.ts):.3f}]", # type: ignore[attr-defined] show_header=True, show_edge=True, ) # Dynamically build columns based on the first detection's dict keys - if not self.detections: + if not self.detections: # type: ignore[attr-defined] return ( - f" {self.__class__.__name__} [0 detections @ {to_timestamp(self.image.ts):.3f}]" + f" {self.__class__.__name__} [0 detections @ {to_timestamp(self.image.ts):.3f}]" # type: ignore[attr-defined] ) # Cache all repr_dicts to avoid double computation - detection_dicts = [det.to_repr_dict() for det in self] + detection_dicts = [det.to_repr_dict() for det in self] # type: ignore[attr-defined] first_dict = detection_dicts[0] table.add_column("#", style="dim") @@ -89,9 +89,9 @@ def __str__(self) -> str: if float(d[key]) > 0.5 else "red" ) - row.append(Text(f"{d[key]}", style=conf_color)) + row.append(Text(f"{d[key]}", style=conf_color)) # type: ignore[arg-type] elif key == "points" and d.get(key) == "None": - row.append(Text(d.get(key, ""), style="dim")) + row.append(Text(d.get(key, ""), style="dim")) # type: ignore[arg-type] else: row.append(str(d.get(key, ""))) table.add_row(*row) diff --git a/dimos/perception/detection2d/utils.py b/dimos/perception/detection2d/utils.py index c44a013325..a505eef7c8 100644 --- a/dimos/perception/detection2d/utils.py +++ b/dimos/perception/detection2d/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import numpy as np -def filter_detections( +def filter_detections( # type: ignore[no-untyped-def] bboxes, track_ids, class_ids, @@ -93,7 +93,7 @@ def filter_detections( ) -def extract_detection_results(result, class_filter=None, name_filter=None, track_id_filter=None): +def extract_detection_results(result, class_filter=None, name_filter=None, track_id_filter=None): # type: ignore[no-untyped-def] """ Extract and optionally filter detection information from a YOLO result object. @@ -111,11 +111,11 @@ def extract_detection_results(result, class_filter=None, name_filter=None, track - confidences: list of detection confidences - names: list of class names """ - bboxes = [] - track_ids = [] - class_ids = [] - confidences = [] - names = [] + bboxes = [] # type: ignore[var-annotated] + track_ids = [] # type: ignore[var-annotated] + class_ids = [] # type: ignore[var-annotated] + confidences = [] # type: ignore[var-annotated] + names = [] # type: ignore[var-annotated] if result.boxes is None: return bboxes, track_ids, class_ids, confidences, names @@ -155,7 +155,7 @@ def extract_detection_results(result, class_filter=None, name_filter=None, track return bboxes, track_ids, class_ids, confidences, names -def plot_results( +def plot_results( # type: ignore[no-untyped-def] image, bboxes, track_ids, class_ids, confidences, names: Sequence[str], alpha: float = 0.5 ): """ @@ -208,7 +208,7 @@ def plot_results( return vis_img -def calculate_depth_from_bbox(depth_map, bbox): +def calculate_depth_from_bbox(depth_map, bbox): # type: ignore[no-untyped-def] """ Calculate the average depth of an object within a bounding box. Uses the 25th to 75th percentile range to filter outliers. @@ -245,7 +245,7 @@ def calculate_depth_from_bbox(depth_map, bbox): return None -def calculate_distance_angle_from_bbox(bbox, depth: int, camera_intrinsics): +def calculate_distance_angle_from_bbox(bbox, depth: int, camera_intrinsics): # type: ignore[no-untyped-def] """ Calculate distance and angle to object center based on bbox and depth. @@ -280,7 +280,7 @@ def calculate_distance_angle_from_bbox(bbox, depth: int, camera_intrinsics): return distance, angle -def calculate_object_size_from_bbox(bbox, depth: int, camera_intrinsics): +def calculate_object_size_from_bbox(bbox, depth: int, camera_intrinsics): # type: ignore[no-untyped-def] """ Estimate physical width and height of object in meters. diff --git a/dimos/perception/grasp_generation/grasp_generation.py b/dimos/perception/grasp_generation/grasp_generation.py index adca8dd3e0..4f2e4b68a1 100644 --- a/dimos/perception/grasp_generation/grasp_generation.py +++ b/dimos/perception/grasp_generation/grasp_generation.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,13 +19,13 @@ import asyncio import numpy as np -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] from dimos.perception.grasp_generation.utils import parse_grasp_results from dimos.types.manipulation import ObjectData from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.grasp_generation") +logger = setup_logger() class HostedGraspGenerator: @@ -45,7 +45,7 @@ def __init__(self, server_url: str) -> None: def generate_grasps_from_objects( self, objects: list[ObjectData], full_pcd: o3d.geometry.PointCloud - ) -> list[dict]: + ) -> list[dict]: # type: ignore[type-arg] """ Generate grasps from ObjectData objects using grasp generator. @@ -74,8 +74,8 @@ def generate_grasps_from_objects( continue colors = None - if "colors_numpy" in obj and obj["colors_numpy"] is not None: - colors = obj["colors_numpy"] + if "colors_numpy" in obj and obj["colors_numpy"] is not None: # type: ignore[typeddict-item] + colors = obj["colors_numpy"] # type: ignore[typeddict-item] if isinstance(colors, np.ndarray) and colors.size > 0: if ( colors.shape[0] != points.shape[0] @@ -112,8 +112,10 @@ def generate_grasps_from_objects( return [] def _send_grasp_request_sync( - self, points: np.ndarray, colors: np.ndarray | None - ) -> list[dict] | None: + self, + points: np.ndarray, # type: ignore[type-arg] + colors: np.ndarray | None, # type: ignore[type-arg] + ) -> list[dict] | None: # type: ignore[type-arg] """Send synchronous grasp request to grasp server.""" try: @@ -148,8 +150,10 @@ def _send_grasp_request_sync( return None async def _async_grasp_request( - self, points: np.ndarray, colors: np.ndarray - ) -> list[dict] | None: + self, + points: np.ndarray, # type: ignore[type-arg] + colors: np.ndarray, # type: ignore[type-arg] + ) -> list[dict] | None: # type: ignore[type-arg] """Async grasp request helper.""" import json @@ -184,7 +188,7 @@ async def _async_grasp_request( logger.error(f"Async grasp request failed: {e}") return None - def _convert_grasp_format(self, grasps: list[dict]) -> list[dict]: + def _convert_grasp_format(self, grasps: list[dict]) -> list[dict]: # type: ignore[type-arg] """Convert Dimensional Grasp format to visualization format.""" converted = [] @@ -207,7 +211,7 @@ def _convert_grasp_format(self, grasps: list[dict]) -> list[dict]: converted.sort(key=lambda x: x["score"], reverse=True) return converted - def _rotation_matrix_to_euler(self, rotation_matrix: np.ndarray) -> dict[str, float]: + def _rotation_matrix_to_euler(self, rotation_matrix: np.ndarray) -> dict[str, float]: # type: ignore[type-arg] """Convert rotation matrix to Euler angles (in radians).""" sy = np.sqrt(rotation_matrix[0, 0] ** 2 + rotation_matrix[1, 0] ** 2) diff --git a/dimos/perception/grasp_generation/utils.py b/dimos/perception/grasp_generation/utils.py index d83d02e596..492a3d1df4 100644 --- a/dimos/perception/grasp_generation/utils.py +++ b/dimos/perception/grasp_generation/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ import cv2 import numpy as np -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] from dimos.perception.common.utils import project_3d_points_to_2d def create_gripper_geometry( - grasp_data: dict, + grasp_data: dict, # type: ignore[type-arg] finger_length: float = 0.08, finger_thickness: float = 0.004, ) -> list[o3d.geometry.TriangleMesh]: @@ -146,7 +146,8 @@ def create_gripper_geometry( def create_all_gripper_geometries( - grasp_list: list[dict], max_grasps: int = -1 + grasp_list: list[dict], # type: ignore[type-arg] + max_grasps: int = -1, ) -> list[o3d.geometry.TriangleMesh]: """ Create gripper geometries for multiple grasps. @@ -170,13 +171,13 @@ def create_all_gripper_geometries( def draw_grasps_on_image( - image: np.ndarray, - grasp_data: dict | dict[int | str, list[dict]] | list[dict], - camera_intrinsics: list[float] | np.ndarray, # [fx, fy, cx, cy] or 3x3 matrix + image: np.ndarray, # type: ignore[type-arg] + grasp_data: dict | dict[int | str, list[dict]] | list[dict], # type: ignore[type-arg] + camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] # [fx, fy, cx, cy] or 3x3 matrix max_grasps: int = -1, # -1 means show all grasps finger_length: float = 0.08, # Match 3D gripper finger_thickness: float = 0.004, # Match 3D gripper -) -> np.ndarray: +) -> np.ndarray: # type: ignore[type-arg] """ Draw fork-like gripper visualizations on the image matching 3D gripper design. @@ -224,7 +225,7 @@ def draw_grasps_on_image( grasps_to_draw = grasps_to_draw[:max_grasps] # Define grasp colors (solid red to match 3D design) - def get_grasp_color(index: int) -> tuple: + def get_grasp_color(index: int) -> tuple: # type: ignore[type-arg] # Use solid red color for all grasps to match 3D design return (0, 0, 255) # Red in BGR format for OpenCV @@ -256,22 +257,22 @@ def get_grasp_color(index: int) -> tuple: left_finger_points = np.array( [ [ - width / 2 - finger_width / 2, + width / 2 - finger_width / 2, # type: ignore[operator] -finger_length, -finger_thickness / 2, ], # Back left [ - width / 2 + finger_width / 2, + width / 2 + finger_width / 2, # type: ignore[operator] -finger_length, -finger_thickness / 2, ], # Back right [ - width / 2 + finger_width / 2, + width / 2 + finger_width / 2, # type: ignore[operator] 0, -finger_thickness / 2, ], # Front right (at origin) [ - width / 2 - finger_width / 2, + width / 2 - finger_width / 2, # type: ignore[operator] 0, -finger_thickness / 2, ], # Front left (at origin) @@ -282,22 +283,22 @@ def get_grasp_color(index: int) -> tuple: right_finger_points = np.array( [ [ - -width / 2 - finger_width / 2, + -width / 2 - finger_width / 2, # type: ignore[operator] -finger_length, -finger_thickness / 2, ], # Back left [ - -width / 2 + finger_width / 2, + -width / 2 + finger_width / 2, # type: ignore[operator] -finger_length, -finger_thickness / 2, ], # Back right [ - -width / 2 + finger_width / 2, + -width / 2 + finger_width / 2, # type: ignore[operator] 0, -finger_thickness / 2, ], # Front right (at origin) [ - -width / 2 - finger_width / 2, + -width / 2 - finger_width / 2, # type: ignore[operator] 0, -finger_thickness / 2, ], # Front left (at origin) @@ -308,22 +309,22 @@ def get_grasp_color(index: int) -> tuple: base_points = np.array( [ [ - -width / 2 - finger_width / 2, + -width / 2 - finger_width / 2, # type: ignore[operator] -finger_length - finger_thickness, -finger_thickness / 2, ], # Back left [ - width / 2 + finger_width / 2, + width / 2 + finger_width / 2, # type: ignore[operator] -finger_length - finger_thickness, -finger_thickness / 2, ], # Back right [ - width / 2 + finger_width / 2, + width / 2 + finger_width / 2, # type: ignore[operator] -finger_length, -finger_thickness / 2, ], # Front right [ - -width / 2 - finger_width / 2, + -width / 2 - finger_width / 2, # type: ignore[operator] -finger_length, -finger_thickness / 2, ], # Front left @@ -357,15 +358,15 @@ def get_grasp_color(index: int) -> tuple: ) # Transform all points to world frame - def transform_points(points): + def transform_points(points): # type: ignore[no-untyped-def] # Apply rotation and translation world_points = (rotation_matrix @ points.T).T + translation return world_points - left_finger_world = transform_points(left_finger_points) - right_finger_world = transform_points(right_finger_points) - base_world = transform_points(base_points) - handle_world = transform_points(handle_points) + left_finger_world = transform_points(left_finger_points) # type: ignore[no-untyped-call] + right_finger_world = transform_points(right_finger_points) # type: ignore[no-untyped-call] + base_world = transform_points(base_points) # type: ignore[no-untyped-call] + handle_world = transform_points(handle_points) # type: ignore[no-untyped-call] # Project to 2D left_finger_2d = project_3d_points_to_2d(left_finger_world, camera_matrix) @@ -400,7 +401,7 @@ def transform_points(points): return result -def get_standard_coordinate_transform(): +def get_standard_coordinate_transform(): # type: ignore[no-untyped-def] """ Get a standard coordinate transformation matrix for consistent visualization. @@ -426,7 +427,7 @@ def get_standard_coordinate_transform(): def visualize_grasps_3d( point_cloud: o3d.geometry.PointCloud, - grasp_list: list[dict], + grasp_list: list[dict], # type: ignore[type-arg] max_grasps: int = -1, ) -> None: """ @@ -438,7 +439,7 @@ def visualize_grasps_3d( max_grasps: Maximum number of grasps to visualize """ # Apply standard coordinate transformation - transform = get_standard_coordinate_transform() + transform = get_standard_coordinate_transform() # type: ignore[no-untyped-call] # Transform point cloud pc_copy = o3d.geometry.PointCloud(point_cloud) @@ -459,7 +460,7 @@ def visualize_grasps_3d( o3d.visualization.draw_geometries(geometries, window_name="3D Grasp Visualization") -def parse_grasp_results(grasps: list[dict]) -> list[dict]: +def parse_grasp_results(grasps: list[dict]) -> list[dict]: # type: ignore[type-arg] """ Parse grasp results into visualization format. @@ -499,10 +500,10 @@ def parse_grasp_results(grasps: list[dict]) -> list[dict]: def create_grasp_overlay( - rgb_image: np.ndarray, - grasps: list[dict], - camera_intrinsics: list[float] | np.ndarray, -) -> np.ndarray: + rgb_image: np.ndarray, # type: ignore[type-arg] + grasps: list[dict], # type: ignore[type-arg] + camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] +) -> np.ndarray: # type: ignore[type-arg] """ Create grasp visualization overlay on RGB image. diff --git a/dimos/perception/object_detection_stream.py b/dimos/perception/object_detection_stream.py index a82cbe9db5..4d93e3ddd4 100644 --- a/dimos/perception/object_detection_stream.py +++ b/dimos/perception/object_detection_stream.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ import numpy as np from reactivex import Observable, operators as ops -from dimos.perception.detection2d.yolo_2d_det import Yolo2DDetector +from dimos.perception.detection2d.yolo_2d_det import ( # type: ignore[import-not-found, import-untyped] + Yolo2DDetector, +) try: - from dimos.perception.detection2d.detic_2d_det import Detic2DDetector + from dimos.perception.detection2d.detic_2d_det import ( # type: ignore[import-not-found, import-untyped] + Detic2DDetector, + ) DETIC_AVAILABLE = True except (ModuleNotFoundError, ImportError): @@ -30,20 +34,20 @@ from dimos.models.depth.metric3d import Metric3D from dimos.perception.common.utils import draw_object_detection_visualization -from dimos.perception.detection2d.utils import ( +from dimos.perception.detection2d.utils import ( # type: ignore[attr-defined] calculate_depth_from_bbox, calculate_object_size_from_bbox, calculate_position_rotation_from_bbox, ) from dimos.types.vector import Vector from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import transform_robot_to_map +from dimos.utils.transform_utils import transform_robot_to_map # type: ignore[attr-defined] if TYPE_CHECKING: from dimos.types.manipulation import ObjectData # Initialize logger for the ObjectDetectionStream -logger = setup_logger("dimos.perception.object_detection_stream") +logger = setup_logger() class ObjectDetectionStream: @@ -58,16 +62,16 @@ class ObjectDetectionStream: Provides a stream of structured object data with position and rotation information. """ - def __init__( + def __init__( # type: ignore[no-untyped-def] self, camera_intrinsics=None, # [fx, fy, cx, cy] device: str = "cuda", gt_depth_scale: float = 1000.0, min_confidence: float = 0.7, class_filter=None, # Optional list of class names to filter (e.g., ["person", "car"]) - get_pose: Callable | None = None, # Optional function to transform coordinates to map frame + get_pose: Callable | None = None, # type: ignore[type-arg] # Optional function to transform coordinates to map frame detector: Detic2DDetector | Yolo2DDetector | None = None, - video_stream: Observable = None, + video_stream: Observable = None, # type: ignore[assignment, type-arg] disable_depth: bool = False, # Flag to disable monocular Metric3D depth estimation draw_masks: bool = False, # Flag to enable drawing segmentation masks ) -> None: @@ -114,10 +118,10 @@ def __init__( self.depth_model = None if not disable_depth: try: - self.depth_model = Metric3D(gt_depth_scale) + self.depth_model = Metric3D(gt_depth_scale=gt_depth_scale) if camera_intrinsics is not None: - self.depth_model.update_intrinsic(camera_intrinsics) + self.depth_model.update_intrinsic(camera_intrinsics) # type: ignore[no-untyped-call] # Create 3x3 camera matrix for calculations fx, fy, cx, cy = camera_intrinsics @@ -141,7 +145,7 @@ def __init__( if video_stream is not None: self.stream = self.create_stream(video_stream) - def create_stream(self, video_stream: Observable) -> Observable: + def create_stream(self, video_stream: Observable) -> Observable: # type: ignore[type-arg] """ Create an Observable stream of object data from a video stream. @@ -153,17 +157,17 @@ def create_stream(self, video_stream: Observable) -> Observable: with position and rotation information """ - def process_frame(frame): + def process_frame(frame): # type: ignore[no-untyped-def] # TODO: More modular detector output interface - bboxes, track_ids, class_ids, confidences, names, *mask_data = ( + bboxes, track_ids, class_ids, confidences, names, *mask_data = ( # type: ignore[misc] *self.detector.process_image(frame), [], ) masks = ( - mask_data[0] - if mask_data and len(mask_data[0]) == len(bboxes) - else [None] * len(bboxes) + mask_data[0] # type: ignore[has-type] + if mask_data and len(mask_data[0]) == len(bboxes) # type: ignore[has-type] + else [None] * len(bboxes) # type: ignore[has-type] ) # Create visualization @@ -172,24 +176,24 @@ def process_frame(frame): # Process detections objects = [] if not self.disable_depth: - depth_map = self.depth_model.infer_depth(frame) + depth_map = self.depth_model.infer_depth(frame) # type: ignore[union-attr] depth_map = np.array(depth_map) else: depth_map = None - for i, bbox in enumerate(bboxes): + for i, bbox in enumerate(bboxes): # type: ignore[has-type] # Skip if confidence is too low - if i < len(confidences) and confidences[i] < self.min_confidence: + if i < len(confidences) and confidences[i] < self.min_confidence: # type: ignore[has-type] continue # Skip if class filter is active and class not in filter - class_name = names[i] if i < len(names) else None + class_name = names[i] if i < len(names) else None # type: ignore[has-type] if self.class_filter and class_name not in self.class_filter: continue if not self.disable_depth and depth_map is not None: # Get depth for this object - depth = calculate_depth_from_bbox(depth_map, bbox) + depth = calculate_depth_from_bbox(depth_map, bbox) # type: ignore[no-untyped-call] if depth is None: # Skip objects with invalid depth continue @@ -216,19 +220,19 @@ def process_frame(frame): else: depth = -1 - position = Vector(0, 0, 0) - rotation = Vector(0, 0, 0) + position = Vector(0, 0, 0) # type: ignore[arg-type] + rotation = Vector(0, 0, 0) # type: ignore[arg-type] width = -1 height = -1 # Create a properly typed ObjectData instance object_data: ObjectData = { - "object_id": track_ids[i] if i < len(track_ids) else -1, + "object_id": track_ids[i] if i < len(track_ids) else -1, # type: ignore[has-type] "bbox": bbox, "depth": depth, - "confidence": confidences[i] if i < len(confidences) else None, - "class_id": class_ids[i] if i < len(class_ids) else None, - "label": class_name, + "confidence": confidences[i] if i < len(confidences) else None, # type: ignore[has-type, typeddict-item] + "class_id": class_ids[i] if i < len(class_ids) else None, # type: ignore[has-type, typeddict-item] + "label": class_name, # type: ignore[typeddict-item] "position": position, "rotation": rotation, "size": {"width": width, "height": height}, @@ -248,7 +252,7 @@ def process_frame(frame): return self.stream - def get_stream(self): + def get_stream(self): # type: ignore[no-untyped-def] """ Returns the current detection stream if available. Creates a new one with the provided video_stream if not already created. @@ -262,7 +266,7 @@ def get_stream(self): ) return self.stream - def get_formatted_stream(self): + def get_formatted_stream(self): # type: ignore[no-untyped-def] """ Returns a formatted stream of object detection data for better readability. This is especially useful for LLMs like Claude that need structured text input. @@ -275,7 +279,7 @@ def get_formatted_stream(self): "Stream not initialized. Either provide a video_stream during initialization or call create_stream first." ) - def format_detection_data(result): + def format_detection_data(result): # type: ignore[no-untyped-def] # Extract objects from result objects = result.get("objects", []) diff --git a/dimos/perception/object_tracker.py b/dimos/perception/object_tracker.py index f5fa48581a..9260003ce2 100644 --- a/dimos/perception/object_tracker.py +++ b/dimos/perception/object_tracker.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from dataclasses import dataclass import threading import time import cv2 -from dimos_lcm.sensor_msgs import CameraInfo # Import LCM messages from dimos_lcm.vision_msgs import ( @@ -27,10 +27,14 @@ import numpy as np from reactivex.disposable import Disposable -from dimos.core import In, Module, Out, rpc +from dimos.core import In, Module, ModuleConfig, Out, rpc from dimos.manipulation.visual_servoing.utils import visualize_detections_3d from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.msgs.sensor_msgs import ( + CameraInfo, + Image, + ImageFormat, +) from dimos.msgs.std_msgs import Header from dimos.msgs.vision_msgs import Detection2DArray, Detection3DArray from dimos.protocol.tf import TF @@ -42,27 +46,32 @@ yaw_towards_point, ) -logger = setup_logger("dimos.perception.object_tracker") +logger = setup_logger() + + +@dataclass +class ObjectTrackingConfig(ModuleConfig): + frame_id: str = "camera_link" -class ObjectTracking(Module): +class ObjectTracking(Module[ObjectTrackingConfig]): """Module for object tracking with LCM input/output.""" # LCM inputs - color_image: In[Image] = None - depth: In[Image] = None - camera_info: In[CameraInfo] = None + color_image: In[Image] + depth: In[Image] + camera_info: In[CameraInfo] # LCM outputs - detection2darray: Out[Detection2DArray] = None - detection3darray: Out[Detection3DArray] = None - tracked_overlay: Out[Image] = None # Visualization output + detection2darray: Out[Detection2DArray] + detection3darray: Out[Detection3DArray] + tracked_overlay: Out[Image] # Visualization output + + default_config = ObjectTrackingConfig + config: ObjectTrackingConfig def __init__( - self, - reid_threshold: int = 10, - reid_fail_tolerance: int = 5, - frame_id: str = "camera_link", + self, reid_threshold: int = 10, reid_fail_tolerance: int = 5, **kwargs: object ) -> None: """ Initialize an object tracking module using OpenCV's CSRT tracker with ORB re-ID. @@ -73,25 +82,23 @@ def __init__( reid_threshold: Minimum good feature matches needed to confirm re-ID. reid_fail_tolerance: Number of consecutive frames Re-ID can fail before tracking is stopped. - frame_id: TF frame ID for the camera (default: "camera_link") """ # Call parent Module init - super().__init__() + super().__init__(**kwargs) self.camera_intrinsics = None self.reid_threshold = reid_threshold self.reid_fail_tolerance = reid_fail_tolerance - self.frame_id = frame_id self.tracker = None self.tracking_bbox = None # Stores (x, y, w, h) for tracker initialization self.tracking_initialized = False - self.orb = cv2.ORB_create() + self.orb = cv2.ORB_create() # type: ignore[attr-defined] self.bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False) self.original_des = None # Store original ORB descriptors self.original_kps = None # Store original ORB keypoints self.reid_fail_count = 0 # Counter for consecutive re-id failures - self.last_good_matches = [] # Store good matches for visualization + self.last_good_matches = [] # type: ignore[var-annotated] # Store good matches for visualization self.last_roi_kps = None # Store last ROI keypoints for visualization self.last_roi_bbox = None # Store last ROI bbox for visualization self.reid_confirmed = False # Store current reid confirmation state @@ -99,8 +106,8 @@ def __init__( self.reid_warmup_frames = 3 # Number of frames before REID starts self._frame_lock = threading.Lock() - self._latest_rgb_frame: np.ndarray | None = None - self._latest_depth_frame: np.ndarray | None = None + self._latest_rgb_frame: np.ndarray | None = None # type: ignore[type-arg] + self._latest_depth_frame: np.ndarray | None = None # type: ignore[type-arg] self._latest_camera_info: CameraInfo | None = None # Tracking thread control @@ -122,7 +129,7 @@ def start(self) -> None: super().start() # Subscribe to aligned rgb and depth streams - def on_aligned_frames(frames_tuple) -> None: + def on_aligned_frames(frames_tuple) -> None: # type: ignore[no-untyped-def] rgb_msg, depth_msg = frames_tuple with self._frame_lock: self._latest_rgb_frame = rgb_msg.data @@ -135,8 +142,8 @@ def on_aligned_frames(frames_tuple) -> None: # Create aligned observable for RGB and depth aligned_frames = align_timestamped( - self.color_image.observable(), - self.depth.observable(), + self.color_image.observable(), # type: ignore[no-untyped-call] + self.depth.observable(), # type: ignore[no-untyped-call] buffer_size=2.0, # 2 second buffer match_tolerance=0.5, # 500ms tolerance ) @@ -148,15 +155,15 @@ def on_camera_info(camera_info_msg: CameraInfo) -> None: self._latest_camera_info = camera_info_msg # Extract intrinsics from camera info K matrix # K is a 3x3 matrix in row-major order: [fx, 0, cx, 0, fy, cy, 0, 0, 1] - self.camera_intrinsics = [ + self.camera_intrinsics = [ # type: ignore[assignment] camera_info_msg.K[0], camera_info_msg.K[4], camera_info_msg.K[2], camera_info_msg.K[5], ] - unsub = self.camera_info.subscribe(on_camera_info) - self._disposables.add(Disposable(unsub)) + unsub = self.camera_info.subscribe(on_camera_info) # type: ignore[assignment] + self._disposables.add(Disposable(unsub)) # type: ignore[arg-type] @rpc def stop(self) -> None: @@ -173,7 +180,7 @@ def stop(self) -> None: def track( self, bbox: list[float], - ) -> dict: + ) -> dict: # type: ignore[type-arg] """ Initialize tracking with a bounding box and process current frame. @@ -193,15 +200,15 @@ def track( logger.warning(f"Invalid initial bbox provided: {bbox}. Tracking not started.") # Set tracking parameters - self.tracking_bbox = (x1, y1, w, h) # Store in (x, y, w, h) format - self.tracker = cv2.legacy.TrackerCSRT_create() + self.tracking_bbox = (x1, y1, w, h) # type: ignore[assignment] # Store in (x, y, w, h) format + self.tracker = cv2.legacy.TrackerCSRT_create() # type: ignore[attr-defined] self.tracking_initialized = False self.original_des = None self.reid_fail_count = 0 logger.info(f"Tracking target set with bbox: {self.tracking_bbox}") # Extract initial features - roi = self._latest_rgb_frame[y1:y2, x1:x2] + roi = self._latest_rgb_frame[y1:y2, x1:x2] # type: ignore[index] if roi.size > 0: self.original_kps, self.original_des = self.orb.detectAndCompute(roi, None) if self.original_des is None: @@ -210,7 +217,7 @@ def track( logger.info(f"Initial ORB features extracted: {len(self.original_des)}") # Initialize the tracker - init_success = self.tracker.init(self._latest_rgb_frame, self.tracking_bbox) + init_success = self.tracker.init(self._latest_rgb_frame, self.tracking_bbox) # type: ignore[attr-defined] if init_success: self.tracking_initialized = True self.tracking_frame_count = 0 # Reset frame counter @@ -228,7 +235,7 @@ def track( # Return initial tracking result return {"status": "tracking_started", "bbox": self.tracking_bbox} - def reid(self, frame, current_bbox) -> bool: + def reid(self, frame, current_bbox) -> bool: # type: ignore[no-untyped-def] """Check if features in current_bbox match stored original features.""" # During warm-up period, always return True if self.tracking_frame_count < self.reid_warmup_frames: @@ -546,20 +553,20 @@ def _process_tracking(self) -> None: viz_msg = Image.from_numpy(viz_image) self.tracked_overlay.publish(viz_msg) - def _draw_reid_matches(self, image: np.ndarray) -> np.ndarray: + def _draw_reid_matches(self, image: np.ndarray) -> np.ndarray: # type: ignore[type-arg] """Draw REID feature matches on the image.""" viz_image = image.copy() - x1, y1, _x2, _y2 = self.last_roi_bbox + x1, y1, _x2, _y2 = self.last_roi_bbox # type: ignore[misc] # Draw keypoints from current ROI in green - for kp in self.last_roi_kps: - pt = (int(kp.pt[0] + x1), int(kp.pt[1] + y1)) + for kp in self.last_roi_kps: # type: ignore[attr-defined] + pt = (int(kp.pt[0] + x1), int(kp.pt[1] + y1)) # type: ignore[has-type] cv2.circle(viz_image, pt, 3, (0, 255, 0), -1) for match in self.last_good_matches: - current_kp = self.last_roi_kps[match.trainIdx] - pt_current = (int(current_kp.pt[0] + x1), int(current_kp.pt[1] + y1)) + current_kp = self.last_roi_kps[match.trainIdx] # type: ignore[index] + pt_current = (int(current_kp.pt[0] + x1), int(current_kp.pt[1] + y1)) # type: ignore[has-type] # Draw a larger circle for matched points in yellow cv2.circle(viz_image, pt_current, 5, (0, 255, 255), 2) # Yellow for matched points @@ -590,7 +597,7 @@ def _draw_reid_matches(self, image: np.ndarray) -> np.ndarray: return viz_image - def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> float | None: + def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> float | None: # type: ignore[type-arg] """Calculate depth from bbox using the 25th percentile of closest points. Args: diff --git a/dimos/perception/object_tracker_2d.py b/dimos/perception/object_tracker_2d.py index 0256b7beb9..f5d39745c3 100644 --- a/dimos/perception/object_tracker_2d.py +++ b/dimos/perception/object_tracker_2d.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from dataclasses import dataclass import logging import threading import time @@ -30,36 +31,34 @@ import numpy as np from reactivex.disposable import Disposable -from dimos.core import In, Module, Out, rpc +from dimos.core import In, Module, ModuleConfig, Out, rpc from dimos.msgs.sensor_msgs import Image, ImageFormat from dimos.msgs.std_msgs import Header from dimos.msgs.vision_msgs import Detection2DArray from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.object_tracker_2d", level=logging.INFO) +logger = setup_logger(level=logging.INFO) -class ObjectTracker2D(Module): - """Pure 2D object tracking module using OpenCV's CSRT tracker.""" +@dataclass +class ObjectTracker2DConfig(ModuleConfig): + frame_id: str = "camera_link" - color_image: In[Image] = None - detection2darray: Out[Detection2DArray] = None - tracked_overlay: Out[Image] = None # Visualization output +class ObjectTracker2D(Module[ObjectTracker2DConfig]): + """Pure 2D object tracking module using OpenCV's CSRT tracker.""" - def __init__( - self, - frame_id: str = "camera_link", - ) -> None: - """ - Initialize 2D object tracking module using OpenCV's CSRT tracker. + color_image: In[Image] - Args: - frame_id: TF frame ID for the camera (default: "camera_link") - """ - super().__init__() + detection2darray: Out[Detection2DArray] + tracked_overlay: Out[Image] # Visualization output + + default_config = ObjectTracker2DConfig + config: ObjectTracker2DConfig - self.frame_id = frame_id + def __init__(self, **kwargs: object) -> None: + """Initialize 2D object tracking module using OpenCV's CSRT tracker.""" + super().__init__(**kwargs) # Tracker state self.tracker = None @@ -73,7 +72,7 @@ def __init__( # Frame management self._frame_lock = threading.Lock() - self._latest_rgb_frame: np.ndarray | None = None + self._latest_rgb_frame: np.ndarray | None = None # type: ignore[type-arg] self._frame_arrival_time: float | None = None # Tracking thread control @@ -109,7 +108,7 @@ def stop(self) -> None: super().stop() @rpc - def track(self, bbox: list[float]) -> dict: + def track(self, bbox: list[float]) -> dict: # type: ignore[type-arg] """ Initialize tracking with a bounding box. @@ -130,14 +129,14 @@ def track(self, bbox: list[float]) -> dict: logger.warning(f"Invalid initial bbox provided: {bbox}. Tracking not started.") return {"status": "invalid_bbox"} - self.tracking_bbox = (x1, y1, w, h) - self.tracker = cv2.legacy.TrackerCSRT_create() + self.tracking_bbox = (x1, y1, w, h) # type: ignore[assignment] + self.tracker = cv2.legacy.TrackerCSRT_create() # type: ignore[attr-defined] self.tracking_initialized = False logger.info(f"Tracking target set with bbox: {self.tracking_bbox}") # Convert RGB to BGR for CSRT (OpenCV expects BGR) frame_bgr = cv2.cvtColor(self._latest_rgb_frame, cv2.COLOR_RGB2BGR) - init_success = self.tracker.init(frame_bgr, self.tracking_bbox) + init_success = self.tracker.init(frame_bgr, self.tracking_bbox) # type: ignore[attr-defined] if init_success: self.tracking_initialized = True logger.info("Tracker initialized successfully.") @@ -290,7 +289,7 @@ def _process_tracking(self) -> None: viz_msg = Image.from_numpy(viz_copy, format=ImageFormat.RGB) self.tracked_overlay.publish(viz_msg) - def _draw_visualization(self, image: np.ndarray, bbox: list[int]) -> np.ndarray: + def _draw_visualization(self, image: np.ndarray, bbox: list[int]) -> np.ndarray: # type: ignore[type-arg] """Draw tracking visualization.""" viz_image = image.copy() x1, y1, x2, y2 = bbox diff --git a/dimos/perception/object_tracker_3d.py b/dimos/perception/object_tracker_3d.py index 231ae26748..22846e1e2f 100644 --- a/dimos/perception/object_tracker_3d.py +++ b/dimos/perception/object_tracker_3d.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,7 +15,10 @@ # Import LCM messages from dimos_lcm.sensor_msgs import CameraInfo -from dimos_lcm.vision_msgs import Detection3D, ObjectHypothesisWithPose +from dimos_lcm.vision_msgs import ( + Detection3D, + ObjectHypothesisWithPose, +) import numpy as np from dimos.core import In, Out, rpc @@ -34,20 +37,20 @@ yaw_towards_point, ) -logger = setup_logger("dimos.perception.object_tracker_3d") +logger = setup_logger() class ObjectTracker3D(ObjectTracker2D): """3D object tracking module extending ObjectTracker2D with depth capabilities.""" # Additional inputs (2D tracker already has color_image) - depth: In[Image] = None - camera_info: In[CameraInfo] = None + depth: In[Image] + camera_info: In[CameraInfo] # Additional outputs (2D tracker already has detection2darray and tracked_overlay) - detection3darray: Out[Detection3DArray] = None + detection3darray: Out[Detection3DArray] - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] """ Initialize 3D object tracking module. @@ -58,7 +61,7 @@ def __init__(self, **kwargs) -> None: # Additional state for 3D tracking self.camera_intrinsics = None - self._latest_depth_frame: np.ndarray | None = None + self._latest_depth_frame: np.ndarray | None = None # type: ignore[type-arg] self._latest_camera_info: CameraInfo | None = None # TF publisher for tracked object @@ -72,7 +75,7 @@ def start(self) -> None: super().start() # Subscribe to aligned RGB and depth streams - def on_aligned_frames(frames_tuple) -> None: + def on_aligned_frames(frames_tuple) -> None: # type: ignore[no-untyped-def] rgb_msg, depth_msg = frames_tuple with self._frame_lock: self._latest_rgb_frame = rgb_msg.data @@ -85,8 +88,8 @@ def on_aligned_frames(frames_tuple) -> None: # Create aligned observable for RGB and depth aligned_frames = align_timestamped( - self.color_image.observable(), - self.depth.observable(), + self.color_image.observable(), # type: ignore[no-untyped-call] + self.depth.observable(), # type: ignore[no-untyped-call] buffer_size=2.0, # 2 second buffer match_tolerance=0.5, # 500ms tolerance ) @@ -97,7 +100,7 @@ def on_aligned_frames(frames_tuple) -> None: def on_camera_info(camera_info_msg: CameraInfo) -> None: self._latest_camera_info = camera_info_msg # Extract intrinsics: K is [fx, 0, cx, 0, fy, cy, 0, 0, 1] - self.camera_intrinsics = [ + self.camera_intrinsics = [ # type: ignore[assignment] camera_info_msg.K[0], camera_info_msg.K[4], camera_info_msg.K[2], @@ -174,17 +177,17 @@ def _create_detection3d_from_2d(self, detection2d: Detection2DArray) -> Detectio y2 = int(center_y + height / 2) # Get depth value - depth_value = self._get_depth_from_bbox([x1, y1, x2, y2], self._latest_depth_frame) + depth_value = self._get_depth_from_bbox([x1, y1, x2, y2], self._latest_depth_frame) # type: ignore[arg-type] if depth_value is None or depth_value <= 0: return None - fx, fy, cx, cy = self.camera_intrinsics + fx, fy, cx, cy = self.camera_intrinsics # type: ignore[misc] # Convert pixel coordinates to 3D in optical frame z_optical = depth_value - x_optical = (center_x - cx) * z_optical / fx - y_optical = (center_y - cy) * z_optical / fy + x_optical = (center_x - cx) * z_optical / fx # type: ignore[has-type] + y_optical = (center_y - cy) * z_optical / fy # type: ignore[has-type] # Create pose in optical frame optical_pose = Pose() @@ -200,8 +203,8 @@ def _create_detection3d_from_2d(self, detection2d: Detection2DArray) -> Detectio robot_pose.orientation = euler_to_quaternion(euler) # Estimate object size in meters - size_x = width * z_optical / fx - size_y = height * z_optical / fy + size_x = width * z_optical / fx # type: ignore[has-type] + size_y = height * z_optical / fy # type: ignore[has-type] size_z = 0.1 # Default depth size # Create Detection3D @@ -240,7 +243,7 @@ def _create_detection3d_from_2d(self, detection2d: Detection2DArray) -> Detectio return detection3darray - def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> float | None: + def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> float | None: # type: ignore[type-arg] """ Calculate depth from bbox using the 25th percentile of closest points. @@ -273,21 +276,21 @@ def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> floa return None - def _draw_reid_overlay(self, image: np.ndarray) -> np.ndarray: + def _draw_reid_overlay(self, image: np.ndarray) -> np.ndarray: # type: ignore[type-arg] """Draw Re-ID feature matches on visualization.""" import cv2 viz_image = image.copy() - x1, y1, _x2, _y2 = self.last_roi_bbox + x1, y1, _x2, _y2 = self.last_roi_bbox # type: ignore[attr-defined] # Draw keypoints - for kp in self.last_roi_kps: + for kp in self.last_roi_kps: # type: ignore[attr-defined] pt = (int(kp.pt[0] + x1), int(kp.pt[1] + y1)) cv2.circle(viz_image, pt, 3, (0, 255, 0), -1) # Draw matches - for match in self.last_good_matches: - current_kp = self.last_roi_kps[match.trainIdx] + for match in self.last_good_matches: # type: ignore[attr-defined] + current_kp = self.last_roi_kps[match.trainIdx] # type: ignore[attr-defined] pt_current = (int(current_kp.pt[0] + x1), int(current_kp.pt[1] + y1)) cv2.circle(viz_image, pt_current, 5, (0, 255, 255), 2) @@ -295,7 +298,7 @@ def _draw_reid_overlay(self, image: np.ndarray) -> np.ndarray: cv2.circle(viz_image, pt_current, 2, (intensity, intensity, 255), -1) # Draw match count - text = f"REID: {len(self.last_good_matches)}/{len(self.last_roi_kps)}" + text = f"REID: {len(self.last_good_matches)}/{len(self.last_roi_kps)}" # type: ignore[attr-defined] cv2.putText(viz_image, text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) return viz_image diff --git a/dimos/perception/person_tracker.py b/dimos/perception/person_tracker.py index 16b505578b..a138467850 100644 --- a/dimos/perception/person_tracker.py +++ b/dimos/perception/person_tracker.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,22 +22,24 @@ from dimos.msgs.sensor_msgs import Image from dimos.perception.common.ibvs import PersonDistanceEstimator from dimos.perception.detection2d.utils import filter_detections -from dimos.perception.detection2d.yolo_2d_det import Yolo2DDetector +from dimos.perception.detection2d.yolo_2d_det import ( # type: ignore[import-not-found, import-untyped] + Yolo2DDetector, +) from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.person_tracker") +logger = setup_logger() class PersonTrackingStream(Module): """Module for person tracking with LCM input/output.""" # LCM inputs - video: In[Image] = None + video: In[Image] # LCM outputs - tracking_data: Out[dict] = None + tracking_data: Out[dict] # type: ignore[type-arg] - def __init__( + def __init__( # type: ignore[no-untyped-def] self, camera_intrinsics=None, camera_pitch: float = 0.0, @@ -84,7 +86,7 @@ def __init__( ) # For tracking latest frame data - self._latest_frame: np.ndarray | None = None + self._latest_frame: np.ndarray | None = None # type: ignore[type-arg] self._process_interval = 0.1 # Process at 10Hz # Tracking state - starts disabled @@ -107,8 +109,8 @@ def set_video(image_msg: Image) -> None: self._disposables.add(Disposable(unsub)) # Start periodic processing - unsub = interval(self._process_interval).subscribe(lambda _: self._process_frame()) - self._disposables.add(unsub) + unsub = interval(self._process_interval).subscribe(lambda _: self._process_frame()) # type: ignore[assignment] + self._disposables.add(unsub) # type: ignore[arg-type] logger.info("PersonTracking module started and subscribed to LCM streams") @@ -126,13 +128,13 @@ def _process_frame(self) -> None: return # Process frame through tracking pipeline - result = self._process_tracking(self._latest_frame) + result = self._process_tracking(self._latest_frame) # type: ignore[no-untyped-call] # Publish result to LCM if result: self.tracking_data.publish(result) - def _process_tracking(self, frame): + def _process_tracking(self, frame): # type: ignore[no-untyped-def] """Process a single frame for person tracking.""" # Detect people in the frame bboxes, track_ids, class_ids, confidences, names = self.detector.process_image(frame) @@ -236,17 +238,17 @@ def is_tracking_enabled(self) -> bool: return self._tracking_enabled @rpc - def get_tracking_data(self) -> dict: + def get_tracking_data(self) -> dict: # type: ignore[type-arg] """Get the latest tracking data. Returns: Dictionary containing tracking results """ if self._latest_frame is not None: - return self._process_tracking(self._latest_frame) + return self._process_tracking(self._latest_frame) # type: ignore[no-any-return, no-untyped-call] return {"frame": None, "viz_frame": None, "targets": []} - def create_stream(self, video_stream: Observable) -> Observable: + def create_stream(self, video_stream: Observable) -> Observable: # type: ignore[type-arg] """ Create an Observable stream of person tracking results from a video stream. diff --git a/dimos/perception/pointcloud/cuboid_fit.py b/dimos/perception/pointcloud/cuboid_fit.py index 376ae08da0..dfec2d9297 100644 --- a/dimos/perception/pointcloud/cuboid_fit.py +++ b/dimos/perception/pointcloud/cuboid_fit.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,12 +15,13 @@ import cv2 import numpy as np -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] def fit_cuboid( - points: np.ndarray | o3d.geometry.PointCloud, method: str = "minimal" -) -> dict | None: + points: np.ndarray | o3d.geometry.PointCloud, # type: ignore[type-arg] + method: str = "minimal", +) -> dict | None: # type: ignore[type-arg] """ Fit a cuboid to a point cloud using Open3D's built-in methods. @@ -103,7 +104,7 @@ def fit_cuboid( return None -def fit_cuboid_simple(points: np.ndarray | o3d.geometry.PointCloud) -> dict | None: +def fit_cuboid_simple(points: np.ndarray | o3d.geometry.PointCloud) -> dict | None: # type: ignore[type-arg] """ Simple wrapper for minimal oriented bounding box fitting. @@ -118,7 +119,10 @@ def fit_cuboid_simple(points: np.ndarray | o3d.geometry.PointCloud) -> dict | No def _compute_fitting_error( - points: np.ndarray, center: np.ndarray, dimensions: np.ndarray, rotation: np.ndarray + points: np.ndarray, # type: ignore[type-arg] + center: np.ndarray, # type: ignore[type-arg] + dimensions: np.ndarray, # type: ignore[type-arg] + rotation: np.ndarray, # type: ignore[type-arg] ) -> float: """ Compute fitting error as mean squared distance from points to cuboid surface. @@ -154,8 +158,10 @@ def _compute_fitting_error( def get_cuboid_corners( - center: np.ndarray, dimensions: np.ndarray, rotation: np.ndarray -) -> np.ndarray: + center: np.ndarray, # type: ignore[type-arg] + dimensions: np.ndarray, # type: ignore[type-arg] + rotation: np.ndarray, # type: ignore[type-arg] +) -> np.ndarray: # type: ignore[type-arg] """ Get the 8 corners of a cuboid. @@ -185,19 +191,19 @@ def get_cuboid_corners( ) # Apply rotation and translation - return corners_local @ rotation.T + center + return corners_local @ rotation.T + center # type: ignore[no-any-return] def visualize_cuboid_on_image( - image: np.ndarray, - cuboid_params: dict, - camera_matrix: np.ndarray, - extrinsic_rotation: np.ndarray | None = None, - extrinsic_translation: np.ndarray | None = None, + image: np.ndarray, # type: ignore[type-arg] + cuboid_params: dict, # type: ignore[type-arg] + camera_matrix: np.ndarray, # type: ignore[type-arg] + extrinsic_rotation: np.ndarray | None = None, # type: ignore[type-arg] + extrinsic_translation: np.ndarray | None = None, # type: ignore[type-arg] color: tuple[int, int, int] = (0, 255, 0), thickness: int = 2, show_dimensions: bool = True, -) -> np.ndarray: +) -> np.ndarray: # type: ignore[type-arg] """ Draw a fitted cuboid on an image using camera projection. @@ -244,7 +250,7 @@ def visualize_cuboid_on_image( try: # Project 3D corners to image coordinates - corners_img, _ = cv2.projectPoints( + corners_img, _ = cv2.projectPoints( # type: ignore[call-overload] corners.astype(np.float32), np.zeros(3), np.zeros(3), # No additional rotation/translation @@ -320,7 +326,7 @@ def visualize_cuboid_on_image( return vis_img -def compute_cuboid_volume(cuboid_params: dict) -> float: +def compute_cuboid_volume(cuboid_params: dict) -> float: # type: ignore[type-arg] """ Compute the volume of a cuboid. @@ -337,7 +343,7 @@ def compute_cuboid_volume(cuboid_params: dict) -> float: return float(np.prod(dims)) -def compute_cuboid_surface_area(cuboid_params: dict) -> float: +def compute_cuboid_surface_area(cuboid_params: dict) -> float: # type: ignore[type-arg] """ Compute the surface area of a cuboid. @@ -351,10 +357,10 @@ def compute_cuboid_surface_area(cuboid_params: dict) -> float: raise ValueError("cuboid_params must contain 'dimensions' key") dims = cuboid_params["dimensions"] - return 2.0 * (dims[0] * dims[1] + dims[1] * dims[2] + dims[2] * dims[0]) + return 2.0 * (dims[0] * dims[1] + dims[1] * dims[2] + dims[2] * dims[0]) # type: ignore[no-any-return] -def check_cuboid_quality(cuboid_params: dict, points: np.ndarray) -> dict: +def check_cuboid_quality(cuboid_params: dict, points: np.ndarray) -> dict: # type: ignore[type-arg] """ Assess the quality of a cuboid fit. @@ -404,7 +410,7 @@ def check_cuboid_quality(cuboid_params: dict, points: np.ndarray) -> dict: # Backward compatibility -def visualize_fit(image, cuboid_params, camera_matrix, R=None, t=None): +def visualize_fit(image, cuboid_params, camera_matrix, R=None, t=None): # type: ignore[no-untyped-def] """ Legacy function for backward compatibility. Use visualize_cuboid_on_image instead. diff --git a/dimos/perception/pointcloud/pointcloud_filtering.py b/dimos/perception/pointcloud/pointcloud_filtering.py index 4ca8a0c84b..d6aa2b835f 100644 --- a/dimos/perception/pointcloud/pointcloud_filtering.py +++ b/dimos/perception/pointcloud/pointcloud_filtering.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ import cv2 import numpy as np -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] import torch from dimos.perception.pointcloud.cuboid_fit import fit_cuboid @@ -37,8 +37,8 @@ class PointcloudFiltering: def __init__( self, - color_intrinsics: str | list[float] | np.ndarray | None = None, - depth_intrinsics: str | list[float] | np.ndarray | None = None, + color_intrinsics: str | list[float] | np.ndarray | None = None, # type: ignore[type-arg] + depth_intrinsics: str | list[float] | np.ndarray | None = None, # type: ignore[type-arg] color_weight: float = 0.3, enable_statistical_filtering: bool = True, statistical_neighbors: int = 20, @@ -106,21 +106,24 @@ def __init__( # Store the full point cloud self.full_pcd = None - def generate_color_from_id(self, object_id: int) -> np.ndarray: + def generate_color_from_id(self, object_id: int) -> np.ndarray: # type: ignore[type-arg] """Generate a consistent color for a given object ID.""" np.random.seed(object_id) color = np.random.randint(0, 255, 3, dtype=np.uint8) np.random.seed(None) return color - def _validate_inputs( - self, color_img: np.ndarray, depth_img: np.ndarray, objects: list[ObjectData] + def _validate_inputs( # type: ignore[no-untyped-def] + self, + color_img: np.ndarray, # type: ignore[type-arg] + depth_img: np.ndarray, # type: ignore[type-arg] + objects: list[ObjectData], ): """Validate input parameters.""" if color_img.shape[:2] != depth_img.shape: raise ValueError("Color and depth image dimensions don't match") - def _prepare_masks(self, masks: list[np.ndarray], target_shape: tuple) -> list[np.ndarray]: + def _prepare_masks(self, masks: list[np.ndarray], target_shape: tuple) -> list[np.ndarray]: # type: ignore[type-arg] """Prepare and validate masks to match target shape.""" processed_masks = [] for mask in masks: @@ -147,7 +150,9 @@ def _prepare_masks(self, masks: list[np.ndarray], target_shape: tuple) -> list[n return processed_masks def _apply_color_mask( - self, pcd: o3d.geometry.PointCloud, rgb_color: np.ndarray + self, + pcd: o3d.geometry.PointCloud, + rgb_color: np.ndarray, # type: ignore[type-arg] ) -> o3d.geometry.PointCloud: """Apply weighted color mask to point cloud.""" if len(np.asarray(pcd.colors)) > 0: @@ -184,7 +189,7 @@ def _apply_subsampling(self, pcd: o3d.geometry.PointCloud) -> o3d.geometry.Point return pcd.voxel_down_sample(self.voxel_size) return pcd - def _extract_masks_from_objects(self, objects: list[ObjectData]) -> list[np.ndarray]: + def _extract_masks_from_objects(self, objects: list[ObjectData]) -> list[np.ndarray]: # type: ignore[type-arg] """Extract segmentation masks from ObjectData objects.""" return [obj["segmentation_mask"] for obj in objects] @@ -193,7 +198,10 @@ def get_full_point_cloud(self) -> o3d.geometry.PointCloud: return self._apply_subsampling(self.full_pcd) def process_images( - self, color_img: np.ndarray, depth_img: np.ndarray, objects: list[ObjectData] + self, + color_img: np.ndarray, # type: ignore[type-arg] + depth_img: np.ndarray, # type: ignore[type-arg] + objects: list[ObjectData], ) -> list[ObjectData]: """ Process color and depth images with object detection results to create filtered point clouds. @@ -267,7 +275,11 @@ def process_images( # Create point clouds efficiently self.full_pcd, masked_pcds = create_point_cloud_and_extract_masks( - color_img, depth_img, processed_masks, self.depth_camera_matrix, depth_scale=1.0 + color_img, + depth_img, + processed_masks, + self.depth_camera_matrix, # type: ignore[arg-type] + depth_scale=1.0, ) # Process each object and update ObjectData @@ -346,7 +358,7 @@ def process_images( colors_array = np.zeros((len(points_array), 3), dtype=np.float32) updated_obj["point_cloud_numpy"] = points_array - updated_obj["colors_numpy"] = colors_array + updated_obj["colors_numpy"] = colors_array # type: ignore[typeddict-unknown-key] updated_objects.append(updated_obj) diff --git a/dimos/perception/pointcloud/test_pointcloud_filtering.py b/dimos/perception/pointcloud/test_pointcloud_filtering.py index 719feeb984..4ac7e5cb2d 100644 --- a/dimos/perception/pointcloud/test_pointcloud_filtering.py +++ b/dimos/perception/pointcloud/test_pointcloud_filtering.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/pointcloud/utils.py b/dimos/perception/pointcloud/utils.py index 97dc8d3716..b2bb561000 100644 --- a/dimos/perception/pointcloud/utils.py +++ b/dimos/perception/pointcloud/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,16 +24,16 @@ import cv2 import numpy as np -import open3d as o3d -from scipy.spatial import cKDTree -import yaml +import open3d as o3d # type: ignore[import-untyped] +from scipy.spatial import cKDTree # type: ignore[import-untyped] +import yaml # type: ignore[import-untyped] from dimos.perception.common.utils import project_3d_points_to_2d def load_camera_matrix_from_yaml( - camera_info: str | list[float] | np.ndarray | dict | None, -) -> np.ndarray | None: + camera_info: str | list[float] | np.ndarray | dict | None, # type: ignore[type-arg] +) -> np.ndarray | None: # type: ignore[type-arg] """ Load camera intrinsic matrix from various input formats. @@ -85,7 +85,7 @@ def load_camera_matrix_from_yaml( ) -def _extract_matrix_from_dict(data: dict) -> np.ndarray: +def _extract_matrix_from_dict(data: dict) -> np.ndarray: # type: ignore[type-arg] """Extract camera matrix from dictionary with various formats.""" # ROS format with 'K' field (most common) if "K" in data: @@ -122,9 +122,9 @@ def _extract_matrix_from_dict(data: dict) -> np.ndarray: def create_o3d_point_cloud_from_rgbd( - color_img: np.ndarray, - depth_img: np.ndarray, - intrinsic: np.ndarray, + color_img: np.ndarray, # type: ignore[type-arg] + depth_img: np.ndarray, # type: ignore[type-arg] + intrinsic: np.ndarray, # type: ignore[type-arg] depth_scale: float = 1.0, depth_trunc: float = 3.0, ) -> o3d.geometry.PointCloud: @@ -199,10 +199,10 @@ def create_o3d_point_cloud_from_rgbd( def create_point_cloud_and_extract_masks( - color_img: np.ndarray, - depth_img: np.ndarray, - masks: list[np.ndarray], - intrinsic: np.ndarray, + color_img: np.ndarray, # type: ignore[type-arg] + depth_img: np.ndarray, # type: ignore[type-arg] + masks: list[np.ndarray], # type: ignore[type-arg] + intrinsic: np.ndarray, # type: ignore[type-arg] depth_scale: float = 1.0, depth_trunc: float = 3.0, ) -> tuple[o3d.geometry.PointCloud, list[o3d.geometry.PointCloud]]: @@ -269,7 +269,7 @@ def create_point_cloud_and_extract_masks( def filter_point_cloud_statistical( pcd: o3d.geometry.PointCloud, nb_neighbors: int = 20, std_ratio: float = 2.0 -) -> tuple[o3d.geometry.PointCloud, np.ndarray]: +) -> tuple[o3d.geometry.PointCloud, np.ndarray]: # type: ignore[type-arg] """ Apply statistical outlier filtering to point cloud. @@ -284,12 +284,12 @@ def filter_point_cloud_statistical( if len(np.asarray(pcd.points)) == 0: return pcd, np.array([]) - return pcd.remove_statistical_outlier(nb_neighbors=nb_neighbors, std_ratio=std_ratio) + return pcd.remove_statistical_outlier(nb_neighbors=nb_neighbors, std_ratio=std_ratio) # type: ignore[no-any-return] def filter_point_cloud_radius( pcd: o3d.geometry.PointCloud, nb_points: int = 16, radius: float = 0.05 -) -> tuple[o3d.geometry.PointCloud, np.ndarray]: +) -> tuple[o3d.geometry.PointCloud, np.ndarray]: # type: ignore[type-arg] """ Apply radius-based outlier filtering to point cloud. @@ -304,17 +304,17 @@ def filter_point_cloud_radius( if len(np.asarray(pcd.points)) == 0: return pcd, np.array([]) - return pcd.remove_radius_outlier(nb_points=nb_points, radius=radius) + return pcd.remove_radius_outlier(nb_points=nb_points, radius=radius) # type: ignore[no-any-return] def overlay_point_clouds_on_image( - base_image: np.ndarray, + base_image: np.ndarray, # type: ignore[type-arg] point_clouds: list[o3d.geometry.PointCloud], - camera_intrinsics: list[float] | np.ndarray, + camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] colors: list[tuple[int, int, int]], point_size: int = 2, alpha: float = 0.7, -) -> np.ndarray: +) -> np.ndarray: # type: ignore[type-arg] """ Overlay multiple colored point clouds onto an image. @@ -368,7 +368,7 @@ def overlay_point_clouds_on_image( # Ensure color is a tuple of integers for OpenCV if isinstance(color, list | tuple | np.ndarray): - color = tuple(int(c) for c in color[:3]) + color = tuple(int(c) for c in color[:3]) # type: ignore[assignment] else: color = (255, 255, 255) @@ -385,10 +385,10 @@ def overlay_point_clouds_on_image( def create_point_cloud_overlay_visualization( - base_image: np.ndarray, - objects: list[dict], - intrinsics: np.ndarray, -) -> np.ndarray: + base_image: np.ndarray, # type: ignore[type-arg] + objects: list[dict], # type: ignore[type-arg] + intrinsics: np.ndarray, # type: ignore[type-arg] +) -> np.ndarray: # type: ignore[type-arg] """ Create a visualization showing object point clouds and bounding boxes overlaid on a base image. @@ -457,7 +457,7 @@ def create_point_cloud_overlay_visualization( return result -def create_3d_bounding_box_corners(position, rotation, size: int): +def create_3d_bounding_box_corners(position, rotation, size: int): # type: ignore[no-untyped-def] """ Create 8 corners of a 3D bounding box from position, rotation, and size. @@ -504,9 +504,9 @@ def create_3d_bounding_box_corners(position, rotation, size: int): ) # Get dimensions - width = size.get("width", 0.1) - height = size.get("height", 0.1) - depth = size.get("depth", 0.1) + width = size.get("width", 0.1) # type: ignore[attr-defined] + height = size.get("height", 0.1) # type: ignore[attr-defined] + depth = size.get("depth", 0.1) # type: ignore[attr-defined] # Create 8 corners of the bounding box (before rotation) corners = np.array( @@ -528,7 +528,7 @@ def create_3d_bounding_box_corners(position, rotation, size: int): return rotated_corners -def draw_3d_bounding_box_on_image(image, corners_2d, color, thickness: int = 2) -> None: +def draw_3d_bounding_box_on_image(image, corners_2d, color, thickness: int = 2) -> None: # type: ignore[no-untyped-def] """ Draw a 3D bounding box on an image using projected 2D corners. @@ -563,7 +563,7 @@ def draw_3d_bounding_box_on_image(image, corners_2d, color, thickness: int = 2) def extract_and_cluster_misc_points( full_pcd: o3d.geometry.PointCloud, - all_objects: list[dict], + all_objects: list[dict], # type: ignore[type-arg] eps: float = 0.03, min_points: int = 100, enable_filtering: bool = True, @@ -813,7 +813,7 @@ def _cluster_point_cloud_dbscan( return [pcd] # Return original point cloud as fallback -def get_standard_coordinate_transform(): +def get_standard_coordinate_transform(): # type: ignore[no-untyped-def] """ Get a standard coordinate transformation matrix for consistent visualization. @@ -859,7 +859,7 @@ def visualize_clustered_point_clouds( return # Apply standard coordinate transformation - transform = get_standard_coordinate_transform() + transform = get_standard_coordinate_transform() # type: ignore[no-untyped-call] geometries = [] for pcd in clustered_pcds: pcd_copy = o3d.geometry.PointCloud(pcd) @@ -919,7 +919,7 @@ def visualize_pcd( return # Apply standard coordinate transformation - transform = get_standard_coordinate_transform() + transform = get_standard_coordinate_transform() # type: ignore[no-untyped-call] pcd_copy = o3d.geometry.PointCloud(pcd) pcd_copy.transform(transform) geometries = [pcd_copy] @@ -982,7 +982,7 @@ def visualize_voxel_grid( coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame( size=coordinate_frame_size ) - coordinate_frame.transform(get_standard_coordinate_transform()) + coordinate_frame.transform(get_standard_coordinate_transform()) # type: ignore[no-untyped-call] geometries.append(coordinate_frame) print(f"Visualizing voxel grid with {len(voxel_grid.get_voxels())} voxels") @@ -1002,8 +1002,8 @@ def visualize_voxel_grid( def combine_object_pointclouds( - point_clouds: list[np.ndarray] | list[o3d.geometry.PointCloud], - colors: list[np.ndarray] | None = None, + point_clouds: list[np.ndarray] | list[o3d.geometry.PointCloud], # type: ignore[type-arg] + colors: list[np.ndarray] | None = None, # type: ignore[type-arg] ) -> o3d.geometry.PointCloud: """ Combine multiple point clouds into a single Open3D point cloud. @@ -1028,8 +1028,8 @@ def combine_object_pointclouds( points = np.asarray(pcd.points) all_points.append(points) if pcd.has_colors(): - colors = np.asarray(pcd.colors) - all_colors.append(colors) + colors = np.asarray(pcd.colors) # type: ignore[assignment] + all_colors.append(colors) # type: ignore[arg-type] if not all_points: return o3d.geometry.PointCloud() @@ -1044,10 +1044,10 @@ def combine_object_pointclouds( def extract_centroids_from_masks( - rgb_image: np.ndarray, - depth_image: np.ndarray, - masks: list[np.ndarray], - camera_intrinsics: list[float] | np.ndarray, + rgb_image: np.ndarray, # type: ignore[type-arg] + depth_image: np.ndarray, # type: ignore[type-arg] + masks: list[np.ndarray], # type: ignore[type-arg] + camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] ) -> list[dict[str, Any]]: """ Extract 3D centroids and orientations from segmentation masks. @@ -1069,10 +1069,10 @@ def extract_centroids_from_masks( if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: fx, fy, cx, cy = camera_intrinsics else: - fx = camera_intrinsics[0, 0] - fy = camera_intrinsics[1, 1] - cx = camera_intrinsics[0, 2] - cy = camera_intrinsics[1, 2] + fx = camera_intrinsics[0, 0] # type: ignore[call-overload] + fy = camera_intrinsics[1, 1] # type: ignore[call-overload] + cx = camera_intrinsics[0, 2] # type: ignore[call-overload] + cy = camera_intrinsics[1, 2] # type: ignore[call-overload] results = [] diff --git a/dimos/perception/segmentation/config/custom_tracker.yaml b/dimos/perception/segmentation/config/custom_tracker.yaml index 4386473086..7a6748ebf6 100644 --- a/dimos/perception/segmentation/config/custom_tracker.yaml +++ b/dimos/perception/segmentation/config/custom_tracker.yaml @@ -18,4 +18,4 @@ gmc_method: sparseOptFlow # method of global motion compensation # ReID model related thresh (not supported yet) proximity_thresh: 0.6 appearance_thresh: 0.35 -with_reid: False \ No newline at end of file +with_reid: False diff --git a/dimos/perception/segmentation/image_analyzer.py b/dimos/perception/segmentation/image_analyzer.py index 074ee7d605..06db712ac7 100644 --- a/dimos/perception/segmentation/image_analyzer.py +++ b/dimos/perception/segmentation/image_analyzer.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ def __init__(self) -> None: """ self.client = OpenAI() - def encode_image(self, image): + def encode_image(self, image): # type: ignore[no-untyped-def] """ Encodes an image to Base64. @@ -53,7 +53,7 @@ def encode_image(self, image): _, buffer = cv2.imencode(".jpg", image) return base64.b64encode(buffer).decode("utf-8") - def analyze_images(self, images, detail: str = "auto", prompt_type: str = "normal"): + def analyze_images(self, images, detail: str = "auto", prompt_type: str = "normal"): # type: ignore[no-untyped-def] """ Takes a list of cropped images and returns descriptions from OpenAI's Vision model. @@ -69,7 +69,7 @@ def analyze_images(self, images, detail: str = "auto", prompt_type: str = "norma { "type": "image_url", "image_url": { - "url": f"data:image/jpeg;base64,{self.encode_image(img)}", + "url": f"data:image/jpeg;base64,{self.encode_image(img)}", # type: ignore[no-untyped-call] "detail": detail, }, } @@ -86,7 +86,7 @@ def analyze_images(self, images, detail: str = "auto", prompt_type: str = "norma response = self.client.chat.completions.create( model="gpt-4o-mini", messages=[ - { + { # type: ignore[list-item, misc] "role": "user", "content": [{"type": "text", "text": prompt}, *image_data], } diff --git a/dimos/perception/segmentation/sam_2d_seg.py b/dimos/perception/segmentation/sam_2d_seg.py index b13ebc4c65..741f71a9ab 100644 --- a/dimos/perception/segmentation/sam_2d_seg.py +++ b/dimos/perception/segmentation/sam_2d_seg.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,8 +19,9 @@ import time import cv2 -import onnxruntime -from ultralytics import FastSAM +import onnxruntime # type: ignore[import-untyped] +import torch +from ultralytics import FastSAM # type: ignore[attr-defined, import-not-found] from dimos.perception.common.detection2d_tracker import get_tracked_results, target2dTracker from dimos.perception.segmentation.image_analyzer import ImageAnalyzer @@ -31,10 +32,9 @@ plot_results, ) from dimos.utils.data import get_data -from dimos.utils.gpu_utils import is_cuda_available from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.perception.segmentation.sam_2d_seg") +logger = setup_logger() class Sam2DSegmenter: @@ -48,14 +48,20 @@ def __init__( use_rich_labeling: bool = False, use_filtering: bool = True, ) -> None: - if is_cuda_available(): + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): logger.info("Using CUDA for SAM 2d segmenter") if hasattr(onnxruntime, "preload_dlls"): # Handles CUDA 11 / onnxruntime-gpu<=1.18 onnxruntime.preload_dlls(cuda=True, cudnn=True) self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + logger.info("Using Metal for SAM 2d segmenter") + self.device = "mps" else: logger.info("Using CPU for SAM 2d segmenter") self.device = "cpu" + # Core components self.model = FastSAM(get_data(model_path) / model_name) self.use_tracker = use_tracker @@ -86,13 +92,13 @@ def __init__( self.image_analyzer = ImageAnalyzer() self.min_analysis_interval = min_analysis_interval self.last_analysis_time = 0 - self.to_be_analyzed = deque() - self.object_names = {} + self.to_be_analyzed = deque() # type: ignore[var-annotated] + self.object_names = {} # type: ignore[var-annotated] self.analysis_executor = ThreadPoolExecutor(max_workers=1) self.current_future = None self.current_queue_ids = None - def process_image(self, image): + def process_image(self, image): # type: ignore[no-untyped-def] """Process an image and return segmentation results.""" results = self.model.track( source=image, @@ -145,7 +151,7 @@ def process_image(self, image): # Get tracked results tracked_masks, tracked_bboxes, tracked_target_ids, tracked_probs, tracked_names = ( - get_tracked_results(tracked_targets) + get_tracked_results(tracked_targets) # type: ignore[no-untyped-call] ) if self.use_analyzer: @@ -211,7 +217,7 @@ def process_image(self, image): ) return [], [], [], [], [] - def check_analysis_status(self, tracked_target_ids): + def check_analysis_status(self, tracked_target_ids): # type: ignore[no-untyped-def] """Check if analysis is complete and prepare new queue if needed.""" if not self.use_analyzer: return None, None @@ -255,12 +261,12 @@ def check_analysis_status(self, tracked_target_ids): return queue_indices, queue_ids return None, None - def run_analysis(self, frame, tracked_bboxes, tracked_target_ids) -> None: + def run_analysis(self, frame, tracked_bboxes, tracked_target_ids) -> None: # type: ignore[no-untyped-def] """Run queue image analysis in background.""" if not self.use_analyzer: return - queue_indices, queue_ids = self.check_analysis_status(tracked_target_ids) + queue_indices, queue_ids = self.check_analysis_status(tracked_target_ids) # type: ignore[no-untyped-call] if queue_indices: selected_bboxes = [tracked_bboxes[i] for i in queue_indices] cropped_images = crop_images_from_bboxes(frame, selected_bboxes) @@ -274,11 +280,11 @@ def run_analysis(self, frame, tracked_bboxes, tracked_target_ids) -> None: else: prompt_type = "normal" - self.current_future = self.analysis_executor.submit( + self.current_future = self.analysis_executor.submit( # type: ignore[assignment] self.image_analyzer.analyze_images, cropped_images, prompt_type=prompt_type ) - def get_object_names(self, track_ids, tracked_names: Sequence[str]): + def get_object_names(self, track_ids, tracked_names: Sequence[str]): # type: ignore[no-untyped-def] """Get object names for the given track IDs, falling back to tracked names.""" if not self.use_analyzer: return tracked_names @@ -288,7 +294,7 @@ def get_object_names(self, track_ids, tracked_names: Sequence[str]): for track_id, tracked_name in zip(track_ids, tracked_names, strict=False) ] - def visualize_results( + def visualize_results( # type: ignore[no-untyped-def] self, image, masks, bboxes, track_ids, probs: Sequence[float], names: Sequence[str] ): """Generate an overlay visualization with segmentation results and object names.""" @@ -333,7 +339,7 @@ def main() -> None: time.time() # Process image and get results - masks, bboxes, target_ids, probs, names = segmenter.process_image(frame) + masks, bboxes, target_ids, probs, names = segmenter.process_image(frame) # type: ignore[no-untyped-call] # Run analysis if enabled if segmenter.use_analyzer: diff --git a/dimos/perception/segmentation/test_sam_2d_seg.py b/dimos/perception/segmentation/test_sam_2d_seg.py index 23eaf02fa3..a9222ed2f2 100644 --- a/dimos/perception/segmentation/test_sam_2d_seg.py +++ b/dimos/perception/segmentation/test_sam_2d_seg.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/segmentation/utils.py b/dimos/perception/segmentation/utils.py index 24d6ce4bf2..a23a256ca2 100644 --- a/dimos/perception/segmentation/utils.py +++ b/dimos/perception/segmentation/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,13 +29,13 @@ def __init__( :param min_count: Minimum number of appearances required :param count_window: Number of latest frames to consider for counting """ - self.history = [] + self.history = [] # type: ignore[var-annotated] self.history_size = history_size self.min_count = min_count self.count_window = count_window - self.total_counts = {} + self.total_counts = {} # type: ignore[var-annotated] - def update(self, track_ids): + def update(self, track_ids): # type: ignore[no-untyped-def] # Add new frame's track IDs to history self.history.append(track_ids) if len(self.history) > self.history_size: @@ -57,12 +57,12 @@ def update(self, track_ids): # Return IDs that appear often enough return [track_id for track_id, count in id_counts.items() if count >= self.min_count] - def get_total_counts(self): + def get_total_counts(self): # type: ignore[no-untyped-def] """Returns the total count of each tracking ID seen over time, limited to history size.""" return self.total_counts -def extract_masks_bboxes_probs_names(result, max_size: float = 0.7): +def extract_masks_bboxes_probs_names(result, max_size: float = 0.7): # type: ignore[no-untyped-def] """ Extracts masks, bounding boxes, probabilities, and class names from one Ultralytics result object. @@ -73,12 +73,12 @@ def extract_masks_bboxes_probs_names(result, max_size: float = 0.7): Returns: tuple: (masks, bboxes, track_ids, probs, names, areas) """ - masks = [] - bboxes = [] - track_ids = [] - probs = [] - names = [] - areas = [] + masks = [] # type: ignore[var-annotated] + bboxes = [] # type: ignore[var-annotated] + track_ids = [] # type: ignore[var-annotated] + probs = [] # type: ignore[var-annotated] + names = [] # type: ignore[var-annotated] + areas = [] # type: ignore[var-annotated] if result.masks is None: return masks, bboxes, track_ids, probs, names, areas @@ -114,7 +114,7 @@ def extract_masks_bboxes_probs_names(result, max_size: float = 0.7): return masks, bboxes, track_ids, probs, names, areas -def compute_texture_map(frame, blur_size: int = 3): +def compute_texture_map(frame, blur_size: int = 3): # type: ignore[no-untyped-def] """ Compute texture map using gradient statistics. Returns high values for textured regions and low values for smooth regions. @@ -152,7 +152,7 @@ def compute_texture_map(frame, blur_size: int = 3): return texture_map -def filter_segmentation_results( +def filter_segmentation_results( # type: ignore[no-untyped-def] frame, masks, bboxes, @@ -210,7 +210,7 @@ def filter_segmentation_results( if texture_value >= texture_threshold: mask_sum[mask > 0] = i filtered_texture_values.append( - texture_value.item() + texture_value.item() # type: ignore[union-attr] ) # Store the texture value as a Python float # Get indices that appear in mask_sum (these are the masks we want to keep) @@ -240,7 +240,7 @@ def filter_segmentation_results( ) -def plot_results( +def plot_results( # type: ignore[no-untyped-def] image, masks, bboxes, @@ -313,7 +313,7 @@ def plot_results( return result -def crop_images_from_bboxes(image, bboxes, buffer: int = 0): +def crop_images_from_bboxes(image, bboxes, buffer: int = 0): # type: ignore[no-untyped-def] """ Crops regions from an image based on bounding boxes with an optional buffer. diff --git a/dimos/perception/spatial_perception.py b/dimos/perception/spatial_perception.py index a11ccd615c..013d242ba8 100644 --- a/dimos/perception/spatial_perception.py +++ b/dimos/perception/spatial_perception.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,25 +19,27 @@ from datetime import datetime import os import time -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional import uuid import cv2 import numpy as np -from reactivex import Observable, disposable, interval, operators as ops +from reactivex import Observable, interval, operators as ops from reactivex.disposable import Disposable -from dimos.agents.memory.image_embedding import ImageEmbeddingProvider -from dimos.agents.memory.spatial_vector_db import SpatialVectorDB -from dimos.agents.memory.visual_memory import VisualMemory +from dimos import spec +from dimos.agents_deprecated.memory.image_embedding import ImageEmbeddingProvider +from dimos.agents_deprecated.memory.spatial_vector_db import SpatialVectorDB +from dimos.agents_deprecated.memory.visual_memory import VisualMemory from dimos.constants import DIMOS_PROJECT_ROOT -from dimos.core import In, Module, rpc -from dimos.msgs.geometry_msgs import Pose, PoseStamped, Vector3 +from dimos.core import DimosCluster, In, Module, rpc from dimos.msgs.sensor_msgs import Image from dimos.types.robot_location import RobotLocation -from dimos.types.vector import Vector from dimos.utils.logging_config import setup_logger +if TYPE_CHECKING: + from dimos.msgs.geometry_msgs import Vector3 + _OUTPUT_DIR = DIMOS_PROJECT_ROOT / "assets" / "output" _MEMORY_DIR = _OUTPUT_DIR / "memory" _SPATIAL_MEMORY_DIR = _MEMORY_DIR / "spatial_memory" @@ -45,7 +47,7 @@ _VISUAL_MEMORY_PATH = _SPATIAL_MEMORY_DIR / "visual_memory.pkl" -logger = setup_logger(__file__) +logger = setup_logger() class SpatialMemory(Module): @@ -59,8 +61,7 @@ class SpatialMemory(Module): """ # LCM inputs - color_image: In[Image] = None - odom: In[PoseStamped] = None + color_image: In[Image] def __init__( self, @@ -148,7 +149,8 @@ def __init__( try: logger.info(f"Loading existing visual memory from {visual_memory_path}...") self._visual_memory = VisualMemory.load( - visual_memory_path, output_dir=output_dir + visual_memory_path, # type: ignore[arg-type] + output_dir=output_dir, ) logger.info(f"Loaded {self._visual_memory.count()} images from previous runs") except Exception as e: @@ -172,15 +174,11 @@ def __init__( self.frame_count: int = 0 self.stored_frame_count: int = 0 - # For tracking stream subscription - self._subscription = None - # List to store robot locations self.robot_locations: list[RobotLocation] = [] # Track latest data for processing - self._latest_video_frame: np.ndarray | None = None - self._latest_odom: PoseStamped | None = None + self._latest_video_frame: np.ndarray | None = None # type: ignore[type-arg] self._process_interval = 1 logger.info(f"SpatialMemory initialized with model {embedding_model}") @@ -199,23 +197,15 @@ def set_video(image_msg: Image) -> None: else: logger.warning("Received image message without data attribute") - def set_odom(odom_msg: PoseStamped) -> None: - self._latest_odom = odom_msg - unsub = self.color_image.subscribe(set_video) self._disposables.add(Disposable(unsub)) - unsub = self.odom.subscribe(set_odom) - self._disposables.add(Disposable(unsub)) - # Start periodic processing using interval - unsub = interval(self._process_interval).subscribe(lambda _: self._process_frame()) + unsub = interval(self._process_interval).subscribe(lambda _: self._process_frame()) # type: ignore[assignment] self._disposables.add(Disposable(unsub)) @rpc def stop(self) -> None: - self.stop_continuous_processing() - # Save data before shutdown self.save() @@ -226,17 +216,12 @@ def stop(self) -> None: def _process_frame(self) -> None: """Process the latest frame with pose data if available.""" - if self._latest_video_frame is None or self._latest_odom is None: + tf = self.tf.get("map", "base_link") + if self._latest_video_frame is None or tf is None: return - # Extract position and rotation from odometry - position = self._latest_odom.position - orientation = self._latest_odom.orientation - # Create Pose object with position and orientation - current_pose = Pose( - position=Vector3(position.x, position.y, position.z), orientation=orientation - ) + current_pose = tf.to_pose() # Process the frame directly try: @@ -272,9 +257,8 @@ def _process_frame(self) -> None: frame_embedding = self.embedding_provider.get_embedding(self._latest_video_frame) frame_id = f"frame_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}" - # Get euler angles from quaternion orientation for metadata - euler = orientation.to_euler() + euler = tf.rotation.to_euler() # Create metadata dictionary with primitive types only metadata = { @@ -318,7 +302,7 @@ def _process_frame(self) -> None: @rpc def query_by_location( self, x: float, y: float, radius: float = 2.0, limit: int = 5 - ) -> list[dict]: + ) -> list[dict]: # type: ignore[type-arg] """ Query the vector database for images near the specified location. @@ -333,72 +317,6 @@ def query_by_location( """ return self.vector_db.query_by_location(x, y, radius, limit) - def start_continuous_processing( - self, video_stream: Observable, get_pose: callable - ) -> disposable.Disposable: - """ - Start continuous processing of video frames from an Observable stream. - - Args: - video_stream: Observable of video frames - get_pose: Callable that returns position and rotation for each frame - - Returns: - Disposable subscription that can be used to stop processing - """ - # Stop any existing subscription - self.stop_continuous_processing() - - # Map each video frame to include transform data - combined_stream = video_stream.pipe( - ops.map(lambda video_frame: {"frame": video_frame, **get_pose()}), - # Filter out bad transforms - ops.filter( - lambda data: data.get("position") is not None and data.get("rotation") is not None - ), - ) - - # Process with spatial memory - result_stream = self.process_stream(combined_stream) - - # Subscribe to the result stream - self._subscription = result_stream.subscribe( - on_next=self._on_frame_processed, - on_error=lambda e: logger.error(f"Error in spatial memory stream: {e}"), - on_completed=lambda: logger.info("Spatial memory stream completed"), - ) - - logger.info("Continuous spatial memory processing started") - return self._subscription - - def stop_continuous_processing(self) -> None: - """ - Stop continuous processing of video frames. - """ - if self._subscription is not None: - try: - self._subscription.dispose() - self._subscription = None - logger.info("Stopped continuous spatial memory processing") - except Exception as e: - logger.error(f"Error stopping spatial memory processing: {e}") - - def _on_frame_processed(self, result: dict[str, Any]) -> None: - """ - Handle updates from the spatial memory processing stream. - """ - # Log successful frame storage (if stored) - position = result.get("position") - if position is not None: - logger.debug( - f"Spatial memory updated with frame at ({position[0]:.2f}, {position[1]:.2f}, {position[2]:.2f})" - ) - - # Periodically save visual memory to disk (e.g., every 100 frames) - if self._visual_memory is not None and self.visual_memory_path is not None: - if self.stored_frame_count % 100 == 0: - self.save() - @rpc def save(self) -> bool: """ @@ -416,7 +334,7 @@ def save(self) -> bool: logger.error(f"Failed to save visual memory: {e}") return False - def process_stream(self, combined_stream: Observable) -> Observable: + def process_stream(self, combined_stream: Observable) -> Observable: # type: ignore[type-arg] """ Process a combined stream of video frames and positions. @@ -431,7 +349,7 @@ def process_stream(self, combined_stream: Observable) -> Observable: Observable of processing results, including the stored frame and its metadata """ - def process_combined_data(data): + def process_combined_data(data): # type: ignore[no-untyped-def] self.frame_count += 1 frame = data.get("frame") @@ -510,7 +428,7 @@ def process_combined_data(data): ) @rpc - def query_by_image(self, image: np.ndarray, limit: int = 5) -> list[dict]: + def query_by_image(self, image: np.ndarray, limit: int = 5) -> list[dict]: # type: ignore[type-arg] """ Query the vector database for images similar to the provided image. @@ -525,7 +443,7 @@ def query_by_image(self, image: np.ndarray, limit: int = 5) -> list[dict]: return self.vector_db.query_by_embedding(embedding, limit) @rpc - def query_by_text(self, text: str, limit: int = 5) -> list[dict]: + def query_by_text(self, text: str, limit: int = 5) -> list[dict]: # type: ignore[type-arg] """ Query the vector database for images matching the provided text description. @@ -583,29 +501,21 @@ def add_named_location( Returns: True if successfully added, False otherwise """ - # Use current position/rotation if not provided - if position is None and self._latest_odom is not None: - pos = self._latest_odom.position - position = [pos.x, pos.y, pos.z] - - if rotation is None and self._latest_odom is not None: - euler = self._latest_odom.orientation.to_euler() - rotation = [euler.x, euler.y, euler.z] - - if position is None: + tf = self.tf.get("map", "base_link") + if not tf: logger.error("No position available for robot location") return False # Create RobotLocation object - location = RobotLocation( + location = RobotLocation( # type: ignore[call-arg] name=name, - position=Vector(position), - rotation=Vector(rotation) if rotation else Vector([0, 0, 0]), + position=tf.translation, + rotation=tf.rotation.to_euler(), description=description or f"Location: {name}", timestamp=time.time(), ) - return self.add_robot_location(location) + return self.add_robot_location(location) # type: ignore[no-any-return] @rpc def get_robot_locations(self) -> list[RobotLocation]: @@ -662,6 +572,16 @@ def query_tagged_location(self, query: str) -> RobotLocation | None: return None +def deploy( # type: ignore[no-untyped-def] + dimos: DimosCluster, + camera: spec.Camera, +): + spatial_memory = dimos.deploy(SpatialMemory, db_path="/tmp/spatial_memory_db") # type: ignore[attr-defined] + spatial_memory.color_image.connect(camera.color_image) + spatial_memory.start() + return spatial_memory + + spatial_memory = SpatialMemory.blueprint -__all__ = ["SpatialMemory", "spatial_memory"] +__all__ = ["SpatialMemory", "deploy", "spatial_memory"] diff --git a/dimos/perception/test_spatial_memory.py b/dimos/perception/test_spatial_memory.py index f42638df73..d4b188ced3 100644 --- a/dimos/perception/test_spatial_memory.py +++ b/dimos/perception/test_spatial_memory.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/perception/test_spatial_memory_module.py b/dimos/perception/test_spatial_memory_module.py index f89c975d89..48a2b2750f 100644 --- a/dimos/perception/test_spatial_memory_module.py +++ b/dimos/perception/test_spatial_memory_module.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ from dimos.utils.logging_config import setup_logger from dimos.utils.testing import TimedSensorReplay -logger = setup_logger("test_spatial_memory_module") +logger = setup_logger() pubsub.lcm.autoconf() @@ -38,7 +38,7 @@ class VideoReplayModule(Module): """Module that replays video data from TimedSensorReplay.""" - video_out: Out[Image] = None + video_out: Out[Image] def __init__(self, video_path: str) -> None: super().__init__() @@ -75,7 +75,7 @@ def stop(self) -> None: class OdometryReplayModule(Module): """Module that replays odometry data from TimedSensorReplay.""" - odom_out: Out[Odometry] = None + odom_out: Out[Odometry] def __init__(self, odom_path: str) -> None: super().__init__() diff --git a/dimos/protocol/encode/__init__.py b/dimos/protocol/encode/__init__.py index 66bbbbb21c..87386a09e5 100644 --- a/dimos/protocol/encode/__init__.py +++ b/dimos/protocol/encode/__init__.py @@ -44,7 +44,7 @@ def encode(msg: MsgT) -> bytes: @staticmethod def decode(data: bytes) -> MsgT: - return json.loads(data.decode("utf-8")) + return json.loads(data.decode("utf-8")) # type: ignore[no-any-return] class LCM(Encoder[LCMMsgT, bytes]): @@ -64,7 +64,7 @@ def decode(data: bytes) -> LCMMsgT: ) -class LCMTypedEncoder(LCM, Generic[LCMMsgT]): +class LCMTypedEncoder(LCM, Generic[LCMMsgT]): # type: ignore[type-arg] """Typed LCM encoder for specific message types.""" def __init__(self, message_type: type[LCMMsgT]) -> None: @@ -81,7 +81,7 @@ def decode(data: bytes) -> LCMMsgT: def create_lcm_typed_encoder(message_type: type[LCMMsgT]) -> type[LCMTypedEncoder[LCMMsgT]]: """Factory function to create a typed LCM encoder for a specific message type.""" - class SpecificLCMEncoder(LCMTypedEncoder): + class SpecificLCMEncoder(LCMTypedEncoder): # type: ignore[type-arg] @staticmethod def decode(data: bytes) -> LCMMsgT: return message_type.decode(data) # type: ignore[return-value] diff --git a/dimos/protocol/mcp/README.md b/dimos/protocol/mcp/README.md new file mode 100644 index 0000000000..2a3c382484 --- /dev/null +++ b/dimos/protocol/mcp/README.md @@ -0,0 +1,30 @@ +# DimOS MCP Server + +Expose DimOS robot skills to Claude Code via Model Context Protocol. + +## Setup + +Add to Claude Code (one command): +```bash +claude mcp add --transport stdio dimos --scope project -- python -m dimos.protocol.mcp +``` + +## Usage + +**Terminal 1** - Start DimOS: +```bash +dimos --replay run unitree-go2-agentic +``` + +**Claude Code** - Use robot skills: +``` +> move forward 1 meter +> go to the kitchen +> tag this location as "desk" +``` + +## How It Works + +1. `llm_agent(mcp_port=9990)` in the blueprint starts a TCP server +2. Claude Code spawns the bridge (`--bridge`) which connects to `localhost:9990` +3. Skills are exposed as MCP tools (e.g., `relative_move`, `navigate_with_text`) diff --git a/dimos/protocol/mcp/__init__.py b/dimos/protocol/mcp/__init__.py new file mode 100644 index 0000000000..51432ba0cf --- /dev/null +++ b/dimos/protocol/mcp/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.protocol.mcp.mcp import MCPModule + +__all__ = ["MCPModule"] diff --git a/dimos/protocol/mcp/__main__.py b/dimos/protocol/mcp/__main__.py new file mode 100644 index 0000000000..a58e59d367 --- /dev/null +++ b/dimos/protocol/mcp/__main__.py @@ -0,0 +1,36 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""CLI entry point for Dimensional MCP Bridge. + +Connects Claude Code (or other MCP clients) to a running DimOS agent. + +Usage: + python -m dimos.protocol.mcp # Bridge to running DimOS on default port +""" + +from __future__ import annotations + +import asyncio + +from dimos.protocol.mcp.bridge import main as bridge_main + + +def main() -> None: + """Main entry point - connects to running DimOS via bridge.""" + asyncio.run(bridge_main()) + + +if __name__ == "__main__": + main() diff --git a/dimos/protocol/mcp/bridge.py b/dimos/protocol/mcp/bridge.py new file mode 100644 index 0000000000..0b09997798 --- /dev/null +++ b/dimos/protocol/mcp/bridge.py @@ -0,0 +1,53 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""MCP Bridge - Connects stdio (Claude Code) to TCP (DimOS Agent).""" + +import asyncio +import os +import sys + +DEFAULT_PORT = 9990 + + +async def main() -> None: + port = int(os.environ.get("MCP_PORT", DEFAULT_PORT)) + host = os.environ.get("MCP_HOST", "localhost") + + reader, writer = await asyncio.open_connection(host, port) + sys.stderr.write(f"MCP Bridge connected to {host}:{port}\n") + + async def stdin_to_tcp() -> None: + loop = asyncio.get_event_loop() + while True: + line = await loop.run_in_executor(None, sys.stdin.readline) + if not line: + break + writer.write(line.encode()) + await writer.drain() + + async def tcp_to_stdout() -> None: + while True: + data = await reader.readline() + if not data: + break + sys.stdout.write(data.decode()) + sys.stdout.flush() + + await asyncio.gather(stdin_to_tcp(), tcp_to_stdout()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/dimos/protocol/mcp/mcp.py b/dimos/protocol/mcp/mcp.py new file mode 100644 index 0000000000..f7427cd613 --- /dev/null +++ b/dimos/protocol/mcp/mcp.py @@ -0,0 +1,133 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import asyncio +import json +from typing import TYPE_CHECKING, Any +import uuid + +from dimos.core import Module, rpc +from dimos.protocol.skill.coordinator import SkillCoordinator, SkillStateEnum + +if TYPE_CHECKING: + from dimos.protocol.skill.coordinator import SkillState + + +class MCPModule(Module): + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(*args, **kwargs) + self.coordinator = SkillCoordinator() + self._server: asyncio.AbstractServer | None = None + self._server_future: object | None = None + + @rpc + def start(self) -> None: + super().start() + self.coordinator.start() + self._start_server() + + @rpc + def stop(self) -> None: + if self._server: + self._server.close() + loop = self._loop + assert loop is not None + asyncio.run_coroutine_threadsafe(self._server.wait_closed(), loop).result() + self._server = None + if self._server_future and hasattr(self._server_future, "cancel"): + self._server_future.cancel() + self.coordinator.stop() + super().stop() + + @rpc + def register_skills(self, container) -> None: # type: ignore[no-untyped-def] + self.coordinator.register_skills(container) + + def _start_server(self, port: int = 9990) -> None: + async def handle_client(reader, writer) -> None: # type: ignore[no-untyped-def] + while True: + if not (data := await reader.readline()): + break + response = await self._handle_request(json.loads(data.decode())) + writer.write(json.dumps(response).encode() + b"\n") + await writer.drain() + writer.close() + + async def start_server() -> None: + self._server = await asyncio.start_server(handle_client, "0.0.0.0", port) + await self._server.serve_forever() + + loop = self._loop + assert loop is not None + self._server_future = asyncio.run_coroutine_threadsafe(start_server(), loop) + + async def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: + method = request.get("method", "") + params = request.get("params", {}) or {} + req_id = request.get("id") + if method == "initialize": + init_result = { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": {"name": "dimensional", "version": "1.0.0"}, + } + return {"jsonrpc": "2.0", "id": req_id, "result": init_result} + if method == "tools/list": + tools = [ + { + "name": c.name, + "description": c.schema.get("function", {}).get("description", ""), + "inputSchema": c.schema.get("function", {}).get("parameters", {}), + } + for c in self.coordinator.skills().values() + if not c.hide_skill + ] + return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": tools}} + if method == "tools/call": + name = params.get("name") + args = params.get("arguments") or {} + if not isinstance(name, str): + return { + "jsonrpc": "2.0", + "id": req_id, + "error": {"code": -32602, "message": "Missing or invalid tool name"}, + } + if not isinstance(args, dict): + args = {} + call_id = str(uuid.uuid4()) + self.coordinator.call_skill(call_id, name, args) + result: SkillState | None = self.coordinator._skill_state.get(call_id) + try: + await asyncio.wait_for(self.coordinator.wait_for_updates(), timeout=5.0) + except asyncio.TimeoutError: + pass + if result is None: + text = "Skill not found" + elif result.state == SkillStateEnum.completed: + text = str(result.content()) if result.content() else "Completed" + elif result.state == SkillStateEnum.error: + text = f"Error: {result.content()}" + else: + text = f"Started ({result.state.name})" + return { + "jsonrpc": "2.0", + "id": req_id, + "result": {"content": [{"type": "text", "text": text}]}, + } + return { + "jsonrpc": "2.0", + "id": req_id, + "error": {"code": -32601, "message": f"Unknown: {method}"}, + } diff --git a/dimos/protocol/mcp/test_mcp_module.py b/dimos/protocol/mcp/test_mcp_module.py new file mode 100644 index 0000000000..1deb5b9057 --- /dev/null +++ b/dimos/protocol/mcp/test_mcp_module.py @@ -0,0 +1,208 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import asyncio +import json +import os +from pathlib import Path +import socket +import subprocess +import sys +import threading + +import pytest + +from dimos.protocol.mcp.mcp import MCPModule +from dimos.protocol.skill.coordinator import SkillStateEnum +from dimos.protocol.skill.skill import SkillContainer, skill + + +def test_unitree_blueprint_has_mcp() -> None: + contents = Path("dimos/robot/unitree_webrtc/unitree_go2_blueprints.py").read_text() + assert "agentic_mcp" in contents + assert "MCPModule.blueprint()" in contents + + +def test_mcp_module_request_flow() -> None: + class DummySkill: + def __init__(self) -> None: + self.name = "add" + self.hide_skill = False + self.schema = {"function": {"description": "", "parameters": {"type": "object"}}} + + class DummyState: + def __init__(self, content: int) -> None: + self.state = SkillStateEnum.completed + self._content = content + + def content(self) -> int: + return self._content + + class DummyCoordinator: + def __init__(self) -> None: + self._skill_state: dict[str, DummyState] = {} + + def skills(self) -> dict[str, DummySkill]: + return {"add": DummySkill()} + + def call_skill(self, call_id: str, _name: str, args: dict[str, int]) -> None: + self._skill_state[call_id] = DummyState(args["x"] + args["y"]) + + async def wait_for_updates(self) -> bool: + return True + + mcp = MCPModule.__new__(MCPModule) + mcp.coordinator = DummyCoordinator() + + response = asyncio.run(mcp._handle_request({"method": "tools/list", "id": 1})) + assert response["result"]["tools"][0]["name"] == "add" + + response = asyncio.run( + mcp._handle_request( + { + "method": "tools/call", + "id": 2, + "params": {"name": "add", "arguments": {"x": 2, "y": 3}}, + } + ) + ) + assert response["result"]["content"][0]["text"] == "5" + + +def test_mcp_module_handles_hidden_and_errors() -> None: + class DummySkill: + def __init__(self, name: str, hide_skill: bool) -> None: + self.name = name + self.hide_skill = hide_skill + self.schema = {"function": {"description": "", "parameters": {"type": "object"}}} + + class DummyState: + def __init__(self, state: SkillStateEnum, content: str | None) -> None: + self.state = state + self._content = content + + def content(self) -> str | None: + return self._content + + class DummyCoordinator: + def __init__(self) -> None: + self._skill_state: dict[str, DummyState] = {} + self._skills = { + "visible": DummySkill("visible", False), + "hidden": DummySkill("hidden", True), + "fail": DummySkill("fail", False), + } + + def skills(self) -> dict[str, DummySkill]: + return self._skills + + def call_skill(self, call_id: str, name: str, _args: dict[str, int]) -> None: + if name == "fail": + self._skill_state[call_id] = DummyState(SkillStateEnum.error, "boom") + elif name in self._skills: + self._skill_state[call_id] = DummyState(SkillStateEnum.running, None) + + async def wait_for_updates(self) -> bool: + return True + + mcp = MCPModule.__new__(MCPModule) + mcp.coordinator = DummyCoordinator() + + response = asyncio.run(mcp._handle_request({"method": "tools/list", "id": 1})) + tool_names = {tool["name"] for tool in response["result"]["tools"]} + assert "visible" in tool_names + assert "hidden" not in tool_names + + response = asyncio.run( + mcp._handle_request( + {"method": "tools/call", "id": 2, "params": {"name": "fail", "arguments": {}}} + ) + ) + assert "Error:" in response["result"]["content"][0]["text"] + + +def test_mcp_end_to_end_lcm_bridge() -> None: + try: + import lcm # type: ignore[import-untyped] + + lcm.LCM() + except Exception as exc: + if os.environ.get("CI"): + pytest.fail(f"LCM unavailable for MCP end-to-end test: {exc}") + pytest.skip("LCM unavailable for MCP end-to-end test.") + + try: + socket.socket(socket.AF_INET, socket.SOCK_STREAM).close() + except PermissionError: + if os.environ.get("CI"): + pytest.fail("Socket creation not permitted in CI environment.") + pytest.skip("Socket creation not permitted in this environment.") + + class TestSkills(SkillContainer): + @skill() + def add(self, x: int, y: int) -> int: + return x + y + + mcp = MCPModule() + mcp.start() + + try: + mcp.register_skills(TestSkills()) + + env = {"MCP_HOST": "127.0.0.1", "MCP_PORT": "9990"} + proc = subprocess.Popen( + [sys.executable, "-m", "dimos.protocol.mcp"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={**os.environ, **env}, + text=True, + ) + try: + request = {"jsonrpc": "2.0", "id": 1, "method": "tools/list"} + proc.stdin.write(json.dumps(request) + "\n") + proc.stdin.flush() + stdout = proc.stdout.readline() + assert '"tools"' in stdout + assert '"add"' in stdout + finally: + proc.terminate() + proc.wait(timeout=5) + + proc = subprocess.Popen( + [sys.executable, "-m", "dimos.protocol.mcp"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={**os.environ, **env}, + text=True, + ) + try: + request = { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": {"name": "add", "arguments": {"x": 2, "y": 3}}, + } + proc.stdin.write(json.dumps(request) + "\n") + proc.stdin.flush() + stdout = proc.stdout.readline() + assert "5" in stdout + finally: + proc.terminate() + proc.wait(timeout=5) + finally: + mcp.stop() diff --git a/dimos/protocol/pubsub/jpeg_shm.py b/dimos/protocol/pubsub/jpeg_shm.py index 68a97ec6b6..c61848c57a 100644 --- a/dimos/protocol/pubsub/jpeg_shm.py +++ b/dimos/protocol/pubsub/jpeg_shm.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,5 +16,5 @@ from dimos.protocol.pubsub.shmpubsub import SharedMemoryPubSubBase -class JpegSharedMemory(JpegSharedMemoryEncoderMixin, SharedMemoryPubSubBase): +class JpegSharedMemory(JpegSharedMemoryEncoderMixin, SharedMemoryPubSubBase): # type: ignore[misc] pass diff --git a/dimos/protocol/pubsub/lcmpubsub.py b/dimos/protocol/pubsub/lcmpubsub.py index ef158ffb30..9207e7dfc0 100644 --- a/dimos/protocol/pubsub/lcmpubsub.py +++ b/dimos/protocol/pubsub/lcmpubsub.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable -from turbojpeg import TurboJPEG +from turbojpeg import TurboJPEG # type: ignore[import-untyped] from dimos.msgs.sensor_msgs import Image from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ImageFormat @@ -29,7 +29,7 @@ from collections.abc import Callable import threading -logger = setup_logger(__name__) +logger = setup_logger() @runtime_checkable @@ -63,8 +63,7 @@ class LCMPubSubBase(LCMService, PubSub[Topic, Any]): _thread: threading.Thread | None _callbacks: dict[str, list[Callable[[Any], None]]] - def __init__(self, **kwargs) -> None: - LCMService.__init__(self, **kwargs) + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) self._callbacks = {} @@ -73,6 +72,7 @@ def publish(self, topic: Topic, message: bytes) -> None: if self.l is None: logger.error("Tried to publish after LCM was closed") return + self.l.publish(str(topic), message) def subscribe( @@ -88,6 +88,9 @@ def noop() -> None: lcm_subscription = self.l.subscribe(str(topic), lambda _, msg: callback(msg, topic)) + # Set queue capacity to 10000 to handle high-volume bursts + lcm_subscription.set_queue_capacity(10000) + def unsubscribe() -> None: if self.l is None: return @@ -110,18 +113,18 @@ def decode(self, msg: bytes, topic: Topic) -> LCMMsg: class JpegEncoderMixin(PubSubEncoderMixin[Topic, Any]): def encode(self, msg: LCMMsg, _: Topic) -> bytes: - return msg.lcm_jpeg_encode() + return msg.lcm_jpeg_encode() # type: ignore[attr-defined, no-any-return] def decode(self, msg: bytes, topic: Topic) -> LCMMsg: if topic.lcm_type is None: raise ValueError( f"Cannot decode message for topic '{topic.topic}': no lcm_type specified" ) - return topic.lcm_type.lcm_jpeg_decode(msg) + return topic.lcm_type.lcm_jpeg_decode(msg) # type: ignore[attr-defined, no-any-return] class JpegSharedMemoryEncoderMixin(PubSubEncoderMixin[str, Image]): - def __init__(self, quality: int = 75, **kwargs): + def __init__(self, quality: int = 75, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) self.jpeg = TurboJPEG() self.quality = quality @@ -131,7 +134,7 @@ def encode(self, msg: Any, _topic: str) -> bytes: raise ValueError("Can only encode images.") bgr_image = msg.to_bgr().to_opencv() - return self.jpeg.encode(bgr_image, quality=self.quality) + return self.jpeg.encode(bgr_image, quality=self.quality) # type: ignore[no-any-return] def decode(self, msg: bytes, _topic: str) -> Image: bgr_array = self.jpeg.decode(msg) @@ -145,7 +148,7 @@ class LCM( class PickleLCM( - PickleEncoderMixin, + PickleEncoderMixin, # type: ignore[type-arg] LCMPubSubBase, ): ... diff --git a/dimos/protocol/pubsub/memory.py b/dimos/protocol/pubsub/memory.py index 513dfd32cd..e46fc10500 100644 --- a/dimos/protocol/pubsub/memory.py +++ b/dimos/protocol/pubsub/memory.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ def unsubscribe(self, topic: str, callback: Callable[[Any, str], None]) -> None: pass -class MemoryWithJSONEncoder(PubSubEncoderMixin, Memory): +class MemoryWithJSONEncoder(PubSubEncoderMixin, Memory): # type: ignore[type-arg] """Memory PubSub with JSON encoding/decoding.""" def encode(self, msg: Any, topic: str) -> bytes: diff --git a/dimos/protocol/pubsub/redispubsub.py b/dimos/protocol/pubsub/redispubsub.py index 7d6c798f2c..6cc089e953 100644 --- a/dimos/protocol/pubsub/redispubsub.py +++ b/dimos/protocol/pubsub/redispubsub.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ from types import TracebackType from typing import Any -import redis +import redis # type: ignore[import-not-found] from dimos.protocol.pubsub.spec import PubSub from dimos.protocol.service.spec import Service @@ -40,7 +40,7 @@ class Redis(PubSub[str, Any], Service[RedisConfig]): default_config = RedisConfig - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) # Redis connections @@ -56,13 +56,13 @@ def start(self) -> None: """Start the Redis pub/sub service.""" if self._running: return - self._connect() + self._connect() # type: ignore[no-untyped-call] def stop(self) -> None: """Stop the Redis pub/sub service.""" self.close() - def _connect(self): + def _connect(self): # type: ignore[no-untyped-def] """Connect to Redis and set up pub/sub.""" try: self._client = redis.Redis( @@ -73,14 +73,14 @@ def _connect(self): **self.config.kwargs, ) # Test connection - self._client.ping() + self._client.ping() # type: ignore[attr-defined] - self._pubsub = self._client.pubsub() + self._pubsub = self._client.pubsub() # type: ignore[attr-defined] self._running = True # Start listener thread - self._listener_thread = threading.Thread(target=self._listen_loop, daemon=True) - self._listener_thread.start() + self._listener_thread = threading.Thread(target=self._listen_loop, daemon=True) # type: ignore[assignment] + self._listener_thread.start() # type: ignore[attr-defined] except Exception as e: raise ConnectionError( @@ -186,7 +186,7 @@ def close(self) -> None: self._callbacks.clear() - def __enter__(self): + def __enter__(self): # type: ignore[no-untyped-def] return self def __exit__( diff --git a/dimos/protocol/pubsub/shm/ipc_factory.py b/dimos/protocol/pubsub/shm/ipc_factory.py index 9aedbfa1c4..5f69c3dbd1 100644 --- a/dimos/protocol/pubsub/shm/ipc_factory.py +++ b/dimos/protocol/pubsub/shm/ipc_factory.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -62,30 +62,30 @@ def device(self) -> str: # "cpu" or "cuda" @property @abstractmethod - def shape(self) -> tuple: ... + def shape(self) -> tuple: ... # type: ignore[type-arg] @property @abstractmethod - def dtype(self) -> np.dtype: ... + def dtype(self) -> np.dtype: ... # type: ignore[type-arg] @abstractmethod - def publish(self, frame) -> None: + def publish(self, frame) -> None: # type: ignore[no-untyped-def] """Write into inactive buffer, then flip visible index (write control last).""" ... @abstractmethod - def read(self, last_seq: int = -1, require_new: bool = True): + def read(self, last_seq: int = -1, require_new: bool = True): # type: ignore[no-untyped-def] """Return (seq:int, ts_ns:int, view-or-None).""" ... @abstractmethod - def descriptor(self) -> dict: + def descriptor(self) -> dict: # type: ignore[type-arg] """Tiny JSON-safe descriptor (names/handles/shape/dtype/device).""" ... @classmethod @abstractmethod - def attach(cls, desc: dict) -> "FrameChannel": + def attach(cls, desc: dict) -> "FrameChannel": # type: ignore[type-arg] """Attach in another process.""" ... @@ -116,7 +116,7 @@ def _safe_unlink(name: str) -> None: class CpuShmChannel(FrameChannel): - def __init__( + def __init__( # type: ignore[no-untyped-def] self, shape, dtype=np.uint8, @@ -128,7 +128,7 @@ def __init__( self._dtype = np.dtype(dtype) self._nbytes = int(self._dtype.itemsize * np.prod(self._shape)) - def _create_or_open(name: str, size: int): + def _create_or_open(name: str, size: int): # type: ignore[no-untyped-def] try: shm = SharedMemory(create=True, size=size, name=name) owner = True @@ -147,7 +147,7 @@ def _create_or_open(name: str, size: int): self._shm_ctrl, own_c = _create_or_open(ctrl_name, 24) self._is_owner = own_d and own_c - self._ctrl = np.ndarray((3,), dtype=np.int64, buffer=self._shm_ctrl.buf) + self._ctrl = np.ndarray((3,), dtype=np.int64, buffer=self._shm_ctrl.buf) # type: ignore[var-annotated] if self._is_owner: self._ctrl[:] = 0 # initialize only once @@ -163,7 +163,7 @@ def _create_or_open(name: str, size: int): else None ) - def descriptor(self): + def descriptor(self): # type: ignore[no-untyped-def] return { "kind": "cpu", "shape": self._shape, @@ -178,19 +178,19 @@ def device(self) -> str: return "cpu" @property - def shape(self): + def shape(self): # type: ignore[no-untyped-def] return self._shape @property - def dtype(self): + def dtype(self): # type: ignore[no-untyped-def] return self._dtype - def publish(self, frame) -> None: + def publish(self, frame) -> None: # type: ignore[no-untyped-def] assert isinstance(frame, np.ndarray) assert frame.shape == self._shape and frame.dtype == self._dtype active = int(self._ctrl[2]) inactive = 1 - active - view = np.ndarray( + view = np.ndarray( # type: ignore[var-annotated] self._shape, dtype=self._dtype, buffer=self._shm_data.buf, @@ -203,12 +203,12 @@ def publish(self, frame) -> None: self._ctrl[2] = inactive self._ctrl[0] += 1 - def read(self, last_seq: int = -1, require_new: bool = True): + def read(self, last_seq: int = -1, require_new: bool = True): # type: ignore[no-untyped-def] for _ in range(3): seq1 = int(self._ctrl[0]) idx = int(self._ctrl[2]) ts = int(self._ctrl[1]) - view = np.ndarray( + view = np.ndarray( # type: ignore[var-annotated] self._shape, dtype=self._dtype, buffer=self._shm_data.buf, offset=idx * self._nbytes ) if seq1 == int(self._ctrl[0]): @@ -217,7 +217,7 @@ def read(self, last_seq: int = -1, require_new: bool = True): return seq1, ts, view return last_seq, 0, None - def descriptor(self): + def descriptor(self): # type: ignore[no-redef, no-untyped-def] return { "kind": "cpu", "shape": self._shape, @@ -228,13 +228,13 @@ def descriptor(self): } @classmethod - def attach(cls, desc: str): + def attach(cls, desc: str): # type: ignore[no-untyped-def, override] obj = object.__new__(cls) - obj._shape = tuple(desc["shape"]) - obj._dtype = np.dtype(desc["dtype"]) - obj._nbytes = int(desc["nbytes"]) - data_name = desc["data_name"] - ctrl_name = desc["ctrl_name"] + obj._shape = tuple(desc["shape"]) # type: ignore[index] + obj._dtype = np.dtype(desc["dtype"]) # type: ignore[index] + obj._nbytes = int(desc["nbytes"]) # type: ignore[index] + data_name = desc["data_name"] # type: ignore[index] + ctrl_name = desc["ctrl_name"] # type: ignore[index] try: obj._shm_data = _open_shm_with_retry(data_name) obj._shm_ctrl = _open_shm_with_retry(ctrl_name) @@ -287,13 +287,13 @@ class CPU_IPC_Factory: """Creates/attaches CPU shared-memory channels.""" @staticmethod - def create(shape, dtype=np.uint8) -> CpuShmChannel: + def create(shape, dtype=np.uint8) -> CpuShmChannel: # type: ignore[no-untyped-def] return CpuShmChannel(shape, dtype=dtype) @staticmethod - def attach(desc: dict) -> CpuShmChannel: + def attach(desc: dict) -> CpuShmChannel: # type: ignore[type-arg] assert desc.get("kind") == "cpu", "Descriptor kind mismatch" - return CpuShmChannel.attach(desc) + return CpuShmChannel.attach(desc) # type: ignore[arg-type, no-any-return] # --------------------------- @@ -301,7 +301,7 @@ def attach(desc: dict) -> CpuShmChannel: # --------------------------- -def make_frame_channel( +def make_frame_channel( # type: ignore[no-untyped-def] shape, dtype=np.uint8, prefer: str = "auto", device: int = 0 ) -> FrameChannel: """Choose CUDA IPC if available (or requested), otherwise CPU SHM.""" diff --git a/dimos/protocol/pubsub/shmpubsub.py b/dimos/protocol/pubsub/shmpubsub.py index bbbf2192d7..0006020f6c 100644 --- a/dimos/protocol/pubsub/shmpubsub.py +++ b/dimos/protocol/pubsub/shmpubsub.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ if TYPE_CHECKING: from collections.abc import Callable -logger = setup_logger("dimos.protocol.pubsub.sharedmemory") +logger = setup_logger() # -------------------------------------------------------------------------------------- @@ -88,7 +88,7 @@ class _TopicState: "thread", ) - def __init__(self, channel, capacity: int, cp_mod) -> None: + def __init__(self, channel, capacity: int, cp_mod) -> None: # type: ignore[no-untyped-def] self.channel = channel self.capacity = int(capacity) self.shape = (self.capacity + 20,) # +20 for header: length(4) + uuid(16) @@ -211,7 +211,7 @@ def _unsub() -> None: # ----- Capacity mgmt ---------------------------------------------------- - def reconfigure(self, topic: str, *, capacity: int) -> dict: + def reconfigure(self, topic: str, *, capacity: int) -> dict: # type: ignore[type-arg] """Change payload capacity (bytes) for a topic; returns new descriptor.""" st = self._ensure_topic(topic) new_cap = int(capacity) @@ -221,7 +221,7 @@ def reconfigure(self, topic: str, *, capacity: int) -> dict: st.shape = new_shape st.dtype = np.uint8 st.last_seq = -1 - return desc + return desc # type: ignore[no-any-return] # ----- Internals -------------------------------------------------------- @@ -233,8 +233,9 @@ def _ensure_topic(self, topic: str) -> _TopicState: cap = int(self.config.default_capacity) def _names_for_topic(topic: str, capacity: int) -> tuple[str, str]: - # Python’s SharedMemory requires names without a leading '/' - h = hashlib.blake2b(f"{topic}:{capacity}".encode(), digest_size=12).hexdigest() + # Python's SharedMemory requires names without a leading '/' + # Use shorter digest to avoid macOS shared memory name length limits + h = hashlib.blake2b(f"{topic}:{capacity}".encode(), digest_size=8).hexdigest() return f"psm_{h}_data", f"psm_{h}_ctrl" data_name, ctrl_name = _names_for_topic(topic, cap) diff --git a/dimos/protocol/pubsub/spec.py b/dimos/protocol/pubsub/spec.py index ef5a4f450f..28fce3faee 100644 --- a/dimos/protocol/pubsub/spec.py +++ b/dimos/protocol/pubsub/spec.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ TopicT = TypeVar("TopicT") -logger = setup_logger(__name__) +logger = setup_logger() class PubSub(Generic[TopicT, MsgT], ABC): @@ -55,10 +55,10 @@ def unsubscribe(self) -> None: self._unsubscribe_fn() # context-manager helper - def __enter__(self): + def __enter__(self): # type: ignore[no-untyped-def] return self - def __exit__(self, *exc) -> None: + def __exit__(self, *exc) -> None: # type: ignore[no-untyped-def] self.unsubscribe() # public helper: returns disposable object @@ -83,7 +83,7 @@ def _cb(msg: MsgT, topic: TopicT) -> None: # async context manager returning a queue @asynccontextmanager - async def queue(self, topic: TopicT, *, max_pending: int | None = None): + async def queue(self, topic: TopicT, *, max_pending: int | None = None): # type: ignore[no-untyped-def] q: asyncio.Queue[MsgT] = asyncio.Queue(maxsize=max_pending or 0) def _queue_cb(msg: MsgT, topic: TopicT) -> None: @@ -114,13 +114,13 @@ def encode(self, msg: MsgT, topic: TopicT) -> bytes: ... @abstractmethod def decode(self, msg: bytes, topic: TopicT) -> MsgT: ... - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) - self._encode_callback_map: dict = {} + self._encode_callback_map: dict = {} # type: ignore[type-arg] def publish(self, topic: TopicT, message: MsgT) -> None: """Encode the message and publish it.""" - if getattr(self, "_stop_event", None) is not None and self._stop_event.is_set(): + if getattr(self, "_stop_event", None) is not None and self._stop_event.is_set(): # type: ignore[attr-defined] return encoded_message = self.encode(message, topic) if encoded_message is None: @@ -136,11 +136,11 @@ def wrapper_cb(encoded_data: bytes, topic: TopicT) -> None: decoded_message = self.decode(encoded_data, topic) callback(decoded_message, topic) - return super().subscribe(topic, wrapper_cb) # type: ignore[misc] + return super().subscribe(topic, wrapper_cb) # type: ignore[misc, no-any-return] class PickleEncoderMixin(PubSubEncoderMixin[TopicT, MsgT]): - def encode(self, msg: MsgT, *_: TopicT) -> bytes: + def encode(self, msg: MsgT, *_: TopicT) -> bytes: # type: ignore[return] try: return pickle.dumps(msg) except Exception as e: @@ -151,4 +151,4 @@ def encode(self, msg: MsgT, *_: TopicT) -> bytes: print("Tried to pickle:", msg) def decode(self, msg: bytes, _: TopicT) -> MsgT: - return pickle.loads(msg) + return pickle.loads(msg) # type: ignore[no-any-return] diff --git a/dimos/protocol/pubsub/test_encoder.py b/dimos/protocol/pubsub/test_encoder.py index 9a47c14105..f39bd170d5 100644 --- a/dimos/protocol/pubsub/test_encoder.py +++ b/dimos/protocol/pubsub/test_encoder.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/protocol/pubsub/test_lcmpubsub.py b/dimos/protocol/pubsub/test_lcmpubsub.py index b089483164..d06bf20716 100644 --- a/dimos/protocol/pubsub/test_lcmpubsub.py +++ b/dimos/protocol/pubsub/test_lcmpubsub.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/protocol/pubsub/test_spec.py b/dimos/protocol/pubsub/test_spec.py index 2bc8ae3ea1..91e8514b70 100644 --- a/dimos/protocol/pubsub/test_spec.py +++ b/dimos/protocol/pubsub/test_spec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import pytest from dimos.msgs.geometry_msgs import Vector3 +from dimos.protocol.pubsub.lcmpubsub import LCM, Topic from dimos.protocol.pubsub.memory import Memory @@ -61,27 +62,21 @@ def redis_context(): print("Redis not available") -try: - from dimos.protocol.pubsub.lcmpubsub import LCM, Topic +@contextmanager +def lcm_context(): + lcm_pubsub = LCM(autoconf=True) + lcm_pubsub.start() + yield lcm_pubsub + lcm_pubsub.stop() - @contextmanager - def lcm_context(): - lcm_pubsub = LCM(autoconf=True) - lcm_pubsub.start() - yield lcm_pubsub - lcm_pubsub.stop() - testdata.append( - ( - lcm_context, - Topic(topic="/test_topic", lcm_type=Vector3), - [Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9)], # Using Vector3 as mock data, - ) +testdata.append( + ( + lcm_context, + Topic(topic="/test_topic", lcm_type=Vector3), + [Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9)], # Using Vector3 as mock data, ) - -except (ConnectionError, ImportError): - # either redis is not installed or the server is not running - print("LCM not available") +) from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory @@ -263,3 +258,40 @@ async def consume_messages() -> None: # Verify all messages were received in order assert len(received_messages) == len(messages_to_send) assert received_messages == messages_to_send + + +@pytest.mark.parametrize("pubsub_context, topic, values", testdata) +def test_high_volume_messages(pubsub_context, topic, values) -> None: + """Test that all 5000 messages are received correctly.""" + with pubsub_context() as x: + # Create a list to capture received messages + received_messages = [] + last_message_time = [time.time()] # Use list to allow modification in callback + + # Define callback function + def callback(message, topic) -> None: + received_messages.append(message) + last_message_time[0] = time.time() + + # Subscribe to the topic + x.subscribe(topic, callback) + + # Publish 10000 messages + num_messages = 10000 + for _ in range(num_messages): + x.publish(topic, values[0]) + + # Wait until no messages received for 0.5 seconds + timeout = 1.0 # Maximum time to wait + stable_duration = 0.1 # Time without new messages to consider done + start_time = time.time() + + while time.time() - start_time < timeout: + if time.time() - last_message_time[0] >= stable_duration: + break + time.sleep(0.1) + + # Capture count and clear list to avoid printing huge list on failure + received_len = len(received_messages) + received_messages.clear() + assert received_len == num_messages, f"Expected {num_messages} messages, got {received_len}" diff --git a/dimos/protocol/rpc/__init__.py b/dimos/protocol/rpc/__init__.py index 4061c9e9cf..1eb892d956 100644 --- a/dimos/protocol/rpc/__init__.py +++ b/dimos/protocol/rpc/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,5 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.protocol.rpc.pubsubrpc import LCMRPC, ShmRPC from dimos.protocol.rpc.spec import RPCClient, RPCServer, RPCSpec + +__all__ = ["LCMRPC", "RPCClient", "RPCServer", "RPCSpec", "ShmRPC"] diff --git a/dimos/protocol/rpc/lcmrpc.py b/dimos/protocol/rpc/lcmrpc.py deleted file mode 100644 index 7ff98b1338..0000000000 --- a/dimos/protocol/rpc/lcmrpc.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.constants import LCM_MAX_CHANNEL_NAME_LENGTH -from dimos.protocol.pubsub.lcmpubsub import PickleLCM, Topic -from dimos.protocol.rpc.pubsubrpc import PassThroughPubSubRPC -from dimos.utils.generic import short_id - - -class LCMRPC(PassThroughPubSubRPC, PickleLCM): - def topicgen(self, name: str, req_or_res: bool) -> Topic: - suffix = "res" if req_or_res else "req" - topic = f"/rpc/{name}/{suffix}" - if len(topic) > LCM_MAX_CHANNEL_NAME_LENGTH: - topic = f"/rpc/{short_id(name)}/{suffix}" - return Topic(topic=topic) diff --git a/dimos/protocol/rpc/off_test_pubsubrpc.py b/dimos/protocol/rpc/off_test_pubsubrpc.py deleted file mode 100644 index 940baad2f7..0000000000 --- a/dimos/protocol/rpc/off_test_pubsubrpc.py +++ /dev/null @@ -1,216 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from contextlib import contextmanager -import time - -import pytest - -from dimos.core import Module, rpc, start -from dimos.protocol.rpc.lcmrpc import LCMRPC -from dimos.protocol.service.lcmservice import autoconf - -testgrid: list[Callable] = [] - - -# test module we'll use for binding RPC methods -class MyModule(Module): - @rpc - def add(self, a: int, b: int = 30) -> int: - print(f"A + B = {a + b}") - return a + b - - @rpc - def subtract(self, a: int, b: int) -> int: - print(f"A - B = {a - b}") - return a - b - - -# This tests a generic RPC-over-PubSub implementation that can be used via any -# pubsub transport such as LCM or Redis in this test. -# -# (For transport systems that have call/reply type of functionaltity, we will -# not use PubSubRPC but implement protocol native RPC conforimg to -# RPCClient/RPCServer spec in spec.py) - - -# LCMRPC (mixed in PassThroughPubSubRPC into lcm pubsub) -@contextmanager -def lcm_rpc_context(): - server = LCMRPC(autoconf=True) - client = LCMRPC(autoconf=True) - server.start() - client.start() - yield [server, client] - server.stop() - client.stop() - - -testgrid.append(lcm_rpc_context) - - -# RedisRPC (mixed in in PassThroughPubSubRPC into redis pubsub) -try: - from dimos.protocol.rpc.redisrpc import RedisRPC - - @contextmanager - def redis_rpc_context(): - server = RedisRPC() - client = RedisRPC() - server.start() - client.start() - yield [server, client] - server.stop() - client.stop() - - testgrid.append(redis_rpc_context) - -except (ConnectionError, ImportError): - print("Redis not available") - - -@pytest.mark.parametrize("rpc_context", testgrid) -def test_basics(rpc_context) -> None: - with rpc_context() as (server, client): - - def remote_function(a: int, b: int): - return a + b - - # You can bind an arbitrary function to arbitrary name - # topics are: - # - # - /rpc/add/req - # - /rpc/add/res - server.serve_rpc(remote_function, "add") - - msgs = [] - - def receive_msg(response) -> None: - msgs.append(response) - print(f"Received response: {response}") - - client.call("add", ([1, 2], {}), receive_msg) - - time.sleep(0.1) - assert len(msgs) > 0 - - -@pytest.mark.parametrize("rpc_context", testgrid) -def test_module_autobind(rpc_context) -> None: - with rpc_context() as (server, client): - module = MyModule() - print("\n") - - # We take an endpoint name from __class__.__name__, - # so topics are: - # - # - /rpc/MyModule/method_name1/req - # - /rpc/MyModule/method_name1/res - # - # - /rpc/MyModule/method_name2/req - # - /rpc/MyModule/method_name2/res - # - # etc - server.serve_module_rpc(module) - - # can override the __class__.__name__ with something else - server.serve_module_rpc(module, "testmodule") - - msgs = [] - - def receive_msg(msg) -> None: - msgs.append(msg) - - client.call("MyModule/add", ([1, 2], {}), receive_msg) - client.call("testmodule/subtract", ([3, 1], {}), receive_msg) - - time.sleep(0.1) - assert len(msgs) == 2 - assert msgs == [3, 2] - - -# Default rpc.call() either doesn't wait for response or accepts a callback -# but also we support different calling strategies, -# -# can do blocking calls -@pytest.mark.parametrize("rpc_context", testgrid) -def test_sync(rpc_context) -> None: - with rpc_context() as (server, client): - module = MyModule() - print("\n") - - server.serve_module_rpc(module) - assert 3 == client.call_sync("MyModule/add", ([1, 2], {}))[0] - - -# Default rpc.call() either doesn't wait for response or accepts a callback -# but also we support different calling strategies, -# -# can do blocking calls -@pytest.mark.parametrize("rpc_context", testgrid) -def test_kwargs(rpc_context) -> None: - with rpc_context() as (server, client): - module = MyModule() - print("\n") - - server.serve_module_rpc(module) - - assert 3 == client.call_sync("MyModule/add", ([1, 2], {}))[0] - - -# or async calls as well -@pytest.mark.parametrize("rpc_context", testgrid) -@pytest.mark.asyncio -async def test_async(rpc_context) -> None: - with rpc_context() as (server, client): - module = MyModule() - print("\n") - server.serve_module_rpc(module) - assert 3 == await client.call_async("MyModule/add", ([1, 2], {})) - - -# or async calls as well -@pytest.mark.module -def test_rpc_full_deploy() -> None: - autoconf() - - # test module we'll use for binding RPC methods - class CallerModule(Module): - remote: Callable[[int, int], int] - - def __init__(self, remote: Callable[[int, int], int]) -> None: - self.remote = remote - super().__init__() - - @rpc - def add(self, a: int, b: int = 30) -> int: - return self.remote(a, b) - - dimos = start(2) - - module = dimos.deploy(MyModule) - caller = dimos.deploy(CallerModule, module.add) - - print("deployed", module) - print("deployed", caller) - - # standard list args - assert caller.add(1, 2) == 3 - # default args - assert caller.add(1) == 31 - # kwargs - assert caller.add(1, b=1) == 2 - - dimos.shutdown() diff --git a/dimos/protocol/rpc/pubsubrpc.py b/dimos/protocol/rpc/pubsubrpc.py index 033cb7a5e2..05df80aec0 100644 --- a/dimos/protocol/rpc/pubsubrpc.py +++ b/dimos/protocol/rpc/pubsubrpc.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ from abc import abstractmethod from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +import threading import time from typing import ( TYPE_CHECKING, @@ -25,21 +27,26 @@ TypeVar, ) +from dimos.constants import LCM_MAX_CHANNEL_NAME_LENGTH +from dimos.protocol.pubsub.lcmpubsub import PickleLCM, Topic +from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory from dimos.protocol.pubsub.spec import PubSub +from dimos.protocol.rpc.rpc_utils import deserialize_exception, serialize_exception from dimos.protocol.rpc.spec import Args, RPCSpec +from dimos.utils.generic import short_id from dimos.utils.logging_config import setup_logger if TYPE_CHECKING: from types import FunctionType -logger = setup_logger(__file__) +logger = setup_logger() MsgT = TypeVar("MsgT") TopicT = TypeVar("TopicT") # (name, true_if_response_topic) -> TopicT TopicGen = Callable[[str, bool], TopicT] -MsgGen = Callable[[str, list], MsgT] +MsgGen = Callable[[str, list], MsgT] # type: ignore[type-arg] class RPCReq(TypedDict): @@ -48,60 +55,197 @@ class RPCReq(TypedDict): args: Args -class RPCRes(TypedDict): +class RPCRes(TypedDict, total=False): id: float res: Any + exception: dict[str, Any] | None # Contains exception info: type, message, traceback class PubSubRPCMixin(RPCSpec, PubSub[TopicT, MsgT], Generic[TopicT, MsgT]): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # Thread pool for RPC handler execution (prevents deadlock in nested calls) + self._call_thread_pool: ThreadPoolExecutor | None = None + self._call_thread_pool_lock = threading.RLock() + self._call_thread_pool_max_workers = 50 + + # Shared response subscriptions: one per RPC name instead of one per call + # Maps str(topic_res) -> (subscription, {msg_id -> callback}) + self._response_subs: dict[str, tuple[Any, dict[float, Callable[..., Any]]]] = {} + self._response_subs_lock = threading.RLock() + + # Message ID counter for unique IDs even with concurrent calls + self._msg_id_counter = 0 + self._msg_id_lock = threading.Lock() + + def __getstate__(self) -> dict[str, Any]: + state: dict[str, Any] + if hasattr(super(), "__getstate__"): + state = super().__getstate__() # type: ignore[assignment] + else: + state = self.__dict__.copy() + + # Exclude unpicklable attributes when serializing. + state.pop("_call_thread_pool", None) + state.pop("_call_thread_pool_lock", None) + state.pop("_response_subs", None) + state.pop("_response_subs_lock", None) + state.pop("_msg_id_lock", None) + + return state + + def __setstate__(self, state: dict[str, Any]) -> None: + if hasattr(super(), "__setstate__"): + super().__setstate__(state) # type: ignore[misc] + else: + self.__dict__.update(state) + + # Restore unserializable attributes. + self._call_thread_pool = None + self._call_thread_pool_lock = threading.RLock() + self._response_subs = {} + self._response_subs_lock = threading.RLock() + self._msg_id_lock = threading.Lock() + @abstractmethod def topicgen(self, name: str, req_or_res: bool) -> TopicT: ... - @abstractmethod - def _decodeRPCRes(self, msg: MsgT) -> RPCRes: ... + def _encodeRPCReq(self, req: RPCReq) -> dict[str, Any]: + return dict(req) - @abstractmethod - def _decodeRPCReq(self, msg: MsgT) -> RPCReq: ... + def _decodeRPCRes(self, msg: dict[Any, Any]) -> RPCRes: + return msg # type: ignore[return-value] - @abstractmethod - def _encodeRPCReq(self, res: RPCReq) -> MsgT: ... + def _encodeRPCRes(self, res: RPCRes) -> dict[str, Any]: + return dict(res) - @abstractmethod - def _encodeRPCRes(self, res: RPCRes) -> MsgT: ... + def _decodeRPCReq(self, msg: dict[Any, Any]) -> RPCReq: + return msg # type: ignore[return-value] - def call(self, name: str, arguments: Args, cb: Callable | None): + def _get_call_thread_pool(self) -> ThreadPoolExecutor: + """Get or create the thread pool for RPC handler execution (lazy initialization).""" + with self._call_thread_pool_lock: + if self._call_thread_pool is None: + self._call_thread_pool = ThreadPoolExecutor( + max_workers=self._call_thread_pool_max_workers + ) + return self._call_thread_pool + + def _shutdown_thread_pool(self) -> None: + """Safely shutdown the thread pool with deadlock prevention.""" + with self._call_thread_pool_lock: + if self._call_thread_pool: + # Check if we're being called from within the thread pool + # to avoid "cannot join current thread" error + current_thread = threading.current_thread() + is_pool_thread = False + + # Check if current thread is one of the pool's threads + if hasattr(self._call_thread_pool, "_threads"): + is_pool_thread = current_thread in self._call_thread_pool._threads + elif "ThreadPoolExecutor" in current_thread.name: + # Fallback: check thread name pattern + is_pool_thread = True + + # Don't wait if we're in a pool thread to avoid deadlock + self._call_thread_pool.shutdown(wait=not is_pool_thread) + self._call_thread_pool = None + + def stop(self) -> None: + """Stop the RPC service and cleanup thread pool. + + Subclasses that override this method should call super().stop() + to ensure the thread pool is properly shutdown. + """ + self._shutdown_thread_pool() + + # Cleanup shared response subscriptions + with self._response_subs_lock: + for unsub, _ in self._response_subs.values(): + unsub() + self._response_subs.clear() + + # Call parent stop if it exists + if hasattr(super(), "stop"): + super().stop() # type: ignore[misc] + + def call(self, name: str, arguments: Args, cb: Callable | None): # type: ignore[no-untyped-def, type-arg] if cb is None: return self.call_nowait(name, arguments) return self.call_cb(name, arguments, cb) - def call_cb(self, name: str, arguments: Args, cb: Callable) -> Any: + def call_cb(self, name: str, arguments: Args, cb: Callable[..., Any]) -> Any: topic_req = self.topicgen(name, False) topic_res = self.topicgen(name, True) - msg_id = float(time.time()) + + # Generate unique msg_id: timestamp + counter for concurrent calls + with self._msg_id_lock: + self._msg_id_counter += 1 + msg_id = time.time() + (self._msg_id_counter / 1_000_000) req: RPCReq = {"name": name, "args": arguments, "id": msg_id} - def receive_response(msg: MsgT, _: TopicT) -> None: - res = self._decodeRPCRes(msg) - if res.get("id") != msg_id: - return - time.sleep(0.01) - if unsub is not None: - unsub() - cb(res.get("res")) + # Get or create shared subscription for this RPC's response topic + topic_res_key = str(topic_res) + with self._response_subs_lock: + if topic_res_key not in self._response_subs: + # Create shared handler that routes to callbacks by msg_id + callbacks_dict: dict[float, Callable[..., Any]] = {} + + def shared_response_handler(msg: MsgT, _: TopicT) -> None: + res = self._decodeRPCRes(msg) # type: ignore[arg-type] + res_id = res.get("id") + if res_id is None: + return + + # Look up callback for this msg_id + with self._response_subs_lock: + callback = callbacks_dict.pop(res_id, None) + + if callback is None: + return # No callback registered (already handled or timed out) - unsub = self.subscribe(topic_res, receive_response) + # Check if response contains an exception + exc_data = res.get("exception") + if exc_data: + # Reconstruct the exception and pass it to the callback + from typing import cast - self.publish(topic_req, self._encodeRPCReq(req)) - return unsub + from dimos.protocol.rpc.rpc_utils import SerializedException + + exc = deserialize_exception(cast("SerializedException", exc_data)) + callback(exc) + else: + # Normal response - pass the result + callback(res.get("res")) + + # Create single shared subscription + unsub = self.subscribe(topic_res, shared_response_handler) + self._response_subs[topic_res_key] = (unsub, callbacks_dict) + + # Register this call's callback + _, callbacks_dict = self._response_subs[topic_res_key] + callbacks_dict[msg_id] = cb + + # Publish request + self.publish(topic_req, self._encodeRPCReq(req)) # type: ignore[arg-type] + + # Return unsubscribe function that removes this callback from the dict + def unsubscribe_callback() -> None: + with self._response_subs_lock: + if topic_res_key in self._response_subs: + _, callbacks_dict = self._response_subs[topic_res_key] + callbacks_dict.pop(msg_id, None) + + return unsubscribe_callback def call_nowait(self, name: str, arguments: Args) -> None: topic_req = self.topicgen(name, False) req: RPCReq = {"name": name, "args": arguments, "id": None} - self.publish(topic_req, self._encodeRPCReq(req)) + self.publish(topic_req, self._encodeRPCReq(req)) # type: ignore[arg-type] - def serve_rpc(self, f: FunctionType, name: str | None = None): + def serve_rpc(self, f: FunctionType, name: str | None = None): # type: ignore[no-untyped-def, override] if not name: name = f.__name__ @@ -109,10 +253,11 @@ def serve_rpc(self, f: FunctionType, name: str | None = None): topic_res = self.topicgen(name, True) def receive_call(msg: MsgT, _: TopicT) -> None: - req = self._decodeRPCReq(msg) + req = self._decodeRPCReq(msg) # type: ignore[arg-type] if req.get("name") != name: return + args = req.get("args") if args is None: return @@ -124,31 +269,50 @@ def execute_and_respond() -> None: response = f(*args[0], **args[1]) req_id = req.get("id") if req_id is not None: - self.publish(topic_res, self._encodeRPCRes({"id": req_id, "res": response})) + self.publish(topic_res, self._encodeRPCRes({"id": req_id, "res": response})) # type: ignore[arg-type] + except Exception as e: logger.exception(f"Exception in RPC handler for {name}: {e}", exc_info=e) + # Send exception data to client if this was a request with an ID + req_id = req.get("id") + if req_id is not None: + exc_data = serialize_exception(e) + # Type ignore: SerializedException is compatible with dict[str, Any] + self.publish( + topic_res, + self._encodeRPCRes({"id": req_id, "exception": exc_data}), # type: ignore[arg-type, typeddict-item] + ) - get_thread_pool = getattr(self, "_get_call_thread_pool", None) - if get_thread_pool: - get_thread_pool().submit(execute_and_respond) - else: - execute_and_respond() + # Always use thread pool to execute RPC handlers (prevents deadlock) + self._get_call_thread_pool().submit(execute_and_respond) return self.subscribe(topic_req, receive_call) -# simple PUBSUB RPC implementation that doesn't encode -# special request/response messages, assumes pubsub implementation -# supports generic dictionary pubsub -class PassThroughPubSubRPC(PubSubRPCMixin[TopicT, dict], Generic[TopicT]): - def _encodeRPCReq(self, req: RPCReq) -> dict: - return dict(req) - - def _decodeRPCRes(self, msg: dict) -> RPCRes: - return msg # type: ignore[return-value] - - def _encodeRPCRes(self, res: RPCRes) -> dict: - return dict(res) - - def _decodeRPCReq(self, msg: dict) -> RPCReq: - return msg # type: ignore[return-value] +class LCMRPC(PubSubRPCMixin[Topic, Any], PickleLCM): + def __init__(self, **kwargs: Any) -> None: + # Need to ensure PickleLCM gets initialized properly + # This is due to the diamond inheritance pattern with multiple base classes + PickleLCM.__init__(self, **kwargs) + # Initialize PubSubRPCMixin's thread pool + PubSubRPCMixin.__init__(self, **kwargs) + + def topicgen(self, name: str, req_or_res: bool) -> Topic: + suffix = "res" if req_or_res else "req" + topic = f"/rpc/{name}/{suffix}" + if len(topic) > LCM_MAX_CHANNEL_NAME_LENGTH: + topic = f"/rpc/{short_id(name)}/{suffix}" + return Topic(topic=topic) + + +class ShmRPC(PubSubRPCMixin[str, Any], PickleSharedMemory): + def __init__(self, prefer: str = "cpu", **kwargs: Any) -> None: + # Need to ensure SharedMemory gets initialized properly + # This is due to the diamond inheritance pattern with multiple base classes + PickleSharedMemory.__init__(self, prefer=prefer, **kwargs) + # Initialize PubSubRPCMixin's thread pool + PubSubRPCMixin.__init__(self, **kwargs) + + def topicgen(self, name: str, req_or_res: bool) -> str: + suffix = "res" if req_or_res else "req" + return f"/rpc/{name}/{suffix}" diff --git a/dimos/protocol/rpc/redisrpc.py b/dimos/protocol/rpc/redisrpc.py index b0a715fe43..aa8a5b87c5 100644 --- a/dimos/protocol/rpc/redisrpc.py +++ b/dimos/protocol/rpc/redisrpc.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,9 +13,9 @@ # limitations under the License. from dimos.protocol.pubsub.redispubsub import Redis -from dimos.protocol.rpc.pubsubrpc import PassThroughPubSubRPC +from dimos.protocol.rpc.pubsubrpc import PubSubRPCMixin -class RedisRPC(PassThroughPubSubRPC, Redis): +class RedisRPC(PubSubRPCMixin, Redis): # type: ignore[type-arg] def topicgen(self, name: str, req_or_res: bool) -> str: return f"/rpc/{name}/{'res' if req_or_res else 'req'}" diff --git a/dimos/protocol/rpc/rpc_utils.py b/dimos/protocol/rpc/rpc_utils.py new file mode 100644 index 0000000000..26ab281e45 --- /dev/null +++ b/dimos/protocol/rpc/rpc_utils.py @@ -0,0 +1,104 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for serializing and deserializing exceptions for RPC transport.""" + +from __future__ import annotations + +import traceback +from typing import Any, TypedDict + + +class SerializedException(TypedDict): + """Type for serialized exception data.""" + + type_name: str + type_module: str + args: tuple[Any, ...] + traceback: str + + +class RemoteError(Exception): + """Exception that was raised on a remote RPC server. + + Preserves the original exception type and full stack trace from the remote side. + """ + + def __init__( + self, type_name: str, type_module: str, args: tuple[Any, ...], traceback: str + ) -> None: + super().__init__(*args if args else (f"Remote exception: {type_name}",)) + self.remote_type = f"{type_module}.{type_name}" + self.remote_traceback = traceback + + def __str__(self) -> str: + base_msg = super().__str__() + return ( + f"[Remote {self.remote_type}] {base_msg}\n\nRemote traceback:\n{self.remote_traceback}" + ) + + +def serialize_exception(exc: Exception) -> SerializedException: + """Convert an exception to a transferable format. + + Args: + exc: The exception to serialize + + Returns: + A dictionary containing the exception data that can be transferred + """ + # Get the full traceback as a string + tb_str = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + + return SerializedException( + type_name=type(exc).__name__, + type_module=type(exc).__module__, + args=exc.args, + traceback=tb_str, + ) + + +def deserialize_exception(exc_data: SerializedException) -> Exception: + """Reconstruct an exception from serialized data. + + For builtin exceptions, instantiates the actual type. + For custom exceptions, returns a RemoteError. + + Args: + exc_data: The serialized exception data + + Returns: + An exception that can be raised with full type and traceback info + """ + type_name = exc_data.get("type_name", "Exception") + type_module = exc_data.get("type_module", "builtins") + args: tuple[Any, ...] = exc_data.get("args", ()) + tb_str = exc_data.get("traceback", "") + + # Only reconstruct builtin exceptions + if type_module == "builtins": + try: + import builtins + + exc_class = getattr(builtins, type_name, None) + if exc_class and issubclass(exc_class, BaseException): + exc = exc_class(*args) + # Add remote traceback as __cause__ for context + exc.__cause__ = RemoteError(type_name, type_module, args, tb_str) + return exc # type: ignore[no-any-return] + except (AttributeError, TypeError): + pass + + # Use RemoteError for non-builtin or if reconstruction failed + return RemoteError(type_name, type_module, args, tb_str) diff --git a/dimos/protocol/rpc/spec.py b/dimos/protocol/rpc/spec.py index 283b84f1dd..1c502abe24 100644 --- a/dimos/protocol/rpc/spec.py +++ b/dimos/protocol/rpc/spec.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,13 +21,13 @@ class Empty: ... -Args = tuple[list, dict[str, Any]] +Args = tuple[list, dict[str, Any]] # type: ignore[type-arg] # module that we can inspect for RPCs class RPCInspectable(Protocol): @property - def rpcs(self) -> dict[str, Callable]: ... + def rpcs(self) -> dict[str, Callable]: ... # type: ignore[type-arg] class RPCClient(Protocol): @@ -39,31 +39,43 @@ def call(self, name: str, arguments: Args, cb: None) -> None: ... @overload def call(self, name: str, arguments: Args, cb: Callable[[Any], None]) -> Callable[[], Any]: ... - def call(self, name: str, arguments: Args, cb: Callable | None) -> Callable[[], Any] | None: ... + def call(self, name: str, arguments: Args, cb: Callable | None) -> Callable[[], Any] | None: ... # type: ignore[type-arg] # we expect to crash if we don't get a return value after 10 seconds # but callers can override this timeout for extra long functions def call_sync( - self, name: str, arguments: Args, rpc_timeout: float | None = 30.0 + self, name: str, arguments: Args, rpc_timeout: float | None = 120.0 ) -> tuple[Any, Callable[[], None]]: + if name == "start": + rpc_timeout = 1200.0 # starting modules can take longer event = threading.Event() - def receive_value(val) -> None: - event.result = val # attach to event + def receive_value(val) -> None: # type: ignore[no-untyped-def] + event.result = val # type: ignore[attr-defined] # attach to event event.set() unsub_fn = self.call(name, arguments, receive_value) if not event.wait(rpc_timeout): raise TimeoutError(f"RPC call to '{name}' timed out after {rpc_timeout} seconds") - return event.result, unsub_fn + + # Check if the result is an exception and raise it + result = event.result # type: ignore[attr-defined] + if isinstance(result, BaseException): + raise result + + return result, unsub_fn async def call_async(self, name: str, arguments: Args) -> Any: loop = asyncio.get_event_loop() future = loop.create_future() - def receive_value(val) -> None: + def receive_value(val) -> None: # type: ignore[no-untyped-def] try: - loop.call_soon_threadsafe(future.set_result, val) + # Check if the value is an exception + if isinstance(val, BaseException): + loop.call_soon_threadsafe(future.set_exception, val) + else: + loop.call_soon_threadsafe(future.set_result, val) except Exception as e: loop.call_soon_threadsafe(future.set_exception, e) @@ -73,14 +85,14 @@ def receive_value(val) -> None: class RPCServer(Protocol): - def serve_rpc(self, f: Callable, name: str) -> Callable[[], None]: ... + def serve_rpc(self, f: Callable, name: str) -> Callable[[], None]: ... # type: ignore[type-arg] def serve_module_rpc(self, module: RPCInspectable, name: str | None = None) -> None: for fname in module.rpcs.keys(): if not name: name = module.__class__.__name__ - def override_f(*args, fname=fname, **kwargs): + def override_f(*args, fname=fname, **kwargs): # type: ignore[no-untyped-def] return getattr(module, fname)(*args, **kwargs) topic = name + "/" + fname diff --git a/dimos/protocol/rpc/test_lcmrpc.py b/dimos/protocol/rpc/test_lcmrpc.py index 6ee00b23e0..f31d20cf19 100644 --- a/dimos/protocol/rpc/test_lcmrpc.py +++ b/dimos/protocol/rpc/test_lcmrpc.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import pytest from dimos.constants import LCM_MAX_CHANNEL_NAME_LENGTH -from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.protocol.rpc import LCMRPC @pytest.fixture diff --git a/dimos/protocol/rpc/test_lcmrpc_timeout.py b/dimos/protocol/rpc/test_lcmrpc_timeout.py deleted file mode 100644 index 74cf4963c7..0000000000 --- a/dimos/protocol/rpc/test_lcmrpc_timeout.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import time - -import pytest - -from dimos.protocol.rpc.lcmrpc import LCMRPC -from dimos.protocol.service.lcmservice import autoconf - - -@pytest.fixture(scope="session", autouse=True) -def setup_lcm_autoconf(): - """Setup LCM autoconf once for the entire test session""" - autoconf() - yield - - -@pytest.fixture -def lcm_server(): - """Fixture that provides started LCMRPC server""" - server = LCMRPC() - server.start() - - yield server - - server.stop() - - -@pytest.fixture -def lcm_client(): - """Fixture that provides started LCMRPC client""" - client = LCMRPC() - client.start() - - yield client - - client.stop() - - -def test_lcmrpc_timeout_no_reply(lcm_server, lcm_client) -> None: - """Test that RPC calls timeout when no reply is received""" - server = lcm_server - client = lcm_client - - # Track if the function was called - function_called = threading.Event() - - # Serve a function that never responds - def never_responds(a: int, b: int): - # Signal that the function was called - function_called.set() - # Simulating a server that receives the request but never sends a reply - time.sleep(1) # Long sleep to ensure timeout happens first - return a + b - - server.serve_rpc(never_responds, "slow_add") - - # Test with call_sync and explicit timeout - start_time = time.time() - - # Should raise TimeoutError when timeout occurs - with pytest.raises(TimeoutError, match="RPC call to 'slow_add' timed out after 0.1 seconds"): - client.call_sync("slow_add", ([1, 2], {}), rpc_timeout=0.1) - - elapsed = time.time() - start_time - - # Should timeout after ~0.1 seconds - assert elapsed < 0.3, f"Timeout took too long: {elapsed}s" - - # Verify the function was actually called - assert function_called.wait(0.5), "Server function was never called" - - -def test_lcmrpc_timeout_nonexistent_service(lcm_client) -> None: - """Test that RPC calls timeout when calling a non-existent service""" - client = lcm_client - - # Call a service that doesn't exist - start_time = time.time() - - # Should raise TimeoutError when timeout occurs - with pytest.raises( - TimeoutError, match="RPC call to 'nonexistent/service' timed out after 0.1 seconds" - ): - client.call_sync("nonexistent/service", ([1, 2], {}), rpc_timeout=0.1) - - elapsed = time.time() - start_time - - # Should timeout after ~0.1 seconds - assert elapsed < 0.3, f"Timeout took too long: {elapsed}s" - - -def test_lcmrpc_callback_with_timeout(lcm_server, lcm_client) -> None: - """Test that callback-based RPC calls handle timeouts properly""" - server = lcm_server - client = lcm_client - # Track if the function was called - function_called = threading.Event() - - # Serve a function that never responds - def never_responds(a: int, b: int): - function_called.set() - time.sleep(1) - return a + b - - server.serve_rpc(never_responds, "slow_add") - - callback_called = threading.Event() - received_value = [] - - def callback(value) -> None: - received_value.append(value) - callback_called.set() - - # Make the call with callback - unsub = client.call("slow_add", ([1, 2], {}), callback) - - # Wait for a short time - callback should not be called - callback_called.wait(0.2) - assert not callback_called.is_set(), "Callback should not have been called" - assert len(received_value) == 0 - - # Verify the server function was actually called - assert function_called.wait(0.5), "Server function was never called" - - # Clean up - unsubscribe if possible - if unsub: - unsub() - - -def test_lcmrpc_normal_operation(lcm_server, lcm_client) -> None: - """Sanity check that normal RPC calls still work""" - server = lcm_server - client = lcm_client - - def quick_add(a: int, b: int): - return a + b - - server.serve_rpc(quick_add, "add") - - # Normal call should work quickly - start_time = time.time() - result = client.call_sync("add", ([5, 3], {}), rpc_timeout=0.5)[0] - elapsed = time.time() - start_time - - assert result == 8 - assert elapsed < 0.2, f"Normal call took too long: {elapsed}s" - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/dimos/protocol/rpc/test_rpc_utils.py b/dimos/protocol/rpc/test_rpc_utils.py new file mode 100644 index 0000000000..b5e6253aaf --- /dev/null +++ b/dimos/protocol/rpc/test_rpc_utils.py @@ -0,0 +1,70 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for RPC exception serialization utilities.""" + +from dimos.protocol.rpc.rpc_utils import ( + RemoteError, + deserialize_exception, + serialize_exception, +) + + +def test_exception_builtin_serialization(): + """Test serialization and deserialization of exceptions.""" + + # Test with a builtin exception + try: + raise ValueError("test error", 42) + except ValueError as e: + serialized = serialize_exception(e) + + # Check serialized format + assert serialized["type_name"] == "ValueError" + assert serialized["type_module"] == "builtins" + assert serialized["args"] == ("test error", 42) + assert "Traceback" in serialized["traceback"] + assert "test error" in serialized["traceback"] + + # Deserialize and check we get a real ValueError back + deserialized = deserialize_exception(serialized) + assert isinstance(deserialized, ValueError) + assert deserialized.args == ("test error", 42) + # Check that remote traceback is attached as cause + assert isinstance(deserialized.__cause__, RemoteError) + assert "test error" in deserialized.__cause__.remote_traceback + + +def test_exception_custom_serialization(): + # Test with a custom exception + class CustomError(Exception): + pass + + try: + raise CustomError("custom message") + except CustomError as e: + serialized = serialize_exception(e) + + # Check serialized format + assert serialized["type_name"] == "CustomError" + # Module name varies when running under pytest vs directly + assert serialized["type_module"] in ("__main__", "dimos.protocol.rpc.test_rpc_utils") + assert serialized["args"] == ("custom message",) + + # Deserialize - should get RemoteError since it's not builtin + deserialized = deserialize_exception(serialized) + assert isinstance(deserialized, RemoteError) + assert "CustomError" in deserialized.remote_type + assert "custom message" in str(deserialized) + assert "custom message" in deserialized.remote_traceback diff --git a/dimos/protocol/rpc/test_spec.py b/dimos/protocol/rpc/test_spec.py new file mode 100644 index 0000000000..9fb8f65eb7 --- /dev/null +++ b/dimos/protocol/rpc/test_spec.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python3 + +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Grid tests for RPC implementations to ensure spec compliance.""" + +import asyncio +from collections.abc import Callable +from contextlib import contextmanager +import threading +import time +from typing import Any + +import pytest + +from dimos.protocol.rpc.pubsubrpc import LCMRPC, ShmRPC +from dimos.protocol.rpc.rpc_utils import RemoteError + + +class CustomTestError(Exception): + """Custom exception for testing.""" + + pass + + +# Build testdata list with available implementations +testdata: list[tuple[Callable[[], Any], str]] = [] + + +# Context managers for different RPC implementations +@contextmanager +def lcm_rpc_context(): + """Context manager for LCMRPC implementation.""" + from dimos.protocol.service.lcmservice import autoconf + + autoconf() + server = LCMRPC() + client = LCMRPC() + server.start() + client.start() + + try: + yield server, client + finally: + server.stop() + client.stop() + + +testdata.append((lcm_rpc_context, "lcm")) + + +@contextmanager +def shm_rpc_context(): + """Context manager for Shared Memory RPC implementation.""" + # Create two separate instances that communicate through shared memory segments + server = ShmRPC(prefer="cpu") + client = ShmRPC(prefer="cpu") + server.start() + client.start() + + try: + yield server, client + finally: + server.stop() + client.stop() + + +testdata.append((shm_rpc_context, "shm")) + +# Try to add RedisRPC if available +try: + from dimos.protocol.rpc.redisrpc import RedisRPC + + @contextmanager + def redis_rpc_context(): + """Context manager for RedisRPC implementation.""" + server = RedisRPC() + client = RedisRPC() + server.start() + client.start() + + try: + yield server, client + finally: + server.stop() + client.stop() + + testdata.append((redis_rpc_context, "redis")) +except (ImportError, ConnectionError): + print("RedisRPC not available") + + +# Test functions that will be served +def add_function(a: int, b: int) -> int: + """Simple addition function for testing.""" + return a + b + + +def failing_function(msg: str) -> str: + """Function that raises exceptions for testing.""" + if msg == "fail": + raise ValueError("Test error message") + elif msg == "custom": + raise CustomTestError("Custom error") + return f"Success: {msg}" + + +def slow_function(delay: float) -> str: + """Function that takes time to execute.""" + time.sleep(delay) + return f"Completed after {delay} seconds" + + +# Grid tests + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +def test_basic_sync_call(rpc_context, impl_name: str) -> None: + """Test basic synchronous RPC calls.""" + with rpc_context() as (server, client): + # Serve the function + unsub = server.serve_rpc(add_function, "add") + + try: + # Make sync call + result, _ = client.call_sync("add", ([5, 3], {}), rpc_timeout=2.0) + assert result == 8 + + # Test with different arguments + result, _ = client.call_sync("add", ([10, -2], {}), rpc_timeout=2.0) + assert result == 8 + + finally: + unsub() + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +@pytest.mark.asyncio +@pytest.mark.skip( + reason="Async RPC calls have a deadlock issue when run in the full test suite (works in isolation)" +) +async def test_async_call(rpc_context, impl_name: str) -> None: + """Test asynchronous RPC calls.""" + with rpc_context() as (server, client): + # Serve the function + unsub = server.serve_rpc(add_function, "add_async") + + try: + # Make async call + result = await client.call_async("add_async", ([7, 4], {})) + assert result == 11 + + # Test multiple async calls + results = await asyncio.gather( + client.call_async("add_async", ([1, 2], {})), + client.call_async("add_async", ([3, 4], {})), + client.call_async("add_async", ([5, 6], {})), + ) + assert results == [3, 7, 11] + + finally: + unsub() + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +def test_callback_call(rpc_context, impl_name: str) -> None: + """Test callback-based RPC calls.""" + with rpc_context() as (server, client): + # Serve the function + unsub_server = server.serve_rpc(add_function, "add_callback") + + try: + # Test with callback + event = threading.Event() + received_value = None + + def callback(val) -> None: + nonlocal received_value + received_value = val + event.set() + + client.call("add_callback", ([20, 22], {}), callback) + assert event.wait(2.0) + assert received_value == 42 + + finally: + unsub_server() + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +def test_exception_handling_sync(rpc_context, impl_name: str) -> None: + """Test that exceptions are properly passed through sync RPC calls.""" + with rpc_context() as (server, client): + # Serve the function that can raise exceptions + unsub = server.serve_rpc(failing_function, "test_exc") + + try: + # Test successful call + result, _ = client.call_sync("test_exc", (["ok"], {}), rpc_timeout=2.0) + assert result == "Success: ok" + + # Test builtin exception - should raise actual ValueError + with pytest.raises(ValueError) as exc_info: + client.call_sync("test_exc", (["fail"], {}), rpc_timeout=2.0) + assert "Test error message" in str(exc_info.value) + # Check that the cause contains the remote traceback + assert isinstance(exc_info.value.__cause__, RemoteError) + assert "failing_function" in exc_info.value.__cause__.remote_traceback + + # Test custom exception - should raise RemoteError + with pytest.raises(RemoteError) as exc_info: + client.call_sync("test_exc", (["custom"], {}), rpc_timeout=2.0) + assert "Custom error" in str(exc_info.value) + assert "CustomTestError" in exc_info.value.remote_type + assert "failing_function" in exc_info.value.remote_traceback + + finally: + unsub() + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +@pytest.mark.asyncio +async def test_exception_handling_async(rpc_context, impl_name: str) -> None: + """Test that exceptions are properly passed through async RPC calls.""" + with rpc_context() as (server, client): + # Serve the function that can raise exceptions + unsub = server.serve_rpc(failing_function, "test_exc_async") + + try: + # Test successful call + result = await client.call_async("test_exc_async", (["ok"], {})) + assert result == "Success: ok" + + # Test builtin exception + with pytest.raises(ValueError) as exc_info: + await client.call_async("test_exc_async", (["fail"], {})) + assert "Test error message" in str(exc_info.value) + assert isinstance(exc_info.value.__cause__, RemoteError) + + # Test custom exception + with pytest.raises(RemoteError) as exc_info: + await client.call_async("test_exc_async", (["custom"], {})) + assert "Custom error" in str(exc_info.value) + assert "CustomTestError" in exc_info.value.remote_type + + finally: + unsub() + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +def test_exception_handling_callback(rpc_context, impl_name: str) -> None: + """Test that exceptions are properly passed through callback-based RPC calls.""" + with rpc_context() as (server, client): + # Serve the function that can raise exceptions + unsub_server = server.serve_rpc(failing_function, "test_exc_cb") + + try: + # Test with callback - exception should be passed to callback + event = threading.Event() + received_value = None + + def callback(val) -> None: + nonlocal received_value + received_value = val + event.set() + + # Test successful call + client.call("test_exc_cb", (["ok"], {}), callback) + assert event.wait(2.0) + assert received_value == "Success: ok" + event.clear() + + # Test failed call - exception should be passed to callback + client.call("test_exc_cb", (["fail"], {}), callback) + assert event.wait(2.0) + assert isinstance(received_value, ValueError) + assert "Test error message" in str(received_value) + assert isinstance(received_value.__cause__, RemoteError) + + finally: + unsub_server() + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +def test_timeout(rpc_context, impl_name: str) -> None: + """Test that RPC calls properly timeout.""" + with rpc_context() as (server, client): + # Serve a slow function + unsub = server.serve_rpc(slow_function, "slow") + + try: + # Call with short timeout should fail + # Using 10 seconds sleep to ensure it would definitely timeout + with pytest.raises(TimeoutError) as exc_info: + client.call_sync("slow", ([2.0], {}), rpc_timeout=0.1) + assert "timed out" in str(exc_info.value) + + # Call with sufficient timeout should succeed + result, _ = client.call_sync("slow", ([0.01], {}), rpc_timeout=1.0) + assert "Completed after 0.01 seconds" in result + + finally: + unsub() + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +def test_nonexistent_service(rpc_context, impl_name: str) -> None: + """Test calling a service that doesn't exist.""" + with rpc_context() as (_server, client): + # Don't serve any function, just try to call + with pytest.raises(TimeoutError) as exc_info: + client.call_sync("nonexistent", ([1, 2], {}), rpc_timeout=0.1) + assert "nonexistent" in str(exc_info.value) + assert "timed out" in str(exc_info.value) + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +def test_multiple_services(rpc_context, impl_name: str) -> None: + """Test serving multiple RPC functions simultaneously.""" + with rpc_context() as (server, client): + # Serve multiple functions + unsub1 = server.serve_rpc(add_function, "service1") + unsub2 = server.serve_rpc(lambda x: x * 2, "service2") + unsub3 = server.serve_rpc(lambda s: s.upper(), "service3") + + try: + # Call all services + result1, _ = client.call_sync("service1", ([3, 4], {}), rpc_timeout=1.0) + assert result1 == 7 + + result2, _ = client.call_sync("service2", ([21], {}), rpc_timeout=1.0) + assert result2 == 42 + + result3, _ = client.call_sync("service3", (["hello"], {}), rpc_timeout=1.0) + assert result3 == "HELLO" + + finally: + unsub1() + unsub2() + unsub3() + + +@pytest.mark.parametrize("rpc_context, impl_name", testdata) +def test_concurrent_calls(rpc_context, impl_name: str) -> None: + """Test making multiple concurrent RPC calls.""" + # Skip for SharedMemory - double-buffered architecture can't handle concurrent bursts + # The channel only holds 2 frames, so 1000 rapid concurrent responses overwrite each other + if impl_name == "shm": + pytest.skip("SharedMemory uses double-buffering; can't handle 1000 concurrent responses") + + with rpc_context() as (server, client): + # Serve a function that we'll call concurrently + unsub = server.serve_rpc(add_function, "concurrent_add") + + try: + # Make multiple concurrent calls using threads + results = [] + threads = [] + + def make_call(a, b) -> None: + result, _ = client.call_sync("concurrent_add", ([a, b], {}), rpc_timeout=2.0) + results.append(result) + + # Start 1000 concurrent calls + for i in range(1000): + t = threading.Thread(target=make_call, args=(i, i + 1)) + threads.append(t) + t.start() + + # Wait for all threads to complete + for t in threads: + t.join(timeout=10.0) + + # Verify all calls succeeded + assert len(results) == 1000 + # Results should be [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] but may be in any order + expected = [i + (i + 1) for i in range(1000)] + assert sorted(results) == sorted(expected) + + finally: + unsub() + + +if __name__ == "__main__": + # Run tests for debugging + pytest.main([__file__, "-v"]) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 1b19a5cfeb..a0ca8c4796 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ from dataclasses import dataclass from functools import cache import os +import platform import subprocess import sys import threading @@ -29,14 +30,14 @@ from dimos.protocol.service.spec import Service from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.protocol.service.lcmservice") +logger = setup_logger() @cache def check_root() -> bool: """Return True if the current process is running as root (UID 0).""" try: - return os.geteuid() == 0 # type: ignore[attr-defined] + return os.geteuid() == 0 except AttributeError: # Platforms without geteuid (e.g. Windows) – assume non-root. return False @@ -48,56 +49,103 @@ def check_multicast() -> list[str]: sudo = "" if check_root() else "sudo " - # Check if loopback interface has multicast enabled - try: - result = subprocess.run(["ip", "link", "show", "lo"], capture_output=True, text=True) - if "MULTICAST" not in result.stdout: - commands_needed.append(f"{sudo}ifconfig lo multicast") - except Exception: - commands_needed.append(f"{sudo}ifconfig lo multicast") + system = platform.system() - # Check if multicast route exists - try: - result = subprocess.run( - ["ip", "route", "show", "224.0.0.0/4"], capture_output=True, text=True - ) - if not result.stdout.strip(): - commands_needed.append(f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo") - except Exception: - commands_needed.append(f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo") + if system == "Linux": + # Linux commands + loopback_interface = "lo" + # Check if loopback interface has multicast enabled + try: + result = subprocess.run( + ["ip", "link", "show", loopback_interface], capture_output=True, text=True + ) + if "MULTICAST" not in result.stdout: + commands_needed.append(f"{sudo}ifconfig {loopback_interface} multicast") + except Exception: + commands_needed.append(f"{sudo}ifconfig {loopback_interface} multicast") + + # Check if multicast route exists + try: + result = subprocess.run( + ["ip", "route", "show", "224.0.0.0/4"], capture_output=True, text=True + ) + if not result.stdout.strip(): + commands_needed.append( + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev {loopback_interface}" + ) + except Exception: + commands_needed.append( + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev {loopback_interface}" + ) + + elif system == "Darwin": # macOS + loopback_interface = "lo0" + # Check if multicast route exists + try: + result = subprocess.run(["netstat", "-nr"], capture_output=True, text=True) + route_exists = "224.0.0.0/4" in result.stdout or "224.0.0/4" in result.stdout + if not route_exists: + commands_needed.append( + f"{sudo}route add -net 224.0.0.0/4 -interface {loopback_interface}" + ) + except Exception: + commands_needed.append( + f"{sudo}route add -net 224.0.0.0/4 -interface {loopback_interface}" + ) + + else: + # For other systems, skip multicast configuration + logger.warning(f"Multicast configuration not supported on {system}") return commands_needed +def _set_net_value(commands_needed: list[str], sudo: str, name: str, value: int) -> int | None: + try: + result = subprocess.run(["sysctl", name], capture_output=True, text=True) + if result.returncode == 0: + current = int(result.stdout.replace(":", "=").split("=")[1].strip()) + else: + current = None + if not current or current < value: + commands_needed.append(f"{sudo}sysctl -w {name}={value}") + return current + except: + commands_needed.append(f"{sudo}sysctl -w {name}={value}") + return None + + +TARGET_RMEM_SIZE = 2097152 # prev was 67108864 +TARGET_MAX_SOCKET_BUFFER_SIZE_MACOS = 8388608 +TARGET_MAX_DGRAM_SIZE_MACOS = 65535 + + def check_buffers() -> tuple[list[str], int | None]: """Check if buffer configuration is needed and return required commands and current size. Returns: Tuple of (commands_needed, current_max_buffer_size) """ - commands_needed = [] + commands_needed: list[str] = [] current_max = None sudo = "" if check_root() else "sudo " - - # Check current buffer settings - try: - result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) - current_max = int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None - if not current_max or current_max < 2097152: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") - except: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") - - try: - result = subprocess.run(["sysctl", "net.core.rmem_default"], capture_output=True, text=True) - current_default = ( - int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None + system = platform.system() + + if system == "Linux": + # Linux buffer configuration + current_max = _set_net_value(commands_needed, sudo, "net.core.rmem_max", TARGET_RMEM_SIZE) + _set_net_value(commands_needed, sudo, "net.core.rmem_default", TARGET_RMEM_SIZE) + elif system == "Darwin": # macOS + # macOS buffer configuration - check and set UDP buffer related sysctls + current_max = _set_net_value( + commands_needed, sudo, "kern.ipc.maxsockbuf", TARGET_MAX_SOCKET_BUFFER_SIZE_MACOS ) - if not current_default or current_default < 2097152: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") - except: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") + _set_net_value(commands_needed, sudo, "net.inet.udp.recvspace", TARGET_RMEM_SIZE) + _set_net_value(commands_needed, sudo, "net.inet.udp.maxdgram", TARGET_MAX_DGRAM_SIZE_MACOS) + else: + # For other systems, skip buffer configuration + logger.warning(f"Buffer configuration not supported on {system}") return commands_needed, current_max @@ -145,6 +193,8 @@ def autoconf() -> None: logger.info("CI environment detected: Skipping automatic system configuration.") return + platform.system() + commands_needed = [] # Check multicast configuration @@ -187,6 +237,9 @@ def autoconf() -> None: logger.info("System configuration completed.") +_DEFAULT_LCM_URL_MACOS = "udpm://239.255.76.67:7667?ttl=0" + + @dataclass class LCMConfig: ttl: int = 0 @@ -194,6 +247,11 @@ class LCMConfig: autoconf: bool = True lcm: lcm.LCM | None = None + def __post_init__(self) -> None: + if self.url is None and platform.system() == "Darwin": + # On macOS, use multicast with TTL=0 to keep traffic local + self.url = _DEFAULT_LCM_URL_MACOS + @runtime_checkable class LCMMsg(Protocol): @@ -220,6 +278,9 @@ def __str__(self) -> str: return f"{self.topic}#{self.lcm_type.msg_name}" +_LCM_LOOP_TIMEOUT = 50 + + class LCMService(Service[LCMConfig]): default_config = LCMConfig l: lcm.LCM | None @@ -229,22 +290,20 @@ class LCMService(Service[LCMConfig]): _call_thread_pool: ThreadPoolExecutor | None = None _call_thread_pool_lock: threading.RLock = threading.RLock() - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) # we support passing an existing LCM instance if self.config.lcm: - # TODO: If we pass LCM in, it's unsafe to use in this thread and the _loop thread. self.l = self.config.lcm else: self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() self._l_lock = threading.Lock() - self._stop_event = threading.Event() self._thread = None - def __getstate__(self): + def __getstate__(self): # type: ignore[no-untyped-def] """Exclude unpicklable runtime attributes when serializing.""" state = self.__dict__.copy() # Remove unpicklable attributes @@ -256,7 +315,7 @@ def __getstate__(self): state.pop("_call_thread_pool_lock", None) return state - def __setstate__(self, state) -> None: + def __setstate__(self, state) -> None: # type: ignore[no-untyped-def] """Restore object from pickled state.""" self.__dict__.update(state) # Reinitialize runtime attributes @@ -295,7 +354,7 @@ def _lcm_loop(self) -> None: with self._l_lock: if self.l is None: break - self.l.handle_timeout(50) + self.l.handle_timeout(_LCM_LOOP_TIMEOUT) except Exception as e: stack_trace = traceback.format_exc() print(f"Error in LCM handling: {e}\n{stack_trace}") diff --git a/dimos/protocol/service/spec.py b/dimos/protocol/service/spec.py index d55c1bfacf..c4e6758614 100644 --- a/dimos/protocol/service/spec.py +++ b/dimos/protocol/service/spec.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,13 +22,17 @@ class Configurable(Generic[ConfigT]): default_config: type[ConfigT] - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] self.config: ConfigT = self.default_config(**kwargs) class Service(Configurable[ConfigT], ABC): def start(self) -> None: - super().start() + # Only call super().start() if it exists + if hasattr(super(), "start"): + super().start() # type: ignore[misc] def stop(self) -> None: - super().stop() + # Only call super().stop() if it exists + if hasattr(super(), "stop"): + super().stop() # type: ignore[misc] diff --git a/dimos/protocol/service/test_lcmservice.py b/dimos/protocol/service/test_lcmservice.py index 1c9a51b2e5..faf50a945e 100644 --- a/dimos/protocol/service/test_lcmservice.py +++ b/dimos/protocol/service/test_lcmservice.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ import pytest from dimos.protocol.service.lcmservice import ( + TARGET_MAX_DGRAM_SIZE_MACOS, + TARGET_MAX_SOCKET_BUFFER_SIZE_MACOS, + TARGET_RMEM_SIZE, autoconf, check_buffers, check_multicast, @@ -33,390 +36,532 @@ def get_sudo_prefix() -> str: def test_check_multicast_all_configured() -> None: """Test check_multicast when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock successful checks with realistic output format - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", - "returncode": 0, - }, - )(), - type("MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0})(), - ] - - result = check_multicast() - assert result == [] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock successful checks with realistic output format + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0} + )(), + ] + + result = check_multicast() + assert result == [] def test_check_multicast_missing_multicast_flag() -> None: """Test check_multicast when loopback interface lacks multicast.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock interface without MULTICAST flag (realistic current system state) - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", - "returncode": 0, - }, - )(), - type("MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0})(), - ] - - result = check_multicast() - sudo = get_sudo_prefix() - assert result == [f"{sudo}ifconfig lo multicast"] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock interface without MULTICAST flag (realistic current system state) + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0} + )(), + ] + + result = check_multicast() + sudo = get_sudo_prefix() + assert result == [f"{sudo}ifconfig lo multicast"] def test_check_multicast_missing_route() -> None: """Test check_multicast when multicast route is missing.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock missing route - interface has multicast but no route - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", - "returncode": 0, - }, - )(), - type("MockResult", (), {"stdout": "", "returncode": 0})(), # Empty output - no route - ] - - result = check_multicast() - sudo = get_sudo_prefix() - assert result == [f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock missing route - interface has multicast but no route + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "", "returncode": 0} + )(), # Empty output - no route + ] + + result = check_multicast() + sudo = get_sudo_prefix() + assert result == [f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"] def test_check_multicast_all_missing() -> None: """Test check_multicast when both multicast flag and route are missing (current system state).""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock both missing - matches actual current system state - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", - "returncode": 0, - }, - )(), - type("MockResult", (), {"stdout": "", "returncode": 0})(), # Empty output - no route - ] - - result = check_multicast() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}ifconfig lo multicast", - f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", - ] - assert result == expected + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock both missing - matches actual current system state + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "", "returncode": 0} + )(), # Empty output - no route + ] + + result = check_multicast() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}ifconfig lo multicast", + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", + ] + assert result == expected def test_check_multicast_subprocess_exception() -> None: """Test check_multicast when subprocess calls fail.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock subprocess exceptions - mock_run.side_effect = Exception("Command failed") + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock subprocess exceptions + mock_run.side_effect = Exception("Command failed") + + result = check_multicast() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}ifconfig lo multicast", + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", + ] + assert result == expected - result = check_multicast() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}ifconfig lo multicast", - f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", - ] - assert result == expected + +def test_check_multicast_macos() -> None: + """Test check_multicast on macOS when configuration is needed.""" + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock netstat -nr to not contain the multicast route + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "default 192.168.1.1 UGScg en0", + "returncode": 0, + }, + )(), + ] + + result = check_multicast() + sudo = get_sudo_prefix() + expected = [f"{sudo}route add -net 224.0.0.0/4 -interface lo0"] + assert result == expected def test_check_buffers_all_configured() -> None: """Test check_buffers when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock sufficient buffer sizes - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0})(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} - )(), - ] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock sufficient buffer sizes + mock_run.side_effect = [ + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 67108864", "returncode": 0} + )(), + type( + "MockResult", + (), + {"stdout": "net.core.rmem_default = 16777216", "returncode": 0}, + )(), + ] - commands, buffer_size = check_buffers() - assert commands == [] - assert buffer_size == 2097152 + commands, buffer_size = check_buffers() + assert commands == [] + assert buffer_size >= TARGET_RMEM_SIZE def test_check_buffers_low_max_buffer() -> None: """Test check_buffers when rmem_max is too low.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock low rmem_max - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0})(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} - )(), - ] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock low rmem_max + mock_run.side_effect = [ + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + )(), + type( + "MockResult", + (), + {"stdout": f"net.core.rmem_default = {TARGET_RMEM_SIZE}", "returncode": 0}, + )(), + ] - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - assert commands == [f"{sudo}sysctl -w net.core.rmem_max=2097152"] - assert buffer_size == 1048576 + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + assert commands == [f"{sudo}sysctl -w net.core.rmem_max={TARGET_RMEM_SIZE}"] + assert buffer_size == 1048576 def test_check_buffers_low_default_buffer() -> None: """Test check_buffers when rmem_default is too low.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock low rmem_default - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0})(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} - )(), - ] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock low rmem_default + mock_run.side_effect = [ + type( + "MockResult", + (), + {"stdout": f"net.core.rmem_max = {TARGET_RMEM_SIZE}", "returncode": 0}, + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + )(), + ] - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - assert commands == [f"{sudo}sysctl -w net.core.rmem_default=2097152"] - assert buffer_size == 2097152 + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + assert commands == [f"{sudo}sysctl -w net.core.rmem_default={TARGET_RMEM_SIZE}"] + assert buffer_size == TARGET_RMEM_SIZE def test_check_buffers_both_low() -> None: """Test check_buffers when both buffer sizes are too low.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock both low - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0})(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} - )(), - ] - - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}sysctl -w net.core.rmem_max=2097152", - f"{sudo}sysctl -w net.core.rmem_default=2097152", - ] - assert commands == expected - assert buffer_size == 1048576 + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock both low + mock_run.side_effect = [ + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + )(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max={TARGET_RMEM_SIZE}", + f"{sudo}sysctl -w net.core.rmem_default={TARGET_RMEM_SIZE}", + ] + assert commands == expected + assert buffer_size == 1048576 def test_check_buffers_subprocess_exception() -> None: """Test check_buffers when subprocess calls fail.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock subprocess exceptions - mock_run.side_effect = Exception("Command failed") - - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}sysctl -w net.core.rmem_max=2097152", - f"{sudo}sysctl -w net.core.rmem_default=2097152", - ] - assert commands == expected - assert buffer_size is None + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock subprocess exceptions + mock_run.side_effect = Exception("Command failed") + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max={TARGET_RMEM_SIZE}", + f"{sudo}sysctl -w net.core.rmem_default={TARGET_RMEM_SIZE}", + ] + assert commands == expected + assert buffer_size is None def test_check_buffers_parsing_error() -> None: """Test check_buffers when output parsing fails.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock malformed output - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "invalid output", "returncode": 0})(), - type("MockResult", (), {"stdout": "also invalid", "returncode": 0})(), - ] - - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}sysctl -w net.core.rmem_max=2097152", - f"{sudo}sysctl -w net.core.rmem_default=2097152", - ] - assert commands == expected - assert buffer_size is None + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock malformed output + mock_run.side_effect = [ + type("MockResult", (), {"stdout": "invalid output", "returncode": 0})(), + type("MockResult", (), {"stdout": "also invalid", "returncode": 0})(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max={TARGET_RMEM_SIZE}", + f"{sudo}sysctl -w net.core.rmem_default={TARGET_RMEM_SIZE}", + ] + assert commands == expected + assert buffer_size is None def test_check_buffers_dev_container() -> None: """Test check_buffers in dev container where sysctl fails.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock dev container behavior - sysctl returns non-zero - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_max: No such file or directory", - "returncode": 255, - }, - )(), - type( - "MockResult", - (), - { - "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_default: No such file or directory", - "returncode": 255, - }, - )(), - ] - - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}sysctl -w net.core.rmem_max=2097152", - f"{sudo}sysctl -w net.core.rmem_default=2097152", - ] - assert commands == expected - assert buffer_size is None - - -def test_autoconf_no_config_needed() -> None: - """Test autoconf when no configuration is needed.""" - # Clear CI environment variable for this test - with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock all checks passing + # Mock dev container behavior - sysctl returns non-zero mock_run.side_effect = [ - # check_multicast calls type( "MockResult", (), { - "stdout": "1: lo: mtu 65536", - "returncode": 0, + "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_max: No such file or directory", + "returncode": 255, }, )(), type( - "MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0} - )(), - # check_buffers calls - type( - "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} - )(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + "MockResult", + (), + { + "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_default: No such file or directory", + "returncode": 255, + }, )(), ] - with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: - autoconf() - # Should not log anything when no config is needed - mock_logger.info.assert_not_called() - mock_logger.error.assert_not_called() - mock_logger.warning.assert_not_called() + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max={TARGET_RMEM_SIZE}", + f"{sudo}sysctl -w net.core.rmem_default={TARGET_RMEM_SIZE}", + ] + assert commands == expected + assert buffer_size is None -def test_autoconf_with_config_needed_success() -> None: - """Test autoconf when configuration is needed and commands succeed.""" - # Clear CI environment variable for this test - with patch.dict(os.environ, {"CI": ""}, clear=False): +def test_check_buffers_macos_all_configured() -> None: + """Test check_buffers on macOS when system is properly configured.""" + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock checks failing, then mock the execution succeeding + # Mock sufficient buffer sizes for macOS mock_run.side_effect = [ - # check_multicast calls type( "MockResult", (), - {"stdout": "1: lo: mtu 65536", "returncode": 0}, + { + "stdout": f"kern.ipc.maxsockbuf: {TARGET_MAX_SOCKET_BUFFER_SIZE_MACOS}", + "returncode": 0, + }, )(), - type("MockResult", (), {"stdout": "", "returncode": 0})(), - # check_buffers calls type( - "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + "MockResult", + (), + {"stdout": f"net.inet.udp.recvspace: {TARGET_RMEM_SIZE}", "returncode": 0}, )(), type( - "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + "MockResult", + (), + { + "stdout": f"net.inet.udp.maxdgram: {TARGET_MAX_DGRAM_SIZE_MACOS}", + "returncode": 0, + }, )(), - # Command execution calls - type( - "MockResult", (), {"stdout": "success", "returncode": 0} - )(), # ifconfig lo multicast - type("MockResult", (), {"stdout": "success", "returncode": 0})(), # route add... - type("MockResult", (), {"stdout": "success", "returncode": 0})(), # sysctl rmem_max - type( - "MockResult", (), {"stdout": "success", "returncode": 0} - )(), # sysctl rmem_default ] - from unittest.mock import call - - with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: - autoconf() - - sudo = get_sudo_prefix() - # Verify the expected log calls - expected_info_calls = [ - call("System configuration required. Executing commands..."), - call(f" Running: {sudo}ifconfig lo multicast"), - call(" āœ“ Success"), - call(f" Running: {sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"), - call(" āœ“ Success"), - call(f" Running: {sudo}sysctl -w net.core.rmem_max=2097152"), - call(" āœ“ Success"), - call(f" Running: {sudo}sysctl -w net.core.rmem_default=2097152"), - call(" āœ“ Success"), - call("System configuration completed."), - ] + commands, buffer_size = check_buffers() + assert commands == [] + assert buffer_size == TARGET_MAX_SOCKET_BUFFER_SIZE_MACOS - mock_logger.info.assert_has_calls(expected_info_calls) - -def test_autoconf_with_command_failures() -> None: - """Test autoconf when some commands fail.""" - # Clear CI environment variable for this test - with patch.dict(os.environ, {"CI": ""}, clear=False): +def test_check_buffers_macos_needs_config() -> None: + """Test check_buffers on macOS when configuration is needed.""" + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Mock checks failing, then mock some commands failing + mock_max_sock_buf_size = 4194304 + # Mock low buffer sizes for macOS mock_run.side_effect = [ - # check_multicast calls type( "MockResult", (), - {"stdout": "1: lo: mtu 65536", "returncode": 0}, + {"stdout": f"kern.ipc.maxsockbuf: {mock_max_sock_buf_size}", "returncode": 0}, )(), - type("MockResult", (), {"stdout": "", "returncode": 0})(), - # check_buffers calls (no buffer issues for simpler test) type( - "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} + "MockResult", (), {"stdout": "net.inet.udp.recvspace: 1048576", "returncode": 0} )(), type( - "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + "MockResult", (), {"stdout": "net.inet.udp.maxdgram: 32768", "returncode": 0} )(), - # Command execution calls - first succeeds, second fails - type( - "MockResult", (), {"stdout": "success", "returncode": 0} - )(), # ifconfig lo multicast - subprocess.CalledProcessError( - 1, - [ - *get_sudo_prefix().split(), - "route", - "add", - "-net", - "224.0.0.0", - "netmask", - "240.0.0.0", - "dev", - "lo", - ], - "Permission denied", - "Operation not permitted", - ), ] - with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: - # The function should raise on multicast/route failures - with pytest.raises(subprocess.CalledProcessError): + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w kern.ipc.maxsockbuf={TARGET_MAX_SOCKET_BUFFER_SIZE_MACOS}", + f"{sudo}sysctl -w net.inet.udp.recvspace={TARGET_RMEM_SIZE}", + f"{sudo}sysctl -w net.inet.udp.maxdgram={TARGET_MAX_DGRAM_SIZE_MACOS}", + ] + assert commands == expected + assert buffer_size == mock_max_sock_buf_size + + +def test_autoconf_no_config_needed() -> None: + """Test autoconf when no configuration is needed.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock all checks passing + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536", + "returncode": 0, + }, + )(), + type( + "MockResult", + (), + {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0}, + )(), + # check_buffers calls + type( + "MockResult", + (), + {"stdout": f"net.core.rmem_max = {TARGET_RMEM_SIZE}", "returncode": 0}, + )(), + type( + "MockResult", + (), + {"stdout": f"net.core.rmem_default = {TARGET_RMEM_SIZE}", "returncode": 0}, + )(), + ] + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: autoconf() + # Should not log anything when no config is needed + mock_logger.info.assert_not_called() + mock_logger.error.assert_not_called() + mock_logger.warning.assert_not_called() + + +def test_autoconf_with_config_needed_success() -> None: + """Test autoconf when configuration is needed and commands succeed.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock checks failing, then mock the execution succeeding + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + {"stdout": "1: lo: mtu 65536", "returncode": 0}, + )(), + type("MockResult", (), {"stdout": "", "returncode": 0})(), + # check_buffers calls + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + )(), + type( + "MockResult", + (), + {"stdout": "net.core.rmem_default = 1048576", "returncode": 0}, + )(), + # Command execution calls + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # ifconfig lo multicast + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # route add... + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # sysctl rmem_max + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # sysctl rmem_default + ] + + from unittest.mock import call + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: + autoconf() + + sudo = get_sudo_prefix() + # Verify the expected log calls + expected_info_calls = [ + call("System configuration required. Executing commands..."), + call(f" Running: {sudo}ifconfig lo multicast"), + call(" āœ“ Success"), + call(f" Running: {sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"), + call(" āœ“ Success"), + call(f" Running: {sudo}sysctl -w net.core.rmem_max={TARGET_RMEM_SIZE}"), + call(" āœ“ Success"), + call( + f" Running: {sudo}sysctl -w net.core.rmem_default={TARGET_RMEM_SIZE}" + ), + call(" āœ“ Success"), + call("System configuration completed."), + ] + + mock_logger.info.assert_has_calls(expected_info_calls) + + +def test_autoconf_with_command_failures() -> None: + """Test autoconf when some commands fail.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock checks failing, then mock some commands failing + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + {"stdout": "1: lo: mtu 65536", "returncode": 0}, + )(), + type("MockResult", (), {"stdout": "", "returncode": 0})(), + # check_buffers calls (no buffer issues for simpler test) + type( + "MockResult", + (), + {"stdout": f"net.core.rmem_max = {TARGET_RMEM_SIZE}", "returncode": 0}, + )(), + type( + "MockResult", + (), + {"stdout": f"net.core.rmem_default = {TARGET_RMEM_SIZE}", "returncode": 0}, + )(), + # Command execution calls - first succeeds, second fails + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # ifconfig lo multicast + subprocess.CalledProcessError( + 1, + [ + *get_sudo_prefix().split(), + "route", + "add", + "-net", + "224.0.0.0", + "netmask", + "240.0.0.0", + "dev", + "lo", + ], + "Permission denied", + "Operation not permitted", + ), + ] + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: + # The function should raise on multicast/route failures + with pytest.raises(subprocess.CalledProcessError): + autoconf() - # Verify it logged the failure before raising - info_calls = [call[0][0] for call in mock_logger.info.call_args_list] - error_calls = [call[0][0] for call in mock_logger.error.call_args_list] + # Verify it logged the failure before raising + info_calls = [call[0][0] for call in mock_logger.info.call_args_list] + error_calls = [call[0][0] for call in mock_logger.error.call_args_list] - assert "System configuration required. Executing commands..." in info_calls - assert " āœ“ Success" in info_calls # First command succeeded - assert any( - "āœ— Failed to configure multicast" in call for call in error_calls - ) # Second command failed + assert "System configuration required. Executing commands..." in info_calls + assert " āœ“ Success" in info_calls # First command succeeded + assert any( + "āœ— Failed to configure multicast" in call for call in error_calls + ) # Second command failed diff --git a/dimos/protocol/service/test_spec.py b/dimos/protocol/service/test_spec.py index 9842f9c49f..efb24d7e38 100644 --- a/dimos/protocol/service/test_spec.py +++ b/dimos/protocol/service/test_spec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/protocol/skill/comms.py b/dimos/protocol/skill/comms.py index b0adecf5c5..0720140b79 100644 --- a/dimos/protocol/skill/comms.py +++ b/dimos/protocol/skill/comms.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar from dimos.protocol.pubsub.lcmpubsub import PickleLCM -from dimos.protocol.service import Service +from dimos.protocol.service import Service # type: ignore[attr-defined] from dimos.protocol.skill.type import SkillMsg if TYPE_CHECKING: @@ -32,10 +32,10 @@ class SkillCommsSpec: @abstractmethod - def publish(self, msg: SkillMsg) -> None: ... + def publish(self, msg: SkillMsg) -> None: ... # type: ignore[type-arg] @abstractmethod - def subscribe(self, cb: Callable[[SkillMsg], None]) -> None: ... + def subscribe(self, cb: Callable[[SkillMsg], None]) -> None: ... # type: ignore[type-arg] @abstractmethod def start(self) -> None: ... @@ -56,10 +56,10 @@ class PubSubCommsConfig(Generic[TopicT, MsgT]): # implementation of the SkillComms using any standard PubSub mechanism -class PubSubComms(Service[PubSubCommsConfig], SkillCommsSpec): - default_config: type[PubSubCommsConfig] = PubSubCommsConfig +class PubSubComms(Service[PubSubCommsConfig], SkillCommsSpec): # type: ignore[type-arg] + default_config: type[PubSubCommsConfig] = PubSubCommsConfig # type: ignore[type-arg] - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) pubsub_config = getattr(self.config, "pubsub", None) if pubsub_config is not None: @@ -79,17 +79,17 @@ def start(self) -> None: def stop(self) -> None: self.pubsub.stop() - def publish(self, msg: SkillMsg) -> None: + def publish(self, msg: SkillMsg) -> None: # type: ignore[type-arg] self.pubsub.publish(self.config.topic, msg) - def subscribe(self, cb: Callable[[SkillMsg], None]) -> None: + def subscribe(self, cb: Callable[[SkillMsg], None]) -> None: # type: ignore[type-arg] self.pubsub.subscribe(self.config.topic, lambda msg, topic: cb(msg)) @dataclass -class LCMCommsConfig(PubSubCommsConfig[str, SkillMsg]): +class LCMCommsConfig(PubSubCommsConfig[str, SkillMsg]): # type: ignore[type-arg] topic: str = "/skill" - pubsub: type[PubSub] | PubSub | None = PickleLCM + pubsub: type[PubSub] | PubSub | None = PickleLCM # type: ignore[type-arg] # lcm needs to be started only if receiving # skill comms are broadcast only in modules so we don't autostart autostart: bool = False diff --git a/dimos/protocol/skill/coordinator.py b/dimos/protocol/skill/coordinator.py index a672ceacee..95fc8844d4 100644 --- a/dimos/protocol/skill/coordinator.py +++ b/dimos/protocol/skill/coordinator.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ from dataclasses import dataclass from enum import Enum import json -import threading import time from typing import Any, Literal @@ -28,18 +27,18 @@ from rich.text import Text from dimos.core import rpc -from dimos.core.module import Module, get_loop +from dimos.core.module import Module, ModuleConfig from dimos.protocol.skill.comms import LCMSkillComms, SkillCommsSpec -from dimos.protocol.skill.skill import SkillConfig, SkillContainer +from dimos.protocol.skill.skill import SkillConfig, SkillContainer # type: ignore[attr-defined] from dimos.protocol.skill.type import MsgType, Output, Reducer, Return, SkillMsg, Stream from dimos.protocol.skill.utils import interpret_tool_call_args from dimos.utils.logging_config import setup_logger -logger = setup_logger(__file__) +logger = setup_logger() @dataclass -class SkillCoordinatorConfig: +class SkillCoordinatorConfig(ModuleConfig): skill_transport: type[SkillCommsSpec] = LCMSkillComms @@ -70,11 +69,11 @@ class SkillState: msg_count: int = 0 sent_tool_msg: bool = False - start_msg: SkillMsg[Literal[MsgType.start]] = None - end_msg: SkillMsg[Literal[MsgType.ret]] = None - error_msg: SkillMsg[Literal[MsgType.error]] = None - ret_msg: SkillMsg[Literal[MsgType.ret]] = None - reduced_stream_msg: list[SkillMsg[Literal[MsgType.reduced_stream]]] = None + start_msg: SkillMsg[Literal[MsgType.start]] = None # type: ignore[assignment] + end_msg: SkillMsg[Literal[MsgType.ret]] = None # type: ignore[assignment] + error_msg: SkillMsg[Literal[MsgType.error]] = None # type: ignore[assignment] + ret_msg: SkillMsg[Literal[MsgType.ret]] = None # type: ignore[assignment] + reduced_stream_msg: list[SkillMsg[Literal[MsgType.reduced_stream]]] = None # type: ignore[assignment] def __init__(self, call_id: str, name: str, skill_config: SkillConfig | None = None) -> None: super().__init__() @@ -101,22 +100,22 @@ def duration(self) -> float: else: return 0.0 - def content(self) -> dict[str, Any] | str | int | float | None: + def content(self) -> dict[str, Any] | str | int | float | None: # type: ignore[return] if self.state == SkillStateEnum.running: if self.reduced_stream_msg: - return self.reduced_stream_msg.content + return self.reduced_stream_msg.content # type: ignore[attr-defined, no-any-return] if self.state == SkillStateEnum.completed: if self.reduced_stream_msg: # are we a streaming skill? - return self.reduced_stream_msg.content - return self.ret_msg.content + return self.reduced_stream_msg.content # type: ignore[attr-defined, no-any-return] + return self.ret_msg.content # type: ignore[return-value] if self.state == SkillStateEnum.error: print("Error msg:", self.error_msg.content) if self.reduced_stream_msg: - (self.reduced_stream_msg.content + "\n" + self.error_msg.content) + (self.reduced_stream_msg.content + "\n" + self.error_msg.content) # type: ignore[attr-defined] else: - return self.error_msg.content + return self.error_msg.content # type: ignore[return-value] def agent_encode(self) -> ToolMessage | str: # tool call can emit a single ToolMessage @@ -126,7 +125,7 @@ def agent_encode(self) -> ToolMessage | str: if not self.sent_tool_msg: self.sent_tool_msg = True return ToolMessage( - self.content() or "Querying, please wait, you will receive a response soon.", + self.content() or "Querying, please wait, you will receive a response soon.", # type: ignore[arg-type] name=self.name, tool_call_id=self.call_id, ) @@ -142,11 +141,11 @@ def agent_encode(self) -> ToolMessage | str: ) # returns True if the agent should be called for this message - def handle_msg(self, msg: SkillMsg) -> bool: + def handle_msg(self, msg: SkillMsg) -> bool: # type: ignore[type-arg] self.msg_count += 1 if msg.type == MsgType.stream: self.state = SkillStateEnum.running - self.reduced_stream_msg = self.skill_config.reducer(self.reduced_stream_msg, msg) + self.reduced_stream_msg = self.skill_config.reducer(self.reduced_stream_msg, msg) # type: ignore[arg-type, assignment] if ( self.skill_config.stream == Stream.none @@ -271,18 +270,15 @@ class SkillCoordinator(Module): _skill_state: SkillStateDict # key is call_id, not skill_name _skills: dict[str, SkillConfig] _updates_available: asyncio.Event | None - _loop: asyncio.AbstractEventLoop | None - _loop_thread: threading.Thread | None _agent_loop: asyncio.AbstractEventLoop | None - def __init__(self) -> None: - # TODO: Why isn't this super().__init__() ? - SkillContainer.__init__(self) - self._loop, self._loop_thread = get_loop() + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) self._static_containers = [] self._dynamic_containers = [] self._skills = {} self._skill_state = SkillStateDict() + # Defer event creation until we're in the correct loop context self._updates_available = None self._agent_loop = None @@ -313,7 +309,7 @@ def _ensure_updates_available(self) -> asyncio.Event: else: ... # print(f"[DEBUG] Reusing _updates_available event {id(self._updates_available)}") - return self._updates_available + return self._updates_available # type: ignore[return-value] @rpc def start(self) -> None: @@ -346,9 +342,9 @@ def __len__(self) -> int: # this can be converted to non-langchain json schema output # and langchain takes this output as well # just faster for now - def get_tools(self) -> list[dict]: + def get_tools(self) -> list[dict]: # type: ignore[type-arg] return [ - langchain_tool(skill_config.f) + langchain_tool(skill_config.f) # type: ignore[arg-type, misc] for skill_config in self.skills().values() if not skill_config.hide_skill ] @@ -382,7 +378,7 @@ def call_skill( arg_list, arg_keywords = interpret_tool_call_args(args) - return skill_config.call( + return skill_config.call( # type: ignore[no-any-return] call_id, *arg_list, **arg_keywords, @@ -392,7 +388,7 @@ def call_skill( # Updates local skill state (appends to streamed data if needed etc) # # Checks if agent needs to be notified (if ToolConfig has Return=call_agent or Stream=call_agent) - def handle_message(self, msg: SkillMsg) -> None: + def handle_message(self, msg: SkillMsg) -> None: # type: ignore[type-arg] if self._closed_coord: import traceback @@ -460,7 +456,7 @@ def has_passive_skills(self) -> bool: return False return True - async def wait_for_updates(self, timeout: float | None = None) -> True: + async def wait_for_updates(self, timeout: float | None = None) -> True: # type: ignore[valid-type] """Wait for skill updates to become available. This method should be called by the agent when it's ready to receive updates. @@ -540,8 +536,8 @@ def generate_snapshot(self, clear: bool = True) -> SkillStateDict: logger.info(f"Skill {skill_run.name} (call_id={call_id}) finished") to_delete.append(call_id) if skill_run.state == SkillStateEnum.error: - error_msg = skill_run.error_msg.content.get("msg", "Unknown error") - error_traceback = skill_run.error_msg.content.get( + error_msg = skill_run.error_msg.content.get("msg", "Unknown error") # type: ignore[union-attr] + error_traceback = skill_run.error_msg.content.get( # type: ignore[union-attr] "traceback", "No traceback available" ) @@ -560,7 +556,7 @@ def generate_snapshot(self, clear: bool = True) -> SkillStateDict: logger.debug( f"Resetting accumulator for skill {skill_run.name} (call_id={call_id})" ) - skill_run.reduced_stream_msg = None + skill_run.reduced_stream_msg = None # type: ignore[assignment] for call_id in to_delete: logger.debug(f"Call {call_id} finished, removing from state") diff --git a/dimos/protocol/skill/schema.py b/dimos/protocol/skill/schema.py index 49dc1caa37..3b265f9c1b 100644 --- a/dimos/protocol/skill/schema.py +++ b/dimos/protocol/skill/schema.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ from typing import Union, get_args, get_origin -def python_type_to_json_schema(python_type) -> dict: +def python_type_to_json_schema(python_type) -> dict: # type: ignore[no-untyped-def, type-arg] """Convert Python type annotations to JSON Schema format.""" # Handle None/NoneType if python_type is type(None) or python_type is None: @@ -60,7 +60,7 @@ def python_type_to_json_schema(python_type) -> dict: return type_map.get(python_type, {"type": "string"}) -def function_to_schema(func) -> dict: +def function_to_schema(func) -> dict: # type: ignore[no-untyped-def, type-arg] """Convert a function to OpenAI function schema format.""" try: signature = inspect.signature(func) diff --git a/dimos/protocol/skill/skill.py b/dimos/protocol/skill/skill.py index 7ad260eaa5..373bb463a7 100644 --- a/dimos/protocol/skill/skill.py +++ b/dimos/protocol/skill/skill.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -63,14 +63,14 @@ def rpc(fn: Callable[..., Any]) -> Callable[..., Any]: def skill( - reducer: Reducer = Reducer.latest, + reducer: Reducer = Reducer.latest, # type: ignore[assignment] stream: Stream = Stream.none, ret: Return = Return.call_agent, output: Output = Output.standard, hide_skill: bool = False, -) -> Callable: +) -> Callable: # type: ignore[type-arg] def decorator(f: Callable[..., Any]) -> Any: - def wrapper(self, *args, **kwargs): + def wrapper(self, *args, **kwargs): # type: ignore[no-untyped-def] skill = f"{f.__name__}" call_id = kwargs.get("call_id", None) @@ -95,7 +95,7 @@ def wrapper(self, *args, **kwargs): skill_config = SkillConfig( name=f.__name__, - reducer=reducer, + reducer=reducer, # type: ignore[arg-type] stream=stream, # if stream is passive, ret must be passive too ret=ret.passive if stream == Stream.passive else ret, @@ -121,7 +121,7 @@ class SkillContainerConfig: def threaded(f: Callable[..., Any]) -> Callable[..., None]: """Decorator to run a function in a thread pool.""" - def wrapper(self, *args, **kwargs): + def wrapper(self, *args, **kwargs): # type: ignore[no-untyped-def] if self._skill_thread_pool is None: self._skill_thread_pool = ThreadPoolExecutor( max_workers=50, thread_name_prefix="skill_worker" @@ -170,7 +170,7 @@ def stop(self) -> None: # Continue the MRO chain if there's a parent stop() method if hasattr(super(), "stop"): - super().stop() + super().stop() # type: ignore[misc] # TODO: figure out standard args/kwargs passing format, # use same interface as skill coordinator call_skill method @@ -195,7 +195,7 @@ def call_skill( # check if the skill returned a coroutine, if it is, block until it resolves if isinstance(val, asyncio.Future): - val = asyncio.run(val) + val = asyncio.run(val) # type: ignore[arg-type] # check if the skill is a generator, if it is, we need to iterate over it if hasattr(val, "__iter__") and not isinstance(val, str): diff --git a/dimos/protocol/skill/test_coordinator.py b/dimos/protocol/skill/test_coordinator.py index e8d8c45a0c..acaad98dda 100644 --- a/dimos/protocol/skill/test_coordinator.py +++ b/dimos/protocol/skill/test_coordinator.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import datetime import time -import pytest +import pytest # type: ignore[import-not-found] from dimos.core import Module, rpc from dimos.msgs.sensor_msgs import Image @@ -47,57 +47,60 @@ def delayadd(self, x: int, y: int) -> int: time.sleep(0.3) return x + y - @skill(stream=Stream.call_agent, reducer=Reducer.all) + @skill(stream=Stream.call_agent, reducer=Reducer.all) # type: ignore[arg-type] def counter(self, count_to: int, delay: float | None = 0.05) -> Generator[int, None, None]: """Counts from 1 to count_to, with an optional delay between counts.""" for i in range(1, count_to + 1): - if delay > 0: + if delay is not None and delay > 0: time.sleep(delay) yield i - @skill(stream=Stream.passive, reducer=Reducer.sum) + @skill(stream=Stream.passive, reducer=Reducer.sum) # type: ignore[arg-type] def counter_passive_sum( self, count_to: int, delay: float | None = 0.05 ) -> Generator[int, None, None]: """Counts from 1 to count_to, with an optional delay between counts.""" for i in range(1, count_to + 1): - if delay > 0: + if delay is not None and delay > 0: time.sleep(delay) yield i - @skill(stream=Stream.passive, reducer=Reducer.latest) + @skill(stream=Stream.passive, reducer=Reducer.latest) # type: ignore[arg-type] def current_time(self, frequency: float | None = 10) -> Generator[str, None, None]: """Provides current time.""" while True: yield str(datetime.datetime.now()) - time.sleep(1 / frequency) + if frequency is not None: + time.sleep(1 / frequency) - @skill(stream=Stream.passive, reducer=Reducer.latest) + @skill(stream=Stream.passive, reducer=Reducer.latest) # type: ignore[arg-type] def uptime_seconds(self, frequency: float | None = 10) -> Generator[float, None, None]: """Provides current uptime.""" start_time = datetime.datetime.now() while True: yield (datetime.datetime.now() - start_time).total_seconds() - time.sleep(1 / frequency) + if frequency is not None: + time.sleep(1 / frequency) @skill() def current_date(self, frequency: float | None = 10) -> str: """Provides current date.""" - return datetime.datetime.now() + return str(datetime.datetime.now()) @skill(output=Output.image) - def take_photo(self) -> str: + def take_photo(self) -> Image: """Takes a camera photo""" print("Taking photo...") - img = Image.from_file(get_data("cafe-smol.jpg")) + img = Image.from_file(str(get_data("cafe-smol.jpg"))) print("Photo taken.") return img -@pytest.mark.asyncio +@pytest.mark.asyncio # type: ignore[untyped-decorator] async def test_coordinator_parallel_calls() -> None: + container = SkillContainerTest() skillCoordinator = SkillCoordinator() - skillCoordinator.register_skills(SkillContainerTest()) + skillCoordinator.register_skills(container) skillCoordinator.start() skillCoordinator.call_skill("test-call-0", "add", {"args": [0, 2]}) @@ -112,7 +115,7 @@ async def test_coordinator_parallel_calls() -> None: skill_id = f"test-call-{cnt}" tool_msg = skillstates[skill_id].agent_encode() - assert tool_msg.content == cnt + 2 + assert tool_msg.content == cnt + 2 # type: ignore[union-attr] cnt += 1 if cnt < 5: @@ -129,10 +132,11 @@ async def test_coordinator_parallel_calls() -> None: await asyncio.sleep(0.1 * cnt) + container.stop() skillCoordinator.stop() -@pytest.mark.asyncio +@pytest.mark.asyncio # type: ignore[untyped-decorator] async def test_coordinator_generator() -> None: container = SkillContainerTest() skillCoordinator = SkillCoordinator() diff --git a/dimos/protocol/skill/test_utils.py b/dimos/protocol/skill/test_utils.py index db332357fe..d9fe9f6f91 100644 --- a/dimos/protocol/skill/test_utils.py +++ b/dimos/protocol/skill/test_utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/protocol/skill/type.py b/dimos/protocol/skill/type.py index 9b1c4ce5f5..7881dcd94e 100644 --- a/dimos/protocol/skill/type.py +++ b/dimos/protocol/skill/type.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -59,15 +59,15 @@ class SkillConfig: ret: Return output: Output schema: dict[str, Any] - f: Callable | None = None + f: Callable | None = None # type: ignore[type-arg] autostart: bool = False hide_skill: bool = False - def bind(self, f: Callable) -> SkillConfig: + def bind(self, f: Callable) -> SkillConfig: # type: ignore[type-arg] self.f = f return self - def call(self, call_id, *args, **kwargs) -> Any: + def call(self, call_id, *args, **kwargs) -> Any: # type: ignore[no-untyped-def] if self.f is None: raise ValueError( "Function is not bound to the SkillConfig. This should be called only within AgentListener." @@ -101,8 +101,8 @@ class MsgType(Enum): def maybe_encode(something: Any) -> str: if hasattr(something, "agent_encode"): - return something.agent_encode() - return something + return something.agent_encode() # type: ignore[no-any-return] + return something # type: ignore[no-any-return] class SkillMsg(Timestamped, Generic[M]): @@ -110,7 +110,7 @@ class SkillMsg(Timestamped, Generic[M]): type: M call_id: str skill_name: str - content: str | int | float | dict | list + content: str | int | float | dict | list # type: ignore[type-arg] def __init__( self, @@ -136,7 +136,7 @@ def end(self) -> bool: def start(self) -> bool: return self.type == MsgType.start - def __str__(self) -> str: + def __str__(self) -> str: # type: ignore[return] time_ago = time.time() - self.ts if self.type == MsgType.start: @@ -167,7 +167,7 @@ def __str__(self) -> str: SimpleReducerF = Callable[[A | None, C], A] -def make_reducer(simple_reducer: SimpleReducerF) -> ReducerF: +def make_reducer(simple_reducer: SimpleReducerF) -> ReducerF: # type: ignore[type-arg] """ Converts a naive reducer function into a standard reducer function. The naive reducer function should accept an accumulator and a message, @@ -214,7 +214,7 @@ def sum_reducer( ) -> SkillMsg[Literal[MsgType.reduced_stream]]: """Sum reducer that adds values together.""" acc_value = accumulator.content if accumulator else None - new_value = acc_value + msg.content if acc_value else msg.content + new_value = acc_value + msg.content if acc_value else msg.content # type: ignore[operator] return _make_skill_msg(msg, new_value) @@ -232,7 +232,7 @@ def all_reducer( ) -> SkillMsg[Literal[MsgType.reduced_stream]]: """All reducer that collects all values into a list.""" acc_value = accumulator.content if accumulator else None - new_value = [*acc_value, msg.content] if acc_value else [msg.content] + new_value = [*acc_value, msg.content] if acc_value else [msg.content] # type: ignore[misc] return _make_skill_msg(msg, new_value) @@ -242,7 +242,7 @@ def accumulate_list( ) -> SkillMsg[Literal[MsgType.reduced_stream]]: """All reducer that collects all values into a list.""" acc_value = accumulator.content if accumulator else [] - return _make_skill_msg(msg, acc_value + msg.content) + return _make_skill_msg(msg, acc_value + msg.content) # type: ignore[operator] def accumulate_dict( @@ -251,7 +251,7 @@ def accumulate_dict( ) -> SkillMsg[Literal[MsgType.reduced_stream]]: """All reducer that collects all values into a list.""" acc_value = accumulator.content if accumulator else {} - return _make_skill_msg(msg, {**acc_value, **msg.content}) + return _make_skill_msg(msg, {**acc_value, **msg.content}) # type: ignore[dict-item] def accumulate_string( @@ -260,7 +260,7 @@ def accumulate_string( ) -> SkillMsg[Literal[MsgType.reduced_stream]]: """All reducer that collects all values into a list.""" acc_value = accumulator.content if accumulator else "" - return _make_skill_msg(msg, acc_value + "\n" + msg.content) + return _make_skill_msg(msg, acc_value + "\n" + msg.content) # type: ignore[operator] class Reducer: diff --git a/dimos/protocol/skill/utils.py b/dimos/protocol/skill/utils.py index f3d052070f..278134c525 100644 --- a/dimos/protocol/skill/utils.py +++ b/dimos/protocol/skill/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/protocol/tf/__init__.py b/dimos/protocol/tf/__init__.py index 96cdbcf285..cb00dbde3c 100644 --- a/dimos/protocol/tf/__init__.py +++ b/dimos/protocol/tf/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/protocol/tf/test_tf.py b/dimos/protocol/tf/test_tf.py index c25e1014f9..0b5b332c3d 100644 --- a/dimos/protocol/tf/test_tf.py +++ b/dimos/protocol/tf/test_tf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/protocol/tf/tf.py b/dimos/protocol/tf/tf.py index f60e216176..3688b013cf 100644 --- a/dimos/protocol/tf/tf.py +++ b/dimos/protocol/tf/tf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ from dimos.msgs.tf2_msgs import TFMessage from dimos.protocol.pubsub.lcmpubsub import LCM, Topic from dimos.protocol.pubsub.spec import PubSub -from dimos.protocol.service.lcmservice import Service +from dimos.protocol.service.lcmservice import Service # type: ignore[attr-defined] from dimos.types.timestamped import TimestampedCollection CONFIG = TypeVar("CONFIG") @@ -39,7 +39,7 @@ class TFConfig: # generic specification for transform service class TFSpec(Service[TFConfig]): - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) @abstractmethod @@ -52,7 +52,7 @@ def get_frames(self) -> set[str]: return set() @abstractmethod - def get( + def get( # type: ignore[no-untyped-def] self, parent_frame: str, child_frame: str, @@ -81,7 +81,7 @@ def add(self, transform: Transform) -> None: super().add(transform) self._prune_old_transforms(transform.ts) - def _prune_old_transforms(self, current_time) -> None: + def _prune_old_transforms(self, current_time) -> None: # type: ignore[no-untyped-def] if not self._items: return @@ -172,17 +172,17 @@ def get_transform( # Check forward direction key = (parent_frame, child_frame) if key in self.buffers: - return self.buffers[key].get(time_point, time_tolerance) + return self.buffers[key].get(time_point, time_tolerance) # type: ignore[arg-type] # Check reverse direction and return inverse reverse_key = (child_frame, parent_frame) if reverse_key in self.buffers: - transform = self.buffers[reverse_key].get(time_point, time_tolerance) + transform = self.buffers[reverse_key].get(time_point, time_tolerance) # type: ignore[arg-type] return transform.inverse() if transform else None return None - def get(self, *args, **kwargs) -> Transform | None: + def get(self, *args, **kwargs) -> Transform | None: # type: ignore[no-untyped-def] simple = self.get_transform(*args, **kwargs) if simple is not None: return simple @@ -267,14 +267,14 @@ def __str__(self) -> str: @dataclass class PubSubTFConfig(TFConfig): topic: Topic | None = None # Required field but needs default for dataclass inheritance - pubsub: type[PubSub] | PubSub | None = None + pubsub: type[PubSub] | PubSub | None = None # type: ignore[type-arg] autostart: bool = True class PubSubTF(MultiTBuffer, TFSpec): default_config: type[PubSubTFConfig] = PubSubTFConfig - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] TFSpec.__init__(self, **kwargs) MultiTBuffer.__init__(self, self.config.buffer_size) @@ -287,7 +287,7 @@ def __init__(self, **kwargs) -> None: else: raise ValueError("PubSub configuration is missing") - if self.config.autostart: + if self.config.autostart: # type: ignore[attr-defined] self.start() def start(self, sub: bool = True) -> None: @@ -341,7 +341,7 @@ def receive_msg(self, msg: TFMessage, topic: Topic) -> None: @dataclass class LCMPubsubConfig(PubSubTFConfig): topic: Topic = field(default_factory=lambda: Topic("/tf", TFMessage)) - pubsub: type[PubSub] | PubSub | None = LCM + pubsub: type[PubSub] | PubSub | None = LCM # type: ignore[type-arg] autostart: bool = True diff --git a/dimos/protocol/tf/tflcmcpp.py b/dimos/protocol/tf/tflcmcpp.py index 0d5b31b9b6..158a68d3d8 100644 --- a/dimos/protocol/tf/tflcmcpp.py +++ b/dimos/protocol/tf/tflcmcpp.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -36,10 +36,10 @@ class TFLCM(TFSpec, LCMService): default_config = Union[TFConfig, LCMConfig] - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) - import tf_lcm_py as tf + import tf_lcm_py as tf # type: ignore[import-not-found] self.l = tf.LCM() self.buffer = tf.Buffer(self.config.buffer_size) @@ -58,7 +58,7 @@ def send_static(self, *args: Transform) -> None: for t in args: self.static_broadcaster.send_static_transform(t) - def lookup( + def lookup( # type: ignore[no-untyped-def] self, parent_frame: str, child_frame: str, @@ -81,7 +81,7 @@ def can_transform( if isinstance(time_point, float): time_point = datetime.fromtimestamp(time_point) - return self.buffer.can_transform(parent_frame, child_frame, time_point) + return self.buffer.can_transform(parent_frame, child_frame, time_point) # type: ignore[no-any-return] def get_frames(self) -> set[str]: return set(self.buffer.get_all_frame_names()) diff --git a/dimos/robot/agilex/README.md b/dimos/robot/agilex/README.md index 1e678cae65..8342a6045e 100644 --- a/dimos/robot/agilex/README.md +++ b/dimos/robot/agilex/README.md @@ -21,27 +21,27 @@ from dimos.types.robot_capabilities import RobotCapability class YourRobot: """Your robot implementation.""" - + def __init__(self, robot_capabilities: Optional[List[RobotCapability]] = None): # Core components self.dimos = None self.modules = {} self.skill_library = SkillLibrary() - + # Define capabilities self.capabilities = robot_capabilities or [ RobotCapability.VISION, RobotCapability.MANIPULATION, ] - + async def start(self): """Start the robot modules.""" # Initialize DIMOS with worker count self.dimos = core.start(2) # Number of workers needed - + # Deploy modules # ... (see Module System section) - + def stop(self): """Stop all modules and clean up.""" # Stop modules @@ -96,7 +96,7 @@ self.camera.color_image.transport = core.LCMTransport( Image # Message type ) self.camera.depth_image.transport = core.LCMTransport( - "/camera/depth_image", + "/camera/depth_image", Image ) ``` @@ -135,30 +135,30 @@ The run file pattern for agent integration: #!/usr/bin/env python3 import asyncio import reactivex as rx -from dimos.agents.claude_agent import ClaudeAgent +from dimos.agents_deprecated.claude_agent import ClaudeAgent from dimos.web.robot_web_interface import RobotWebInterface def main(): # 1. Create and start robot robot = YourRobot() asyncio.run(robot.start()) - + # 2. Set up skills skills = robot.get_skills() skills.add(YourSkill) skills.create_instance("YourSkill", robot=robot) - + # 3. Set up reactive streams agent_response_subject = rx.subject.Subject() agent_response_stream = agent_response_subject.pipe(ops.share()) - + # 4. Create web interface web_interface = RobotWebInterface( port=5555, text_streams={"agent_responses": agent_response_stream}, audio_subject=rx.subject.Subject() ) - + # 5. Create agent agent = ClaudeAgent( dev_name="your_agent", @@ -167,12 +167,12 @@ def main(): system_query="Your system prompt here", model_name="claude-3-5-haiku-latest" ) - + # 6. Connect agent responses agent.get_response_observable().subscribe( lambda x: agent_response_subject.on_next(x) ) - + # 7. Run interface web_interface.run() ``` @@ -205,51 +205,51 @@ class MyRobot: self.camera = None self.manipulation = None self.skill_library = SkillLibrary() - + self.capabilities = robot_capabilities or [ RobotCapability.VISION, RobotCapability.MANIPULATION, ] - + async def start(self): # Start DIMOS self.dimos = core.start(2) - + # Enable LCM pubsub.lcm.autoconf() - + # Deploy camera self.camera = self.dimos.deploy( CameraModule, camera_id=0, fps=30 ) - + # Configure camera LCM self.camera.color_image.transport = core.LCMTransport("/camera/rgb", Image) self.camera.depth_image.transport = core.LCMTransport("/camera/depth", Image) self.camera.camera_info.transport = core.LCMTransport("/camera/info", CameraInfo) - + # Deploy manipulation self.manipulation = self.dimos.deploy(ManipulationModule) - + # Connect modules self.manipulation.rgb_image.connect(self.camera.color_image) self.manipulation.depth_image.connect(self.camera.depth_image) self.manipulation.camera_info.connect(self.camera.camera_info) - + # Configure manipulation output self.manipulation.viz_image.transport = core.LCMTransport("/viz/output", Image) - + # Start modules self.camera.start() self.manipulation.start() - + await asyncio.sleep(2) # Allow initialization - + def get_skills(self): return self.skill_library - + def stop(self): if self.manipulation: self.manipulation.stop() @@ -266,7 +266,7 @@ class MyRobot: import asyncio import os from my_robot import MyRobot -from dimos.agents.claude_agent import ClaudeAgent +from dimos.agents_deprecated.claude_agent import ClaudeAgent from dimos.skills.basic import BasicSkill from dimos.web.robot_web_interface import RobotWebInterface import reactivex as rx @@ -279,29 +279,29 @@ def main(): if not os.getenv("ANTHROPIC_API_KEY"): print("Please set ANTHROPIC_API_KEY") return - + # Create robot robot = MyRobot() - + try: # Start robot asyncio.run(robot.start()) - + # Set up skills skills = robot.get_skills() skills.add(BasicSkill) skills.create_instance("BasicSkill", robot=robot) - + # Set up streams agent_response_subject = rx.subject.Subject() agent_response_stream = agent_response_subject.pipe(ops.share()) - + # Create web interface web_interface = RobotWebInterface( port=5555, text_streams={"agent_responses": agent_response_stream} ) - + # Create agent agent = ClaudeAgent( dev_name="my_agent", @@ -309,17 +309,17 @@ def main(): skills=skills, system_query=SYSTEM_PROMPT ) - + # Connect responses agent.get_response_observable().subscribe( lambda x: agent_response_subject.on_next(x) ) - + print("Robot ready at http://localhost:5555") - + # Run web_interface.run() - + finally: robot.stop() @@ -341,7 +341,7 @@ from dimos.skills import Skill, skill class BasicSkill(Skill): def __init__(self, robot): self.robot = robot - + def run(self, action: str): # Implement skill logic return f"Performed: {action}" @@ -368,4 +368,4 @@ class BasicSkill(Skill): 1. **"Module not started"**: Ensure start() is called after deployment 2. **"No data received"**: Check LCM transport configuration 3. **"Connection failed"**: Verify input/output types match -4. **"Cleanup errors"**: Stop modules before closing DIMOS \ No newline at end of file +4. **"Cleanup errors"**: Stop modules before closing DIMOS diff --git a/dimos/robot/agilex/README_CN.md b/dimos/robot/agilex/README_CN.md index 482a09dd6d..a8d79ebec1 100644 --- a/dimos/robot/agilex/README_CN.md +++ b/dimos/robot/agilex/README_CN.md @@ -21,27 +21,27 @@ from dimos.types.robot_capabilities import RobotCapability class YourRobot: """ę‚Øēš„ęœŗå™Øäŗŗå®žēŽ°ć€‚""" - + def __init__(self, robot_capabilities: Optional[List[RobotCapability]] = None): # ę øåæƒē»„ä»¶ self.dimos = None self.modules = {} self.skill_library = SkillLibrary() - + # å®šä¹‰čƒ½åŠ› self.capabilities = robot_capabilities or [ RobotCapability.VISION, RobotCapability.MANIPULATION, ] - + async def start(self): """åÆåŠØęœŗå™ØäŗŗęØ”å—ć€‚""" # 初始化 DIMOSļ¼ŒęŒ‡å®šå·„ä½œēŗæēØ‹ę•° self.dimos = core.start(2) # éœ€č¦ēš„å·„ä½œēŗæēØ‹ę•° - + # éƒØē½²ęØ”å— # ... (å‚č§ęØ”å—ē³»ē»Ÿē« čŠ‚) - + def stop(self): """åœę­¢ę‰€ęœ‰ęØ”å—å¹¶ęø…ē†čµ„ęŗć€‚""" # åœę­¢ęØ”å— @@ -96,7 +96,7 @@ self.camera.color_image.transport = core.LCMTransport( Image # ę¶ˆęÆē±»åž‹ ) self.camera.depth_image.transport = core.LCMTransport( - "/camera/depth_image", + "/camera/depth_image", Image ) ``` @@ -135,30 +135,30 @@ self.manipulation.camera_info.connect(self.camera.camera_info) #!/usr/bin/env python3 import asyncio import reactivex as rx -from dimos.agents.claude_agent import ClaudeAgent +from dimos.agents_deprecated.claude_agent import ClaudeAgent from dimos.web.robot_web_interface import RobotWebInterface def main(): # 1. åˆ›å»ŗå¹¶åÆåŠØęœŗå™Øäŗŗ robot = YourRobot() asyncio.run(robot.start()) - + # 2. č®¾ē½®ęŠ€čƒ½ skills = robot.get_skills() skills.add(YourSkill) skills.create_instance("YourSkill", robot=robot) - + # 3. č®¾ē½®å“åŗ”å¼ęµ agent_response_subject = rx.subject.Subject() agent_response_stream = agent_response_subject.pipe(ops.share()) - + # 4. åˆ›å»ŗ Web ē•Œé¢ web_interface = RobotWebInterface( port=5555, text_streams={"agent_responses": agent_response_stream}, audio_subject=rx.subject.Subject() ) - + # 5. åˆ›å»ŗę™ŗčƒ½ä½“ agent = ClaudeAgent( dev_name="your_agent", @@ -167,12 +167,12 @@ def main(): system_query="ę‚Øēš„ē³»ē»Ÿęē¤ŗčÆ", model_name="claude-3-5-haiku-latest" ) - + # 6. čæžęŽ„ę™ŗčƒ½ä½“å“åŗ” agent.get_response_observable().subscribe( lambda x: agent_response_subject.on_next(x) ) - + # 7. čæč”Œē•Œé¢ web_interface.run() ``` @@ -205,51 +205,51 @@ class MyRobot: self.camera = None self.manipulation = None self.skill_library = SkillLibrary() - + self.capabilities = robot_capabilities or [ RobotCapability.VISION, RobotCapability.MANIPULATION, ] - + async def start(self): # 启动 DIMOS self.dimos = core.start(2) - + # 启用 LCM pubsub.lcm.autoconf() - + # éƒØē½²ē›øęœŗ self.camera = self.dimos.deploy( CameraModule, camera_id=0, fps=30 ) - + # é…ē½®ē›øęœŗ LCM self.camera.color_image.transport = core.LCMTransport("/camera/rgb", Image) self.camera.depth_image.transport = core.LCMTransport("/camera/depth", Image) self.camera.camera_info.transport = core.LCMTransport("/camera/info", CameraInfo) - + # éƒØē½²ę“ä½œęØ”å— self.manipulation = self.dimos.deploy(ManipulationModule) - + # čæžęŽ„ęØ”å— self.manipulation.rgb_image.connect(self.camera.color_image) self.manipulation.depth_image.connect(self.camera.depth_image) self.manipulation.camera_info.connect(self.camera.camera_info) - + # é…ē½®ę“ä½œč¾“å‡ŗ self.manipulation.viz_image.transport = core.LCMTransport("/viz/output", Image) - + # åÆåŠØęØ”å— self.camera.start() self.manipulation.start() - + await asyncio.sleep(2) # å…č®øåˆå§‹åŒ– - + def get_skills(self): return self.skill_library - + def stop(self): if self.manipulation: self.manipulation.stop() @@ -266,7 +266,7 @@ class MyRobot: import asyncio import os from my_robot import MyRobot -from dimos.agents.claude_agent import ClaudeAgent +from dimos.agents_deprecated.claude_agent import ClaudeAgent from dimos.skills.basic import BasicSkill from dimos.web.robot_web_interface import RobotWebInterface import reactivex as rx @@ -279,29 +279,29 @@ def main(): if not os.getenv("ANTHROPIC_API_KEY"): print("请设置 ANTHROPIC_API_KEY") return - + # åˆ›å»ŗęœŗå™Øäŗŗ robot = MyRobot() - + try: # åÆåŠØęœŗå™Øäŗŗ asyncio.run(robot.start()) - + # č®¾ē½®ęŠ€čƒ½ skills = robot.get_skills() skills.add(BasicSkill) skills.create_instance("BasicSkill", robot=robot) - + # 设置流 agent_response_subject = rx.subject.Subject() agent_response_stream = agent_response_subject.pipe(ops.share()) - + # åˆ›å»ŗ Web ē•Œé¢ web_interface = RobotWebInterface( port=5555, text_streams={"agent_responses": agent_response_stream} ) - + # åˆ›å»ŗę™ŗčƒ½ä½“ agent = ClaudeAgent( dev_name="my_agent", @@ -309,17 +309,17 @@ def main(): skills=skills, system_query=SYSTEM_PROMPT ) - + # čæžęŽ„å“åŗ” agent.get_response_observable().subscribe( lambda x: agent_response_subject.on_next(x) ) - + print("ęœŗå™Øäŗŗå°±ē»Ŗļ¼Œč®æé—® http://localhost:5555") - + # 运蔌 web_interface.run() - + finally: robot.stop() @@ -341,7 +341,7 @@ from dimos.skills import Skill, skill class BasicSkill(Skill): def __init__(self, robot): self.robot = robot - + def run(self, action: str): # å®žēŽ°ęŠ€čƒ½é€»č¾‘ return f"å·²ę‰§č”Œļ¼š{action}" @@ -381,28 +381,28 @@ from dimos.core import Module, In, Out, rpc class CustomModule(Module): # å®šä¹‰č¾“å…„ - input_data: In[DataType] = None - - # å®šä¹‰č¾“å‡ŗ - output_data: Out[DataType] = None - + input_data: In[DataType] + + # å®šä¹‰č¾“å‡ŗ + output_data: Out[DataType] + def __init__(self, param1, param2, **kwargs): super().__init__(**kwargs) self.param1 = param1 self.param2 = param2 - + @rpc def start(self): """åÆåŠØęØ”å—å¤„ē†ć€‚""" self.input_data.subscribe(self._process_data) - + def _process_data(self, data): """å¤„ē†č¾“å…„ę•°ę®ć€‚""" # 处理逻辑 result = self.process(data) # å‘åøƒč¾“å‡ŗ self.output_data.publish(result) - + @rpc def stop(self): """åœę­¢ęØ”å—ć€‚""" @@ -429,25 +429,25 @@ class ComplexSkill(Skill): def __init__(self, robot, **kwargs): super().__init__(**kwargs) self.robot = robot - + def run(self, target: str, location: Optional[str] = None): """ę‰§č”ŒęŠ€čƒ½é€»č¾‘ć€‚""" try: # 1. ę„ŸēŸ„é˜¶ę®µ object_info = self.robot.detect_object(target) - + # 2. č§„åˆ’é˜¶ę®µ if location: plan = self.robot.plan_movement(object_info, location) - + # 3. ę‰§č”Œé˜¶ę®µ result = self.robot.execute_plan(plan) - + return { "success": True, "message": f"成功移动 {target} 到 {location}" } - + except Exception as e: return { "success": False, @@ -462,4 +462,4 @@ class ComplexSkill(Skill): 3. **延迟加载**ļ¼šä»…åœØéœ€č¦ę—¶åˆå§‹åŒ–é‡åž‹ęØ”å— 4. **čµ„ęŗę± åŒ–**ļ¼šé‡ē”Øę˜‚č“µēš„čµ„ęŗļ¼ˆå¦‚ē„žē»ē½‘ē»œęØ”åž‹ļ¼‰ -åøŒęœ›ęœ¬ęŒ‡å—čƒ½åø®åŠ©ę‚Øåæ«é€ŸäøŠę‰‹ DIMOS ęœŗå™Øäŗŗå¼€å‘ļ¼ \ No newline at end of file +åøŒęœ›ęœ¬ęŒ‡å—čƒ½åø®åŠ©ę‚Øåæ«é€ŸäøŠę‰‹ DIMOS ęœŗå™Øäŗŗå¼€å‘ļ¼ diff --git a/dimos/robot/agilex/piper_arm.py b/dimos/robot/agilex/piper_arm.py index 642d39c7cb..29624b9a4c 100644 --- a/dimos/robot/agilex/piper_arm.py +++ b/dimos/robot/agilex/piper_arm.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ from dimos_lcm.sensor_msgs import CameraInfo from dimos import core -from dimos.hardware.camera.zed import ZEDModule +from dimos.hardware.sensors.camera.zed import ZEDModule from dimos.manipulation.visual_servoing.manipulation_module import ManipulationModule from dimos.msgs.sensor_msgs import Image from dimos.protocol import pubsub @@ -28,7 +28,7 @@ from dimos.types.robot_capabilities import RobotCapability from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.robot.agilex.piper_arm") +logger = setup_logger() class PiperArmRobot(Robot): @@ -39,7 +39,7 @@ def __init__(self, robot_capabilities: list[RobotCapability] | None = None) -> N self.dimos = None self.stereo_camera = None self.manipulation_interface = None - self.skill_library = SkillLibrary() + self.skill_library = SkillLibrary() # type: ignore[assignment] # Initialize capabilities self.capabilities = robot_capabilities or [ @@ -50,15 +50,15 @@ def __init__(self, robot_capabilities: list[RobotCapability] | None = None) -> N async def start(self) -> None: """Start the robot modules.""" # Start Dimos - self.dimos = core.start(2) # Need 2 workers for ZED and manipulation modules + self.dimos = core.start(2) # type: ignore[assignment] # Need 2 workers for ZED and manipulation modules self.foxglove_bridge = FoxgloveBridge() # Enable LCM auto-configuration - pubsub.lcm.autoconf() + pubsub.lcm.autoconf() # type: ignore[attr-defined] # Deploy ZED module logger.info("Deploying ZED module...") - self.stereo_camera = self.dimos.deploy( + self.stereo_camera = self.dimos.deploy( # type: ignore[attr-defined] ZEDModule, camera_id=0, resolution="HD720", @@ -70,43 +70,43 @@ async def start(self) -> None: ) # Configure ZED LCM transports - self.stereo_camera.color_image.transport = core.LCMTransport("/zed/color_image", Image) - self.stereo_camera.depth_image.transport = core.LCMTransport("/zed/depth_image", Image) - self.stereo_camera.camera_info.transport = core.LCMTransport("/zed/camera_info", CameraInfo) + self.stereo_camera.color_image.transport = core.LCMTransport("/zed/color_image", Image) # type: ignore[attr-defined] + self.stereo_camera.depth_image.transport = core.LCMTransport("/zed/depth_image", Image) # type: ignore[attr-defined] + self.stereo_camera.camera_info.transport = core.LCMTransport("/zed/camera_info", CameraInfo) # type: ignore[attr-defined] # Deploy manipulation module logger.info("Deploying manipulation module...") - self.manipulation_interface = self.dimos.deploy(ManipulationModule) + self.manipulation_interface = self.dimos.deploy(ManipulationModule) # type: ignore[attr-defined] # Connect manipulation inputs to ZED outputs - self.manipulation_interface.rgb_image.connect(self.stereo_camera.color_image) - self.manipulation_interface.depth_image.connect(self.stereo_camera.depth_image) - self.manipulation_interface.camera_info.connect(self.stereo_camera.camera_info) + self.manipulation_interface.rgb_image.connect(self.stereo_camera.color_image) # type: ignore[attr-defined] + self.manipulation_interface.depth_image.connect(self.stereo_camera.depth_image) # type: ignore[attr-defined] + self.manipulation_interface.camera_info.connect(self.stereo_camera.camera_info) # type: ignore[attr-defined] # Configure manipulation output - self.manipulation_interface.viz_image.transport = core.LCMTransport( + self.manipulation_interface.viz_image.transport = core.LCMTransport( # type: ignore[attr-defined] "/manipulation/viz", Image ) # Print module info logger.info("Modules configured:") print("\nZED Module:") - print(self.stereo_camera.io()) + print(self.stereo_camera.io()) # type: ignore[attr-defined] print("\nManipulation Module:") - print(self.manipulation_interface.io()) + print(self.manipulation_interface.io()) # type: ignore[attr-defined] # Start modules logger.info("Starting modules...") self.foxglove_bridge.start() - self.stereo_camera.start() - self.manipulation_interface.start() + self.stereo_camera.start() # type: ignore[attr-defined] + self.manipulation_interface.start() # type: ignore[attr-defined] # Give modules time to initialize await asyncio.sleep(2) logger.info("PiperArmRobot initialized and started") - def pick_and_place( + def pick_and_place( # type: ignore[no-untyped-def] self, pick_x: int, pick_y: int, place_x: int | None = None, place_y: int | None = None ): """Execute pick and place task. @@ -126,7 +126,7 @@ def pick_and_place( logger.error("Manipulation module not initialized") return False - def handle_keyboard_command(self, key: str): + def handle_keyboard_command(self, key: str): # type: ignore[no-untyped-def] """Pass keyboard commands to manipulation module. Args: @@ -163,7 +163,7 @@ def stop(self) -> None: async def run_piper_arm() -> None: """Run the Piper Arm robot.""" - robot = PiperArmRobot() + robot = PiperArmRobot() # type: ignore[abstract] await robot.start() @@ -174,7 +174,7 @@ async def run_piper_arm() -> None: except KeyboardInterrupt: logger.info("Keyboard interrupt received") finally: - await robot.stop() + await robot.stop() # type: ignore[func-returns-value] if __name__ == "__main__": diff --git a/dimos/robot/agilex/run.py b/dimos/robot/agilex/run.py index 90258e5d82..64e0ae5470 100644 --- a/dimos/robot/agilex/run.py +++ b/dimos/robot/agilex/run.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import reactivex as rx import reactivex.operators as ops -from dimos.agents.claude_agent import ClaudeAgent +from dimos.agents_deprecated.claude_agent import ClaudeAgent from dimos.robot.agilex.piper_arm import PiperArmRobot from dimos.skills.kill_skill import KillSkill from dimos.skills.manipulation.pick_and_place import PickAndPlace @@ -34,7 +34,7 @@ from dimos.utils.logging_config import setup_logger from dimos.web.robot_web_interface import RobotWebInterface -logger = setup_logger("dimos.robot.agilex.run") +logger = setup_logger() # Load environment variables load_dotenv() @@ -71,7 +71,7 @@ Remember: You're here to assist with manipulation tasks. Be helpful, precise, and always prioritize safe operation of the robot.""" -def main(): +def main(): # type: ignore[no-untyped-def] """Main entry point.""" print("\n" + "=" * 60) print("Piper Arm Robot with Claude Agent") @@ -94,7 +94,7 @@ def main(): logger.info("Starting Piper Arm Robot with Agent") # Create robot instance - robot = PiperArmRobot() + robot = PiperArmRobot() # type: ignore[abstract] try: # Start the robot (this is async, so we need asyncio.run) @@ -103,7 +103,7 @@ def main(): logger.info("Robot initialized successfully") # Set up skill library - skills = robot.get_skills() + skills = robot.get_skills() # type: ignore[no-untyped-call] skills.add(PickAndPlace) skills.add(KillSkill) @@ -114,12 +114,12 @@ def main(): logger.info(f"Skills registered: {[skill.__name__ for skill in skills.get_class_skills()]}") # Set up streams for agent and web interface - agent_response_subject = rx.subject.Subject() + agent_response_subject = rx.subject.Subject() # type: ignore[var-annotated] agent_response_stream = agent_response_subject.pipe(ops.share()) - audio_subject = rx.subject.Subject() + audio_subject = rx.subject.Subject() # type: ignore[var-annotated] # Set up streams for web interface - streams = {} + streams = {} # type: ignore[var-annotated] text_streams = { "agent_responses": agent_response_stream, @@ -136,7 +136,7 @@ def main(): raise # Set up speech-to-text - stt_node = stt() + stt_node = stt() # type: ignore[no-untyped-call] stt_node.consume_audio(audio_subject.pipe(ops.share())) # Create Claude agent @@ -155,7 +155,7 @@ def main(): agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) # Set up text-to-speech for agent responses - tts_node = tts() + tts_node = tts() # type: ignore[no-untyped-call] tts_node.consume_text(agent.get_response_observable()) logger.info("=" * 60) @@ -187,4 +187,4 @@ def main(): if __name__ == "__main__": - main() + main() # type: ignore[no-untyped-call] diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index c177723e66..f989098f05 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,36 +16,77 @@ # The blueprints are defined as import strings so as not to trigger unnecessary imports. all_blueprints = { - "unitree-go2": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard", + "unitree-go2": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:nav", "unitree-go2-basic": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:basic", - "unitree-go2-shm": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard_with_shm", - "unitree-go2-jpegshm": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard_with_jpegshm", - "unitree-go2-jpeglcm": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard_with_jpeglcm", + "unitree-go2-nav": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:nav", + "unitree-go2-detection": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:detection", + "unitree-go2-spatial": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:spatial", "unitree-go2-agentic": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic", + "unitree-go2-agentic-mcp": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic_mcp", + "unitree-go2-agentic-ollama": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic_ollama", + "unitree-go2-agentic-huggingface": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic_huggingface", + "unitree-go2-vlm-stream-test": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:vlm_stream_test", + "unitree-g1": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:standard", + "unitree-g1-sim": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:standard_sim", + "unitree-g1-basic": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:basic_ros", + "unitree-g1-basic-sim": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:basic_sim", + "unitree-g1-shm": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:standard_with_shm", + "unitree-g1-agentic": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:agentic", + "unitree-g1-agentic-sim": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:agentic_sim", + "unitree-g1-joystick": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:with_joystick", + "unitree-g1-full": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:full_featured", + "unitree-g1-detection": "dimos.robot.unitree_webrtc.unitree_g1_blueprints:detection", + # xArm manipulator blueprints + "xarm-servo": "dimos.hardware.manipulators.xarm.xarm_blueprints:xarm_servo", + "xarm5-servo": "dimos.hardware.manipulators.xarm.xarm_blueprints:xarm5_servo", + "xarm7-servo": "dimos.hardware.manipulators.xarm.xarm_blueprints:xarm7_servo", + "xarm-cartesian": "dimos.hardware.manipulators.xarm.xarm_blueprints:xarm_cartesian", + "xarm-trajectory": "dimos.hardware.manipulators.xarm.xarm_blueprints:xarm_trajectory", + # Piper manipulator blueprints + "piper-servo": "dimos.hardware.manipulators.piper.piper_blueprints:piper_servo", + "piper-cartesian": "dimos.hardware.manipulators.piper.piper_blueprints:piper_cartesian", + "piper-trajectory": "dimos.hardware.manipulators.piper.piper_blueprints:piper_trajectory", + # Demo blueprints "demo-osm": "dimos.mapping.osm.demo_osm:demo_osm", + "demo-skill": "dimos.agents.skills.demo_skill:demo_skill", + "demo-gps-nav": "dimos.agents.skills.demo_gps_nav:demo_gps_nav_skill", + "demo-google-maps-skill": "dimos.agents.skills.demo_google_maps_skill:demo_google_maps_skill", "demo-remapping": "dimos.robot.unitree_webrtc.demo_remapping:remapping", "demo-remapping-transport": "dimos.robot.unitree_webrtc.demo_remapping:remapping_and_transport", + "demo-error-on-name-conflicts": "dimos.robot.unitree_webrtc.demo_error_on_name_conflicts:blueprint", } all_modules = { - "astar_planner": "dimos.navigation.global_planner.planner", - "behavior_tree_navigator": "dimos.navigation.bt_navigator.navigator", - "connection": "dimos.robot.unitree_webrtc.unitree_go2", + "replanning_a_star_planner": "dimos.navigation.replanning_a_star.module", + "camera_module": "dimos.hardware.camera.module", "depth_module": "dimos.robot.unitree_webrtc.depth_module", "detection_2d": "dimos.perception.detection2d.module2D", "foxglove_bridge": "dimos.robot.foxglove_bridge", - "holonomic_local_planner": "dimos.navigation.local_planner.holonomic_local_planner", - "human_input": "dimos.agents2.cli.human", - "llm_agent": "dimos.agents2.agent", + "g1_connection": "dimos.robot.unitree.connection.g1", + "g1_joystick": "dimos.robot.unitree_webrtc.g1_joystick_module", + "g1_skills": "dimos.robot.unitree_webrtc.unitree_g1_skill_container", + "google_maps_skill": "dimos.agents.skills.google_maps_skill_container", + "gps_nav_skill": "dimos.agents.skills.gps_nav_skill", + "human_input": "dimos.agents.cli.human", + "keyboard_teleop": "dimos.robot.unitree_webrtc.keyboard_teleop", + "llm_agent": "dimos.agents.agent", "mapper": "dimos.robot.unitree_webrtc.type.map", - "navigation_skill": "dimos.agents2.skills.navigation", + "navigation_skill": "dimos.agents.skills.navigation", "object_tracking": "dimos.perception.object_tracker", - "osm_skill": "dimos.agents2.skills.osm.py", + "osm_skill": "dimos.agents.skills.osm", + "ros_nav": "dimos.navigation.rosnav", "spatial_memory": "dimos.perception.spatial_perception", + "speak_skill": "dimos.agents.skills.speak_skill", + "unitree_skills": "dimos.robot.unitree_webrtc.unitree_skill_container", "utilization": "dimos.utils.monitoring", "wavefront_frontier_explorer": "dimos.navigation.frontier_exploration.wavefront_frontier_goal_selector", "websocket_vis": "dimos.web.websocket_vis.websocket_vis_module", + "web_input": "dimos.agents.cli.web", + # xArm manipulator modules + "xarm_driver": "dimos.hardware.manipulators.xarm.xarm_driver", + "cartesian_motion_controller": "dimos.manipulation.control.servo_control.cartesian_motion_controller", + "joint_trajectory_controller": "dimos.manipulation.control.trajectory_controller.joint_trajectory_controller", } @@ -54,11 +95,11 @@ def get_blueprint_by_name(name: str) -> ModuleBlueprintSet: raise ValueError(f"Unknown blueprint set name: {name}") module_path, attr = all_blueprints[name].split(":") module = __import__(module_path, fromlist=[attr]) - return getattr(module, attr) + return getattr(module, attr) # type: ignore[no-any-return] def get_module_by_name(name: str) -> ModuleBlueprintSet: if name not in all_modules: raise ValueError(f"Unknown module name: {name}") python_module = __import__(all_modules[name], fromlist=[name]) - return getattr(python_module, name)() + return getattr(python_module, name)() # type: ignore[no-any-return] diff --git a/dimos/robot/cli/README.md b/dimos/robot/cli/README.md index da1d7443da..63087f48b8 100644 --- a/dimos/robot/cli/README.md +++ b/dimos/robot/cli/README.md @@ -5,19 +5,19 @@ To avoid having so many runfiles, I created a common script to run any blueprint For example, to run the standard Unitree Go2 blueprint run: ```bash -dimos-robot run unitree-go2 +dimos run unitree-go2 ``` For the one with agents run: ```bash -dimos-robot run unitree-go2-agentic +dimos run unitree-go2-agentic ``` You can dynamically connect additional modules. For example: ```bash -dimos-robot run unitree-go2 --extra-module llm_agent --extra-module human_input --extra-module navigation_skill +dimos run unitree-go2 --extra-module llm_agent --extra-module human_input --extra-module navigation_skill ``` ## Definitions @@ -43,23 +43,23 @@ This tool also initializes the global config and passes it to the blueprint. ```python class GlobalConfig(BaseSettings): robot_ip: str | None = None - use_simulation: bool = False - use_replay: bool = False + simulation: bool = False + replay: bool = False n_dask_workers: int = 2 ``` Configuration values can be set from multiple places in order of precedence (later entries override earlier ones): -- Default value defined on GlobalConfig. (`use_simulation = False`) -- Value defined in `.env` (`USE_SIMULATION=true`) -- Value in the environment variable (`USE_SIMULATION=true`) -- Value coming from the CLI (`--use-simulation` or `--no-use-simulation`) -- Value defined on the blueprint (`blueprint.global_config(use_simulation=True)`) +- Default value defined on GlobalConfig. (`simulation = False`) +- Value defined in `.env` (`SIMULATION=true`) +- Value in the environment variable (`SIMULATION=true`) +- Value defined on the blueprint (`blueprint.global_config(simulation=True)`) +- Value coming from the CLI (`--simulation` or `--no-simulation`) For environment variables/`.env` values, you have to prefix the name with `DIMOS_`. For the command line, you call it like this: ```bash -dimos-robot --use-simulation run unitree-go2 -``` \ No newline at end of file +dimos --simulation run unitree-go2 +``` diff --git a/dimos/robot/cli/dimos.py b/dimos/robot/cli/dimos.py new file mode 100644 index 0000000000..5cf09e02e3 --- /dev/null +++ b/dimos/robot/cli/dimos.py @@ -0,0 +1,201 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +import inspect +import sys +from typing import Any, Optional, get_args, get_origin + +import typer + +from dimos.core.blueprints import autoconnect +from dimos.core.global_config import GlobalConfig +from dimos.protocol import pubsub +from dimos.robot.all_blueprints import all_blueprints, get_blueprint_by_name, get_module_by_name +from dimos.robot.cli.topic import topic_echo, topic_send +from dimos.utils.logging_config import setup_exception_handler + +RobotType = Enum("RobotType", {key.replace("-", "_").upper(): key for key in all_blueprints.keys()}) # type: ignore[misc] + +main = typer.Typer( + help="Dimensional CLI", + no_args_is_help=True, +) + + +def create_dynamic_callback(): # type: ignore[no-untyped-def] + fields = GlobalConfig.model_fields + + # Build the function signature dynamically + params = [ + inspect.Parameter("ctx", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=typer.Context), + ] + + # Create parameters for each field in GlobalConfig + for field_name, field_info in fields.items(): + field_type = field_info.annotation + + # Handle Optional types + # Check for Optional/Union with None + if get_origin(field_type) is type(Optional[str]): # noqa: UP045 + inner_types = get_args(field_type) + if len(inner_types) == 2 and type(None) in inner_types: + # It's Optional[T], get the actual type T + actual_type = next(t for t in inner_types if t != type(None)) + else: + actual_type = field_type + else: + actual_type = field_type + + # Convert field name from snake_case to kebab-case for CLI + cli_option_name = field_name.replace("_", "-") + + # Special handling for boolean fields + if actual_type is bool: + # For boolean fields, create --flag/--no-flag pattern + param = inspect.Parameter( + field_name, + inspect.Parameter.KEYWORD_ONLY, + default=typer.Option( + None, # None means use the model's default if not provided + f"--{cli_option_name}/--no-{cli_option_name}", + help=f"Override {field_name} in GlobalConfig", + ), + annotation=Optional[bool], # noqa: UP045 + ) + else: + # For non-boolean fields, use regular option + param = inspect.Parameter( + field_name, + inspect.Parameter.KEYWORD_ONLY, + default=typer.Option( + None, # None means use the model's default if not provided + f"--{cli_option_name}", + help=f"Override {field_name} in GlobalConfig", + ), + annotation=Optional[actual_type], # noqa: UP045 + ) + params.append(param) + + def callback(**kwargs) -> None: # type: ignore[no-untyped-def] + ctx = kwargs.pop("ctx") + ctx.obj = {k: v for k, v in kwargs.items() if v is not None} + + callback.__signature__ = inspect.Signature(params) # type: ignore[attr-defined] + + return callback + + +main.callback()(create_dynamic_callback()) # type: ignore[no-untyped-call] + + +@main.command() +def run( + ctx: typer.Context, + robot_type: RobotType = typer.Argument(..., help="Type of robot to run"), + extra_modules: list[str] = typer.Option( # type: ignore[valid-type] + [], "--extra-module", help="Extra modules to add to the blueprint" + ), +) -> None: + """Start a robot blueprint""" + setup_exception_handler() + + cli_config_overrides: dict[str, Any] = ctx.obj + pubsub.lcm.autoconf() # type: ignore[attr-defined] + blueprint = get_blueprint_by_name(robot_type.value) + + if extra_modules: + loaded_modules = [get_module_by_name(mod_name) for mod_name in extra_modules] # type: ignore[attr-defined] + blueprint = autoconnect(blueprint, *loaded_modules) + + dimos = blueprint.build(cli_config_overrides=cli_config_overrides) + dimos.loop() + + +@main.command() +def show_config(ctx: typer.Context) -> None: + """Show current config settings and their values.""" + cli_config_overrides: dict[str, Any] = ctx.obj + config = GlobalConfig().model_copy(update=cli_config_overrides) + + for field_name, value in config.model_dump().items(): + typer.echo(f"{field_name}: {value}") + + +@main.command() +def list() -> None: + """List all available blueprints.""" + blueprints = [name for name in all_blueprints.keys() if not name.startswith("demo-")] + for blueprint_name in sorted(blueprints): + typer.echo(blueprint_name) + + +@main.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) +def lcmspy(ctx: typer.Context) -> None: + """LCM spy tool for monitoring LCM messages.""" + from dimos.utils.cli.lcmspy.run_lcmspy import main as lcmspy_main + + sys.argv = ["lcmspy", *ctx.args] + lcmspy_main() + + +@main.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) +def skillspy(ctx: typer.Context) -> None: + """Skills spy tool for monitoring skills.""" + from dimos.utils.cli.skillspy.skillspy import main as skillspy_main + + sys.argv = ["skillspy", *ctx.args] + skillspy_main() + + +@main.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) +def agentspy(ctx: typer.Context) -> None: + """Agent spy tool for monitoring agents.""" + from dimos.utils.cli.agentspy.agentspy import main as agentspy_main + + sys.argv = ["agentspy", *ctx.args] + agentspy_main() + + +@main.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) +def humancli(ctx: typer.Context) -> None: + """Interface interacting with agents.""" + from dimos.utils.cli.human.humanclianim import main as humancli_main + + sys.argv = ["humancli", *ctx.args] + humancli_main() + + +topic_app = typer.Typer(help="Topic commands for pub/sub") +main.add_typer(topic_app, name="topic") + + +@topic_app.command() +def echo( + topic: str = typer.Argument(..., help="Topic name to listen on (e.g., /goal_request)"), + type_name: str = typer.Argument(..., help="Message type (e.g., PoseStamped)"), +) -> None: + topic_echo(topic, type_name) + + +@topic_app.command() +def send( + topic: str = typer.Argument(..., help="Topic name to send to (e.g., /goal_request)"), + message_expr: str = typer.Argument(..., help="Python expression for the message"), +) -> None: + topic_send(topic, message_expr) + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/cli/dimos_robot.py b/dimos/robot/cli/dimos_robot.py deleted file mode 100644 index bafa53e4a9..0000000000 --- a/dimos/robot/cli/dimos_robot.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from enum import Enum -import inspect -from typing import Optional, get_args, get_origin - -import typer - -from dimos.core.blueprints import autoconnect -from dimos.core.global_config import GlobalConfig -from dimos.protocol import pubsub -from dimos.robot.all_blueprints import all_blueprints, get_blueprint_by_name, get_module_by_name - -RobotType = Enum("RobotType", {key.replace("-", "_").upper(): key for key in all_blueprints.keys()}) - -main = typer.Typer() - - -def create_dynamic_callback(): - fields = GlobalConfig.model_fields - - # Build the function signature dynamically - params = [ - inspect.Parameter("ctx", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=typer.Context), - ] - - # Create parameters for each field in GlobalConfig - for field_name, field_info in fields.items(): - field_type = field_info.annotation - - # Handle Optional types - # Check for Optional/Union with None - if get_origin(field_type) is type(Optional[str]): # noqa: UP045 - inner_types = get_args(field_type) - if len(inner_types) == 2 and type(None) in inner_types: - # It's Optional[T], get the actual type T - actual_type = next(t for t in inner_types if t != type(None)) - else: - actual_type = field_type - else: - actual_type = field_type - - # Convert field name from snake_case to kebab-case for CLI - cli_option_name = field_name.replace("_", "-") - - # Special handling for boolean fields - if actual_type is bool: - # For boolean fields, create --flag/--no-flag pattern - param = inspect.Parameter( - field_name, - inspect.Parameter.KEYWORD_ONLY, - default=typer.Option( - None, # None means use the model's default if not provided - f"--{cli_option_name}/--no-{cli_option_name}", - help=f"Override {field_name} in GlobalConfig", - ), - annotation=Optional[bool], # noqa: UP045 - ) - else: - # For non-boolean fields, use regular option - param = inspect.Parameter( - field_name, - inspect.Parameter.KEYWORD_ONLY, - default=typer.Option( - None, # None means use the model's default if not provided - f"--{cli_option_name}", - help=f"Override {field_name} in GlobalConfig", - ), - annotation=Optional[actual_type], # noqa: UP045 - ) - params.append(param) - - def callback(**kwargs) -> None: - ctx = kwargs.pop("ctx") - overrides = {k: v for k, v in kwargs.items() if v is not None} - ctx.obj = GlobalConfig().model_copy(update=overrides) - - callback.__signature__ = inspect.Signature(params) - - return callback - - -main.callback()(create_dynamic_callback()) - - -@main.command() -def run( - ctx: typer.Context, - robot_type: RobotType = typer.Argument(..., help="Type of robot to run"), - extra_modules: list[str] = typer.Option( - [], "--extra-module", help="Extra modules to add to the blueprint" - ), -) -> None: - """Run the robot with the specified configuration.""" - config: GlobalConfig = ctx.obj - pubsub.lcm.autoconf() - blueprint = get_blueprint_by_name(robot_type.value) - - if extra_modules: - loaded_modules = [get_module_by_name(mod_name) for mod_name in extra_modules] - blueprint = autoconnect(blueprint, *loaded_modules) - - dimos = blueprint.build(global_config=config) - dimos.loop() - - -@main.command() -def show_config(ctx: typer.Context) -> None: - """Show current configuration status.""" - config: GlobalConfig = ctx.obj - - for field_name, value in config.model_dump().items(): - typer.echo(f"{field_name}: {value}") - - -if __name__ == "__main__": - main() diff --git a/dimos/robot/cli/topic.py b/dimos/robot/cli/topic.py new file mode 100644 index 0000000000..bdd1a29ae6 --- /dev/null +++ b/dimos/robot/cli/topic.py @@ -0,0 +1,102 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +import time + +import typer + +from dimos.core.transport import LCMTransport, pLCMTransport + +_modules_to_try = [ + "dimos.msgs.geometry_msgs", + "dimos.msgs.nav_msgs", + "dimos.msgs.sensor_msgs", + "dimos.msgs.std_msgs", + "dimos.msgs.vision_msgs", + "dimos.msgs.foxglove_msgs", + "dimos.msgs.tf2_msgs", +] + + +def _resolve_type(type_name: str) -> type: + for module_name in _modules_to_try: + try: + module = importlib.import_module(module_name) + if hasattr(module, type_name): + return getattr(module, type_name) # type: ignore[no-any-return] + except ImportError: + continue + + raise ValueError(f"Could not find type '{type_name}' in any known message modules") + + +def topic_echo(topic: str, type_name: str) -> None: + msg_type = _resolve_type(type_name) + use_pickled = getattr(msg_type, "lcm_encode", None) is None + transport: pLCMTransport[object] | LCMTransport[object] = ( + pLCMTransport(topic) if use_pickled else LCMTransport(topic, msg_type) + ) + + def _on_message(msg: object) -> None: + print(msg) + + transport.subscribe(_on_message) + + typer.echo(f"Listening on {topic} for {type_name} messages... (Ctrl+C to stop)") + + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + typer.echo("\nStopped.") + + +def topic_send(topic: str, message_expr: str) -> None: + eval_context: dict[str, object] = {} + modules_to_import = [ + "dimos.msgs.geometry_msgs", + "dimos.msgs.nav_msgs", + "dimos.msgs.sensor_msgs", + "dimos.msgs.std_msgs", + "dimos.msgs.vision_msgs", + "dimos.msgs.foxglove_msgs", + "dimos.msgs.tf2_msgs", + ] + + for module_name in modules_to_import: + try: + module = importlib.import_module(module_name) + for name in getattr(module, "__all__", dir(module)): + if not name.startswith("_"): + obj = getattr(module, name, None) + if obj is not None: + eval_context[name] = obj + except ImportError: + continue + + try: + message = eval(message_expr, eval_context) + except Exception as e: + typer.echo(f"Error parsing message: {e}", err=True) + raise typer.Exit(1) + + msg_type = type(message) + use_pickled = getattr(msg_type, "lcm_encode", None) is None + transport: pLCMTransport[object] | LCMTransport[object] = ( + pLCMTransport(topic) if use_pickled else LCMTransport(topic, msg_type) + ) + + transport.broadcast(None, message) + typer.echo(f"Sent to {topic}: {message}") diff --git a/dimos/robot/connection_interface.py b/dimos/robot/connection_interface.py deleted file mode 100644 index 6480827214..0000000000 --- a/dimos/robot/connection_interface.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - -from reactivex.observable import Observable - -from dimos.types.vector import Vector - -__all__ = ["ConnectionInterface"] - - -class ConnectionInterface(ABC): - """Abstract base class for robot connection interfaces. - - This class defines the minimal interface that all connection types (ROS, WebRTC, etc.) - must implement to provide robot control and data streaming capabilities. - """ - - @abstractmethod - def move(self, velocity: Vector, duration: float = 0.0) -> bool: - """Send movement command to the robot using velocity commands. - - Args: - velocity: Velocity vector [x, y, yaw] where: - x: Forward/backward velocity (m/s) - y: Left/right velocity (m/s) - yaw: Rotational velocity (rad/s) - duration: How long to move (seconds). If 0, command is continuous - - Returns: - bool: True if command was sent successfully - """ - pass - - @abstractmethod - def get_video_stream(self, fps: int = 30) -> Observable | None: - """Get the video stream from the robot's camera. - - Args: - fps: Frames per second for the video stream - - Returns: - Observable: An observable stream of video frames or None if not available - """ - pass - - @abstractmethod - def stop(self) -> bool: - """Stop the robot's movement. - - Returns: - bool: True if stop command was sent successfully - """ - pass - - @abstractmethod - def disconnect(self) -> None: - """Disconnect from the robot and clean up resources.""" - pass diff --git a/dimos/robot/drone/README.md b/dimos/robot/drone/README.md new file mode 100644 index 0000000000..fbd7ddf2ae --- /dev/null +++ b/dimos/robot/drone/README.md @@ -0,0 +1,289 @@ +# DimOS Drone Module + +Complete integration for DJI drones via RosettaDrone MAVLink bridge with visual servoing and autonomous tracking capabilities. + +## Quick Start + +### Test the System +```bash +# Test with replay mode (no hardware needed) +python dimos/robot/drone/drone.py --replay + +# Real drone - indoor (IMU odometry) +python dimos/robot/drone/drone.py + +# Real drone - outdoor (GPS odometry) +python dimos/robot/drone/drone.py --outdoor +``` + +### Python API Usage +```python +from dimos.robot.drone.drone import Drone + +# Connect to drone +drone = Drone(connection_string='udp:0.0.0.0:14550', outdoor=True) # Use outdoor=True for GPS +drone.start() + +# Basic operations +drone.arm() +drone.takeoff(altitude=5.0) +drone.move(Vector3(1.0, 0, 0), duration=2.0) # Forward 1m/s for 2s + +# Visual tracking +drone.tracking.track_object("person", duration=120) # Track for 2 minutes + +# Land and cleanup +drone.land() +drone.stop() +``` + +## Installation + +### Python Package +```bash +# Install DimOS with drone support +pip install -e .[drone] +``` + +### System Dependencies +```bash +# GStreamer for video streaming +sudo apt-get install -y gstreamer1.0-tools gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ + gstreamer1.0-libav python3-gi python3-gi-cairo + +# LCM for communication +sudo apt-get install liblcm-dev +``` + +### Environment Setup +```bash +export DRONE_IP=0.0.0.0 # Listen on all interfaces +export DRONE_VIDEO_PORT=5600 +export DRONE_MAVLINK_PORT=14550 +``` + +## RosettaDrone Setup (Critical) + +RosettaDrone is an Android app that bridges DJI SDK to MAVLink protocol. Without it, the drone cannot communicate with DimOS. + +### Option 1: Pre-built APK +1. Download latest release: https://github.com/RosettaDrone/rosettadrone/releases +2. Install on Android device connected to DJI controller +3. Configure in app: + - MAVLink Target IP: Your computer's IP + - MAVLink Port: 14550 + - Video Port: 5600 + - Enable video streaming + +### Option 2: Build from Source + +#### Prerequisites +- Android Studio +- DJI Developer Account: https://developer.dji.com/ +- Git + +#### Build Steps +```bash +# Clone repository +git clone https://github.com/RosettaDrone/rosettadrone.git +cd rosettadrone + +# Build with Gradle +./gradlew assembleRelease + +# APK will be in: app/build/outputs/apk/release/ +``` + +#### Configure DJI API Key +1. Register app at https://developer.dji.com/user/apps + - Package name: `sq.rogue.rosettadrone` +2. Add key to `app/src/main/AndroidManifest.xml`: +```xml + +``` + +#### Install APK +```bash +adb install -r app/build/outputs/apk/release/rosettadrone-release.apk +``` + +### Hardware Connection +``` +DJI Drone ← Wireless → DJI Controller ← USB → Android Device ← WiFi → DimOS Computer +``` + +1. Connect Android to DJI controller via USB +2. Start RosettaDrone app +3. Wait for "DJI Connected" status +4. Verify "MAVLink Active" shows in app + +## Architecture + +### Module Structure +``` +drone.py # Main orchestrator +ā”œā”€ā”€ connection_module.py # MAVLink communication & skills +ā”œā”€ā”€ camera_module.py # Video processing & depth estimation +ā”œā”€ā”€ tracking_module.py # Visual servoing & object tracking +ā”œā”€ā”€ mavlink_connection.py # Low-level MAVLink protocol +└── dji_video_stream.py # GStreamer video capture +``` + +### Communication Flow +``` +DJI Drone → RosettaDrone → MAVLink UDP → connection_module → LCM Topics + → Video UDP → dji_video_stream → tracking_module +``` + +### LCM Topics +- `/drone/odom` - Position and orientation +- `/drone/status` - Armed state, battery +- `/drone/video` - Camera frames +- `/drone/tracking/cmd_vel` - Tracking velocity commands +- `/drone/tracking/overlay` - Visualization with tracking box + +## Visual Servoing & Tracking + +### Object Tracking +```python +# Track specific object +result = drone.tracking.track_object("red flag", duration=60) + +# Track nearest/most prominent object +result = drone.tracking.track_object(None, duration=60) + +# Stop tracking +drone.tracking.stop_tracking() +``` + +### PID Tuning +Configure in `drone.py` initialization: +```python +# Indoor (gentle, precise) +x_pid_params=(0.001, 0.0, 0.0001, (-0.5, 0.5), None, 30) + +# Outdoor (aggressive, wind-resistant) +x_pid_params=(0.003, 0.0001, 0.0002, (-1.0, 1.0), None, 10) +``` + +Parameters: `(Kp, Ki, Kd, (min_output, max_output), integral_limit, deadband_pixels)` + +### Visual Servoing Flow +1. Qwen model detects object → bounding box +2. CSRT tracker initialized on bbox +3. PID controller computes velocity from pixel error +4. Velocity commands sent via LCM stream +5. Connection module converts to MAVLink commands + +## Available Skills + +### Movement & Control +- `move(vector, duration)` - Move with velocity vector +- `takeoff(altitude)` - Takeoff to altitude +- `land()` - Land at current position +- `arm()/disarm()` - Arm/disarm motors +- `fly_to(lat, lon, alt)` - Fly to GPS coordinates + +### Perception +- `observe()` - Get current camera frame +- `follow_object(description, duration)` - Follow object with servoing + +### Tracking Module +- `track_object(name, duration)` - Track and follow object +- `stop_tracking()` - Stop current tracking +- `get_status()` - Get tracking status + +## Testing + +### Unit Tests +```bash +pytest -s dimos/robot/drone/ +``` + +### Replay Mode (No Hardware) +```python +# Use recorded data for testing +drone = Drone(connection_string='replay') +drone.start() +# All operations work with recorded data +``` + +## Troubleshooting + +### No MAVLink Connection +- Check Android and computer are on same network +- Verify IP address in RosettaDrone matches computer +- Test with: `nc -lu 14550` (should see data) +- Check firewall: `sudo ufw allow 14550/udp` + +### No Video Stream +- Enable video in RosettaDrone settings +- Test with: `nc -lu 5600` (should see data) +- Verify GStreamer installed: `gst-launch-1.0 --version` + +### Tracking Issues +- Increase lighting for better detection +- Adjust PID gains for environment +- Check `max_lost_frames` in tracking module +- Monitor with Foxglove on `ws://localhost:8765` + +### Wrong Movement Direction +- Don't modify coordinate conversions +- Verify with: `pytest test_drone.py::test_ned_to_ros_coordinate_conversion` +- Check camera orientation assumptions + +## Advanced Features + +### Coordinate Systems +- **MAVLink/NED**: X=North, Y=East, Z=Down +- **ROS/DimOS**: X=Forward, Y=Left, Z=Up +- Automatic conversion handled internally + +### Depth Estimation +Camera module can generate depth maps using Metric3D: +```python +# Depth published to /drone/depth and /drone/pointcloud +# Requires GPU with 8GB+ VRAM +``` + +### Foxglove Visualization +Connect Foxglove Studio to `ws://localhost:8765` to see: +- Live video with tracking overlay +- 3D drone position +- Telemetry plots +- Transform tree + +## Network Ports +- **14550**: MAVLink UDP +- **5600**: Video stream UDP +- **8765**: Foxglove WebSocket +- **7667**: LCM messaging + +## Development + +### Adding New Skills +Add to `connection_module.py` with `@skill()` decorator: +```python +@skill() +def my_skill(self, param: float) -> str: + """Skill description for LLM.""" + # Implementation + return "Result" +``` + +### Modifying PID Control +Edit gains in `drone.py` `_deploy_tracking()`: +- Increase Kp for faster response +- Add Ki for steady-state error +- Increase Kd for damping +- Adjust limits for max velocity + +## Safety Notes +- Always test in simulator or with propellers removed first +- Set conservative PID gains initially +- Implement geofencing for outdoor flights +- Monitor battery voltage continuously +- Have manual override ready diff --git a/dimos/robot/drone/__init__.py b/dimos/robot/drone/__init__.py new file mode 100644 index 0000000000..5d4eed4dae --- /dev/null +++ b/dimos/robot/drone/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generic drone module for MAVLink-based drones.""" + +from .camera_module import DroneCameraModule +from .connection_module import DroneConnectionModule +from .drone import Drone +from .mavlink_connection import MavlinkConnection + +__all__ = ["Drone", "DroneCameraModule", "DroneConnectionModule", "MavlinkConnection"] diff --git a/dimos/robot/drone/camera_module.py b/dimos/robot/drone/camera_module.py new file mode 100644 index 0000000000..7806c3eab8 --- /dev/null +++ b/dimos/robot/drone/camera_module.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025-2026 Dimensional Inc. + +"""Camera module for drone with depth estimation.""" + +import threading +import time +from typing import Any + +from dimos_lcm.sensor_msgs import CameraInfo + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.msgs.std_msgs import Header +from dimos.perception.common.utils import colorize_depth +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class DroneCameraModule(Module): + """ + Camera module for drone that processes RGB images to generate depth using Metric3D. + + Subscribes to: + - /video: RGB camera images from drone + + Publishes: + - /drone/color_image: RGB camera images + - /drone/depth_image: Depth images from Metric3D + - /drone/depth_colorized: Colorized depth + - /drone/camera_info: Camera calibration + - /drone/camera_pose: Camera pose from TF + """ + + # Inputs + video: In[Image] + + # Outputs + color_image: Out[Image] + depth_image: Out[Image] + depth_colorized: Out[Image] + camera_info: Out[CameraInfo] + camera_pose: Out[PoseStamped] + + def __init__( + self, + camera_intrinsics: list[float], + world_frame_id: str = "world", + camera_frame_id: str = "camera_link", + base_frame_id: str = "base_link", + gt_depth_scale: float = 2.0, + **kwargs: Any, + ) -> None: + """Initialize drone camera module. + + Args: + camera_intrinsics: [fx, fy, cx, cy] + camera_frame_id: TF frame for camera + base_frame_id: TF frame for drone base + gt_depth_scale: Depth scale factor + """ + super().__init__(**kwargs) + + if len(camera_intrinsics) != 4: + raise ValueError("Camera intrinsics must be [fx, fy, cx, cy]") + + self.camera_intrinsics = camera_intrinsics + self.camera_frame_id = camera_frame_id + self.base_frame_id = base_frame_id + self.world_frame_id = world_frame_id + self.gt_depth_scale = gt_depth_scale + + # Metric3D for depth + self.metric3d: Any = None # Lazy-loaded Metric3D model + + # Processing state + self._running = False + self._latest_frame: Image | None = None + self._processing_thread: threading.Thread | None = None + self._stop_processing = threading.Event() + + logger.info(f"DroneCameraModule initialized with intrinsics: {camera_intrinsics}") + + @rpc + def start(self) -> bool: + """Start the camera module.""" + if self._running: + logger.warning("Camera module already running") + return True + + # Start processing thread for depth (which will init Metric3D and handle video) + self._running = True + self._stop_processing.clear() + self._processing_thread = threading.Thread(target=self._processing_loop, daemon=True) + self._processing_thread.start() + + logger.info("Camera module started") + return True + + def _on_video_frame(self, frame: Image) -> None: + """Handle incoming video frame.""" + if not self._running: + return + + # Publish color image immediately + self.color_image.publish(frame) + + # Store for depth processing + self._latest_frame = frame + + def _processing_loop(self) -> None: + """Process depth estimation in background.""" + # Initialize Metric3D in the background thread + if self.metric3d is None: + try: + from dimos.models.depth.metric3d import Metric3D + + self.metric3d = Metric3D(camera_intrinsics=self.camera_intrinsics) + logger.info("Metric3D initialized") + except Exception as e: + logger.warning(f"Metric3D not available: {e}") + self.metric3d = None + + # Subscribe to video once connection is available + subscribed = False + while not subscribed and not self._stop_processing.is_set(): + try: + if self.video.connection is not None: + self.video.subscribe(self._on_video_frame) + subscribed = True + logger.info("Subscribed to video input") + else: + time.sleep(0.1) + except Exception as e: + logger.debug(f"Waiting for video connection: {e}") + time.sleep(0.1) + + logger.info("Depth processing loop started") + + _reported_error = False + + while not self._stop_processing.is_set(): + if self._latest_frame is not None and self.metric3d is not None: + try: + frame = self._latest_frame + self._latest_frame = None + + # Get numpy array from Image + img_array = frame.data + + # Generate depth + depth_array = self.metric3d.infer_depth(img_array) / self.gt_depth_scale + + # Create header + header = Header(self.camera_frame_id) + + # Publish depth + depth_msg = Image( + data=depth_array, + format=ImageFormat.DEPTH, + frame_id=header.frame_id, + ts=header.ts, + ) + self.depth_image.publish(depth_msg) + + # Publish colorized depth + depth_colorized_array = colorize_depth( + depth_array, max_depth=10.0, overlay_stats=True + ) + if depth_colorized_array is not None: + depth_colorized_msg = Image( + data=depth_colorized_array, + format=ImageFormat.RGB, + frame_id=header.frame_id, + ts=header.ts, + ) + self.depth_colorized.publish(depth_colorized_msg) + + # Publish camera info + self._publish_camera_info(header, img_array.shape) + + # Publish camera pose + self._publish_camera_pose(header) + + except Exception as e: + if not _reported_error: + _reported_error = True + logger.error(f"Error processing depth: {e}") + else: + time.sleep(0.01) + + logger.info("Depth processing loop stopped") + + def _publish_camera_info(self, header: Header, shape: tuple[int, ...]) -> None: + """Publish camera calibration info.""" + try: + fx, fy, cx, cy = self.camera_intrinsics + height, width = shape[:2] + + # Camera matrix K (3x3) + K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] + + # No distortion for now + D = [0.0, 0.0, 0.0, 0.0, 0.0] + + # Identity rotation + R = [1, 0, 0, 0, 1, 0, 0, 0, 1] + + # Projection matrix P (3x4) + P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0] + + msg = CameraInfo( + D_length=len(D), + header=header, + height=height, + width=width, + distortion_model="plumb_bob", + D=D, + K=K, + R=R, + P=P, + binning_x=0, + binning_y=0, + ) + + self.camera_info.publish(msg) + + except Exception as e: + logger.error(f"Error publishing camera info: {e}") + + def _publish_camera_pose(self, header: Header) -> None: + """Publish camera pose from TF.""" + try: + transform = self.tf.get( + parent_frame=self.world_frame_id, + child_frame=self.camera_frame_id, + time_point=header.ts, + time_tolerance=1.0, + ) + + if transform: + pose_msg = PoseStamped( + ts=header.ts, + frame_id=self.camera_frame_id, + position=transform.translation, + orientation=transform.rotation, + ) + self.camera_pose.publish(pose_msg) + + except Exception as e: + logger.error(f"Error publishing camera pose: {e}") + + @rpc + def stop(self) -> None: + """Stop the camera module.""" + if not self._running: + return + + self._running = False + self._stop_processing.set() + + # Wait for thread + if self._processing_thread and self._processing_thread.is_alive(): + self._processing_thread.join(timeout=2.0) + + # Cleanup Metric3D + if self.metric3d: + self.metric3d.cleanup() + + logger.info("Camera module stopped") diff --git a/dimos/robot/drone/connection_module.py b/dimos/robot/drone/connection_module.py new file mode 100644 index 0000000000..865d98c3d3 --- /dev/null +++ b/dimos/robot/drone/connection_module.py @@ -0,0 +1,489 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DimOS module wrapper for drone connection.""" + +from collections.abc import Generator +import json +import threading +import time +from typing import Any + +from dimos_lcm.std_msgs import String +from reactivex.disposable import CompositeDisposable, Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.mapping.types import LatLon +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.protocol.skill.skill import skill +from dimos.protocol.skill.type import Output +from dimos.robot.drone.dji_video_stream import DJIDroneVideoStream +from dimos.robot.drone.mavlink_connection import MavlinkConnection +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +def _add_disposable(composite: CompositeDisposable, item: Disposable | Any) -> None: + if isinstance(item, Disposable): + composite.add(item) + elif callable(item): + composite.add(Disposable(item)) + + +class DroneConnectionModule(Module): + """Module that handles drone sensor data and movement commands.""" + + # Inputs + movecmd: In[Vector3] + movecmd_twist: In[Twist] # Twist commands from tracking/navigation + gps_goal: In[LatLon] + tracking_status: In[Any] + + # Outputs + odom: Out[PoseStamped] + gps_location: Out[LatLon] + status: Out[Any] # JSON status + telemetry: Out[Any] # Full telemetry JSON + video: Out[Image] + follow_object_cmd: Out[Any] + + # Parameters + connection_string: str + + # Internal state + _odom: PoseStamped | None = None + _status: dict[str, Any] = {} + _latest_video_frame: Image | None = None + _latest_telemetry: dict[str, Any] | None = None + _latest_status: dict[str, Any] | None = None + _latest_status_lock: threading.RLock + + def __init__( + self, + connection_string: str = "udp:0.0.0.0:14550", + video_port: int = 5600, + outdoor: bool = False, + *args: Any, + **kwargs: Any, + ) -> None: + """Initialize drone connection module. + + Args: + connection_string: MAVLink connection string + video_port: UDP port for video stream + outdoor: Use GPS only mode (no velocity integration) + """ + self.connection_string = connection_string + self.video_port = video_port + self.outdoor = outdoor + self.connection: MavlinkConnection | None = None + self.video_stream: DJIDroneVideoStream | None = None + self._latest_video_frame = None + self._latest_telemetry = None + self._latest_status = None + self._latest_status_lock = threading.RLock() + self._running = False + self._telemetry_thread: threading.Thread | None = None + Module.__init__(self, *args, **kwargs) + + @rpc + def start(self) -> bool: + """Start the connection and subscribe to sensor streams.""" + # Check for replay mode + if self.connection_string == "replay": + from dimos.robot.drone.dji_video_stream import FakeDJIVideoStream + from dimos.robot.drone.mavlink_connection import FakeMavlinkConnection + + self.connection = FakeMavlinkConnection("replay") + self.video_stream = FakeDJIVideoStream(port=self.video_port) + else: + self.connection = MavlinkConnection(self.connection_string, outdoor=self.outdoor) + self.connection.connect() + + self.video_stream = DJIDroneVideoStream(port=self.video_port) + + if not self.connection.connected: + logger.error("Failed to connect to drone") + return False + + # Start video stream (already created above) + if self.video_stream.start(): + logger.info("Video stream started") + # Subscribe to video, store latest frame and publish it + _add_disposable( + self._disposables, + self.video_stream.get_stream().subscribe(self._store_and_publish_frame), + ) + # # TEMPORARY - DELETE AFTER RECORDING + # from dimos.utils.testing import TimedSensorStorage + # self._video_storage = TimedSensorStorage("drone/video") + # self._video_subscription = self._video_storage.save_stream(self.video_stream.get_stream()).subscribe() + # logger.info("Recording video to data/drone/video/") + else: + logger.warning("Video stream failed to start") + + # Subscribe to drone streams + _add_disposable( + self._disposables, self.connection.odom_stream().subscribe(self._publish_tf) + ) + _add_disposable( + self._disposables, self.connection.status_stream().subscribe(self._publish_status) + ) + _add_disposable( + self._disposables, self.connection.telemetry_stream().subscribe(self._publish_telemetry) + ) + + # Subscribe to movement commands + _add_disposable(self._disposables, self.movecmd.subscribe(self.move)) + + # Subscribe to Twist movement commands + if self.movecmd_twist.transport: + _add_disposable(self._disposables, self.movecmd_twist.subscribe(self._on_move_twist)) + + if self.gps_goal.transport: + _add_disposable(self._disposables, self.gps_goal.subscribe(self._on_gps_goal)) + + if self.tracking_status.transport: + _add_disposable( + self._disposables, self.tracking_status.subscribe(self._on_tracking_status) + ) + + # Start telemetry update thread + import threading + + self._running = True + self._telemetry_thread = threading.Thread(target=self._telemetry_loop, daemon=True) + self._telemetry_thread.start() + + logger.info("Drone connection module started") + return True + + def _store_and_publish_frame(self, frame: Image) -> None: + """Store the latest video frame and publish it.""" + self._latest_video_frame = frame + self.video.publish(frame) + + def _publish_tf(self, msg: PoseStamped) -> None: + """Publish odometry and TF transforms.""" + self._odom = msg + + # Publish odometry + self.odom.publish(msg) + + # Publish base_link transform + base_link = Transform( + translation=msg.position, + rotation=msg.orientation, + frame_id="world", + child_frame_id="base_link", + ts=msg.ts if hasattr(msg, "ts") else time.time(), + ) + self.tf.publish(base_link) + + # Publish camera_link transform (camera mounted on front of drone, no gimbal factored in yet) + camera_link = Transform( + translation=Vector3(0.1, 0.0, -0.05), # 10cm forward, 5cm down + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # No rotation relative to base + frame_id="base_link", + child_frame_id="camera_link", + ts=time.time(), + ) + self.tf.publish(camera_link) + + def _publish_status(self, status: dict[str, Any]) -> None: + """Publish drone status as JSON string.""" + self._status = status + + status_str = String(json.dumps(status)) + self.status.publish(status_str) + + def _publish_telemetry(self, telemetry: dict[str, Any]) -> None: + """Publish full telemetry as JSON string.""" + telemetry_str = String(json.dumps(telemetry)) + self.telemetry.publish(telemetry_str) + self._latest_telemetry = telemetry + + if "GLOBAL_POSITION_INT" in telemetry: + tel = telemetry["GLOBAL_POSITION_INT"] + self.gps_location.publish(LatLon(lat=tel["lat"], lon=tel["lon"])) + + def _telemetry_loop(self) -> None: + """Continuously update telemetry at 30Hz.""" + frame_count = 0 + while self._running: + try: + # Update telemetry from drone + if self.connection is not None: + self.connection.update_telemetry(timeout=0.01) + + # Publish default odometry if we don't have real data yet + if frame_count % 10 == 0: # Every ~3Hz + if self._odom is None: + # Publish default pose + default_pose = PoseStamped( + position=Vector3(0, 0, 0), + orientation=Quaternion(0, 0, 0, 1), + frame_id="world", + ts=time.time(), + ) + self._publish_tf(default_pose) + logger.debug("Publishing default odometry") + + frame_count += 1 + time.sleep(0.033) # ~30Hz + except Exception as e: + logger.debug(f"Telemetry update error: {e}") + time.sleep(0.1) + + @rpc + def get_odom(self) -> PoseStamped | None: + """Get current odometry. + + Returns: + Current pose or None + """ + return self._odom + + @rpc + def get_status(self) -> dict[str, Any]: + """Get current drone status. + + Returns: + Status dictionary + """ + return self._status.copy() + + @skill() + def move(self, vector: Vector3, duration: float = 0.0) -> None: + """Send movement command to drone. + + Args: + vector: Velocity vector [x, y, z] in m/s + duration: How long to move (0 = continuous) + """ + if self.connection: + # Convert dict/list to Vector3 + if isinstance(vector, dict): + vector = Vector3(vector.get("x", 0), vector.get("y", 0), vector.get("z", 0)) + elif isinstance(vector, (list, tuple)): + vector = Vector3( + vector[0] if len(vector) > 0 else 0, + vector[1] if len(vector) > 1 else 0, + vector[2] if len(vector) > 2 else 0, + ) + self.connection.move(vector, duration) + + @skill() + def takeoff(self, altitude: float = 3.0) -> bool: + """Takeoff to specified altitude. + + Args: + altitude: Target altitude in meters + + Returns: + True if takeoff initiated + """ + if self.connection: + return self.connection.takeoff(altitude) + return False + + @skill() + def land(self) -> bool: + """Land the drone. + + Returns: + True if land command sent + """ + if self.connection: + return self.connection.land() + return False + + @skill() + def arm(self) -> bool: + """Arm the drone. + + Returns: + True if armed successfully + """ + if self.connection: + return self.connection.arm() + return False + + @skill() + def disarm(self) -> bool: + """Disarm the drone. + + Returns: + True if disarmed successfully + """ + if self.connection: + return self.connection.disarm() + return False + + @skill() + def set_mode(self, mode: str) -> bool: + """Set flight mode. + + Args: + mode: Flight mode name + + Returns: + True if mode set successfully + """ + if self.connection: + return self.connection.set_mode(mode) + return False + + def move_twist(self, twist: Twist, duration: float = 0.0, lock_altitude: bool = True) -> bool: + """Move using ROS-style Twist commands. + + Args: + twist: Twist message with linear velocities + duration: How long to move (0 = single command) + lock_altitude: If True, ignore Z velocity + + Returns: + True if command sent successfully + """ + if self.connection: + return self.connection.move_twist(twist, duration, lock_altitude) + return False + + @skill() + def is_flying_to_target(self) -> bool: + """Check if drone is currently flying to a GPS target. + + Returns: + True if flying to target, False otherwise + """ + if self.connection and hasattr(self.connection, "is_flying_to_target"): + return self.connection.is_flying_to_target + return False + + @skill() + def fly_to(self, lat: float, lon: float, alt: float) -> str: + """Fly drone to GPS coordinates (blocking operation). + + Args: + lat: Latitude in degrees + lon: Longitude in degrees + alt: Altitude in meters (relative to home) + + Returns: + String message indicating success or failure reason + """ + if self.connection: + return self.connection.fly_to(lat, lon, alt) + return "Failed: No connection to drone" + + @skill() + def follow_object( + self, object_description: str, duration: float = 120.0 + ) -> Generator[str, None, None]: + """Follow an object with visual servoing. + + Example: + + follow_object(object_description="red car", duration=120) + + Args: + object_description (str): A short and clear description of the object. + duration (float, optional): How long to track for. Defaults to 120.0. + """ + msg = {"object_description": object_description, "duration": duration} + self.follow_object_cmd.publish(String(json.dumps(msg))) + + yield "Started trying to track. First, trying to find the object." + + start_time = time.time() + + started_tracking = False + + while time.time() - start_time < duration: + time.sleep(0.01) + with self._latest_status_lock: + if not self._latest_status: + continue + match self._latest_status.get("status"): + case "not_found" | "failed": + yield f"The '{object_description}' object has not been found. Stopped tracking." + break + case "tracking": + # Only return tracking once. + if not started_tracking: + started_tracking = True + yield f"The '{object_description}' object is now being followed." + case "lost": + yield f"The '{object_description}' object has been lost. Stopped tracking." + break + case "stopped": + yield f"Tracking '{object_description}' has stopped." + break + else: + yield f"Stopped tracking '{object_description}'" + + def _on_move_twist(self, msg: Twist) -> None: + """Handle Twist movement commands from tracking/navigation. + + Args: + msg: Twist message with linear and angular velocities + """ + if self.connection: + # Use move_twist to properly handle Twist messages + self.connection.move_twist(msg, duration=0, lock_altitude=True) + + def _on_gps_goal(self, cmd: LatLon) -> None: + if self._latest_telemetry is None or self.connection is None: + return + current_alt = self._latest_telemetry.get("GLOBAL_POSITION_INT", {}).get("relative_alt", 0) + self.connection.fly_to(cmd.lat, cmd.lon, current_alt) + + def _on_tracking_status(self, status: String) -> None: + with self._latest_status_lock: + self._latest_status = json.loads(status.data) + + @rpc + def stop(self) -> None: + """Stop the module.""" + # Stop the telemetry loop + self._running = False + + # Wait for telemetry thread to finish + if self._telemetry_thread and self._telemetry_thread.is_alive(): + self._telemetry_thread.join(timeout=2.0) + + # Stop video stream + if self.video_stream: + self.video_stream.stop() + + # Disconnect from drone + if self.connection: + self.connection.disconnect() + + logger.info("Drone connection module stopped") + + # Call parent stop to clean up Module infrastructure (event loop, LCM, disposables, etc.) + super().stop() + + @skill(output=Output.image) + def observe(self) -> Image | None: + """Returns the latest video frame from the drone camera. Use this skill for any visual world queries. + + This skill provides the current camera view for perception tasks. + Returns None if no frame has been captured yet. + """ + return self._latest_video_frame diff --git a/dimos/robot/drone/dji_video_stream.py b/dimos/robot/drone/dji_video_stream.py new file mode 100644 index 0000000000..2339eacca2 --- /dev/null +++ b/dimos/robot/drone/dji_video_stream.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025-2026 Dimensional Inc. + +"""Video streaming using GStreamer appsink for proper frame extraction.""" + +import functools +import subprocess +import threading +import time +from typing import Any + +import numpy as np +from reactivex import Observable, Subject + +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class DJIDroneVideoStream: + """Capture drone video using GStreamer appsink.""" + + def __init__(self, port: int = 5600, width: int = 640, height: int = 360) -> None: + self.port = port + self.width = width + self.height = height + self._video_subject: Subject[Image] = Subject() + self._process: subprocess.Popen[bytes] | None = None + self._stop_event = threading.Event() + + def start(self) -> bool: + """Start video capture using gst-launch with appsink.""" + try: + # Use appsink to get properly formatted frames + # The ! at the end tells appsink to emit data on stdout in a parseable format + cmd = [ + "gst-launch-1.0", + "-q", + "udpsrc", + f"port={self.port}", + "!", + "application/x-rtp,encoding-name=H264,payload=96", + "!", + "rtph264depay", + "!", + "h264parse", + "!", + "avdec_h264", + "!", + "videoscale", + "!", + f"video/x-raw,width={self.width},height={self.height}", + "!", + "videoconvert", + "!", + "video/x-raw,format=RGB", + "!", + "filesink", + "location=/dev/stdout", + "buffer-mode=2", # Unbuffered output + ] + + logger.info(f"Starting video capture on UDP port {self.port}") + logger.debug(f"Pipeline: {' '.join(cmd)}") + + self._process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0 + ) + + self._stop_event.clear() + + # Start capture thread + self._capture_thread = threading.Thread(target=self._capture_loop, daemon=True) + self._capture_thread.start() + + # Start error monitoring + self._error_thread = threading.Thread(target=self._error_monitor, daemon=True) + self._error_thread.start() + + logger.info("Video stream started") + return True + + except Exception as e: + logger.error(f"Failed to start video stream: {e}") + return False + + def _capture_loop(self) -> None: + """Read frames with fixed size.""" + channels = 3 + frame_size = self.width * self.height * channels + + logger.info( + f"Capturing frames: {self.width}x{self.height} RGB ({frame_size} bytes per frame)" + ) + + frame_count = 0 + total_bytes = 0 + + while not self._stop_event.is_set(): + try: + # Read exactly one frame worth of data + frame_data = b"" + bytes_needed = frame_size + + while bytes_needed > 0 and not self._stop_event.is_set(): + if self._process is None or self._process.stdout is None: + break + chunk = self._process.stdout.read(bytes_needed) + if not chunk: + logger.warning("No data from GStreamer") + time.sleep(0.1) + break + frame_data += chunk + bytes_needed -= len(chunk) + + if len(frame_data) == frame_size: + # We have a complete frame + total_bytes += frame_size + + # Convert to numpy array + frame = np.frombuffer(frame_data, dtype=np.uint8) + frame = frame.reshape((self.height, self.width, channels)) + + # Create Image message (RGB format - matches GStreamer pipeline output) + img_msg = Image.from_numpy(frame, format=ImageFormat.RGB) + + # Publish + self._video_subject.on_next(img_msg) + + frame_count += 1 + if frame_count == 1: + logger.debug(f"First frame captured! Shape: {frame.shape}") + elif frame_count % 100 == 0: + logger.debug( + f"Captured {frame_count} frames ({total_bytes / 1024 / 1024:.1f} MB)" + ) + + except Exception as e: + if not self._stop_event.is_set(): + logger.error(f"Error in capture loop: {e}") + time.sleep(0.1) + + def _error_monitor(self) -> None: + """Monitor GStreamer stderr.""" + while not self._stop_event.is_set() and self._process is not None: + if self._process.stderr is None: + break + line = self._process.stderr.readline() + if line: + msg = line.decode("utf-8").strip() + if "ERROR" in msg or "WARNING" in msg: + logger.warning(f"GStreamer: {msg}") + else: + logger.debug(f"GStreamer: {msg}") + + def stop(self) -> None: + """Stop video stream.""" + self._stop_event.set() + + if self._process: + self._process.terminate() + try: + self._process.wait(timeout=2) + except subprocess.TimeoutExpired: + self._process.kill() + self._process = None + + logger.info("Video stream stopped") + + def get_stream(self) -> Subject[Image]: + """Get the video stream observable.""" + return self._video_subject + + +class FakeDJIVideoStream(DJIDroneVideoStream): + """Replay video for testing.""" + + def __init__(self, port: int = 5600) -> None: + super().__init__(port) + from dimos.utils.data import get_data + + # Ensure data is available + get_data("drone") + + def start(self) -> bool: + """Start replay of recorded video.""" + self._stop_event.clear() + logger.info("Video replay started") + return True + + @functools.cache + def get_stream(self) -> Observable[Image]: # type: ignore[override] + """Get the replay stream directly.""" + from dimos.utils.testing import TimedSensorReplay + + logger.info("Creating video replay stream") + video_store: Any = TimedSensorReplay("drone/video") + stream: Observable[Image] = video_store.stream() + return stream + + def stop(self) -> None: + """Stop replay.""" + self._stop_event.set() + logger.info("Video replay stopped") diff --git a/dimos/robot/drone/drone.py b/dimos/robot/drone/drone.py new file mode 100644 index 0000000000..d2e2f3ee0e --- /dev/null +++ b/dimos/robot/drone/drone.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025-2026 Dimensional Inc. + +"""Main Drone robot class for DimOS.""" + +import functools +import logging +import os +import time +from typing import Any + +from dimos_lcm.sensor_msgs import CameraInfo +from dimos_lcm.std_msgs import String +from reactivex import Observable + +from dimos import core +from dimos.agents.skills.google_maps_skill_container import GoogleMapsSkillContainer +from dimos.agents.skills.osm import OsmSkill +from dimos.mapping.types import LatLon +from dimos.msgs.geometry_msgs import PoseStamped, Twist, Vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.protocol import pubsub +from dimos.robot.drone.camera_module import DroneCameraModule +from dimos.robot.drone.connection_module import DroneConnectionModule +from dimos.robot.drone.drone_tracking_module import DroneTrackingModule +from dimos.robot.foxglove_bridge import FoxgloveBridge + +# LCM not needed in orchestrator - modules handle communication +from dimos.robot.robot import Robot +from dimos.types.robot_capabilities import RobotCapability +from dimos.utils.logging_config import setup_logger +from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule + +logger = setup_logger() + + +class Drone(Robot): + """Generic MAVLink-based drone with video and depth capabilities.""" + + def __init__( + self, + connection_string: str = "udp:0.0.0.0:14550", + video_port: int = 5600, + camera_intrinsics: list[float] | None = None, + output_dir: str | None = None, + outdoor: bool = False, + ) -> None: + """Initialize drone robot. + + Args: + connection_string: MAVLink connection string + video_port: UDP port for video stream + camera_intrinsics: Camera intrinsics [fx, fy, cx, cy] + output_dir: Directory for outputs + outdoor: Use GPS only mode (no velocity integration) + """ + super().__init__() + + self.connection_string = connection_string + self.video_port = video_port + self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") + self.outdoor = outdoor + + if camera_intrinsics is None: + # Assuming 1920x1080 with typical FOV + self.camera_intrinsics = [1000.0, 1000.0, 960.0, 540.0] + else: + self.camera_intrinsics = camera_intrinsics + + self.capabilities = [ + RobotCapability.LOCOMOTION, # Aerial locomotion + RobotCapability.VISION, + ] + + self.dimos: core.DimosCluster | None = None + self.connection: DroneConnectionModule | None = None + self.camera: DroneCameraModule | None = None + self.tracking: DroneTrackingModule | None = None + self.foxglove_bridge: FoxgloveBridge | None = None + self.websocket_vis: WebsocketVisModule | None = None + + self._setup_directories() + + def _setup_directories(self) -> None: + """Setup output directories.""" + os.makedirs(self.output_dir, exist_ok=True) + logger.info(f"Drone outputs will be saved to: {self.output_dir}") + + def start(self) -> None: + """Start the drone system with all modules.""" + logger.info("Starting Drone robot system...") + + # Start DimOS cluster + self.dimos = core.start(4) + + # Deploy modules + self._deploy_connection() + self._deploy_camera() + self._deploy_tracking() + self._deploy_visualization() + self._deploy_navigation() + + # Start modules + self._start_modules() + + logger.info("Drone system initialized and started") + logger.info("Foxglove visualization available at http://localhost:8765") + + def _deploy_connection(self) -> None: + """Deploy and configure connection module.""" + assert self.dimos is not None + logger.info("Deploying connection module...") + + self.connection = self.dimos.deploy( # type: ignore[attr-defined] + DroneConnectionModule, + # connection_string="replay", + connection_string=self.connection_string, + video_port=self.video_port, + outdoor=self.outdoor, + ) + + # Configure LCM transports + self.connection.odom.transport = core.LCMTransport("/drone/odom", PoseStamped) + self.connection.gps_location.transport = core.pLCMTransport("/gps_location") + self.connection.gps_goal.transport = core.pLCMTransport("/gps_goal") + self.connection.status.transport = core.LCMTransport("/drone/status", String) + self.connection.telemetry.transport = core.LCMTransport("/drone/telemetry", String) + self.connection.video.transport = core.LCMTransport("/drone/video", Image) + self.connection.follow_object_cmd.transport = core.LCMTransport( + "/drone/follow_object_cmd", String + ) + self.connection.movecmd.transport = core.LCMTransport("/drone/cmd_vel", Vector3) + self.connection.movecmd_twist.transport = core.LCMTransport( + "/drone/tracking/cmd_vel", Twist + ) + + logger.info("Connection module deployed") + + def _deploy_camera(self) -> None: + """Deploy and configure camera module.""" + assert self.dimos is not None + assert self.connection is not None + logger.info("Deploying camera module...") + + self.camera = self.dimos.deploy( # type: ignore[attr-defined] + DroneCameraModule, camera_intrinsics=self.camera_intrinsics + ) + + # Configure LCM transports + self.camera.color_image.transport = core.LCMTransport("/drone/color_image", Image) + self.camera.depth_image.transport = core.LCMTransport("/drone/depth_image", Image) + self.camera.depth_colorized.transport = core.LCMTransport("/drone/depth_colorized", Image) + self.camera.camera_info.transport = core.LCMTransport("/drone/camera_info", CameraInfo) + self.camera.camera_pose.transport = core.LCMTransport("/drone/camera_pose", PoseStamped) + + # Connect video from connection module to camera module + self.camera.video.connect(self.connection.video) + + logger.info("Camera module deployed") + + def _deploy_tracking(self) -> None: + """Deploy and configure tracking module.""" + assert self.dimos is not None + assert self.connection is not None + logger.info("Deploying tracking module...") + + self.tracking = self.dimos.deploy( # type: ignore[attr-defined] + DroneTrackingModule, + outdoor=self.outdoor, + ) + + self.tracking.tracking_overlay.transport = core.LCMTransport( + "/drone/tracking_overlay", Image + ) + self.tracking.tracking_status.transport = core.LCMTransport( + "/drone/tracking_status", String + ) + self.tracking.cmd_vel.transport = core.LCMTransport("/drone/tracking/cmd_vel", Twist) + + self.tracking.video_input.connect(self.connection.video) + self.tracking.follow_object_cmd.connect(self.connection.follow_object_cmd) + + self.connection.movecmd_twist.connect(self.tracking.cmd_vel) + self.connection.tracking_status.connect(self.tracking.tracking_status) + + logger.info("Tracking module deployed") + + def _deploy_visualization(self) -> None: + """Deploy and configure visualization modules.""" + assert self.dimos is not None + assert self.connection is not None + self.websocket_vis = self.dimos.deploy(WebsocketVisModule) # type: ignore[attr-defined] + # self.websocket_vis.click_goal.transport = core.LCMTransport("/goal_request", PoseStamped) + self.websocket_vis.gps_goal.transport = core.pLCMTransport("/gps_goal") + # self.websocket_vis.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) + # self.websocket_vis.stop_explore_cmd.transport = core.LCMTransport("/stop_explore_cmd", Bool) + self.websocket_vis.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + + self.websocket_vis.odom.connect(self.connection.odom) + self.websocket_vis.gps_location.connect(self.connection.gps_location) + # self.websocket_vis.path.connect(self.global_planner.path) + # self.websocket_vis.global_costmap.connect(self.mapper.global_costmap) + + self.foxglove_bridge = FoxgloveBridge() + + def _deploy_navigation(self) -> None: + assert self.websocket_vis is not None + assert self.connection is not None + # Connect In (subscriber) to Out (publisher) + self.connection.gps_goal.connect(self.websocket_vis.gps_goal) + + def _start_modules(self) -> None: + """Start all deployed modules.""" + assert self.connection is not None + assert self.camera is not None + assert self.tracking is not None + assert self.websocket_vis is not None + assert self.foxglove_bridge is not None + logger.info("Starting modules...") + + # Start connection first + result = self.connection.start() + if not result: + logger.warning("Connection module failed to start (no drone connected?)") + + # Start camera + result = self.camera.start() + if not result: + logger.warning("Camera module failed to start") + + result = self.tracking.start() + if result: + logger.info("Tracking module started successfully") + else: + logger.warning("Tracking module failed to start") + + self.websocket_vis.start() + + # Start Foxglove + self.foxglove_bridge.start() + + logger.info("All modules started") + + # Robot control methods + + def get_odom(self) -> PoseStamped | None: + """Get current odometry. + + Returns: + Current pose or None + """ + if self.connection is None: + return None + result: PoseStamped | None = self.connection.get_odom() + return result + + @functools.cached_property + def gps_position_stream(self) -> Observable[LatLon]: + assert self.connection is not None + return self.connection.gps_location.transport.pure_observable() + + def get_status(self) -> dict[str, Any]: + """Get drone status. + + Returns: + Status dictionary + """ + if self.connection is None: + return {} + result: dict[str, Any] = self.connection.get_status() + return result + + def move(self, vector: Vector3, duration: float = 0.0) -> None: + """Send movement command. + + Args: + vector: Velocity vector [x, y, z] in m/s + duration: How long to move (0 = continuous) + """ + if self.connection is None: + return + self.connection.move(vector, duration) + + def takeoff(self, altitude: float = 3.0) -> bool: + """Takeoff to altitude. + + Args: + altitude: Target altitude in meters + + Returns: + True if takeoff initiated + """ + if self.connection is None: + return False + result: bool = self.connection.takeoff(altitude) + return result + + def land(self) -> bool: + """Land the drone. + + Returns: + True if land command sent + """ + if self.connection is None: + return False + result: bool = self.connection.land() + return result + + def arm(self) -> bool: + """Arm the drone. + + Returns: + True if armed successfully + """ + if self.connection is None: + return False + result: bool = self.connection.arm() + return result + + def disarm(self) -> bool: + """Disarm the drone. + + Returns: + True if disarmed successfully + """ + if self.connection is None: + return False + result: bool = self.connection.disarm() + return result + + def set_mode(self, mode: str) -> bool: + """Set flight mode. + + Args: + mode: Mode name (STABILIZE, GUIDED, LAND, RTL, etc.) + + Returns: + True if mode set successfully + """ + if self.connection is None: + return False + result: bool = self.connection.set_mode(mode) + return result + + def fly_to(self, lat: float, lon: float, alt: float) -> str: + """Fly to GPS coordinates. + + Args: + lat: Latitude in degrees + lon: Longitude in degrees + alt: Altitude in meters (relative to home) + + Returns: + String message indicating success or failure + """ + if self.connection is None: + return "Failed: No connection" + result: str = self.connection.fly_to(lat, lon, alt) + return result + + def cleanup(self) -> None: + self.stop() + + def stop(self) -> None: + """Stop the drone system.""" + logger.info("Stopping drone system...") + + if self.connection: + self.connection.stop() + + if self.camera: + self.camera.stop() + + if self.foxglove_bridge: + self.foxglove_bridge.stop() + + if self.dimos: + self.dimos.close_all() # type: ignore[attr-defined] + + logger.info("Drone system stopped") + + +def main() -> None: + """Main entry point for drone system.""" + import argparse + + parser = argparse.ArgumentParser(description="DimOS Drone System") + parser.add_argument("--replay", action="store_true", help="Use recorded data for testing") + + parser.add_argument( + "--outdoor", + action="store_true", + help="Outdoor mode - use GPS only, no velocity integration", + ) + args = parser.parse_args() + + # Configure logging + setup_logger(level=logging.INFO) + + # Suppress verbose loggers + logging.getLogger("distributed").setLevel(logging.WARNING) + logging.getLogger("asyncio").setLevel(logging.WARNING) + + if args.replay: + connection = "replay" + print("\nšŸ”„ REPLAY MODE - Using drone replay data") + else: + connection = os.getenv("DRONE_CONNECTION", "udp:0.0.0.0:14550") + video_port = int(os.getenv("DRONE_VIDEO_PORT", "5600")) + + print(f""" +╔══════════════════════════════════════════╗ +ā•‘ DimOS Mavlink Drone Runner ā•‘ +╠══════════════════════════════════════════╣ +ā•‘ MAVLink: {connection:<30} ā•‘ +ā•‘ Video: UDP port {video_port:<22}ā•‘ +ā•‘ Foxglove: http://localhost:8765 ā•‘ +ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• + """) + + pubsub.lcm.autoconf() # type: ignore[attr-defined] + + drone = Drone(connection_string=connection, video_port=video_port, outdoor=args.outdoor) + + drone.start() + + print("\nāœ“ Drone system started successfully!") + print("\nLCM Topics:") + print(" • /drone/odom - Odometry (PoseStamped)") + print(" • /drone/status - Status (String/JSON)") + print(" • /drone/telemetry - Full telemetry (String/JSON)") + print(" • /drone/color_image - RGB Video (Image)") + print(" • /drone/depth_image - Depth estimation (Image)") + print(" • /drone/depth_colorized - Colorized depth (Image)") + print(" • /drone/camera_info - Camera calibration") + print(" • /drone/cmd_vel - Movement commands (Vector3)") + print(" • /drone/tracking_overlay - Object tracking visualization (Image)") + print(" • /drone/tracking_status - Tracking status (String/JSON)") + + from dimos.agents import Agent # type: ignore[attr-defined] + from dimos.agents.cli.human import HumanInput + from dimos.agents.spec import Model, Provider + + assert drone.dimos is not None + human_input = drone.dimos.deploy(HumanInput) # type: ignore[attr-defined] + google_maps = drone.dimos.deploy(GoogleMapsSkillContainer) # type: ignore[attr-defined] + osm_skill = drone.dimos.deploy(OsmSkill) # type: ignore[attr-defined] + + google_maps.gps_location.transport = core.pLCMTransport("/gps_location") + osm_skill.gps_location.transport = core.pLCMTransport("/gps_location") + + agent = Agent( + system_prompt="""You are controlling a DJI drone with MAVLink interface. + You have access to drone control skills you are already flying so only run move_twist, set_mode, and fly_to. + When the user gives commands, use the appropriate skills to control the drone. + Always confirm actions and report results. Send fly_to commands only at above 200 meters altitude to be safe. + Here are some GPS locations to remember + 6th and Natoma intersection: 37.78019978319006, -122.40770815020853, + 454 Natoma (Office): 37.780967465525244, -122.40688342010769 + 5th and mission intersection: 37.782598539339695, -122.40649441875473 + 6th and mission intersection: 37.781007204789354, -122.40868447123661""", + model=Model.GPT_4O, + provider=Provider.OPENAI, # type: ignore[attr-defined] + ) + + agent.register_skills(drone.connection) + agent.register_skills(human_input) + agent.register_skills(google_maps) + agent.register_skills(osm_skill) + agent.run_implicit_skill("human") + + agent.start() + agent.loop_thread() + + # Testing + # from dimos_lcm.geometry_msgs import Twist,Vector3 + # twist = Twist() + # twist.linear = Vector3(-0.5, 0.5, 0.5) + # drone.connection.move_twist(twist, duration=2.0, lock_altitude=True) + # time.sleep(10) + # drone.tracking.track_object("water bottle") + while True: + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/drone/drone_tracking_module.py b/dimos/robot/drone/drone_tracking_module.py new file mode 100644 index 0000000000..e6560142d1 --- /dev/null +++ b/dimos/robot/drone/drone_tracking_module.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Drone tracking module with visual servoing for object following.""" + +import json +import threading +import time +from typing import Any + +import cv2 +from dimos_lcm.std_msgs import String +import numpy as np + +from dimos.core import In, Module, Out, rpc +from dimos.models.qwen.video_query import get_bbox_from_qwen_frame +from dimos.msgs.geometry_msgs import Twist, Vector3 +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.robot.drone.drone_visual_servoing_controller import ( + DroneVisualServoingController, + PIDParams, +) +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + +INDOOR_PID_PARAMS: PIDParams = (0.001, 0.0, 0.0001, (-1.0, 1.0), None, 30) +OUTDOOR_PID_PARAMS: PIDParams = (0.05, 0.0, 0.0003, (-5.0, 5.0), None, 10) +INDOOR_MAX_VELOCITY = 1.0 # m/s safety cap for indoor mode + + +class DroneTrackingModule(Module): + """Module for drone object tracking with visual servoing control.""" + + # Inputs + video_input: In[Image] + follow_object_cmd: In[Any] + + # Outputs + tracking_overlay: Out[Image] # Visualization with bbox and crosshairs + tracking_status: Out[Any] # JSON status updates + cmd_vel: Out[Twist] # Velocity commands for drone control + + def __init__( + self, + outdoor: bool = False, + x_pid_params: PIDParams | None = None, + y_pid_params: PIDParams | None = None, + z_pid_params: PIDParams | None = None, + ) -> None: + """Initialize the drone tracking module. + + Args: + outdoor: If True, use aggressive outdoor PID params (5 m/s max). + If False (default), use conservative indoor params (1 m/s max). + x_pid_params: PID parameters for forward/backward control. + If None, uses preset based on outdoor flag. + y_pid_params: PID parameters for left/right strafe control. + If None, uses preset based on outdoor flag. + z_pid_params: Optional PID parameters for altitude control. + """ + super().__init__() + + default_params = OUTDOOR_PID_PARAMS if outdoor else INDOOR_PID_PARAMS + x_pid_params = x_pid_params if x_pid_params is not None else default_params + y_pid_params = y_pid_params if y_pid_params is not None else default_params + + self._outdoor = outdoor + self._max_velocity = None if outdoor else INDOOR_MAX_VELOCITY + + self.servoing_controller = DroneVisualServoingController( + x_pid_params=x_pid_params, y_pid_params=y_pid_params, z_pid_params=z_pid_params + ) + + # Tracking state + self._tracking_active = False + self._tracking_thread: threading.Thread | None = None + self._current_object: str | None = None + self._latest_frame: Image | None = None + self._frame_lock = threading.Lock() + + # Subscribe to video input when transport is set + # (will be done by connection module) + + def _on_new_frame(self, frame: Image) -> None: + """Handle new video frame.""" + with self._frame_lock: + self._latest_frame = frame + + def _on_follow_object_cmd(self, cmd: String) -> None: + msg = json.loads(cmd.data) + self.track_object(msg["object_description"], msg["duration"]) + + def _get_latest_frame(self) -> np.ndarray[Any, np.dtype[Any]] | None: + """Get the latest video frame as numpy array.""" + with self._frame_lock: + if self._latest_frame is None: + return None + # Convert Image to numpy array + data: np.ndarray[Any, np.dtype[Any]] = self._latest_frame.data + return data + + @rpc + def start(self) -> bool: + """Start the tracking module and subscribe to video input.""" + if self.video_input.transport: + self.video_input.subscribe(self._on_new_frame) + logger.info("DroneTrackingModule started - subscribed to video input") + else: + logger.warning("DroneTrackingModule: No video input transport configured") + + if self.follow_object_cmd.transport: + self.follow_object_cmd.subscribe(self._on_follow_object_cmd) + + return True + + @rpc + def stop(self) -> None: + self._stop_tracking() + super().stop() + + @rpc + def track_object(self, object_name: str | None = None, duration: float = 120.0) -> str: + """Track and follow an object using visual servoing. + + Args: + object_name: Name of object to track, or None for most prominent + duration: Maximum tracking duration in seconds + + Returns: + String status message + """ + if self._tracking_active: + return "Already tracking an object" + + # Get current frame + frame = self._get_latest_frame() + if frame is None: + return "Error: No video frame available" + + logger.info(f"Starting track_object for {object_name or 'any object'}") + + try: + # Detect object with Qwen + logger.info("Detecting object with Qwen...") + bbox = get_bbox_from_qwen_frame(frame, object_name) + + if bbox is None: + msg = f"No object detected{' for: ' + object_name if object_name else ''}" + logger.warning(msg) + self._publish_status({"status": "not_found", "object": self._current_object}) + return msg + + logger.info(f"Object detected at bbox: {bbox}") + + # Initialize CSRT tracker (use legacy for OpenCV 4) + try: + tracker = cv2.legacy.TrackerCSRT_create() # type: ignore[attr-defined] + except AttributeError: + tracker = cv2.TrackerCSRT_create() # type: ignore[attr-defined] + + # Convert bbox format from [x1, y1, x2, y2] to [x, y, w, h] + x1, y1, x2, y2 = bbox + x, y, w, h = x1, y1, x2 - x1, y2 - y1 + + # Initialize tracker + success = tracker.init(frame, (x, y, w, h)) + if not success: + self._publish_status({"status": "failed", "object": self._current_object}) + return "Failed to initialize tracker" + + self._current_object = object_name or "object" + self._tracking_active = True + + # Start tracking in thread (non-blocking - caller should poll get_status()) + self._tracking_thread = threading.Thread( + target=self._visual_servoing_loop, args=(tracker, duration), daemon=True + ) + self._tracking_thread.start() + + return f"Tracking started for {self._current_object}. Poll get_status() for updates." + + except Exception as e: + logger.error(f"Tracking error: {e}") + self._stop_tracking() + return f"Tracking failed: {e!s}" + + def _visual_servoing_loop(self, tracker: Any, duration: float) -> None: + """Main visual servoing control loop. + + Args: + tracker: OpenCV tracker instance + duration: Maximum duration in seconds + """ + start_time = time.time() + frame_count = 0 + lost_track_count = 0 + max_lost_frames = 100 + + logger.info("Starting visual servoing loop") + + try: + while self._tracking_active and (time.time() - start_time < duration): + # Get latest frame + frame = self._get_latest_frame() + if frame is None: + time.sleep(0.01) + continue + + frame_count += 1 + + # Update tracker + success, bbox = tracker.update(frame) + + if not success: + lost_track_count += 1 + logger.warning(f"Lost track (count: {lost_track_count})") + + if lost_track_count >= max_lost_frames: + logger.error("Lost track of object") + self._publish_status( + {"status": "lost", "object": self._current_object, "frame": frame_count} + ) + break + continue + else: + lost_track_count = 0 + + # Calculate object center + x, y, w, h = bbox + current_x = x + w / 2 + current_y = y + h / 2 + + # Get frame dimensions + frame_height, frame_width = frame.shape[:2] + center_x = frame_width / 2 + center_y = frame_height / 2 + + # Compute velocity commands + vx, vy, vz = self.servoing_controller.compute_velocity_control( + target_x=current_x, + target_y=current_y, + center_x=center_x, + center_y=center_y, + dt=0.033, # ~30Hz + lock_altitude=True, + ) + + # Clamp velocity for indoor safety + if self._max_velocity is not None: + vx = max(-self._max_velocity, min(self._max_velocity, vx)) + vy = max(-self._max_velocity, min(self._max_velocity, vy)) + + # Publish velocity command via LCM + if self.cmd_vel.transport: + twist = Twist() + twist.linear = Vector3(vx, vy, 0) + twist.angular = Vector3(0, 0, 0) # No rotation for now + self.cmd_vel.publish(twist) + + # Publish visualization if transport is set + if self.tracking_overlay.transport: + overlay = self._draw_tracking_overlay( + frame, (int(x), int(y), int(w), int(h)), (int(current_x), int(current_y)) + ) + overlay_msg = Image.from_numpy(overlay, format=ImageFormat.BGR) + self.tracking_overlay.publish(overlay_msg) + + # Publish status + self._publish_status( + { + "status": "tracking", + "object": self._current_object, + "bbox": [int(x), int(y), int(w), int(h)], + "center": [int(current_x), int(current_y)], + "error": [int(current_x - center_x), int(current_y - center_y)], + "velocity": [float(vx), float(vy), float(vz)], + "frame": frame_count, + } + ) + + # Control loop rate + time.sleep(0.033) # ~30Hz + + except Exception as e: + logger.error(f"Error in servoing loop: {e}") + finally: + # Stop movement by publishing zero velocity + if self.cmd_vel.transport: + stop_twist = Twist() + stop_twist.linear = Vector3(0, 0, 0) + stop_twist.angular = Vector3(0, 0, 0) + self.cmd_vel.publish(stop_twist) + self._tracking_active = False + logger.info(f"Visual servoing loop ended after {frame_count} frames") + + def _draw_tracking_overlay( + self, + frame: np.ndarray[Any, np.dtype[Any]], + bbox: tuple[int, int, int, int], + center: tuple[int, int], + ) -> np.ndarray[Any, np.dtype[Any]]: + """Draw tracking visualization overlay. + + Args: + frame: Current video frame + bbox: Bounding box (x, y, w, h) + center: Object center (x, y) + + Returns: + Frame with overlay drawn + """ + overlay = frame.copy() + x, y, w, h = bbox + + # Draw tracking box (green) + cv2.rectangle(overlay, (x, y), (x + w, y + h), (0, 255, 0), 2) + + # Draw object center (red crosshair) + cv2.drawMarker(overlay, center, (0, 0, 255), cv2.MARKER_CROSS, 20, 2) + + # Draw desired center (blue crosshair) + frame_h, frame_w = frame.shape[:2] + frame_center = (frame_w // 2, frame_h // 2) + cv2.drawMarker(overlay, frame_center, (255, 0, 0), cv2.MARKER_CROSS, 20, 2) + + # Draw line from object to desired center + cv2.line(overlay, center, frame_center, (255, 255, 0), 1) + + # Add status text + status_text = f"Tracking: {self._current_object}" + cv2.putText(overlay, status_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) + + # Add error text + error_x = center[0] - frame_center[0] + error_y = center[1] - frame_center[1] + error_text = f"Error: ({error_x}, {error_y})" + cv2.putText( + overlay, error_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 1 + ) + + return overlay + + def _publish_status(self, status: dict[str, Any]) -> None: + """Publish tracking status as JSON. + + Args: + status: Status dictionary + """ + if self.tracking_status.transport: + status_msg = String(json.dumps(status)) + self.tracking_status.publish(status_msg) + + def _stop_tracking(self) -> None: + """Stop tracking and clean up.""" + self._tracking_active = False + if self._tracking_thread and self._tracking_thread.is_alive(): + self._tracking_thread.join(timeout=1) + + # Send stop command via LCM + if self.cmd_vel.transport: + stop_twist = Twist() + stop_twist.linear = Vector3(0, 0, 0) + stop_twist.angular = Vector3(0, 0, 0) + self.cmd_vel.publish(stop_twist) + + self._publish_status({"status": "stopped", "object": self._current_object}) + + self._current_object = None + logger.info("Tracking stopped") + + @rpc + def stop_tracking(self) -> str: + """Stop current tracking operation.""" + self._stop_tracking() + return "Tracking stopped" + + @rpc + def get_status(self) -> dict[str, Any]: + """Get current tracking status. + + Returns: + Status dictionary + """ + return { + "active": self._tracking_active, + "object": self._current_object, + "has_frame": self._latest_frame is not None, + } diff --git a/dimos/robot/drone/drone_visual_servoing_controller.py b/dimos/robot/drone/drone_visual_servoing_controller.py new file mode 100644 index 0000000000..72e47331f7 --- /dev/null +++ b/dimos/robot/drone/drone_visual_servoing_controller.py @@ -0,0 +1,103 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Minimal visual servoing controller for drone with downward-facing camera.""" + +from typing import TypeAlias + +from dimos.utils.simple_controller import PIDController + +# Type alias for PID parameters tuple +PIDParams: TypeAlias = tuple[float, float, float, tuple[float, float], float | None, int] + + +class DroneVisualServoingController: + """Minimal visual servoing for downward-facing drone camera using velocity-only control.""" + + def __init__( + self, + x_pid_params: PIDParams, + y_pid_params: PIDParams, + z_pid_params: PIDParams | None = None, + ) -> None: + """ + Initialize drone visual servoing controller. + + Args: + x_pid_params: (kp, ki, kd, output_limits, integral_limit, deadband) for forward/back + y_pid_params: (kp, ki, kd, output_limits, integral_limit, deadband) for left/right + z_pid_params: Optional params for altitude control + """ + self.x_pid = PIDController(*x_pid_params) + self.y_pid = PIDController(*y_pid_params) + self.z_pid = PIDController(*z_pid_params) if z_pid_params else None + + def compute_velocity_control( + self, + target_x: float, + target_y: float, # Target position in image (pixels or normalized) + center_x: float = 0.0, + center_y: float = 0.0, # Desired position (usually image center) + target_z: float | None = None, + desired_z: float | None = None, # Optional altitude control + dt: float = 0.1, + lock_altitude: bool = True, + ) -> tuple[float, float, float]: + """ + Compute velocity commands to center target in camera view. + + For downward camera: + - Image X error -> Drone Y velocity (left/right strafe) + - Image Y error -> Drone X velocity (forward/backward) + + Args: + target_x: Target X position in image + target_y: Target Y position in image + center_x: Desired X position (default 0) + center_y: Desired Y position (default 0) + target_z: Current altitude (optional) + desired_z: Desired altitude (optional) + dt: Time step + lock_altitude: If True, vz will always be 0 + + Returns: + tuple: (vx, vy, vz) velocities in m/s + """ + # Compute errors (positive = target is to the right/below center) + error_x = target_x - center_x # Lateral error in image + error_y = target_y - center_y # Forward error in image + + # PID control (swap axes for downward camera) + # For downward camera: object below center (positive error_y) = object is behind drone + # Need to negate: positive error_y should give negative vx (move backward) + vy = self.y_pid.update(error_x, dt) # type: ignore[no-untyped-call] # Image X -> Drone Y (strafe) + vx = -self.x_pid.update(error_y, dt) # type: ignore[no-untyped-call] # Image Y -> Drone X (NEGATED) + + # Optional altitude control + vz = 0.0 + if not lock_altitude and self.z_pid and target_z is not None and desired_z is not None: + error_z = target_z - desired_z + vz = self.z_pid.update(error_z, dt) # type: ignore[no-untyped-call] + + return vx, vy, vz + + def reset(self) -> None: + """Reset all PID controllers.""" + self.x_pid.integral = 0.0 + self.x_pid.prev_error = 0.0 + self.y_pid.integral = 0.0 + self.y_pid.prev_error = 0.0 + if self.z_pid: + self.z_pid.integral = 0.0 + self.z_pid.prev_error = 0.0 diff --git a/dimos/robot/drone/mavlink_connection.py b/dimos/robot/drone/mavlink_connection.py new file mode 100644 index 0000000000..d8a7c97c4a --- /dev/null +++ b/dimos/robot/drone/mavlink_connection.py @@ -0,0 +1,1109 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""MAVLink-based drone connection for DimOS.""" + +import functools +import logging +import time +from typing import Any + +from pymavlink import mavutil # type: ignore[import-not-found, import-untyped] +from reactivex import Subject + +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Twist, Vector3 +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(level=logging.INFO) + + +class MavlinkConnection: + """MAVLink connection for drone control.""" + + def __init__( + self, + connection_string: str = "udp:0.0.0.0:14550", + outdoor: bool = False, + max_velocity: float = 5.0, + ) -> None: + """Initialize drone connection. + + Args: + connection_string: MAVLink connection string + outdoor: Use GPS only mode (no velocity integration) + max_velocity: Maximum velocity in m/s + """ + self.connection_string = connection_string + self.outdoor = outdoor + self.max_velocity = max_velocity + self.mavlink: Any = None # MAVLink connection object + self.connected = False + self.telemetry: dict[str, Any] = {} + + self._odom_subject: Subject[PoseStamped] = Subject() + self._status_subject: Subject[dict[str, Any]] = Subject() + self._telemetry_subject: Subject[dict[str, Any]] = Subject() + self._raw_mavlink_subject: Subject[dict[str, Any]] = Subject() + + # Velocity tracking for smoothing + self.prev_vx = 0.0 + self.prev_vy = 0.0 + self.prev_vz = 0.0 + + # Flag to prevent concurrent fly_to commands + self.flying_to_target = False + + def connect(self) -> bool: + """Connect to drone via MAVLink.""" + try: + logger.info(f"Connecting to {self.connection_string}") + self.mavlink = mavutil.mavlink_connection(self.connection_string) + self.mavlink.wait_heartbeat(timeout=30) + self.connected = True + logger.info(f"Connected to system {self.mavlink.target_system}") + + self.update_telemetry() + return True + except Exception as e: + logger.error(f"Connection failed: {e}") + return False + + def update_telemetry(self, timeout: float = 0.1) -> None: + """Update telemetry data from available messages.""" + if not self.connected: + return + + end_time = time.time() + timeout + while time.time() < end_time: + msg = self.mavlink.recv_match(blocking=False) + if not msg: + time.sleep(0.001) + continue + msg_type = msg.get_type() + msg_dict = msg.to_dict() + if msg_type == "HEARTBEAT": + bool(msg_dict.get("base_mode", 0) & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED) + # print("HEARTBEAT:", msg_dict, "ARMED:", armed) + # print("MESSAGE", msg_dict) + # print("MESSAGE TYPE", msg_type) + # self._raw_mavlink_subject.on_next(msg_dict) + + self.telemetry[msg_type] = msg_dict + + # Apply unit conversions for known fields + if msg_type == "GLOBAL_POSITION_INT": + msg_dict["lat"] = msg_dict.get("lat", 0) / 1e7 + msg_dict["lon"] = msg_dict.get("lon", 0) / 1e7 + msg_dict["alt"] = msg_dict.get("alt", 0) / 1000.0 + msg_dict["relative_alt"] = msg_dict.get("relative_alt", 0) / 1000.0 + msg_dict["vx"] = msg_dict.get("vx", 0) / 100.0 # cm/s to m/s + msg_dict["vy"] = msg_dict.get("vy", 0) / 100.0 + msg_dict["vz"] = msg_dict.get("vz", 0) / 100.0 + msg_dict["hdg"] = msg_dict.get("hdg", 0) / 100.0 # centidegrees to degrees + self._publish_odom() + + elif msg_type == "GPS_RAW_INT": + msg_dict["lat"] = msg_dict.get("lat", 0) / 1e7 + msg_dict["lon"] = msg_dict.get("lon", 0) / 1e7 + msg_dict["alt"] = msg_dict.get("alt", 0) / 1000.0 + msg_dict["vel"] = msg_dict.get("vel", 0) / 100.0 + msg_dict["cog"] = msg_dict.get("cog", 0) / 100.0 + + elif msg_type == "SYS_STATUS": + msg_dict["voltage_battery"] = msg_dict.get("voltage_battery", 0) / 1000.0 + msg_dict["current_battery"] = msg_dict.get("current_battery", 0) / 100.0 + self._publish_status() + + elif msg_type == "POWER_STATUS": + msg_dict["Vcc"] = msg_dict.get("Vcc", 0) / 1000.0 + msg_dict["Vservo"] = msg_dict.get("Vservo", 0) / 1000.0 + + elif msg_type == "HEARTBEAT": + # Extract armed status + base_mode = msg_dict.get("base_mode", 0) + msg_dict["armed"] = bool(base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED) + self._publish_status() + + elif msg_type == "ATTITUDE": + self._publish_odom() + + self.telemetry[msg_type] = msg_dict + + self._publish_telemetry() + + def _publish_odom(self) -> None: + """Publish odometry data - GPS for outdoor mode, velocity integration for indoor mode.""" + attitude = self.telemetry.get("ATTITUDE", {}) + roll = attitude.get("roll", 0) + pitch = attitude.get("pitch", 0) + yaw = attitude.get("yaw", 0) + + # Use heading from GLOBAL_POSITION_INT if no ATTITUDE data + if "roll" not in attitude and "GLOBAL_POSITION_INT" in self.telemetry: + import math + + heading = self.telemetry["GLOBAL_POSITION_INT"].get("hdg", 0) + yaw = math.radians(heading) + + if "roll" not in attitude and "GLOBAL_POSITION_INT" not in self.telemetry: + logger.debug("No attitude or position data available") + return + + # MAVLink --> ROS conversion + # MAVLink: positive pitch = nose up, positive yaw = clockwise + # ROS: positive pitch = nose down, positive yaw = counter-clockwise + quaternion = Quaternion.from_euler(Vector3(roll, -pitch, -yaw)) + + if not hasattr(self, "_position"): + self._position = {"x": 0.0, "y": 0.0, "z": 0.0} + self._last_update = time.time() + if self.outdoor: + self._gps_origin = None + + current_time = time.time() + dt = current_time - self._last_update + + # Get position data from GLOBAL_POSITION_INT + pos_data = self.telemetry.get("GLOBAL_POSITION_INT", {}) + + # Outdoor mode: Use GPS coordinates + if self.outdoor and pos_data: + lat = pos_data.get("lat", 0) # Already in degrees from update_telemetry + lon = pos_data.get("lon", 0) # Already in degrees from update_telemetry + + if lat != 0 and lon != 0: # Valid GPS fix + if self._gps_origin is None: + self._gps_origin = {"lat": lat, "lon": lon} + logger.debug(f"GPS origin set: lat={lat:.7f}, lon={lon:.7f}") + + # Convert GPS to local X/Y coordinates + import math + + R = 6371000 # Earth radius in meters + dlat = math.radians(lat - self._gps_origin["lat"]) + dlon = math.radians(lon - self._gps_origin["lon"]) + + # X = North, Y = West (ROS convention) + self._position["x"] = dlat * R + self._position["y"] = -dlon * R * math.cos(math.radians(self._gps_origin["lat"])) + + # Indoor mode: Use velocity integration (ORIGINAL CODE - UNCHANGED) + elif pos_data and dt > 0: + vx = pos_data.get("vx", 0) # North velocity in m/s (already converted) + vy = pos_data.get("vy", 0) # East velocity in m/s (already converted) + + # +vx is North, +vy is East in NED mavlink frame + # ROS/Foxglove: X=forward(North), Y=left(West), Z=up + self._position["x"] += vx * dt # North → X (forward) + self._position["y"] += -vy * dt # East → -Y (right in ROS, Y points left/West) + + # Altitude handling (same for both modes) + if "ALTITUDE" in self.telemetry: + self._position["z"] = self.telemetry["ALTITUDE"].get("altitude_relative", 0) + elif pos_data: + self._position["z"] = pos_data.get( + "relative_alt", 0 + ) # Already in m from update_telemetry + + self._last_update = current_time + + # Debug logging + mode = "GPS" if self.outdoor else "VELOCITY" + logger.debug( + f"[{mode}] Position: x={self._position['x']:.2f}m, y={self._position['y']:.2f}m, z={self._position['z']:.2f}m" + ) + + pose = PoseStamped( + position=Vector3(self._position["x"], self._position["y"], self._position["z"]), + orientation=quaternion, + frame_id="world", + ts=current_time, + ) + + self._odom_subject.on_next(pose) + + def _publish_status(self) -> None: + """Publish drone status with key telemetry.""" + heartbeat = self.telemetry.get("HEARTBEAT", {}) + sys_status = self.telemetry.get("SYS_STATUS", {}) + gps_raw = self.telemetry.get("GPS_RAW_INT", {}) + global_pos = self.telemetry.get("GLOBAL_POSITION_INT", {}) + altitude = self.telemetry.get("ALTITUDE", {}) + + status = { + "armed": heartbeat.get("armed", False), + "mode": heartbeat.get("custom_mode", -1), + "battery_voltage": sys_status.get("voltage_battery", 0), + "battery_current": sys_status.get("current_battery", 0), + "battery_remaining": sys_status.get("battery_remaining", 0), + "satellites": gps_raw.get("satellites_visible", 0), + "altitude": altitude.get("altitude_relative", global_pos.get("relative_alt", 0)), + "heading": global_pos.get("hdg", 0), + "vx": global_pos.get("vx", 0), + "vy": global_pos.get("vy", 0), + "vz": global_pos.get("vz", 0), + "lat": global_pos.get("lat", 0), + "lon": global_pos.get("lon", 0), + "ts": time.time(), + } + self._status_subject.on_next(status) + + def _publish_telemetry(self) -> None: + """Publish full telemetry data.""" + telemetry_with_ts = self.telemetry.copy() + telemetry_with_ts["timestamp"] = time.time() + self._telemetry_subject.on_next(telemetry_with_ts) + + def move(self, velocity: Vector3, duration: float = 0.0) -> bool: + """Send movement command to drone. + + Args: + velocity: Velocity vector [x, y, z] in m/s + duration: How long to move (0 = continuous) + + Returns: + True if command sent successfully + """ + if not self.connected: + return False + + # MAVLink body frame velocities + forward = velocity.y # Forward/backward + right = velocity.x # Left/right + down = velocity.z # Up/down (negative for DOWN, positive for UP) + + logger.debug(f"Moving: forward={forward}, right={right}, down={down}") + + if duration > 0: + # Send velocity for duration + end_time = time.time() + duration + while time.time() < end_time: + self.mavlink.mav.set_position_target_local_ned_send( + 0, # time_boot_ms + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_FRAME_BODY_NED, + 0b0000111111000111, # type_mask (only velocities) + 0, + 0, + 0, # positions + forward, + right, + down, # velocities + 0, + 0, + 0, # accelerations + 0, + 0, # yaw, yaw_rate + ) + time.sleep(0.1) + self.stop() + else: + # Single velocity command + self.mavlink.mav.set_position_target_local_ned_send( + 0, + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_FRAME_BODY_NED, + 0b0000111111000111, + 0, + 0, + 0, + forward, + right, + down, + 0, + 0, + 0, + 0, + 0, + ) + + return True + + def move_twist(self, twist: Twist, duration: float = 0.0, lock_altitude: bool = True) -> bool: + """Move using ROS-style Twist commands. + + Args: + twist: Twist message with linear velocities (angular.z ignored for now) + duration: How long to move (0 = single command) + lock_altitude: If True, ignore Z velocity and maintain current altitude + + Returns: + True if command sent successfully + """ + if not self.connected: + return False + + # Extract velocities + forward = twist.linear.x # m/s forward (body frame) + right = twist.linear.y # m/s right (body frame) + down = 0.0 if lock_altitude else -twist.linear.z # Lock altitude by default + + if duration > 0: + # Send velocity for duration + end_time = time.time() + duration + while time.time() < end_time: + self.mavlink.mav.set_position_target_local_ned_send( + 0, # time_boot_ms + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_FRAME_BODY_NED, # Body frame for strafing + 0b0000111111000111, # type_mask - velocities only, no rotation + 0, + 0, + 0, # positions (ignored) + forward, + right, + down, # velocities in m/s + 0, + 0, + 0, # accelerations (ignored) + 0, + 0, # yaw, yaw_rate (ignored) + ) + time.sleep(0.05) # 20Hz + # Send stop command + self.stop() + else: + # Send single command for continuous movement + self.mavlink.mav.set_position_target_local_ned_send( + 0, # time_boot_ms + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_FRAME_BODY_NED, # Body frame for strafing + 0b0000111111000111, # type_mask - velocities only, no rotation + 0, + 0, + 0, # positions (ignored) + forward, + right, + down, # velocities in m/s + 0, + 0, + 0, # accelerations (ignored) + 0, + 0, # yaw, yaw_rate (ignored) + ) + + return True + + def stop(self) -> bool: + """Stop all movement.""" + if not self.connected: + return False + + self.mavlink.mav.set_position_target_local_ned_send( + 0, + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_FRAME_BODY_NED, + 0b0000111111000111, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ) + return True + + def rotate_to(self, target_heading_deg: float, timeout: float = 60.0) -> bool: + """Rotate drone to face a specific heading. + + Args: + target_heading_deg: Target heading in degrees (0-360, 0=North, 90=East) + timeout: Maximum time to spend rotating in seconds + + Returns: + True if rotation completed successfully + """ + if not self.connected: + return False + + logger.info(f"Rotating to heading {target_heading_deg:.1f}°") + + import math + import time + + start_time = time.time() + loop_count = 0 + + while time.time() - start_time < timeout: + loop_count += 1 + + # Don't call update_telemetry - let background thread handle it + # Just read the current telemetry which should be continuously updated + + if "GLOBAL_POSITION_INT" not in self.telemetry: + logger.warning("No GLOBAL_POSITION_INT in telemetry dict") + time.sleep(0.1) + continue + + # Debug: Log what's in telemetry + gps_telem = self.telemetry["GLOBAL_POSITION_INT"] + + # Get current heading - check if already converted or still in centidegrees + raw_hdg = gps_telem.get("hdg", 0) + + # Debug logging to figure out the issue + if loop_count % 5 == 0: # Log every 5th iteration + logger.info(f"DEBUG TELEMETRY: raw hdg={raw_hdg}, type={type(raw_hdg)}") + logger.info(f"DEBUG TELEMETRY keys: {list(gps_telem.keys())[:5]}") # First 5 keys + + # Check if hdg is already converted (should be < 360 if in degrees, > 360 if in centidegrees) + if raw_hdg > 360: + logger.info(f"HDG appears to be in centidegrees: {raw_hdg}") + current_heading_deg = raw_hdg / 100.0 + else: + logger.info(f"HDG appears to be in degrees already: {raw_hdg}") + current_heading_deg = raw_hdg + else: + # Normal conversion + if raw_hdg > 360: + current_heading_deg = raw_hdg / 100.0 + else: + current_heading_deg = raw_hdg + + # Normalize to 0-360 + if current_heading_deg > 360: + current_heading_deg = current_heading_deg % 360 + + # Calculate heading error (shortest angular distance) + heading_error = target_heading_deg - current_heading_deg + if heading_error > 180: + heading_error -= 360 + elif heading_error < -180: + heading_error += 360 + + logger.info( + f"ROTATION: current={current_heading_deg:.1f}° → target={target_heading_deg:.1f}° (error={heading_error:.1f}°)" + ) + + # Check if we're close enough + if abs(heading_error) < 10: # Complete within 10 degrees + logger.info( + f"ROTATION COMPLETE: current={current_heading_deg:.1f}° ā‰ˆ target={target_heading_deg:.1f}° (within {abs(heading_error):.1f}°)" + ) + # Don't stop - let fly_to immediately transition to forward movement + return True + + # Calculate yaw rate with minimum speed to avoid slow approach + yaw_rate = heading_error * 0.3 # Higher gain for faster rotation + # Ensure minimum rotation speed of 15 deg/s to avoid crawling near target + if abs(yaw_rate) < 15.0: + yaw_rate = 15.0 if heading_error > 0 else -15.0 + yaw_rate = max(-60.0, min(60.0, yaw_rate)) # Cap at 60 deg/s max + yaw_rate_rad = math.radians(yaw_rate) + + logger.info( + f"ROTATING: yaw_rate={yaw_rate:.1f} deg/s to go from {current_heading_deg:.1f}° → {target_heading_deg:.1f}°" + ) + + # Send rotation command + self.mavlink.mav.set_position_target_local_ned_send( + 0, # time_boot_ms + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_FRAME_BODY_NED, # Body frame for rotation + 0b0000011111111111, # type_mask - ignore everything except yaw_rate + 0, + 0, + 0, # positions (ignored) + 0, + 0, + 0, # velocities (ignored) + 0, + 0, + 0, # accelerations (ignored) + 0, # yaw (ignored) + yaw_rate_rad, # yaw_rate in rad/s + ) + + time.sleep(0.1) # 10Hz control loop + + logger.warning("Rotation timeout") + self.stop() + return False + + def arm(self) -> bool: + """Arm the drone.""" + if not self.connected: + return False + + logger.info("Arming motors...") + self.update_telemetry() + + self.mavlink.mav.command_long_send( + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + ) + + # Wait for ACK + ack = self.mavlink.recv_match(type="COMMAND_ACK", blocking=True, timeout=5) + if ack and ack.command == mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM: + if ack.result == mavutil.mavlink.MAV_RESULT_ACCEPTED: + logger.info("Arm command accepted") + + # Verify armed status + for _i in range(10): + msg = self.mavlink.recv_match(type="HEARTBEAT", blocking=True, timeout=1) + if msg: + armed = msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED + if armed: + logger.info("Motors ARMED successfully!") + return True + time.sleep(0.5) + else: + logger.error(f"Arm failed with result: {ack.result}") + + return False + + def disarm(self) -> bool: + """Disarm the drone.""" + if not self.connected: + return False + + logger.info("Disarming motors...") + + self.mavlink.mav.command_long_send( + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ) + + time.sleep(1) + return True + + def takeoff(self, altitude: float = 3.0) -> bool: + """Takeoff to specified altitude.""" + if not self.connected: + return False + + logger.info(f"Taking off to {altitude}m...") + + # Set GUIDED mode + if not self.set_mode("GUIDED"): + logger.error("Failed to set GUIDED mode for takeoff") + return False + + # Send takeoff command + self.mavlink.mav.command_long_send( + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + altitude, + ) + + logger.info(f"Takeoff command sent for {altitude}m altitude") + return True + + def land(self) -> bool: + """Land the drone at current position.""" + if not self.connected: + return False + + logger.info("Landing...") + + # Send initial land command + self.mavlink.mav.command_long_send( + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_CMD_NAV_LAND, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ) + + # Wait for disarm with confirmations + disarm_count = 0 + for _ in range(120): # 60 seconds max (120 * 0.5s) + # Keep sending land command + self.mavlink.mav.command_long_send( + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_CMD_NAV_LAND, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ) + + # Check armed status + msg = self.mavlink.recv_match(type="HEARTBEAT", blocking=True, timeout=0.5) + if msg: + msg_dict = msg.to_dict() + armed = bool( + msg_dict.get("base_mode", 0) & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED + ) + logger.debug(f"HEARTBEAT: {msg_dict} ARMED: {armed}") + + disarm_count = 0 if armed else disarm_count + 1 + + if disarm_count >= 5: # 2.5 seconds of continuous disarm + logger.info("Drone landed and disarmed") + return True + + time.sleep(0.5) + + logger.warning("Land timeout") + return self.set_mode("LAND") + + def fly_to(self, lat: float, lon: float, alt: float) -> str: + """Fly to GPS coordinates - sends commands continuously until reaching target. + + Args: + lat: Latitude in degrees + lon: Longitude in degrees + alt: Altitude in meters (relative to home) + + Returns: + String message indicating success or failure reason + """ + if not self.connected: + return "Failed: Not connected to drone" + + # Check if already flying to a target + if self.flying_to_target: + logger.warning( + "Already flying to target, ignoring new fly_to command. Wait until completed to send new fly_to command." + ) + return ( + "Already flying to target - wait for completion before sending new fly_to command" + ) + + self.flying_to_target = True + + # Ensure GUIDED mode for GPS navigation + if not self.set_mode("GUIDED"): + logger.error("Failed to set GUIDED mode for GPS navigation") + self.flying_to_target = False + return "Failed: Could not set GUIDED mode for GPS navigation" + + logger.info(f"Flying to GPS: lat={lat:.7f}, lon={lon:.7f}, alt={alt:.1f}m") + + # Reset velocity tracking for smooth start + self.prev_vx = 0.0 + self.prev_vy = 0.0 + self.prev_vz = 0.0 + + # Send velocity commands towards GPS target at 10Hz + acceptance_radius = 30.0 # meters + max_duration = 120 # seconds max flight time + start_time = time.time() + max_speed = self.max_velocity # m/s max speed + + import math + + loop_count = 0 + + try: + while time.time() - start_time < max_duration: + loop_start = time.time() + + # Don't update telemetry here - let background thread handle it + # self.update_telemetry(timeout=0.01) # Removed to prevent message conflicts + + # Check current position from telemetry + if "GLOBAL_POSITION_INT" in self.telemetry: + t1 = time.time() + + # Telemetry already has converted values (see update_telemetry lines 104-107) + current_lat = self.telemetry["GLOBAL_POSITION_INT"].get( + "lat", 0 + ) # Already in degrees + current_lon = self.telemetry["GLOBAL_POSITION_INT"].get( + "lon", 0 + ) # Already in degrees + current_alt = self.telemetry["GLOBAL_POSITION_INT"].get( + "relative_alt", 0 + ) # Already in meters + + t2 = time.time() + + logger.info( + f"DEBUG: Current GPS: lat={current_lat:.10f}, lon={current_lon:.10f}, alt={current_alt:.2f}m" + ) + logger.info( + f"DEBUG: Target GPS: lat={lat:.10f}, lon={lon:.10f}, alt={alt:.2f}m" + ) + + # Calculate vector to target with high precision + dlat = lat - current_lat + dlon = lon - current_lon + dalt = alt - current_alt + + logger.info( + f"DEBUG: Delta: dlat={dlat:.10f}, dlon={dlon:.10f}, dalt={dalt:.2f}m" + ) + + t3 = time.time() + + # Convert lat/lon difference to meters with high precision + # Using more accurate calculation + lat_rad = current_lat * math.pi / 180.0 + meters_per_degree_lat = ( + 111132.92 - 559.82 * math.cos(2 * lat_rad) + 1.175 * math.cos(4 * lat_rad) + ) + meters_per_degree_lon = 111412.84 * math.cos(lat_rad) - 93.5 * math.cos( + 3 * lat_rad + ) + + x_dist = dlat * meters_per_degree_lat # North distance in meters + y_dist = dlon * meters_per_degree_lon # East distance in meters + + logger.info( + f"DEBUG: Distance in meters: North={x_dist:.2f}m, East={y_dist:.2f}m, Up={dalt:.2f}m" + ) + + # Calculate total distance + distance = math.sqrt(x_dist**2 + y_dist**2 + dalt**2) + logger.info(f"DEBUG: Total distance to target: {distance:.2f}m") + + t4 = time.time() + + if distance < acceptance_radius: + logger.info(f"Reached GPS target (within {distance:.1f}m)") + self.stop() + # Return to manual control + self.set_mode("STABILIZE") + logger.info("Returned to STABILIZE mode for manual control") + self.flying_to_target = False + return f"Success: Reached target location (lat={lat:.7f}, lon={lon:.7f}, alt={alt:.1f}m)" + + # Only send velocity commands if we're far enough + if distance > 0.1: + # On first loop, rotate to face the target + if loop_count == 0: + # Calculate bearing to target + bearing_rad = math.atan2( + y_dist, x_dist + ) # East, North -> angle from North + target_heading_deg = math.degrees(bearing_rad) + if target_heading_deg < 0: + target_heading_deg += 360 + + logger.info( + f"Rotating to face target at heading {target_heading_deg:.1f}°" + ) + self.rotate_to(target_heading_deg, timeout=45.0) + logger.info("Rotation complete, starting movement") + + # Now just move towards target (no rotation) + t5 = time.time() + + # Calculate movement speed - maintain max speed until 20m from target + if distance > 20: + speed = max_speed # Full speed when far from target + else: + # Ramp down speed from 20m to target + speed = max( + 0.5, distance / 4.0 + ) # At 20m: 5m/s, at 10m: 2.5m/s, at 2m: 0.5m/s + + # Calculate target velocities + target_vx = (x_dist / distance) * speed # North velocity + target_vy = (y_dist / distance) * speed # East velocity + target_vz = (dalt / distance) * speed # Up velocity (positive = up) + + # Direct velocity assignment (no acceleration limiting) + vx = target_vx + vy = target_vy + vz = target_vz + + # Store for next iteration + self.prev_vx = vx + self.prev_vy = vy + self.prev_vz = vz + + logger.info( + f"MOVING: vx={vx:.3f} vy={vy:.3f} vz={vz:.3f} m/s, distance={distance:.1f}m" + ) + + # Send velocity command in LOCAL_NED frame + self.mavlink.mav.set_position_target_local_ned_send( + 0, # time_boot_ms + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_FRAME_LOCAL_NED, # Local NED for movement + 0b0000111111000111, # type_mask - use velocities only + 0, + 0, + 0, # positions (not used) + vx, + vy, + vz, # velocities in m/s + 0, + 0, + 0, # accelerations (not used) + 0, # yaw (not used) + 0, # yaw_rate (not used) + ) + + # Log if stuck + if loop_count > 20 and loop_count % 10 == 0: + logger.warning( + f"STUCK? Been sending commands for {loop_count} iterations but distance still {distance:.1f}m" + ) + + t6 = time.time() + + # Log timing every 10 loops + loop_count += 1 + if loop_count % 10 == 0: + logger.info( + f"TIMING: telemetry_read={t2 - t1:.4f}s, delta_calc={t3 - t2:.4f}s, " + f"distance_calc={t4 - t3:.4f}s, velocity_calc={t5 - t4:.4f}s, " + f"mavlink_send={t6 - t5:.4f}s, total_loop={t6 - loop_start:.4f}s" + ) + else: + logger.info("DEBUG: Too close to send velocity commands") + + else: + logger.warning("DEBUG: No GLOBAL_POSITION_INT in telemetry!") + + time.sleep(0.1) # Send at 10Hz + + except Exception as e: + logger.error(f"Error during fly_to: {e}") + self.flying_to_target = False # Clear flag immediately + raise # Re-raise the exception so caller sees the error + finally: + # Always clear the flag when exiting + if self.flying_to_target: + logger.info("Stopped sending GPS velocity commands (timeout)") + self.flying_to_target = False + self.set_mode("BRAKE") + time.sleep(0.5) + # Return to manual control + self.set_mode("STABILIZE") + logger.info("Returned to STABILIZE mode for manual control") + + return "Failed: Timeout - did not reach target within 120 seconds" + + def set_mode(self, mode: str) -> bool: + """Set flight mode.""" + if not self.connected: + return False + + mode_mapping = { + "STABILIZE": 0, + "GUIDED": 4, + "LOITER": 5, + "RTL": 6, + "LAND": 9, + "POSHOLD": 16, + "BRAKE": 17, + } + + if mode not in mode_mapping: + logger.error(f"Unknown mode: {mode}") + return False + + mode_id = mode_mapping[mode] + logger.info(f"Setting mode to {mode}") + + self.update_telemetry() + + self.mavlink.mav.command_long_send( + self.mavlink.target_system, + self.mavlink.target_component, + mavutil.mavlink.MAV_CMD_DO_SET_MODE, + 0, + mavutil.mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, + mode_id, + 0, + 0, + 0, + 0, + 0, + ) + + ack = self.mavlink.recv_match(type="COMMAND_ACK", blocking=True, timeout=3) + if ack and ack.result == mavutil.mavlink.MAV_RESULT_ACCEPTED: + logger.info(f"Mode changed to {mode}") + self.telemetry["mode"] = mode_id + return True + + return False + + @functools.cache + def odom_stream(self) -> Subject[PoseStamped]: + """Get odometry stream.""" + return self._odom_subject + + @functools.cache + def status_stream(self) -> Subject[dict[str, Any]]: + """Get status stream.""" + return self._status_subject + + @functools.cache + def telemetry_stream(self) -> Subject[dict[str, Any]]: + """Get full telemetry stream.""" + return self._telemetry_subject + + def get_telemetry(self) -> dict[str, Any]: + """Get current telemetry.""" + # Update telemetry multiple times to ensure we get data + for _ in range(5): + self.update_telemetry(timeout=0.2) + return self.telemetry.copy() + + def disconnect(self) -> None: + """Disconnect from drone.""" + if self.mavlink: + self.mavlink.close() + self.connected = False + logger.info("Disconnected") + + @property + def is_flying_to_target(self) -> bool: + """Check if drone is currently flying to a GPS target.""" + return self.flying_to_target + + def get_video_stream(self, fps: int = 30) -> None: + """Get video stream (to be implemented with GStreamer).""" + # Will be implemented in camera module + return None + + +class FakeMavlinkConnection(MavlinkConnection): + """Replay MAVLink for testing.""" + + def __init__(self, connection_string: str) -> None: + # Call parent init (which no longer calls connect()) + super().__init__(connection_string) + + # Create fake mavlink object + class FakeMavlink: + def __init__(self) -> None: + from dimos.utils.data import get_data + from dimos.utils.testing import TimedSensorReplay + + get_data("drone") + + self.replay: Any = TimedSensorReplay("drone/mavlink") + self.messages: list[dict[str, Any]] = [] + # The stream() method returns an Observable that emits messages with timing + self.replay.stream().subscribe(self.messages.append) + + # Properties that get accessed + self.target_system = 1 + self.target_component = 1 + self.mav = self # self.mavlink.mav is used in many places + + def recv_match( + self, blocking: bool = False, type: Any = None, timeout: Any = None + ) -> Any: + """Return next replay message as fake message object.""" + if not self.messages: + return None + + msg_dict = self.messages.pop(0) + + # Create message object with ALL attributes that might be accessed + class FakeMsg: + def __init__(self, d: dict[str, Any]) -> None: + self._dict = d + # Set any direct attributes that get accessed + self.base_mode = d.get("base_mode", 0) + self.command = d.get("command", 0) + self.result = d.get("result", 0) + + def get_type(self) -> Any: + return self._dict.get("mavpackettype", "") + + def to_dict(self) -> dict[str, Any]: + return self._dict + + # Filter by type if requested + if type and msg_dict.get("type") != type: + return None + + return FakeMsg(msg_dict) + + def wait_heartbeat(self, timeout: int = 30) -> None: + """Fake heartbeat received.""" + pass + + def close(self) -> None: + """Fake close.""" + pass + + # Command methods that get called but don't need to do anything in replay + def command_long_send(self, *args: Any, **kwargs: Any) -> None: + pass + + def set_position_target_local_ned_send(self, *args: Any, **kwargs: Any) -> None: + pass + + def set_position_target_global_int_send(self, *args: Any, **kwargs: Any) -> None: + pass + + # Set up fake mavlink + self.mavlink = FakeMavlink() + self.connected = True + + # Initialize position tracking (parent __init__ doesn't do this since connect wasn't called) + self._position = {"x": 0.0, "y": 0.0, "z": 0.0} + self._last_update = time.time() + + def takeoff(self, altitude: float = 3.0) -> bool: + """Fake takeoff - return immediately without blocking.""" + logger.info(f"[FAKE] Taking off to {altitude}m...") + return True + + def land(self) -> bool: + """Fake land - return immediately without blocking.""" + logger.info("[FAKE] Landing...") + return True diff --git a/dimos/robot/drone/test_drone.py b/dimos/robot/drone/test_drone.py new file mode 100644 index 0000000000..bfbaa9ed54 --- /dev/null +++ b/dimos/robot/drone/test_drone.py @@ -0,0 +1,1038 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025-2026 Dimensional Inc. + +"""Core unit tests for drone module.""" + +import json +import os +import time +import unittest +from unittest.mock import MagicMock, patch + +import numpy as np + +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.robot.drone.connection_module import DroneConnectionModule +from dimos.robot.drone.dji_video_stream import FakeDJIVideoStream +from dimos.robot.drone.drone import Drone +from dimos.robot.drone.mavlink_connection import FakeMavlinkConnection, MavlinkConnection + + +class TestMavlinkProcessing(unittest.TestCase): + """Test MAVLink message processing and coordinate conversions.""" + + def test_mavlink_message_processing(self) -> None: + """Test that MAVLink messages trigger correct odom/tf publishing.""" + conn = MavlinkConnection("udp:0.0.0.0:14550") + + # Mock the mavlink connection + conn.mavlink = MagicMock() + conn.connected = True + + # Track what gets published + published_odom = [] + conn._odom_subject.on_next = lambda x: published_odom.append(x) + + # Create ATTITUDE message and process it + attitude_msg = MagicMock() + attitude_msg.get_type.return_value = "ATTITUDE" + attitude_msg.to_dict.return_value = { + "mavpackettype": "ATTITUDE", + "roll": 0.1, + "pitch": 0.2, # Positive pitch = nose up in MAVLink + "yaw": 0.3, # Positive yaw = clockwise in MAVLink + } + + # Mock recv_match to return our message once then None + def recv_side_effect(*args, **kwargs): + if not hasattr(recv_side_effect, "called"): + recv_side_effect.called = True + return attitude_msg + return None + + conn.mavlink.recv_match = MagicMock(side_effect=recv_side_effect) + + # Process the message + conn.update_telemetry(timeout=0.01) + + # Check telemetry was updated + self.assertEqual(conn.telemetry["ATTITUDE"]["roll"], 0.1) + self.assertEqual(conn.telemetry["ATTITUDE"]["pitch"], 0.2) + self.assertEqual(conn.telemetry["ATTITUDE"]["yaw"], 0.3) + + # Check odom was published with correct coordinate conversion + self.assertEqual(len(published_odom), 1) + pose = published_odom[0] + + # Verify NED to ROS conversion happened + # ROS uses different conventions: positive pitch = nose down, positive yaw = counter-clockwise + # So we expect sign flips in the quaternion conversion + self.assertIsNotNone(pose.orientation) + + def test_position_integration(self) -> None: + """Test velocity integration for indoor flight positioning.""" + conn = MavlinkConnection("udp:0.0.0.0:14550") + conn.mavlink = MagicMock() + conn.connected = True + + # Initialize position tracking + conn._position = {"x": 0.0, "y": 0.0, "z": 0.0} + conn._last_update = time.time() + + # Create GLOBAL_POSITION_INT with velocities + pos_msg = MagicMock() + pos_msg.get_type.return_value = "GLOBAL_POSITION_INT" + pos_msg.to_dict.return_value = { + "mavpackettype": "GLOBAL_POSITION_INT", + "lat": 0, + "lon": 0, + "alt": 0, + "relative_alt": 1000, # 1m in mm + "vx": 100, # 1 m/s North in cm/s + "vy": 200, # 2 m/s East in cm/s + "vz": 0, + "hdg": 0, + } + + def recv_side_effect(*args, **kwargs): + if not hasattr(recv_side_effect, "called"): + recv_side_effect.called = True + return pos_msg + return None + + conn.mavlink.recv_match = MagicMock(side_effect=recv_side_effect) + + # Process with known dt + old_time = conn._last_update + conn.update_telemetry(timeout=0.01) + dt = conn._last_update - old_time + + # Check position was integrated from velocities + # vx=1m/s North → +X in ROS + # vy=2m/s East → -Y in ROS (Y points West) + expected_x = 1.0 * dt # North velocity + expected_y = -2.0 * dt # East velocity (negated for ROS) + + self.assertAlmostEqual(conn._position["x"], expected_x, places=2) + self.assertAlmostEqual(conn._position["y"], expected_y, places=2) + + def test_ned_to_ros_coordinate_conversion(self) -> None: + """Test NED to ROS coordinate system conversion for all axes.""" + conn = MavlinkConnection("udp:0.0.0.0:14550") + conn.mavlink = MagicMock() + conn.connected = True + + # Initialize position + conn._position = {"x": 0.0, "y": 0.0, "z": 0.0} + conn._last_update = time.time() + + # Test with velocities in all directions + # NED: North-East-Down + # ROS: X(forward/North), Y(left/West), Z(up) + pos_msg = MagicMock() + pos_msg.get_type.return_value = "GLOBAL_POSITION_INT" + pos_msg.to_dict.return_value = { + "mavpackettype": "GLOBAL_POSITION_INT", + "lat": 0, + "lon": 0, + "alt": 5000, # 5m altitude in mm + "relative_alt": 5000, + "vx": 300, # 3 m/s North (NED) + "vy": 400, # 4 m/s East (NED) + "vz": -100, # 1 m/s Up (negative in NED for up) + "hdg": 0, + } + + def recv_side_effect(*args, **kwargs): + if not hasattr(recv_side_effect, "called"): + recv_side_effect.called = True + return pos_msg + return None + + conn.mavlink.recv_match = MagicMock(side_effect=recv_side_effect) + + # Process message + old_time = conn._last_update + conn.update_telemetry(timeout=0.01) + dt = conn._last_update - old_time + + # Verify coordinate conversion: + # NED North (vx=3) → ROS +X + # NED East (vy=4) → ROS -Y (ROS Y points West/left) + # NED Down (vz=-1, up) → ROS +Z (ROS Z points up) + + # Position should integrate with converted velocities + self.assertGreater(conn._position["x"], 0) # North → positive X + self.assertLess(conn._position["y"], 0) # East → negative Y + self.assertEqual(conn._position["z"], 5.0) # Altitude from relative_alt (5000mm = 5m) + + # Check X,Y velocity integration (Z is set from altitude, not integrated) + self.assertAlmostEqual(conn._position["x"], 3.0 * dt, places=2) + self.assertAlmostEqual(conn._position["y"], -4.0 * dt, places=2) + + +class TestReplayMode(unittest.TestCase): + """Test replay mode functionality.""" + + def test_fake_mavlink_connection(self) -> None: + """Test FakeMavlinkConnection replays messages correctly.""" + with patch("dimos.utils.testing.TimedSensorReplay") as mock_replay: + # Mock the replay stream + MagicMock() + mock_messages = [ + {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, + {"mavpackettype": "HEARTBEAT", "type": 2, "base_mode": 193}, + ] + + # Make stream emit our messages + mock_replay.return_value.stream.return_value.subscribe = lambda callback: [ + callback(msg) for msg in mock_messages + ] + + conn = FakeMavlinkConnection("replay") + + # Check messages are available + msg1 = conn.mavlink.recv_match() + self.assertIsNotNone(msg1) + self.assertEqual(msg1.get_type(), "ATTITUDE") + + msg2 = conn.mavlink.recv_match() + self.assertIsNotNone(msg2) + self.assertEqual(msg2.get_type(), "HEARTBEAT") + + def test_fake_video_stream_no_throttling(self) -> None: + """Test FakeDJIVideoStream returns replay stream directly.""" + with patch("dimos.utils.testing.TimedSensorReplay") as mock_replay: + mock_stream = MagicMock() + mock_replay.return_value.stream.return_value = mock_stream + + stream = FakeDJIVideoStream(port=5600) + result_stream = stream.get_stream() + + # Verify stream is returned directly without throttling + self.assertEqual(result_stream, mock_stream) + + def test_connection_module_replay_mode(self) -> None: + """Test connection module uses Fake classes in replay mode.""" + with patch("dimos.robot.drone.mavlink_connection.FakeMavlinkConnection") as mock_fake_conn: + with patch("dimos.robot.drone.dji_video_stream.FakeDJIVideoStream") as mock_fake_video: + # Mock the fake connection + mock_conn_instance = MagicMock() + mock_conn_instance.connected = True + mock_conn_instance.odom_stream.return_value.subscribe = MagicMock( + return_value=lambda: None + ) + mock_conn_instance.status_stream.return_value.subscribe = MagicMock( + return_value=lambda: None + ) + mock_conn_instance.telemetry_stream.return_value.subscribe = MagicMock( + return_value=lambda: None + ) + mock_conn_instance.disconnect = MagicMock() + mock_fake_conn.return_value = mock_conn_instance + + # Mock the fake video + mock_video_instance = MagicMock() + mock_video_instance.start.return_value = True + mock_video_instance.get_stream.return_value.subscribe = MagicMock( + return_value=lambda: None + ) + mock_video_instance.stop = MagicMock() + mock_fake_video.return_value = mock_video_instance + + # Create module with replay connection string + module = DroneConnectionModule(connection_string="replay") + module.video = MagicMock() + module.movecmd = MagicMock() + module.movecmd.subscribe = MagicMock(return_value=lambda: None) + module.tf = MagicMock() + + try: + # Start should use Fake classes + result = module.start() + + self.assertTrue(result) + mock_fake_conn.assert_called_once_with("replay") + mock_fake_video.assert_called_once() + finally: + # Always clean up + module.stop() + + def test_connection_module_replay_with_messages(self) -> None: + """Test connection module in replay mode receives and processes messages.""" + + os.environ["DRONE_CONNECTION"] = "replay" + + with patch("dimos.utils.testing.TimedSensorReplay") as mock_replay: + # Set up MAVLink replay stream + mavlink_messages = [ + {"mavpackettype": "HEARTBEAT", "type": 2, "base_mode": 193}, + {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, + { + "mavpackettype": "GLOBAL_POSITION_INT", + "lat": 377810501, + "lon": -1224069671, + "alt": 0, + "relative_alt": 1000, + "vx": 100, + "vy": 0, + "vz": 0, + "hdg": 0, + }, + ] + + # Set up video replay stream + video_frames = [ + np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8), + np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8), + ] + + def create_mavlink_stream(): + stream = MagicMock() + + def subscribe(callback) -> None: + print("\n[TEST] MAVLink replay stream subscribed") + for msg in mavlink_messages: + print(f"[TEST] Replaying MAVLink: {msg['mavpackettype']}") + callback(msg) + + stream.subscribe = subscribe + return stream + + def create_video_stream(): + stream = MagicMock() + + def subscribe(callback) -> None: + print("[TEST] Video replay stream subscribed") + for i, frame in enumerate(video_frames): + print( + f"[TEST] Replaying video frame {i + 1}/{len(video_frames)}, shape: {frame.shape}" + ) + callback(frame) + + stream.subscribe = subscribe + return stream + + # Configure mock replay to return appropriate streams + def replay_side_effect(store_name: str): + print(f"[TEST] TimedSensorReplay created for: {store_name}") + mock = MagicMock() + if "mavlink" in store_name: + mock.stream.return_value = create_mavlink_stream() + elif "video" in store_name: + mock.stream.return_value = create_video_stream() + return mock + + mock_replay.side_effect = replay_side_effect + + # Create and start connection module + module = DroneConnectionModule(connection_string="replay") + + # Mock publishers to track what gets published + published_odom = [] + published_video = [] + published_status = [] + + module.odom = MagicMock( + publish=lambda x: ( + published_odom.append(x), + print( + f"[TEST] Published odom: position=({x.position.x:.2f}, {x.position.y:.2f}, {x.position.z:.2f})" + ), + ) + ) + module.video = MagicMock( + publish=lambda x: ( + published_video.append(x), + print( + f"[TEST] Published video frame with shape: {x.data.shape if hasattr(x, 'data') else 'unknown'}" + ), + ) + ) + module.status = MagicMock( + publish=lambda x: ( + published_status.append(x), + print( + f"[TEST] Published status: {x.data[:50]}..." + if hasattr(x, "data") + else "[TEST] Published status" + ), + ) + ) + module.telemetry = MagicMock() + module.tf = MagicMock() + module.movecmd = MagicMock() + + try: + print("\n[TEST] Starting connection module in replay mode...") + result = module.start() + + # Give time for messages to process + import time + + time.sleep(0.1) + + print(f"\n[TEST] Module started: {result}") + print(f"[TEST] Total odom messages published: {len(published_odom)}") + print(f"[TEST] Total video frames published: {len(published_video)}") + print(f"[TEST] Total status messages published: {len(published_status)}") + + # Verify module started and is processing messages + self.assertTrue(result) + self.assertIsNotNone(module.connection) + self.assertIsNotNone(module.video_stream) + + # Should have published some messages + self.assertGreater( + len(published_odom) + len(published_video) + len(published_status), + 0, + "No messages were published in replay mode", + ) + finally: + # Clean up + module.stop() + + +class TestDroneFullIntegration(unittest.TestCase): + """Full integration test of Drone class with replay mode.""" + + def setUp(self) -> None: + """Set up test environment.""" + # Mock the DimOS core module + self.mock_dimos = MagicMock() + self.mock_dimos.deploy.return_value = MagicMock() + + # Mock pubsub.lcm.autoconf + self.pubsub_patch = patch("dimos.protocol.pubsub.lcm.autoconf") + self.pubsub_patch.start() + + # Mock FoxgloveBridge + self.foxglove_patch = patch("dimos.robot.drone.drone.FoxgloveBridge") + self.mock_foxglove = self.foxglove_patch.start() + + def tearDown(self) -> None: + """Clean up patches.""" + self.pubsub_patch.stop() + self.foxglove_patch.stop() + + @patch("dimos.robot.drone.drone.core.start") + @patch("dimos.utils.testing.TimedSensorReplay") + def test_full_system_with_replay(self, mock_replay, mock_core_start) -> None: + """Test full drone system initialization and operation with replay mode.""" + # Set up mock replay data + mavlink_messages = [ + {"mavpackettype": "HEARTBEAT", "type": 2, "base_mode": 193, "armed": True}, + {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, + { + "mavpackettype": "GLOBAL_POSITION_INT", + "lat": 377810501, + "lon": -1224069671, + "alt": 5000, + "relative_alt": 5000, + "vx": 100, # 1 m/s North + "vy": 200, # 2 m/s East + "vz": -50, # 0.5 m/s Up + "hdg": 9000, # 90 degrees + }, + { + "mavpackettype": "BATTERY_STATUS", + "voltages": [3800, 3800, 3800, 3800], + "battery_remaining": 75, + }, + ] + + video_frames = [ + Image( + data=np.random.randint(0, 255, (360, 640, 3), dtype=np.uint8), + format=ImageFormat.BGR, + ) + ] + + def replay_side_effect(store_name: str): + mock = MagicMock() + if "mavlink" in store_name: + # Create stream that emits MAVLink messages + stream = MagicMock() + stream.subscribe = lambda callback: [callback(msg) for msg in mavlink_messages] + mock.stream.return_value = stream + elif "video" in store_name: + # Create stream that emits video frames + stream = MagicMock() + stream.subscribe = lambda callback: [callback(frame) for frame in video_frames] + mock.stream.return_value = stream + return mock + + mock_replay.side_effect = replay_side_effect + + # Mock DimOS core + mock_core_start.return_value = self.mock_dimos + + # Create drone in replay mode + drone = Drone(connection_string="replay", video_port=5600) + + # Mock the deployed modules + mock_connection = MagicMock() + mock_camera = MagicMock() + + # Set up return values for module methods + mock_connection.start.return_value = True + mock_connection.get_odom.return_value = PoseStamped( + position=Vector3(1.0, 2.0, 3.0), orientation=Quaternion(0, 0, 0, 1), frame_id="world" + ) + mock_connection.get_status.return_value = { + "armed": True, + "battery_voltage": 15.2, + "battery_remaining": 75, + "altitude": 5.0, + } + + mock_camera.start.return_value = True + + # Configure deploy to return our mocked modules + def deploy_side_effect(module_class, **kwargs): + if "DroneConnectionModule" in str(module_class): + return mock_connection + elif "DroneCameraModule" in str(module_class): + return mock_camera + return MagicMock() + + self.mock_dimos.deploy.side_effect = deploy_side_effect + + # Start the drone system + drone.start() + + # Verify modules were deployed + self.assertEqual(self.mock_dimos.deploy.call_count, 4) + + # Test get_odom + odom = drone.get_odom() + self.assertIsNotNone(odom) + self.assertEqual(odom.position.x, 1.0) + self.assertEqual(odom.position.y, 2.0) + self.assertEqual(odom.position.z, 3.0) + + # Test get_status + status = drone.get_status() + self.assertIsNotNone(status) + self.assertTrue(status["armed"]) + self.assertEqual(status["battery_remaining"], 75) + + # Test movement command + drone.move(Vector3(1.0, 0.0, 0.5), duration=2.0) + mock_connection.move.assert_called_once_with(Vector3(1.0, 0.0, 0.5), 2.0) + + # Test control commands + drone.arm() + mock_connection.arm.assert_called_once() + + drone.takeoff(altitude=10.0) + mock_connection.takeoff.assert_called_once_with(10.0) + + drone.land() + mock_connection.land.assert_called_once() + + drone.disarm() + mock_connection.disarm.assert_called_once() + + # Test mode setting + drone.set_mode("GUIDED") + mock_connection.set_mode.assert_called_once_with("GUIDED") + + # Clean up + drone.stop() + + # Verify cleanup was called + mock_connection.stop.assert_called_once() + mock_camera.stop.assert_called_once() + self.mock_dimos.close_all.assert_called_once() + + +class TestDroneControlCommands(unittest.TestCase): + """Test drone control commands with FakeMavlinkConnection.""" + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_arm_disarm_commands(self, mock_get_data, mock_replay) -> None: + """Test arm and disarm commands work with fake connection.""" + # Set up mock replay + mock_stream = MagicMock() + mock_stream.subscribe = lambda callback: None + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + + # Test arm + result = conn.arm() + self.assertIsInstance(result, bool) # Should return bool without crashing + + # Test disarm + result = conn.disarm() + self.assertIsInstance(result, bool) # Should return bool without crashing + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_takeoff_land_commands(self, mock_get_data, mock_replay) -> None: + """Test takeoff and land commands with fake connection.""" + mock_stream = MagicMock() + mock_stream.subscribe = lambda callback: None + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + + # Test takeoff + result = conn.takeoff(altitude=15.0) + # In fake mode, should accept but may return False if no ACK simulation + self.assertIsNotNone(result) + + # Test land + result = conn.land() + self.assertIsNotNone(result) + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_set_mode_command(self, mock_get_data, mock_replay) -> None: + """Test flight mode setting with fake connection.""" + mock_stream = MagicMock() + mock_stream.subscribe = lambda callback: None + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + + # Test various flight modes + modes = ["STABILIZE", "GUIDED", "LAND", "RTL", "LOITER"] + for mode in modes: + result = conn.set_mode(mode) + # Should return True or False but not crash + self.assertIsInstance(result, bool) + + +class TestDronePerception(unittest.TestCase): + """Test drone perception capabilities.""" + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_video_stream_replay(self, mock_get_data, mock_replay) -> None: + """Test video stream works with replay data.""" + # Set up video frames - create a test pattern instead of random noise + import cv2 + + # Create a test pattern image with some structure + test_frame = np.zeros((360, 640, 3), dtype=np.uint8) + # Add some colored rectangles to make it visually obvious + cv2.rectangle(test_frame, (50, 50), (200, 150), (255, 0, 0), -1) # Blue + cv2.rectangle(test_frame, (250, 50), (400, 150), (0, 255, 0), -1) # Green + cv2.rectangle(test_frame, (450, 50), (600, 150), (0, 0, 255), -1) # Red + cv2.putText( + test_frame, + "DRONE TEST FRAME", + (150, 250), + cv2.FONT_HERSHEY_SIMPLEX, + 1.5, + (255, 255, 255), + 2, + ) + + video_frames = [test_frame, test_frame.copy()] + + # Mock replay stream + mock_stream = MagicMock() + received_frames = [] + + def subscribe_side_effect(callback) -> None: + for frame in video_frames: + img = Image(data=frame, format=ImageFormat.BGR) + callback(img) + received_frames.append(img) + + mock_stream.subscribe = subscribe_side_effect + mock_replay.return_value.stream.return_value = mock_stream + + # Create fake video stream + video_stream = FakeDJIVideoStream(port=5600) + stream = video_stream.get_stream() + + # Subscribe to stream + captured_frames = [] + stream.subscribe(captured_frames.append) + + # Verify frames were captured + self.assertEqual(len(received_frames), 2) + for i, frame in enumerate(received_frames): + self.assertIsInstance(frame, Image) + self.assertEqual(frame.data.shape, (360, 640, 3)) + + # Save first frame to file for visual inspection + if i == 0: + import os + + output_path = "/tmp/drone_test_frame.png" + cv2.imwrite(output_path, frame.data) + print(f"\n[TEST] Saved test frame to {output_path} for visual inspection") + if os.path.exists(output_path): + print(f"[TEST] File size: {os.path.getsize(output_path)} bytes") + + +class TestDroneMovementAndOdometry(unittest.TestCase): + """Test drone movement commands and odometry.""" + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_movement_command_conversion(self, mock_get_data, mock_replay) -> None: + """Test movement commands are properly converted from ROS to NED.""" + mock_stream = MagicMock() + mock_stream.subscribe = lambda callback: None + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + + # Test movement in ROS frame + # ROS: X=forward, Y=left, Z=up + velocity_ros = Vector3(2.0, -1.0, 0.5) # Forward 2m/s, right 1m/s, up 0.5m/s + + result = conn.move(velocity_ros, duration=1.0) + self.assertTrue(result) + + # Movement should be converted to NED internally + # The fake connection doesn't actually send commands, but it should not crash + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_odometry_from_replay(self, mock_get_data, mock_replay) -> None: + """Test odometry is properly generated from replay messages.""" + # Set up replay messages + messages = [ + {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, + { + "mavpackettype": "GLOBAL_POSITION_INT", + "lat": 377810501, + "lon": -1224069671, + "alt": 10000, + "relative_alt": 5000, + "vx": 200, # 2 m/s North + "vy": 100, # 1 m/s East + "vz": -50, # 0.5 m/s Up + "hdg": 18000, # 180 degrees + }, + ] + + def replay_stream_subscribe(callback) -> None: + for msg in messages: + callback(msg) + + mock_stream = MagicMock() + mock_stream.subscribe = replay_stream_subscribe + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + + # Collect published odometry + published_odom = [] + conn._odom_subject.subscribe(published_odom.append) + + # Process messages + for _ in range(5): + conn.update_telemetry(timeout=0.01) + + # Should have published odometry + self.assertGreater(len(published_odom), 0) + + # Check odometry message + odom = published_odom[0] + self.assertIsInstance(odom, PoseStamped) + self.assertIsNotNone(odom.orientation) + self.assertEqual(odom.frame_id, "world") + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_position_integration_indoor(self, mock_get_data, mock_replay) -> None: + """Test position integration for indoor flight without GPS.""" + messages = [ + {"mavpackettype": "ATTITUDE", "roll": 0, "pitch": 0, "yaw": 0}, + { + "mavpackettype": "GLOBAL_POSITION_INT", + "lat": 0, # Invalid GPS + "lon": 0, + "alt": 0, + "relative_alt": 2000, # 2m altitude + "vx": 100, # 1 m/s North + "vy": 0, + "vz": 0, + "hdg": 0, + }, + ] + + def replay_stream_subscribe(callback) -> None: + for msg in messages: + callback(msg) + + mock_stream = MagicMock() + mock_stream.subscribe = replay_stream_subscribe + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + + # Process messages multiple times to integrate position + initial_time = time.time() + conn._last_update = initial_time + + for _i in range(3): + conn.update_telemetry(timeout=0.01) + time.sleep(0.1) # Let some time pass for integration + + # Position should have been integrated + self.assertGreater(conn._position["x"], 0) # Moving North + self.assertEqual(conn._position["z"], 2.0) # Altitude from relative_alt + + +class TestDroneStatusAndTelemetry(unittest.TestCase): + """Test drone status and telemetry reporting.""" + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_status_extraction(self, mock_get_data, mock_replay) -> None: + """Test status is properly extracted from MAVLink messages.""" + messages = [ + {"mavpackettype": "HEARTBEAT", "type": 2, "base_mode": 193}, # Armed + { + "mavpackettype": "BATTERY_STATUS", + "voltages": [3700, 3700, 3700, 3700], + "current_battery": -1500, + "battery_remaining": 65, + }, + {"mavpackettype": "GPS_RAW_INT", "satellites_visible": 12, "fix_type": 3}, + {"mavpackettype": "GLOBAL_POSITION_INT", "relative_alt": 8000, "hdg": 27000}, + ] + + def replay_stream_subscribe(callback) -> None: + for msg in messages: + callback(msg) + + mock_stream = MagicMock() + mock_stream.subscribe = replay_stream_subscribe + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + + # Collect published status + published_status = [] + conn._status_subject.subscribe(published_status.append) + + # Process messages + for _ in range(5): + conn.update_telemetry(timeout=0.01) + + # Should have published status + self.assertGreater(len(published_status), 0) + + # Check status fields + status = published_status[-1] # Get latest + self.assertIn("armed", status) + self.assertIn("battery_remaining", status) + self.assertIn("satellites", status) + self.assertIn("altitude", status) + self.assertIn("heading", status) + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_telemetry_json_publishing(self, mock_get_data, mock_replay) -> None: + """Test full telemetry is published as JSON.""" + messages = [ + {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, + {"mavpackettype": "GLOBAL_POSITION_INT", "lat": 377810501, "lon": -1224069671}, + ] + + def replay_stream_subscribe(callback) -> None: + for msg in messages: + callback(msg) + + mock_stream = MagicMock() + mock_stream.subscribe = replay_stream_subscribe + mock_replay.return_value.stream.return_value = mock_stream + + # Create connection module with replay + module = DroneConnectionModule(connection_string="replay") + + # Mock publishers + published_telemetry = [] + module.telemetry = MagicMock(publish=lambda x: published_telemetry.append(x)) + module.status = MagicMock() + module.odom = MagicMock() + module.tf = MagicMock() + module.video = MagicMock() + module.movecmd = MagicMock() + + # Start module + result = module.start() + self.assertTrue(result) + + # Give time for processing + time.sleep(0.2) + + # Stop module + module.stop() + + # Check telemetry was published + self.assertGreater(len(published_telemetry), 0) + + # Telemetry should be JSON string + telem_msg = published_telemetry[0] + self.assertIsNotNone(telem_msg) + + # If it's a String message, check the data + if hasattr(telem_msg, "data"): + telem_dict = json.loads(telem_msg.data) + self.assertIn("timestamp", telem_dict) + + +class TestFlyToErrorHandling(unittest.TestCase): + """Test fly_to() error handling paths.""" + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_concurrency_lock(self, mock_get_data, mock_replay) -> None: + """flying_to_target=True rejects concurrent fly_to() calls.""" + mock_stream = MagicMock() + mock_stream.subscribe = lambda callback: None + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + conn.flying_to_target = True + + result = conn.fly_to(37.0, -122.0, 10.0) + self.assertIn("Already flying to target", result) + + @patch("dimos.utils.testing.TimedSensorReplay") + @patch("dimos.utils.data.get_data") + def test_error_when_not_connected(self, mock_get_data, mock_replay) -> None: + """connected=False returns error immediately.""" + mock_stream = MagicMock() + mock_stream.subscribe = lambda callback: None + mock_replay.return_value.stream.return_value = mock_stream + + conn = FakeMavlinkConnection("replay") + conn.connected = False + + result = conn.fly_to(37.0, -122.0, 10.0) + self.assertIn("Not connected", result) + + +class TestVisualServoingEdgeCases(unittest.TestCase): + """Test DroneVisualServoingController edge cases.""" + + def test_output_clamping(self) -> None: + """Large errors are clamped to max_velocity.""" + from dimos.robot.drone.drone_visual_servoing_controller import ( + DroneVisualServoingController, + ) + + # PID params: (kp, ki, kd, output_limits, integral_limit, deadband) + max_vel = 2.0 + controller = DroneVisualServoingController( + x_pid_params=(1.0, 0.0, 0.0, (-max_vel, max_vel), None, 0), + y_pid_params=(1.0, 0.0, 0.0, (-max_vel, max_vel), None, 0), + ) + + # Large error should be clamped + vx, vy, _vz = controller.compute_velocity_control( + target_x=1000, target_y=1000, center_x=0, center_y=0, dt=0.1 + ) + self.assertLessEqual(abs(vx), max_vel) + self.assertLessEqual(abs(vy), max_vel) + + def test_deadband_prevents_integral_windup(self) -> None: + """Deadband prevents integral accumulation for small errors.""" + from dimos.robot.drone.drone_visual_servoing_controller import ( + DroneVisualServoingController, + ) + + deadband = 10 # pixels + controller = DroneVisualServoingController( + x_pid_params=(0.0, 1.0, 0.0, (-2.0, 2.0), None, deadband), # integral only + y_pid_params=(0.0, 1.0, 0.0, (-2.0, 2.0), None, deadband), + ) + + # With error inside deadband, integral should stay at zero + for _ in range(10): + controller.compute_velocity_control( + target_x=5, target_y=5, center_x=0, center_y=0, dt=0.1 + ) + + # Integral should be zero since error < deadband + self.assertEqual(controller.x_pid.integral, 0.0) + self.assertEqual(controller.y_pid.integral, 0.0) + + def test_reset_clears_integral(self) -> None: + """reset() clears accumulated integral to prevent windup.""" + from dimos.robot.drone.drone_visual_servoing_controller import ( + DroneVisualServoingController, + ) + + controller = DroneVisualServoingController( + x_pid_params=(0.0, 1.0, 0.0, (-10.0, 10.0), None, 0), # Only integral + y_pid_params=(0.0, 1.0, 0.0, (-10.0, 10.0), None, 0), + ) + + # Accumulate integral by calling multiple times with error + for _ in range(10): + controller.compute_velocity_control( + target_x=100, target_y=100, center_x=0, center_y=0, dt=0.1 + ) + + # Integral should be non-zero + self.assertNotEqual(controller.x_pid.integral, 0.0) + + # Reset should clear it + controller.reset() + self.assertEqual(controller.x_pid.integral, 0.0) + self.assertEqual(controller.y_pid.integral, 0.0) + + +class TestVisualServoingVelocity(unittest.TestCase): + """Test visual servoing velocity calculations.""" + + def test_velocity_from_bbox_center_error(self) -> None: + """Bbox center offset produces proportional velocity command.""" + from dimos.robot.drone.drone_visual_servoing_controller import ( + DroneVisualServoingController, + ) + + controller = DroneVisualServoingController( + x_pid_params=(0.01, 0.0, 0.0, (-2.0, 2.0), None, 0), + y_pid_params=(0.01, 0.0, 0.0, (-2.0, 2.0), None, 0), + ) + + # Image center at (320, 180), bbox center at (400, 180) = 80px right + frame_center = (320, 180) + bbox_center = (400, 180) + + vx, vy, _vz = controller.compute_velocity_control( + target_x=bbox_center[0], + target_y=bbox_center[1], + center_x=frame_center[0], + center_y=frame_center[1], + dt=0.1, + ) + + # Object to the right -> drone should strafe right (positive vy) + self.assertGreater(vy, 0) + # No vertical offset -> vx should be ~0 + self.assertAlmostEqual(vx, 0, places=1) + + +if __name__ == "__main__": + unittest.main() diff --git a/dimos/robot/foxglove_bridge.py b/dimos/robot/foxglove_bridge.py index 00c43f6f1b..529a14c838 100644 --- a/dimos/robot/foxglove_bridge.py +++ b/dimos/robot/foxglove_bridge.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,34 +15,61 @@ import asyncio import logging import threading +from typing import TYPE_CHECKING, Any -# this is missing, I'm just trying to import lcm_foxglove_bridge.py from dimos_lcm -from dimos_lcm.foxglove_bridge import FoxgloveBridge as LCMFoxgloveBridge +from dimos_lcm.foxglove_bridge import ( + FoxgloveBridge as LCMFoxgloveBridge, +) -from dimos.core import Module, rpc +from dimos.core import DimosCluster, Module, rpc +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from dimos.core.global_config import GlobalConfig + +logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) +logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) + +logger = setup_logger() class FoxgloveBridge(Module): _thread: threading.Thread _loop: asyncio.AbstractEventLoop - - def __init__(self, *args, shm_channels=None, jpeg_shm_channels=None, **kwargs) -> None: + _global_config: "GlobalConfig | None" = None + + def __init__( + self, + *args: Any, + shm_channels: list[str] | None = None, + jpeg_shm_channels: list[str] | None = None, + global_config: "GlobalConfig | None" = None, + **kwargs: Any, + ) -> None: super().__init__(*args, **kwargs) self.shm_channels = shm_channels or [] self.jpeg_shm_channels = jpeg_shm_channels or [] + self._global_config = global_config @rpc def start(self) -> None: super().start() + # Skip if Rerun is the selected viewer backend + if self._global_config and self._global_config.viewer_backend.startswith("rerun"): + logger.info( + "Foxglove bridge skipped", viewer_backend=self._global_config.viewer_backend + ) + return + def run_bridge() -> None: self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) try: for logger in ["lcm_foxglove_bridge", "FoxgloveServer"]: - logger = logging.getLogger(logger) - logger.setLevel(logging.ERROR) - for handler in logger.handlers: + logger = logging.getLogger(logger) # type: ignore[assignment] + logger.setLevel(logging.ERROR) # type: ignore[attr-defined] + for handler in logger.handlers: # type: ignore[attr-defined] handler.setLevel(logging.ERROR) bridge = LCMFoxgloveBridge( @@ -69,7 +96,25 @@ def stop(self) -> None: super().stop() +def deploy( + dimos: DimosCluster, + shm_channels: list[str] | None = None, +) -> FoxgloveBridge: + if shm_channels is None: + shm_channels = [ + "/image#sensor_msgs.Image", + "/lidar#sensor_msgs.PointCloud2", + "/map#sensor_msgs.PointCloud2", + ] + foxglove_bridge = dimos.deploy( # type: ignore[attr-defined] + FoxgloveBridge, + shm_channels=shm_channels, + ) + foxglove_bridge.start() + return foxglove_bridge # type: ignore[no-any-return] + + foxglove_bridge = FoxgloveBridge.blueprint -__all__ = ["FoxgloveBridge", "foxglove_bridge"] +__all__ = ["FoxgloveBridge", "deploy", "foxglove_bridge"] diff --git a/dimos/robot/position_stream.py b/dimos/robot/position_stream.py index 8cb5966b24..77a86bff4c 100644 --- a/dimos/robot/position_stream.py +++ b/dimos/robot/position_stream.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,14 +21,14 @@ import logging import time -from geometry_msgs.msg import PoseStamped -from nav_msgs.msg import Odometry +from geometry_msgs.msg import PoseStamped # type: ignore[attr-defined] +from nav_msgs.msg import Odometry # type: ignore[attr-defined] from rclpy.node import Node from reactivex import Observable, Subject, operators as ops from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.robot.position_stream", level=logging.INFO) +logger = setup_logger(level=logging.INFO) class PositionStreamProvider: @@ -60,12 +60,12 @@ def __init__( self.pose_topic = pose_topic self.use_odometry = use_odometry - self._subject = Subject() + self._subject = Subject() # type: ignore[var-annotated] self.last_position = None self.last_update_time = None - self._create_subscription() + self._create_subscription() # type: ignore[no-untyped-call] logger.info( f"PositionStreamProvider initialized with " @@ -73,7 +73,7 @@ def __init__( f"{odometry_topic if use_odometry else pose_topic}" ) - def _create_subscription(self): + def _create_subscription(self): # type: ignore[no-untyped-def] """Create the appropriate ROS subscription based on configuration.""" if self.use_odometry: self.subscription = self.ros_node.create_subscription( @@ -128,13 +128,13 @@ def _update_position(self, x: float, y: float) -> None: update_rate = 1.0 / (current_time - self.last_update_time) logger.debug(f"Position update rate: {update_rate:.1f} Hz") - self.last_position = position - self.last_update_time = current_time + self.last_position = position # type: ignore[assignment] + self.last_update_time = current_time # type: ignore[assignment] self._subject.on_next(position) logger.debug(f"Position updated: ({x:.2f}, {y:.2f})") - def get_position_stream(self) -> Observable: + def get_position_stream(self) -> Observable: # type: ignore[type-arg] """ Get an Observable stream of position updates. diff --git a/dimos/robot/recorder.py b/dimos/robot/recorder.py deleted file mode 100644 index acc9c0140e..0000000000 --- a/dimos/robot/recorder.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# UNDER DEVELOPMENT 🚧🚧🚧, NEEDS TESTING - -from collections.abc import Callable -from queue import Queue -import threading -import time -from types import TracebackType -from typing import Literal - -# from dimos.data.recording import Recorder - - -class RobotRecorder: - """A class for recording robot observation and actions. - - Recording at a specified frequency on the observation and action of a robot. It leverages a queue and a worker - thread to handle the recording asynchronously, ensuring that the main operations of the - robot are not blocked. - - Robot class must pass in the `get_state`, `get_observation`, `prepare_action` methods.` - get_state() gets the current state/pose of the robot. - get_observation() captures the observation/image of the robot. - prepare_action() calculates the action between the new and old states. - """ - - def __init__( - self, - get_state: Callable, - get_observation: Callable, - prepare_action: Callable, - frequency_hz: int = 5, - recorder_kwargs: dict | None = None, - on_static: Literal["record", "omit"] = "omit", - ) -> None: - """Initializes the RobotRecorder. - - This constructor sets up the recording mechanism on the given robot, including the recorder instance, - recording frequency, and the asynchronous processing queue and worker thread. It also - initializes attributes to track the last recorded pose and the current instruction. - - Args: - get_state: A function that returns the current state of the robot. - get_observation: A function that captures the observation/image of the robot. - prepare_action: A function that calculates the action between the new and old states. - frequency_hz: Frequency at which to record pose and image data (in Hz). - recorder_kwargs: Keyword arguments to pass to the Recorder constructor. - on_static: Whether to record on static poses or not. If "record", it will record when the robot is not moving. - """ - if recorder_kwargs is None: - recorder_kwargs = {} - self.recorder = Recorder(**recorder_kwargs) - self.task = None - - self.last_recorded_state = None - self.last_image = None - - self.recording = False - self.frequency_hz = frequency_hz - self.record_on_static = on_static == "record" - self.recording_queue = Queue() - - self.get_state = get_state - self.get_observation = get_observation - self.prepare_action = prepare_action - - self._worker_thread = threading.Thread(target=self._process_queue, daemon=True) - self._worker_thread.start() - - def __enter__(self) -> None: - """Enter the context manager, starting the recording.""" - self.start_recording(self.task) - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Exit the context manager, stopping the recording.""" - self.stop_recording() - - def record(self, task: str) -> "RobotRecorder": - """Set the task and return the context manager.""" - self.task = task - return self - - def reset_recorder(self) -> None: - """Reset the recorder.""" - while self.recording: - time.sleep(0.1) - self.recorder.reset() - - def record_from_robot(self) -> None: - """Records the current pose and captures an image at the specified frequency.""" - while self.recording: - start_time = time.perf_counter() - self.record_current_state() - elapsed_time = time.perf_counter() - start_time - # Sleep for the remaining time to maintain the desired frequency - sleep_time = max(0, (1.0 / self.frequency_hz) - elapsed_time) - time.sleep(sleep_time) - - def start_recording(self, task: str = "") -> None: - """Starts the recording of pose and image.""" - if not self.recording: - self.task = task - self.recording = True - self.recording_thread = threading.Thread(target=self.record_from_robot) - self.recording_thread.start() - - def stop_recording(self) -> None: - """Stops the recording of pose and image.""" - if self.recording: - self.recording = False - self.recording_thread.join() - - def _process_queue(self) -> None: - """Processes the recording queue asynchronously.""" - while True: - image, instruction, action, state = self.recording_queue.get() - self.recorder.record( - observation={"image": image, "instruction": instruction}, action=action, state=state - ) - self.recording_queue.task_done() - - def record_current_state(self) -> None: - """Records the current pose and image if the pose has changed.""" - state = self.get_state() - image = self.get_observation() - - # This is the beginning of the episode - if self.last_recorded_state is None: - self.last_recorded_state = state - self.last_image = image - return - - if state != self.last_recorded_state or self.record_on_static: - action = self.prepare_action(self.last_recorded_state, state) - self.recording_queue.put( - ( - self.last_image, - self.task, - action, - self.last_recorded_state, - ), - ) - self.last_image = image - self.last_recorded_state = state - - def record_last_state(self) -> None: - """Records the final pose and image after the movement completes.""" - self.record_current_state() diff --git a/dimos/robot/robot.py b/dimos/robot/robot.py index 002dcb4710..b2b6feaf6d 100644 --- a/dimos/robot/robot.py +++ b/dimos/robot/robot.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,11 +16,6 @@ from abc import ABC, abstractmethod -from reactivex import Observable - -from dimos.mapping.types import LatLon -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.perception.spatial_perception import SpatialMemory from dimos.types.robot_capabilities import RobotCapability @@ -48,7 +43,7 @@ def has_capability(self, capability: RobotCapability) -> bool: """ return capability in self.capabilities - def get_skills(self): + def get_skills(self): # type: ignore[no-untyped-def] """Get the robot's skill library. Returns: @@ -56,38 +51,10 @@ def get_skills(self): """ return self.skill_library + @abstractmethod def cleanup(self) -> None: """Clean up robot resources. Override this method to provide cleanup logic. """ - pass - - -# TODO: Delete -class UnitreeRobot(Robot): - @abstractmethod - def get_odom(self) -> PoseStamped: ... - - @abstractmethod - def explore(self) -> bool: ... - - @abstractmethod - def stop_exploration(self) -> bool: ... - - @abstractmethod - def is_exploration_active(self) -> bool: ... - - @property - @abstractmethod - def spatial_memory(self) -> SpatialMemory | None: ... - - -# TODO: Delete -class GpsRobot(ABC): - @property - @abstractmethod - def gps_position_stream(self) -> Observable[LatLon]: ... - - @abstractmethod - def set_gps_travel_goal_points(self, points: list[LatLon]) -> None: ... + ... diff --git a/dimos/robot/ros_bridge.py b/dimos/robot/ros_bridge.py index b067f88a22..48d201ca32 100644 --- a/dimos/robot/ros_bridge.py +++ b/dimos/robot/ros_bridge.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,21 +21,26 @@ import rclpy from rclpy.executors import SingleThreadedExecutor from rclpy.node import Node - from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSProfile, QoSReliabilityPolicy + from rclpy.qos import ( + QoSDurabilityPolicy, + QoSHistoryPolicy, + QoSProfile, + QoSReliabilityPolicy, + ) except ImportError: - rclpy = None - SingleThreadedExecutor = None - Node = None - QoSProfile = None - QoSReliabilityPolicy = None - QoSHistoryPolicy = None - QoSDurabilityPolicy = None + rclpy = None # type: ignore[assignment] + SingleThreadedExecutor = None # type: ignore[assignment, misc] + Node = None # type: ignore[assignment, misc] + QoSProfile = None # type: ignore[assignment, misc] + QoSReliabilityPolicy = None # type: ignore[assignment, misc] + QoSHistoryPolicy = None # type: ignore[assignment, misc] + QoSDurabilityPolicy = None # type: ignore[assignment, misc] from dimos.core.resource import Resource from dimos.protocol.pubsub.lcmpubsub import LCM, Topic from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.robot.ros_bridge", level=logging.INFO) +logger = setup_logger(level=logging.INFO) class BridgeDirection(Enum): @@ -54,7 +59,7 @@ def __init__(self, node_name: str = "dimos_ros_bridge") -> None: Args: node_name: Name for the ROS node (default: "dimos_ros_bridge") """ - if not rclpy.ok(): + if not rclpy.ok(): # type: ignore[attr-defined] rclpy.init() self.node = Node(node_name) @@ -69,7 +74,7 @@ def __init__(self, node_name: str = "dimos_ros_bridge") -> None: self._bridges: dict[str, dict[str, Any]] = {} - self._qos = QoSProfile( + self._qos = QoSProfile( # type: ignore[no-untyped-call] reliability=QoSReliabilityPolicy.RELIABLE, history=QoSHistoryPolicy.KEEP_LAST, durability=QoSDurabilityPolicy.VOLATILE, @@ -84,9 +89,9 @@ def start(self) -> None: def stop(self) -> None: """Shutdown the bridge and clean up resources.""" self._executor.shutdown() - self.node.destroy_node() + self.node.destroy_node() # type: ignore[no-untyped-call] - if rclpy.ok(): + if rclpy.ok(): # type: ignore[attr-defined] rclpy.shutdown() logger.info("ROSBridge shutdown complete") @@ -138,7 +143,7 @@ def add_topic( if direction == BridgeDirection.ROS_TO_DIMOS: - def ros_callback(msg) -> None: + def ros_callback(msg) -> None: # type: ignore[no-untyped-def] self._ros_to_dimos(msg, dimos_topic, dimos_type, topic_name) ros_subscription = self.node.create_subscription( @@ -149,7 +154,7 @@ def ros_callback(msg) -> None: elif direction == BridgeDirection.DIMOS_TO_ROS: ros_publisher = self.node.create_publisher(ros_type, ros_topic_name, self._qos) - def dimos_callback(msg, _topic) -> None: + def dimos_callback(msg, _topic) -> None: # type: ignore[no-untyped-def] self._dimos_to_ros(msg, ros_publisher, topic_name) dimos_subscription = self.lcm.subscribe(dimos_topic, dimos_callback) @@ -190,10 +195,10 @@ def _ros_to_dimos( dimos_type: DIMOS message type topic_name: Name of the topic for tracking """ - dimos_msg = dimos_type.from_ros_msg(ros_msg) + dimos_msg = dimos_type.from_ros_msg(ros_msg) # type: ignore[attr-defined] self.lcm.publish(dimos_topic, dimos_msg) - def _dimos_to_ros(self, dimos_msg: Any, ros_publisher, _topic_name: str) -> None: + def _dimos_to_ros(self, dimos_msg: Any, ros_publisher, _topic_name: str) -> None: # type: ignore[no-untyped-def] """Convert DIMOS message to ROS and publish. Args: diff --git a/dimos/robot/ros_command_queue.py b/dimos/robot/ros_command_queue.py index 770f44e1a6..86115d7780 100644 --- a/dimos/robot/ros_command_queue.py +++ b/dimos/robot/ros_command_queue.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ from dimos.utils.logging_config import setup_logger # Initialize logger for the ros command queue module -logger = setup_logger("dimos.robot.ros_command_queue") +logger = setup_logger() class CommandType(Enum): @@ -57,7 +57,7 @@ class ROSCommand(NamedTuple): id: str # Unique ID for tracking cmd_type: CommandType # Type of command - execute_func: Callable # Function to execute the command + execute_func: Callable # type: ignore[type-arg] # Function to execute the command params: dict[str, Any] # Parameters for the command (for debugging/logging) priority: int # Priority level (lower is higher priority) timeout: float # How long to wait for this command to complete @@ -73,7 +73,7 @@ class ROSCommandQueue: def __init__( self, - webrtc_func: Callable, + webrtc_func: Callable, # type: ignore[type-arg] is_ready_func: Callable[[], bool] | None = None, is_busy_func: Callable[[], bool] | None = None, debug: bool = True, @@ -93,7 +93,7 @@ def __init__( self._debug = debug # Queue of commands to process - self._queue = PriorityQueue() + self._queue = PriorityQueue() # type: ignore[var-annotated] self._current_command = None self._last_command_time = 0 @@ -110,7 +110,7 @@ def __init__( self._command_count = 0 self._success_count = 0 self._failure_count = 0 - self._command_history = [] + self._command_history = [] # type: ignore[var-annotated] self._max_queue_wait_time = ( 30.0 # Maximum time to wait for robot to be ready before forcing @@ -125,8 +125,8 @@ def start(self) -> None: return self._should_stop = False - self._queue_thread = threading.Thread(target=self._process_queue, daemon=True) - self._queue_thread.start() + self._queue_thread = threading.Thread(target=self._process_queue, daemon=True) # type: ignore[assignment] + self._queue_thread.start() # type: ignore[attr-defined] logger.info("Queue processing thread started") def stop(self, timeout: float = 2.0) -> None: @@ -206,7 +206,7 @@ def execute_webrtc() -> bool: time.sleep(stabilization_delay) # Wait for the robot to complete the command (timeout check) - while self._is_busy_func() and (time.time() - start_time) < timeout: + while self._is_busy_func() and (time.time() - start_time) < timeout: # type: ignore[misc] if ( self._debug and (time.time() - start_time) % 5 < 0.1 ): # Print every ~5 seconds @@ -216,7 +216,7 @@ def execute_webrtc() -> bool: time.sleep(0.1) # Check if we timed out - if self._is_busy_func() and (time.time() - start_time) >= timeout: + if self._is_busy_func() and (time.time() - start_time) >= timeout: # type: ignore[misc] logger.warning(f"WebRTC request timed out: {api_id} (ID: {request_id})") return False @@ -255,10 +255,10 @@ def execute_webrtc() -> bool: return request_id - def queue_action_client_request( + def queue_action_client_request( # type: ignore[no-untyped-def] self, action_name: str, - execute_func: Callable, + execute_func: Callable, # type: ignore[type-arg] priority: int = 0, timeout: float = 30.0, **kwargs, @@ -324,15 +324,15 @@ def _process_queue(self) -> None: logger.debug( f"Robot ready state changed: {self._last_ready_state} -> {is_ready}" ) - self._last_ready_state = is_ready + self._last_ready_state = is_ready # type: ignore[assignment] if is_busy != self._last_busy_state: logger.debug(f"Robot busy state changed: {self._last_busy_state} -> {is_busy}") - self._last_busy_state = is_busy + self._last_busy_state = is_busy # type: ignore[assignment] # If the robot has transitioned to busy, record the time if is_busy: - self._stuck_in_busy_since = current_time + self._stuck_in_busy_since = current_time # type: ignore[assignment] else: self._stuck_in_busy_since = None @@ -359,7 +359,7 @@ def _process_queue(self) -> None: # Get the next command _, _, command = self._queue.get(block=False) self._current_command = command - self._last_command_time = current_time + self._last_command_time = current_time # type: ignore[assignment] # Log the command cmd_info = f"ID: {command.id}, Type: {command.cmd_type.name}" @@ -460,7 +460,7 @@ def _print_queue_status(self) -> None: ) logger.debug(status) - self._last_command_time = current_time + self._last_command_time = current_time # type: ignore[assignment] @property def queue_size(self) -> int: diff --git a/dimos/robot/ros_control.py b/dimos/robot/ros_control.py deleted file mode 100644 index 2e9eb95204..0000000000 --- a/dimos/robot/ros_control.py +++ /dev/null @@ -1,865 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod -from enum import Enum, auto -import math -import threading -import time -from typing import Any - -from builtin_interfaces.msg import Duration -from cv_bridge import CvBridge -from geometry_msgs.msg import Point, Twist, Vector3 -from nav2_msgs.action import Spin -from nav_msgs.msg import OccupancyGrid, Odometry -import rclpy -from rclpy.action import ActionClient -from rclpy.executors import MultiThreadedExecutor -from rclpy.node import Node -from rclpy.qos import ( - QoSDurabilityPolicy, - QoSHistoryPolicy, - QoSProfile, - QoSReliabilityPolicy, -) -from sensor_msgs.msg import CompressedImage, Image -import tf2_ros - -from dimos.robot.connection_interface import ConnectionInterface -from dimos.robot.ros_command_queue import ROSCommandQueue -from dimos.robot.ros_observable_topic import ROSObservableTopicAbility -from dimos.robot.ros_transform import ROSTransformAbility -from dimos.stream.ros_video_provider import ROSVideoProvider -from dimos.types.vector import Vector -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.robot.ros_control") - -__all__ = ["ROSControl", "RobotMode"] - - -class RobotMode(Enum): - """Enum for robot modes""" - - UNKNOWN = auto() - INITIALIZING = auto() - IDLE = auto() - MOVING = auto() - ERROR = auto() - - -class ROSControl(ROSTransformAbility, ROSObservableTopicAbility, ConnectionInterface, ABC): - """Abstract base class for ROS-controlled robots""" - - def __init__( - self, - node_name: str, - camera_topics: dict[str, str] | None = None, - max_linear_velocity: float = 1.0, - mock_connection: bool = False, - max_angular_velocity: float = 2.0, - state_topic: str | None = None, - imu_topic: str | None = None, - state_msg_type: type | None = None, - imu_msg_type: type | None = None, - webrtc_topic: str | None = None, - webrtc_api_topic: str | None = None, - webrtc_msg_type: type | None = None, - move_vel_topic: str | None = None, - pose_topic: str | None = None, - odom_topic: str = "/odom", - global_costmap_topic: str = "map", - costmap_topic: str = "/local_costmap/costmap", - debug: bool = False, - ) -> None: - """ - Initialize base ROS control interface - Args: - node_name: Name for the ROS node - camera_topics: Dictionary of camera topics - max_linear_velocity: Maximum linear velocity (m/s) - max_angular_velocity: Maximum angular velocity (rad/s) - state_topic: Topic name for robot state (optional) - imu_topic: Topic name for IMU data (optional) - state_msg_type: The ROS message type for state data - imu_msg_type: The ROS message type for IMU data - webrtc_topic: Topic for WebRTC commands - webrtc_api_topic: Topic for WebRTC API commands - webrtc_msg_type: The ROS message type for webrtc data - move_vel_topic: Topic for direct movement commands - pose_topic: Topic for pose commands - odom_topic: Topic for odometry data - costmap_topic: Topic for local costmap data - """ - # Initialize rclpy and ROS node if not already running - if not rclpy.ok(): - rclpy.init() - - self._state_topic = state_topic - self._imu_topic = imu_topic - self._odom_topic = odom_topic - self._costmap_topic = costmap_topic - self._state_msg_type = state_msg_type - self._imu_msg_type = imu_msg_type - self._webrtc_msg_type = webrtc_msg_type - self._webrtc_topic = webrtc_topic - self._webrtc_api_topic = webrtc_api_topic - self._node = Node(node_name) - self._global_costmap_topic = global_costmap_topic - self._debug = debug - - # Prepare a multi-threaded executor - self._executor = MultiThreadedExecutor() - - # Movement constraints - self.MAX_LINEAR_VELOCITY = max_linear_velocity - self.MAX_ANGULAR_VELOCITY = max_angular_velocity - - self._subscriptions = [] - - # Track State variables - self._robot_state = None # Full state message - self._imu_state = None # Full IMU message - self._odom_data = None # Odometry data - self._costmap_data = None # Costmap data - self._mode = RobotMode.INITIALIZING - - # Create sensor data QoS profile - sensor_qos = QoSProfile( - reliability=QoSReliabilityPolicy.BEST_EFFORT, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=1, - ) - - command_qos = QoSProfile( - reliability=QoSReliabilityPolicy.RELIABLE, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=10, # Higher depth for commands to ensure delivery - ) - - if self._global_costmap_topic: - self._global_costmap_data = None - self._global_costmap_sub = self._node.create_subscription( - OccupancyGrid, - self._global_costmap_topic, - self._global_costmap_callback, - sensor_qos, - ) - self._subscriptions.append(self._global_costmap_sub) - else: - logger.warning("No costmap topic provided - costmap data tracking will be unavailable") - - # Initialize data handling - self._video_provider = None - self._bridge = None - if camera_topics: - self._bridge = CvBridge() - self._video_provider = ROSVideoProvider(dev_name=f"{node_name}_video") - - # Create subscribers for each topic with sensor QoS - for camera_config in camera_topics.values(): - topic = camera_config["topic"] - msg_type = camera_config["type"] - - logger.info( - f"Subscribing to {topic} with BEST_EFFORT QoS using message type {msg_type.__name__}" - ) - _camera_subscription = self._node.create_subscription( - msg_type, topic, self._image_callback, sensor_qos - ) - self._subscriptions.append(_camera_subscription) - - # Subscribe to state topic if provided - if self._state_topic and self._state_msg_type: - logger.info(f"Subscribing to {state_topic} with BEST_EFFORT QoS") - self._state_sub = self._node.create_subscription( - self._state_msg_type, - self._state_topic, - self._state_callback, - qos_profile=sensor_qos, - ) - self._subscriptions.append(self._state_sub) - else: - logger.warning( - "No state topic andor message type provided - robot state tracking will be unavailable" - ) - - if self._imu_topic and self._imu_msg_type: - self._imu_sub = self._node.create_subscription( - self._imu_msg_type, self._imu_topic, self._imu_callback, sensor_qos - ) - self._subscriptions.append(self._imu_sub) - else: - logger.warning( - "No IMU topic and/or message type provided - IMU data tracking will be unavailable" - ) - - if self._odom_topic: - self._odom_sub = self._node.create_subscription( - Odometry, self._odom_topic, self._odom_callback, sensor_qos - ) - self._subscriptions.append(self._odom_sub) - else: - logger.warning( - "No odometry topic provided - odometry data tracking will be unavailable" - ) - - if self._costmap_topic: - self._costmap_sub = self._node.create_subscription( - OccupancyGrid, self._costmap_topic, self._costmap_callback, sensor_qos - ) - self._subscriptions.append(self._costmap_sub) - else: - logger.warning("No costmap topic provided - costmap data tracking will be unavailable") - - # Nav2 Action Clients - self._spin_client = ActionClient(self._node, Spin, "spin") - - # Wait for action servers - if not mock_connection: - self._spin_client.wait_for_server() - - # Publishers - self._move_vel_pub = self._node.create_publisher(Twist, move_vel_topic, command_qos) - self._pose_pub = self._node.create_publisher(Vector3, pose_topic, command_qos) - - if webrtc_msg_type: - self._webrtc_pub = self._node.create_publisher( - webrtc_msg_type, webrtc_topic, qos_profile=command_qos - ) - - # Initialize command queue - self._command_queue = ROSCommandQueue( - webrtc_func=self.webrtc_req, - is_ready_func=lambda: self._mode == RobotMode.IDLE, - is_busy_func=lambda: self._mode == RobotMode.MOVING, - ) - # Start the queue processing thread - self._command_queue.start() - else: - logger.warning("No WebRTC message type provided - WebRTC commands will be unavailable") - - # Initialize TF Buffer and Listener for transform abilities - self._tf_buffer = tf2_ros.Buffer() - self._tf_listener = tf2_ros.TransformListener(self._tf_buffer, self._node) - logger.info(f"TF Buffer and Listener initialized for {node_name}") - - # Start ROS spin in a background thread via the executor - self._spin_thread = threading.Thread(target=self._ros_spin, daemon=True) - self._spin_thread.start() - - logger.info(f"{node_name} initialized with multi-threaded executor") - print(f"{node_name} initialized with multi-threaded executor") - - def get_global_costmap(self) -> OccupancyGrid | None: - """ - Get current global_costmap data - - Returns: - Optional[OccupancyGrid]: Current global_costmap data or None if not available - """ - if not self._global_costmap_topic: - logger.warning( - "No global_costmap topic provided - global_costmap data tracking will be unavailable" - ) - return None - - if self._global_costmap_data: - return self._global_costmap_data - else: - return None - - def _global_costmap_callback(self, msg) -> None: - """Callback for costmap data""" - self._global_costmap_data = msg - - def _imu_callback(self, msg) -> None: - """Callback for IMU data""" - self._imu_state = msg - # Log IMU state (very verbose) - # logger.debug(f"IMU state updated: {self._imu_state}") - - def _odom_callback(self, msg) -> None: - """Callback for odometry data""" - self._odom_data = msg - - def _costmap_callback(self, msg) -> None: - """Callback for costmap data""" - self._costmap_data = msg - - def _state_callback(self, msg) -> None: - """Callback for state messages to track mode and progress""" - - # Call the abstract method to update RobotMode enum based on the received state - self._robot_state = msg - self._update_mode(msg) - # Log state changes (very verbose) - # logger.debug(f"Robot state updated: {self._robot_state}") - - @property - def robot_state(self) -> Any | None: - """Get the full robot state message""" - return self._robot_state - - def _ros_spin(self) -> None: - """Background thread for spinning the multi-threaded executor.""" - self._executor.add_node(self._node) - try: - self._executor.spin() - finally: - self._executor.shutdown() - - def _clamp_velocity(self, velocity: float, max_velocity: float) -> float: - """Clamp velocity within safe limits""" - return max(min(velocity, max_velocity), -max_velocity) - - @abstractmethod - def _update_mode(self, *args, **kwargs): - """Update robot mode based on state - to be implemented by child classes""" - pass - - def get_state(self) -> Any | None: - """ - Get current robot state - - Base implementation provides common state fields. Child classes should - extend this method to include their specific state information. - - Returns: - ROS msg containing the robot state information - """ - if not self._state_topic: - logger.warning("No state topic provided - robot state tracking will be unavailable") - return None - - return self._robot_state - - def get_imu_state(self) -> Any | None: - """ - Get current IMU state - - Base implementation provides common state fields. Child classes should - extend this method to include their specific state information. - - Returns: - ROS msg containing the IMU state information - """ - if not self._imu_topic: - logger.warning("No IMU topic provided - IMU data tracking will be unavailable") - return None - return self._imu_state - - def get_odometry(self) -> Odometry | None: - """ - Get current odometry data - - Returns: - Optional[Odometry]: Current odometry data or None if not available - """ - if not self._odom_topic: - logger.warning( - "No odometry topic provided - odometry data tracking will be unavailable" - ) - return None - return self._odom_data - - def get_costmap(self) -> OccupancyGrid | None: - """ - Get current costmap data - - Returns: - Optional[OccupancyGrid]: Current costmap data or None if not available - """ - if not self._costmap_topic: - logger.warning("No costmap topic provided - costmap data tracking will be unavailable") - return None - return self._costmap_data - - def _image_callback(self, msg) -> None: - """Convert ROS image to numpy array and push to data stream""" - if self._video_provider and self._bridge: - try: - if isinstance(msg, CompressedImage): - frame = self._bridge.compressed_imgmsg_to_cv2(msg) - elif isinstance(msg, Image): - frame = self._bridge.imgmsg_to_cv2(msg, "bgr8") - else: - logger.error(f"Unsupported image message type: {type(msg)}") - return - self._video_provider.push_data(frame) - except Exception as e: - logger.error(f"Error converting image: {e}") - print(f"Full conversion error: {e!s}") - - @property - def video_provider(self) -> ROSVideoProvider | None: - """Data provider property for streaming data""" - return self._video_provider - - def get_video_stream(self, fps: int = 30) -> Observable | None: - """Get the video stream from the robot's camera. - - Args: - fps: Frames per second for the video stream - - Returns: - Observable: An observable stream of video frames or None if not available - """ - if not self.video_provider: - return None - - return self.video_provider.get_stream(fps=fps) - - def _send_action_client_goal( - self, client, goal_msg, description: str | None = None, time_allowance: float = 20.0 - ) -> bool: - """ - Generic function to send any action client goal and wait for completion. - - Args: - client: The action client to use - goal_msg: The goal message to send - description: Optional description for logging - time_allowance: Maximum time to wait for completion - - Returns: - bool: True if action succeeded, False otherwise - """ - if description: - logger.info(description) - - print(f"[ROSControl] Sending action client goal: {description}") - print(f"[ROSControl] Goal message: {goal_msg}") - - # Reset action result tracking - self._action_success = None - - # Send the goal - send_goal_future = client.send_goal_async(goal_msg, feedback_callback=lambda feedback: None) - send_goal_future.add_done_callback(self._goal_response_callback) - - # Wait for completion - start_time = time.time() - while self._action_success is None and time.time() - start_time < time_allowance: - time.sleep(0.1) - - elapsed = time.time() - start_time - print( - f"[ROSControl] Action completed in {elapsed:.2f}s with result: {self._action_success}" - ) - - # Check result - if self._action_success is None: - logger.error(f"Action timed out after {time_allowance}s") - return False - elif self._action_success: - logger.info("Action succeeded") - return True - else: - logger.error("Action failed") - return False - - def move(self, velocity: Vector, duration: float = 0.0) -> bool: - """Send velocity commands to the robot. - - Args: - velocity: Velocity vector [x, y, yaw] where: - x: Linear velocity in x direction (m/s) - y: Linear velocity in y direction (m/s) - yaw: Angular velocity around z axis (rad/s) - duration: Duration to apply command (seconds). If 0, apply once. - - Returns: - bool: True if command was sent successfully - """ - x, y, yaw = velocity.x, velocity.y, velocity.z - - # Clamp velocities to safe limits - x = self._clamp_velocity(x, self.MAX_LINEAR_VELOCITY) - y = self._clamp_velocity(y, self.MAX_LINEAR_VELOCITY) - yaw = self._clamp_velocity(yaw, self.MAX_ANGULAR_VELOCITY) - - # Create and send command - cmd = Twist() - cmd.linear.x = float(x) - cmd.linear.y = float(y) - cmd.angular.z = float(yaw) - - try: - if duration > 0: - start_time = time.time() - while time.time() - start_time < duration: - self._move_vel_pub.publish(cmd) - time.sleep(0.1) # 10Hz update rate - # Stop after duration - self.stop() - else: - self._move_vel_pub.publish(cmd) - return True - - except Exception as e: - self._logger.error(f"Failed to send movement command: {e}") - return False - - def reverse(self, distance: float, speed: float = 0.5, time_allowance: float = 120) -> bool: - """ - Move the robot backward by a specified distance - - Args: - distance: Distance to move backward in meters (must be positive) - speed: Speed to move at in m/s (default 0.5) - time_allowance: Maximum time to wait for the request to complete - - Returns: - bool: True if movement succeeded - """ - try: - if distance <= 0: - logger.error("Distance must be positive") - return False - - speed = min(abs(speed), self.MAX_LINEAR_VELOCITY) - - # Define function to execute the reverse - def execute_reverse(): - # Create BackUp goal - goal = BackUp.Goal() - goal.target = Point() - goal.target.x = -distance # Negative for backward motion - goal.target.y = 0.0 - goal.target.z = 0.0 - goal.speed = speed # BackUp expects positive speed - goal.time_allowance = Duration(sec=time_allowance) - - print( - f"[ROSControl] execute_reverse: Creating BackUp goal with distance={distance}m, speed={speed}m/s" - ) - print( - f"[ROSControl] execute_reverse: Goal details: x={goal.target.x}, y={goal.target.y}, z={goal.target.z}, speed={goal.speed}" - ) - - logger.info(f"Moving backward: distance={distance}m, speed={speed}m/s") - - result = self._send_action_client_goal( - self._backup_client, - goal, - f"Moving backward {distance}m at {speed}m/s", - time_allowance, - ) - - print(f"[ROSControl] execute_reverse: BackUp action result: {result}") - return result - - # Queue the action - cmd_id = self._command_queue.queue_action_client_request( - action_name="reverse", - execute_func=execute_reverse, - priority=0, - timeout=time_allowance, - distance=distance, - speed=speed, - ) - logger.info( - f"Queued reverse command: {cmd_id} - Distance: {distance}m, Speed: {speed}m/s" - ) - return True - - except Exception as e: - logger.error(f"Backward movement failed: {e}") - import traceback - - logger.error(traceback.format_exc()) - return False - - def spin(self, degrees: float, speed: float = 45.0, time_allowance: float = 120) -> bool: - """ - Rotate the robot by a specified angle - - Args: - degrees: Angle to rotate in degrees (positive for counter-clockwise, negative for clockwise) - speed: Angular speed in degrees/second (default 45.0) - time_allowance: Maximum time to wait for the request to complete - - Returns: - bool: True if movement succeeded - """ - try: - # Convert degrees to radians - angle = math.radians(degrees) - angular_speed = math.radians(abs(speed)) - - # Clamp angular speed - angular_speed = min(angular_speed, self.MAX_ANGULAR_VELOCITY) - time_allowance = max( - int(abs(angle) / angular_speed * 2), 20 - ) # At least 20 seconds or double the expected time - - # Define function to execute the spin - def execute_spin(): - # Create Spin goal - goal = Spin.Goal() - goal.target_yaw = angle # Nav2 Spin action expects radians - goal.time_allowance = Duration(sec=time_allowance) - - logger.info(f"Spinning: angle={degrees}deg ({angle:.2f}rad)") - - return self._send_action_client_goal( - self._spin_client, - goal, - f"Spinning {degrees} degrees at {speed} deg/s", - time_allowance, - ) - - # Queue the action - cmd_id = self._command_queue.queue_action_client_request( - action_name="spin", - execute_func=execute_spin, - priority=0, - timeout=time_allowance, - degrees=degrees, - speed=speed, - ) - logger.info(f"Queued spin command: {cmd_id} - Degrees: {degrees}, Speed: {speed}deg/s") - return True - - except Exception as e: - logger.error(f"Spin movement failed: {e}") - import traceback - - logger.error(traceback.format_exc()) - return False - - def stop(self) -> bool: - """Stop all robot movement""" - try: - # self.navigator.cancelTask() - self._current_velocity = {"x": 0.0, "y": 0.0, "z": 0.0} - self._is_moving = False - return True - except Exception as e: - logger.error(f"Failed to stop movement: {e}") - return False - - def cleanup(self) -> None: - """Cleanup the executor, ROS node, and stop robot.""" - self.stop() - - # Stop the WebRTC queue manager - if self._command_queue: - logger.info("Stopping WebRTC queue manager...") - self._command_queue.stop() - - # Shut down the executor to stop spin loop cleanly - self._executor.shutdown() - - # Destroy node and shutdown rclpy - self._node.destroy_node() - rclpy.shutdown() - - def disconnect(self) -> None: - """Disconnect from the robot and clean up resources.""" - self.cleanup() - - def webrtc_req( - self, - api_id: int, - topic: str | None = None, - parameter: str = "", - priority: int = 0, - request_id: str | None = None, - data=None, - ) -> bool: - """ - Send a WebRTC request command to the robot - - Args: - api_id: The API ID for the command - topic: The API topic to publish to (defaults to self._webrtc_api_topic) - parameter: Optional parameter string - priority: Priority level (0 or 1) - request_id: Optional request ID for tracking (not used in ROS implementation) - data: Optional data dictionary (not used in ROS implementation) - params: Optional params dictionary (not used in ROS implementation) - - Returns: - bool: True if command was sent successfully - """ - try: - # Create and send command - cmd = self._webrtc_msg_type() - cmd.api_id = api_id - cmd.topic = topic if topic is not None else self._webrtc_api_topic - cmd.parameter = parameter - cmd.priority = priority - - self._webrtc_pub.publish(cmd) - logger.info(f"Sent WebRTC request: api_id={api_id}, topic={cmd.topic}") - return True - - except Exception as e: - logger.error(f"Failed to send WebRTC request: {e}") - return False - - def get_robot_mode(self) -> RobotMode: - """ - Get the current robot mode - - Returns: - RobotMode: The current robot mode enum value - """ - return self._mode - - def print_robot_mode(self) -> None: - """Print the current robot mode to the console""" - mode = self.get_robot_mode() - print(f"Current RobotMode: {mode.name}") - print(f"Mode enum: {mode}") - - def queue_webrtc_req( - self, - api_id: int, - topic: str | None = None, - parameter: str = "", - priority: int = 0, - timeout: float = 90.0, - request_id: str | None = None, - data=None, - ) -> str: - """ - Queue a WebRTC request to be sent when the robot is IDLE - - Args: - api_id: The API ID for the command - topic: The topic to publish to (defaults to self._webrtc_api_topic) - parameter: Optional parameter string - priority: Priority level (0 or 1) - timeout: Maximum time to wait for the request to complete - request_id: Optional request ID (if None, one will be generated) - data: Optional data dictionary (not used in ROS implementation) - - Returns: - str: Request ID that can be used to track the request - """ - return self._command_queue.queue_webrtc_request( - api_id=api_id, - topic=topic if topic is not None else self._webrtc_api_topic, - parameter=parameter, - priority=priority, - timeout=timeout, - request_id=request_id, - data=data, - ) - - def move_vel_control(self, x: float, y: float, yaw: float) -> bool: - """ - Send a single velocity command without duration handling. - - Args: - x: Forward/backward velocity (m/s) - y: Left/right velocity (m/s) - yaw: Rotational velocity (rad/s) - - Returns: - bool: True if command was sent successfully - """ - # Clamp velocities to safe limits - x = self._clamp_velocity(x, self.MAX_LINEAR_VELOCITY) - y = self._clamp_velocity(y, self.MAX_LINEAR_VELOCITY) - yaw = self._clamp_velocity(yaw, self.MAX_ANGULAR_VELOCITY) - - # Create and send command - cmd = Twist() - cmd.linear.x = float(x) - cmd.linear.y = float(y) - cmd.angular.z = float(yaw) - - try: - self._move_vel_pub.publish(cmd) - return True - except Exception as e: - logger.error(f"Failed to send velocity command: {e}") - return False - - def pose_command(self, roll: float, pitch: float, yaw: float) -> bool: - """ - Send a pose command to the robot to adjust its body orientation - - Args: - roll: Roll angle in radians - pitch: Pitch angle in radians - yaw: Yaw angle in radians - - Returns: - bool: True if command was sent successfully - """ - # Create the pose command message - cmd = Vector3() - cmd.x = float(roll) # Roll - cmd.y = float(pitch) # Pitch - cmd.z = float(yaw) # Yaw - - try: - self._pose_pub.publish(cmd) - logger.debug(f"Sent pose command: roll={roll}, pitch={pitch}, yaw={yaw}") - return True - except Exception as e: - logger.error(f"Failed to send pose command: {e}") - return False - - def get_position_stream(self): - """ - Get a stream of position updates from ROS. - - Returns: - Observable that emits (x, y) tuples representing the robot's position - """ - from dimos.robot.position_stream import PositionStreamProvider - - # Create a position stream provider - position_provider = PositionStreamProvider( - ros_node=self._node, - odometry_topic="/odom", # Default odometry topic - use_odometry=True, - ) - - return position_provider.get_position_stream() - - def _goal_response_callback(self, future) -> None: - """Handle the goal response.""" - goal_handle = future.result() - if not goal_handle.accepted: - logger.warn("Goal was rejected!") - print("[ROSControl] Goal was REJECTED by the action server") - self._action_success = False - return - - logger.info("Goal accepted") - print("[ROSControl] Goal was ACCEPTED by the action server") - result_future = goal_handle.get_result_async() - result_future.add_done_callback(self._goal_result_callback) - - def _goal_result_callback(self, future) -> None: - """Handle the goal result.""" - try: - result = future.result().result - logger.info("Goal completed") - print(f"[ROSControl] Goal COMPLETED with result: {result}") - self._action_success = True - except Exception as e: - logger.error(f"Goal failed with error: {e}") - print(f"[ROSControl] Goal FAILED with error: {e}") - self._action_success = False diff --git a/dimos/robot/ros_observable_topic.py b/dimos/robot/ros_observable_topic.py deleted file mode 100644 index 7cfc70fd8b..0000000000 --- a/dimos/robot/ros_observable_topic.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import asyncio -from collections.abc import Callable -import enum -import functools -from typing import Any, Union - -from nav_msgs import msg -from rclpy.qos import ( - QoSDurabilityPolicy, - QoSHistoryPolicy, - QoSProfile, - QoSReliabilityPolicy, -) -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import Disposable -from reactivex.scheduler import ThreadPoolScheduler -from rxpy_backpressure import BackPressure - -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.types.vector import Vector -from dimos.utils.logging_config import setup_logger -from dimos.utils.threadpool import get_scheduler - -__all__ = ["QOS", "ROSObservableTopicAbility"] - -TopicType = Union[OccupancyGrid, msg.OccupancyGrid, msg.Odometry] - - -class QOS(enum.Enum): - SENSOR = "sensor" - COMMAND = "command" - - def to_profile(self) -> QoSProfile: - if self == QOS.SENSOR: - return QoSProfile( - reliability=QoSReliabilityPolicy.BEST_EFFORT, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=1, - ) - if self == QOS.COMMAND: - return QoSProfile( - reliability=QoSReliabilityPolicy.RELIABLE, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=10, # Higher depth for commands to ensure delivery - ) - - raise ValueError(f"Unknown QoS enum value: {self}") - - -logger = setup_logger("dimos.robot.ros_control.observable_topic") - - -class ROSObservableTopicAbility: - # Ensures that we can return multiple observables which have multiple subscribers - # consuming the same topic at different (blocking) rates while: - # - # - immediately returning latest value received to new subscribers - # - allowing slow subscribers to consume the topic without blocking fast ones - # - dealing with backpressure from slow subscribers (auto dropping unprocessed messages) - # - # (for more details see corresponding test file) - # - # ROS thread ─► ReplaySubject─► observe_on(pool) ─► backpressure.latest ─► sub1 (fast) - # ā”œā”€ā”€ā–ŗ observe_on(pool) ─► backpressure.latest ─► sub2 (slow) - # └──► observe_on(pool) ─► backpressure.latest ─► sub3 (slower) - # - def _maybe_conversion(self, msg_type: TopicType, callback) -> Callable[[TopicType], Any]: - if msg_type == "Costmap": - return lambda msg: callback(OccupancyGrid.from_msg(msg)) - # just for test, not sure if this Vector auto-instantiation is used irl - if msg_type == Vector: - return lambda msg: callback(Vector.from_msg(msg)) - return callback - - def _sub_msg_type(self, msg_type): - if msg_type == "Costmap": - return msg.OccupancyGrid - - if msg_type == Vector: - return msg.Odometry - - return msg_type - - @functools.cache - def topic( - self, - topic_name: str, - msg_type: TopicType, - qos=QOS.SENSOR, - scheduler: ThreadPoolScheduler | None = None, - drop_unprocessed: bool = True, - ) -> rx.Observable: - if scheduler is None: - scheduler = get_scheduler() - - # Convert QOS to QoSProfile - qos_profile = qos.to_profile() - - # upstream ROS callback - def _on_subscribe(obs, _): - ros_sub = self._node.create_subscription( - self._sub_msg_type(msg_type), - topic_name, - self._maybe_conversion(msg_type, obs.on_next), - qos_profile, - ) - return Disposable(lambda: self._node.destroy_subscription(ros_sub)) - - upstream = rx.create(_on_subscribe) - - # hot, latest-cached core - core = upstream.pipe( - ops.replay(buffer_size=1), - ops.ref_count(), # still synchronous! - ) - - # per-subscriber factory - def per_sub(): - # hop off the ROS thread into the pool - base = core.pipe(ops.observe_on(scheduler)) - - # optional back-pressure handling - if not drop_unprocessed: - return base - - def _subscribe(observer, sch=None): - return base.subscribe(BackPressure.LATEST(observer), scheduler=sch) - - return rx.create(_subscribe) - - # each `.subscribe()` call gets its own async backpressure chain - return rx.defer(lambda *_: per_sub()) - - # If you are not interested in processing streams, just want to fetch the latest stream - # value use this function. It runs a subscription in the background. - # caches latest value for you, always ready to return. - # - # odom = robot.topic_latest("/odom", msg.Odometry) - # the initial call to odom() will block until the first message is received - # - # any time you'd like you can call: - # - # print(f"Latest odom: {odom()}") - # odom.dispose() # clean up the subscription - # - # see test_ros_observable_topic.py test_topic_latest for more details - def topic_latest( - self, topic_name: str, msg_type: TopicType, timeout: float | None = 100.0, qos=QOS.SENSOR - ): - """ - Blocks the current thread until the first message is received, then - returns `reader()` (sync) and keeps one ROS subscription alive - in the background. - - latest_scan = robot.ros_control.topic_latest_blocking("scan", LaserScan) - do_something(latest_scan()) # instant - latest_scan.dispose() # clean up - """ - # one shared observable with a 1-element replay buffer - core = self.topic(topic_name, msg_type, qos=qos).pipe(ops.replay(buffer_size=1)) - conn = core.connect() # starts the ROS subscription immediately - - try: - first_val = core.pipe( - ops.first(), *([ops.timeout(timeout)] if timeout is not None else []) - ).run() - except Exception: - conn.dispose() - msg = f"{topic_name} message not received after {timeout} seconds. Is robot connected?" - logger.error(msg) - raise Exception(msg) - - cache = {"val": first_val} - sub = core.subscribe(lambda v: cache.__setitem__("val", v)) - - def reader(): - return cache["val"] - - reader.dispose = lambda: (sub.dispose(), conn.dispose()) - return reader - - # If you are not interested in processing streams, just want to fetch the latest stream - # value use this function. It runs a subscription in the background. - # caches latest value for you, always ready to return - # - # odom = await robot.topic_latest_async("/odom", msg.Odometry) - # - # async nature of this function allows you to do other stuff while you wait - # for a first message to arrive - # - # any time you'd like you can call: - # - # print(f"Latest odom: {odom()}") - # odom.dispose() # clean up the subscription - # - # see test_ros_observable_topic.py test_topic_latest for more details - async def topic_latest_async( - self, topic_name: str, msg_type: TopicType, qos=QOS.SENSOR, timeout: float = 30.0 - ): - loop = asyncio.get_running_loop() - first = loop.create_future() - cache = {"val": None} - - core = self.topic(topic_name, msg_type, qos=qos) # single ROS callback - - def _on_next(v) -> None: - cache["val"] = v - if not first.done(): - loop.call_soon_threadsafe(first.set_result, v) - - subscription = core.subscribe(_on_next) - - try: - await asyncio.wait_for(first, timeout) - except Exception: - subscription.dispose() - raise - - def reader(): - return cache["val"] - - reader.dispose = subscription.dispose - return reader diff --git a/dimos/robot/ros_transform.py b/dimos/robot/ros_transform.py deleted file mode 100644 index d54eb8cd15..0000000000 --- a/dimos/robot/ros_transform.py +++ /dev/null @@ -1,244 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from geometry_msgs.msg import TransformStamped -import rclpy -from scipy.spatial.transform import Rotation as R -from tf2_geometry_msgs import PointStamped -import tf2_ros -from tf2_ros import Buffer - -from dimos.types.path import Path -from dimos.types.vector import Vector -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.robot.ros_transform") - -__all__ = ["ROSTransformAbility"] - - -def to_euler_rot(msg: TransformStamped) -> [Vector, Vector]: - q = msg.transform.rotation - rotation = R.from_quat([q.x, q.y, q.z, q.w]) - return Vector(rotation.as_euler("xyz", degrees=False)) - - -def to_euler_pos(msg: TransformStamped) -> [Vector, Vector]: - return Vector(msg.transform.translation).to_2d() - - -def to_euler(msg: TransformStamped) -> [Vector, Vector]: - return [to_euler_pos(msg), to_euler_rot(msg)] - - -class ROSTransformAbility: - """Mixin class for handling ROS transforms between coordinate frames""" - - @property - def tf_buffer(self) -> Buffer: - if not hasattr(self, "_tf_buffer"): - self._tf_buffer = tf2_ros.Buffer() - self._tf_listener = tf2_ros.TransformListener(self._tf_buffer, self._node) - logger.info("Transform listener initialized") - - return self._tf_buffer - - def transform_euler_pos( - self, source_frame: str, target_frame: str = "map", timeout: float = 1.0 - ): - return to_euler_pos(self.transform(source_frame, target_frame, timeout)) - - def transform_euler_rot( - self, source_frame: str, target_frame: str = "map", timeout: float = 1.0 - ): - return to_euler_rot(self.transform(source_frame, target_frame, timeout)) - - def transform_euler(self, source_frame: str, target_frame: str = "map", timeout: float = 1.0): - res = self.transform(source_frame, target_frame, timeout) - return to_euler(res) - - def transform( - self, source_frame: str, target_frame: str = "map", timeout: float = 1.0 - ) -> TransformStamped | None: - try: - transform = self.tf_buffer.lookup_transform( - target_frame, - source_frame, - rclpy.time.Time(), - rclpy.duration.Duration(seconds=timeout), - ) - return transform - except ( - tf2_ros.LookupException, - tf2_ros.ConnectivityException, - tf2_ros.ExtrapolationException, - ) as e: - logger.error(f"Transform lookup failed: {e}") - return None - - def transform_point( - self, point: Vector, source_frame: str, target_frame: str = "map", timeout: float = 1.0 - ): - """Transform a point from source_frame to target_frame. - - Args: - point: The point to transform (x, y, z) - source_frame: The source frame of the point - target_frame: The target frame to transform to - timeout: Time to wait for the transform to become available (seconds) - - Returns: - The transformed point as a Vector, or None if the transform failed - """ - try: - # Wait for transform to become available - self.tf_buffer.can_transform( - target_frame, - source_frame, - rclpy.time.Time(), - rclpy.duration.Duration(seconds=timeout), - ) - - # Create a PointStamped message - ps = PointStamped() - ps.header.frame_id = source_frame - ps.header.stamp = rclpy.time.Time().to_msg() # Latest available transform - ps.point.x = point[0] - ps.point.y = point[1] - ps.point.z = point[2] if len(point) > 2 else 0.0 - - # Transform point - transformed_ps = self.tf_buffer.transform( - ps, target_frame, rclpy.duration.Duration(seconds=timeout) - ) - - # Return as Vector type - if len(point) > 2: - return Vector( - transformed_ps.point.x, transformed_ps.point.y, transformed_ps.point.z - ) - else: - return Vector(transformed_ps.point.x, transformed_ps.point.y) - except ( - tf2_ros.LookupException, - tf2_ros.ConnectivityException, - tf2_ros.ExtrapolationException, - ) as e: - logger.error(f"Transform from {source_frame} to {target_frame} failed: {e}") - return None - - def transform_path( - self, path: Path, source_frame: str, target_frame: str = "map", timeout: float = 1.0 - ): - """Transform a path from source_frame to target_frame. - - Args: - path: The path to transform - source_frame: The source frame of the path - target_frame: The target frame to transform to - timeout: Time to wait for the transform to become available (seconds) - - Returns: - The transformed path as a Path, or None if the transform failed - """ - transformed_path = Path() - for point in path: - transformed_point = self.transform_point(point, source_frame, target_frame, timeout) - if transformed_point is not None: - transformed_path.append(transformed_point) - return transformed_path - - def transform_rot( - self, rotation: Vector, source_frame: str, target_frame: str = "map", timeout: float = 1.0 - ): - """Transform a rotation from source_frame to target_frame. - - Args: - rotation: The rotation to transform as Euler angles (x, y, z) in radians - source_frame: The source frame of the rotation - target_frame: The target frame to transform to - timeout: Time to wait for the transform to become available (seconds) - - Returns: - The transformed rotation as a Vector of Euler angles (x, y, z), or None if the transform failed - """ - try: - # Wait for transform to become available - self.tf_buffer.can_transform( - target_frame, - source_frame, - rclpy.time.Time(), - rclpy.duration.Duration(seconds=timeout), - ) - - # Create a rotation matrix from the input Euler angles - input_rotation = R.from_euler("xyz", rotation, degrees=False) - - # Get the transform from source to target frame - transform = self.transform(source_frame, target_frame, timeout) - if transform is None: - return None - - # Extract the rotation from the transform - q = transform.transform.rotation - transform_rotation = R.from_quat([q.x, q.y, q.z, q.w]) - - # Compose the rotations - # The resulting rotation is the composition of the transform rotation and input rotation - result_rotation = transform_rotation * input_rotation - - # Convert back to Euler angles - euler_angles = result_rotation.as_euler("xyz", degrees=False) - - # Return as Vector type - return Vector(euler_angles) - - except ( - tf2_ros.LookupException, - tf2_ros.ConnectivityException, - tf2_ros.ExtrapolationException, - ) as e: - logger.error(f"Transform rotation from {source_frame} to {target_frame} failed: {e}") - return None - - def transform_pose( - self, - position: Vector, - rotation: Vector, - source_frame: str, - target_frame: str = "map", - timeout: float = 1.0, - ): - """Transform a pose from source_frame to target_frame. - - Args: - position: The position to transform - rotation: The rotation to transform - source_frame: The source frame of the pose - target_frame: The target frame to transform to - timeout: Time to wait for the transform to become available (seconds) - - Returns: - Tuple of (transformed_position, transformed_rotation) as Vectors, - or (None, None) if either transform failed - """ - # Transform position - transformed_position = self.transform_point(position, source_frame, target_frame, timeout) - - # Transform rotation - transformed_rotation = self.transform_rot(rotation, source_frame, target_frame, timeout) - - # Return results (both might be None if transforms failed) - return transformed_position, transformed_rotation diff --git a/dimos/robot/test_ros_bridge.py b/dimos/robot/test_ros_bridge.py index 435766b938..cf7b2ac0cf 100644 --- a/dimos/robot/test_ros_bridge.py +++ b/dimos/robot/test_ros_bridge.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -120,6 +120,8 @@ def dimos_callback(msg, _topic) -> None: self.assertAlmostEqual(msg.linear.y, float(i * 2), places=5) self.assertAlmostEqual(msg.angular.z, float(i * 0.1), places=5) + lcm.stop() + def test_dimos_to_ros_twist(self) -> None: """Test DIMOS TwistStamped to ROS conversion and transmission.""" # Set up bridge @@ -227,6 +229,8 @@ def dimos_callback(_msg, _topic) -> None: msg=f"Frequency not preserved for {target_freq}Hz: sent={send_freq:.1f}Hz, received={receive_freq:.1f}Hz", ) + lcm.stop() + def test_pointcloud_conversion(self) -> None: """Test PointCloud2 message conversion with numpy optimization.""" # Set up bridge @@ -280,10 +284,12 @@ def dimos_callback(msg, _topic) -> None: self.assertEqual(len(received_cloud), 1, "Should receive point cloud") # Verify point data - received_points = received_cloud[0].as_numpy() + received_points, _ = received_cloud[0].as_numpy() self.assertEqual(received_points.shape, points.shape) np.testing.assert_array_almost_equal(received_points, points, decimal=5) + lcm.stop() + def test_tf_high_frequency(self) -> None: """Test TF message handling at high frequency.""" # Set up bridge @@ -349,6 +355,8 @@ def dimos_callback(msg, _topic) -> None: msg=f"High frequency TF not preserved: expected={target_freq}Hz, got={receive_freq:.1f}Hz", ) + lcm.stop() + def test_bidirectional_bridge(self) -> None: """Test simultaneous bidirectional message flow.""" # Set up bidirectional bridges for same topic type diff --git a/dimos/robot/test_ros_observable_topic.py b/dimos/robot/test_ros_observable_topic.py deleted file mode 100644 index 0ffed24d35..0000000000 --- a/dimos/robot/test_ros_observable_topic.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import threading -import time - -import pytest - -from dimos.types.vector import Vector -from dimos.utils.logging_config import setup_logger - - -class MockROSNode: - def __init__(self) -> None: - self.logger = setup_logger("ROS") - - self.sub_id_cnt = 0 - self.subs = {} - - def _get_sub_id(self): - sub_id = self.sub_id_cnt - self.sub_id_cnt += 1 - return sub_id - - def create_subscription(self, msg_type, topic_name: str, callback, qos): - # Mock implementation of ROS subscription - - sub_id = self._get_sub_id() - stop_event = threading.Event() - self.subs[sub_id] = stop_event - self.logger.info(f"Subscribed {topic_name} subid {sub_id}") - - # Create message simulation thread - def simulate_messages() -> None: - message_count = 0 - while not stop_event.is_set(): - message_count += 1 - time.sleep(0.1) # 20Hz default publication rate - if topic_name == "/vector": - callback([message_count, message_count]) - else: - callback(message_count) - # cleanup - self.subs.pop(sub_id) - - thread = threading.Thread(target=simulate_messages, daemon=True) - thread.start() - return sub_id - - def destroy_subscription(self, subscription) -> None: - if subscription in self.subs: - self.subs[subscription].set() - self.logger.info(f"Destroyed subscription: {subscription}") - else: - self.logger.info(f"Unknown subscription: {subscription}") - - -# we are doing this in order to avoid importing ROS dependencies if ros tests aren't runnin -@pytest.fixture -def robot(): - from dimos.robot.ros_observable_topic import ROSObservableTopicAbility - - class MockRobot(ROSObservableTopicAbility): - def __init__(self) -> None: - self.logger = setup_logger("ROBOT") - # Initialize the mock ROS node - self._node = MockROSNode() - - return MockRobot() - - -# This test verifies a bunch of basics: -# -# 1. that the system creates a single ROS sub for multiple reactivex subs -# 2. that the system creates a single ROS sub for multiple observers -# 3. that the system unsubscribes from ROS when observers are disposed -# 4. that the system replays the last message to new observers, -# before the new ROS sub starts producing -@pytest.mark.ros -def test_parallel_and_cleanup(robot) -> None: - from nav_msgs import msg - - received_messages = [] - - obs1 = robot.topic("/odom", msg.Odometry) - - print(f"Created subscription: {obs1}") - - subscription1 = obs1.subscribe(lambda x: received_messages.append(x + 2)) - - subscription2 = obs1.subscribe(lambda x: received_messages.append(x + 3)) - - obs2 = robot.topic("/odom", msg.Odometry) - subscription3 = obs2.subscribe(lambda x: received_messages.append(x + 5)) - - time.sleep(0.25) - - # We have 2 messages and 3 subscribers - assert len(received_messages) == 6, "Should have received exactly 6 messages" - - # [1, 1, 1, 2, 2, 2] + - # [2, 3, 5, 2, 3, 5] - # = - for i in [3, 4, 6, 4, 5, 7]: - assert i in received_messages, f"Expected {i} in received messages, got {received_messages}" - - # ensure that ROS end has only a single subscription - assert len(robot._node.subs) == 1, ( - f"Expected 1 subscription, got {len(robot._node.subs)}: {robot._node.subs}" - ) - - subscription1.dispose() - subscription2.dispose() - subscription3.dispose() - - # Make sure that ros end was unsubscribed, thread terminated - time.sleep(0.1) - assert not robot._node.subs, f"Expected empty subs dict, got: {robot._node.subs}" - - # Ensure we replay the last message - second_received = [] - second_sub = obs1.subscribe(lambda x: second_received.append(x)) - - time.sleep(0.075) - # we immediately receive the stored topic message - assert len(second_received) == 1 - - # now that sub is hot, we wait for a second one - time.sleep(0.2) - - # we expect 2, 1 since first message was preserved from a previous ros topic sub - # second one is the first message of the second ros topic sub - assert second_received == [2, 1, 2] - - print(f"Second subscription immediately received {len(second_received)} message(s)") - - second_sub.dispose() - - time.sleep(0.1) - assert not robot._node.subs, f"Expected empty subs dict, got: {robot._node.subs}" - - print("Test completed successfully") - - -# here we test parallel subs and slow observers hogging our topic -# we expect slow observers to skip messages by default -# -# ROS thread ─► ReplaySubject─► observe_on(pool) ─► backpressure.latest ─► sub1 (fast) -# ā”œā”€ā”€ā–ŗ observe_on(pool) ─► backpressure.latest ─► sub2 (slow) -# └──► observe_on(pool) ─► backpressure.latest ─► sub3 (slower) -@pytest.mark.ros -def test_parallel_and_hog(robot) -> None: - from nav_msgs import msg - - obs1 = robot.topic("/odom", msg.Odometry) - obs2 = robot.topic("/odom", msg.Odometry) - - subscriber1_messages = [] - subscriber2_messages = [] - subscriber3_messages = [] - - subscription1 = obs1.subscribe(lambda x: subscriber1_messages.append(x)) - subscription2 = obs1.subscribe(lambda x: time.sleep(0.15) or subscriber2_messages.append(x)) - subscription3 = obs2.subscribe(lambda x: time.sleep(0.25) or subscriber3_messages.append(x)) - - assert len(robot._node.subs) == 1 - - time.sleep(2) - - subscription1.dispose() - subscription2.dispose() - subscription3.dispose() - - print("Subscriber 1 messages:", len(subscriber1_messages), subscriber1_messages) - print("Subscriber 2 messages:", len(subscriber2_messages), subscriber2_messages) - print("Subscriber 3 messages:", len(subscriber3_messages), subscriber3_messages) - - assert len(subscriber1_messages) == 19 - assert len(subscriber2_messages) == 12 - assert len(subscriber3_messages) == 7 - - assert subscriber2_messages[1] != [2] - assert subscriber3_messages[1] != [2] - - time.sleep(0.1) - - assert robot._node.subs == {} - - -@pytest.mark.asyncio -@pytest.mark.ros -async def test_topic_latest_async(robot) -> None: - from nav_msgs import msg - - odom = await robot.topic_latest_async("/odom", msg.Odometry) - assert odom() == 1 - await asyncio.sleep(0.45) - assert odom() == 5 - odom.dispose() - await asyncio.sleep(0.1) - assert robot._node.subs == {} - - -@pytest.mark.ros -def test_topic_auto_conversion(robot) -> None: - odom = robot.topic("/vector", Vector).subscribe(lambda x: print(x)) - time.sleep(0.5) - odom.dispose() - - -@pytest.mark.ros -def test_topic_latest_sync(robot) -> None: - from nav_msgs import msg - - odom = robot.topic_latest("/odom", msg.Odometry) - assert odom() == 1 - time.sleep(0.45) - assert odom() == 5 - odom.dispose() - time.sleep(0.1) - assert robot._node.subs == {} - - -@pytest.mark.ros -def test_topic_latest_sync_benchmark(robot) -> None: - from nav_msgs import msg - - odom = robot.topic_latest("/odom", msg.Odometry) - - start_time = time.time() - for _i in range(100): - odom() - end_time = time.time() - elapsed = end_time - start_time - avg_time = elapsed / 100 - - print("avg time", avg_time) - - assert odom() == 1 - time.sleep(0.45) - assert odom() >= 5 - odom.dispose() - time.sleep(0.1) - assert robot._node.subs == {} diff --git a/dimos/robot/unitree/README.md b/dimos/robot/unitree/README.md deleted file mode 100644 index 5ee389cb31..0000000000 --- a/dimos/robot/unitree/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## Unitree Go2 ROS Control Setup - -Install unitree ros2 workspace as per instructions in https://github.com/dimensionalOS/go2_ros2_sdk/blob/master/README.md - -Run the following command to source the workspace and add dimos to the python path: - -``` -source /home/ros/unitree_ros2_ws/install/setup.bash - -export PYTHONPATH=/home/stash/dimensional/dimos:$PYTHONPATH -``` - -Run the following command to start the ROS control node: - -``` -ros2 launch go2_robot_sdk robot.launch.py -``` - -Run the following command to start the agent: - -``` -python3 dimos/robot/unitree/run_go2_ros.py -``` - - diff --git a/dimos/robot/unitree/__init__.py b/dimos/robot/unitree/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/robot/unitree/connection/__init__.py b/dimos/robot/unitree/connection/__init__.py new file mode 100644 index 0000000000..5c1dff1922 --- /dev/null +++ b/dimos/robot/unitree/connection/__init__.py @@ -0,0 +1,4 @@ +import dimos.robot.unitree.connection.g1 as g1 +import dimos.robot.unitree.connection.go2 as go2 + +__all__ = ["g1", "go2"] diff --git a/dimos/robot/unitree_webrtc/connection.py b/dimos/robot/unitree/connection/connection.py similarity index 81% rename from dimos/robot/unitree_webrtc/connection.py rename to dimos/robot/unitree/connection/connection.py index 4aee995c02..bef0c0b127 100644 --- a/dimos/robot/unitree_webrtc/connection.py +++ b/dimos/robot/unitree/connection/connection.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,37 +17,42 @@ import functools import threading import time -from typing import Literal, TypeAlias +from typing import TypeAlias -from aiortc import MediaStreamTrack -from go2_webrtc_driver.constants import RTC_TOPIC, SPORT_CMD, VUI_COLOR -from go2_webrtc_driver.webrtc_driver import ( # type: ignore[import-not-found] - Go2WebRTCConnection, - WebRTCConnectionMethod, -) import numpy as np +from numpy.typing import NDArray from reactivex import operators as ops from reactivex.observable import Observable from reactivex.subject import Subject +from unitree_webrtc_connect.constants import ( + RTC_TOPIC, + SPORT_CMD, + VUI_COLOR, +) +from unitree_webrtc_connect.webrtc_driver import ( # type: ignore[import-untyped] + UnitreeWebRTCConnection as LegionConnection, + WebRTCConnectionMethod, +) from dimos.core import rpc from dimos.core.resource import Resource from dimos.msgs.geometry_msgs import Pose, Transform, Twist from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ImageFormat from dimos.robot.unitree_webrtc.type.lidar import LidarMessage from dimos.robot.unitree_webrtc.type.lowstate import LowStateMsg from dimos.robot.unitree_webrtc.type.odometry import Odometry from dimos.utils.decorators.decorators import simple_mcache from dimos.utils.reactive import backpressure, callback_to_observable -VideoMessage: TypeAlias = np.ndarray[tuple[int, int, Literal[3]], np.uint8] +VideoMessage: TypeAlias = NDArray[np.uint8] # Shape: (height, width, 3) @dataclass class SerializableVideoFrame: """Pickleable wrapper for av.VideoFrame with all metadata""" - data: np.ndarray + data: np.ndarray # type: ignore[type-arg] pts: int | None = None time: float | None = None dts: int | None = None @@ -56,7 +61,7 @@ class SerializableVideoFrame: format: str | None = None @classmethod - def from_av_frame(cls, frame): + def from_av_frame(cls, frame): # type: ignore[no-untyped-def] return cls( data=frame.to_ndarray(format="rgb24"), pts=frame.pts, @@ -67,7 +72,7 @@ def from_av_frame(cls, frame): format=frame.format.name if hasattr(frame, "format") and frame.format else None, ) - def to_ndarray(self, format=None): + def to_ndarray(self, format=None): # type: ignore[no-untyped-def] return self.data @@ -75,9 +80,9 @@ class UnitreeWebRTCConnection(Resource): def __init__(self, ip: str, mode: str = "ai") -> None: self.ip = ip self.mode = mode - self.stop_timer = None + self.stop_timer: threading.Timer | None = None self.cmd_vel_timeout = 0.2 - self.conn = Go2WebRTCConnection(WebRTCConnectionMethod.LocalSTA, ip=self.ip) + self.conn = LegionConnection(WebRTCConnectionMethod.LocalSTA, ip=self.ip) self.connect() def connect(self) -> None: @@ -126,6 +131,11 @@ def stop(self) -> None: async def async_disconnect() -> None: try: + # Send stop command directly since we're already in the event loop. + self.conn.datachannel.pub_sub.publish_without_callback( + RTC_TOPIC["WIRELESS_CONTROLLER"], + data={"lx": 0, "ly": 0, "rx": 0, "ry": 0}, + ) await self.conn.disconnect() except Exception: pass @@ -195,8 +205,8 @@ async def async_move_duration() -> None: return False # Generic conversion of unitree subscription to Subject (used for all subs) - def unitree_sub_stream(self, topic_name: str): - def subscribe_in_thread(cb) -> None: + def unitree_sub_stream(self, topic_name: str): # type: ignore[no-untyped-def] + def subscribe_in_thread(cb) -> None: # type: ignore[no-untyped-def] # Run the subscription in the background thread that has the event loop def run_subscription() -> None: self.conn.datachannel.pub_sub.subscribe(topic_name, cb) @@ -204,7 +214,7 @@ def run_subscription() -> None: # Use call_soon_threadsafe to run in the background thread self.loop.call_soon_threadsafe(run_subscription) - def unsubscribe_in_thread(cb) -> None: + def unsubscribe_in_thread(cb) -> None: # type: ignore[no-untyped-def] # Run the unsubscription in the background thread that has the event loop def run_unsubscription() -> None: self.conn.datachannel.pub_sub.unsubscribe(topic_name) @@ -218,35 +228,35 @@ def run_unsubscription() -> None: ) # Generic sync API call (we jump into the client thread) - def publish_request(self, topic: str, data: dict): + def publish_request(self, topic: str, data: dict): # type: ignore[no-untyped-def, type-arg] future = asyncio.run_coroutine_threadsafe( self.conn.datachannel.pub_sub.publish_request_new(topic, data), self.loop ) return future.result() @simple_mcache - def raw_lidar_stream(self) -> Subject[LidarMessage]: + def raw_lidar_stream(self) -> Observable[LidarMessage]: return backpressure(self.unitree_sub_stream(RTC_TOPIC["ULIDAR_ARRAY"])) @simple_mcache - def raw_odom_stream(self) -> Subject[Pose]: + def raw_odom_stream(self) -> Observable[Pose]: return backpressure(self.unitree_sub_stream(RTC_TOPIC["ROBOTODOM"])) @simple_mcache - def lidar_stream(self) -> Subject[LidarMessage]: + def lidar_stream(self) -> Observable[LidarMessage]: return backpressure( self.raw_lidar_stream().pipe( - ops.map(lambda raw_frame: LidarMessage.from_msg(raw_frame, ts=time.time())) + ops.map(lambda raw_frame: LidarMessage.from_msg(raw_frame, ts=time.time())) # type: ignore[arg-type] ) ) @simple_mcache - def tf_stream(self) -> Subject[Transform]: + def tf_stream(self) -> Observable[Transform]: base_link = functools.partial(Transform.from_pose, "base_link") return backpressure(self.odom_stream().pipe(ops.map(base_link))) @simple_mcache - def odom_stream(self) -> Subject[Pose]: + def odom_stream(self) -> Observable[Pose]: return backpressure(self.raw_odom_stream().pipe(ops.map(Odometry.from_msg))) @simple_mcache @@ -257,7 +267,8 @@ def video_stream(self) -> Observable[Image]: ops.map( lambda frame: Image.from_numpy( # np.ascontiguousarray(frame.to_ndarray("rgb24")), - frame.to_ndarray(format="rgb24"), + frame.to_ndarray(format="rgb24"), # type: ignore[attr-defined] + format=ImageFormat.RGB, # Frame is RGB24, not BGR frame_id="camera_optical", ) ), @@ -265,11 +276,11 @@ def video_stream(self) -> Observable[Image]: ) @simple_mcache - def lowstate_stream(self) -> Subject[LowStateMsg]: + def lowstate_stream(self) -> Observable[LowStateMsg]: return backpressure(self.unitree_sub_stream(RTC_TOPIC["LOW_STATE"])) - def standup_ai(self): - return self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["BalanceStand"]}) + def standup_ai(self) -> bool: + return self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["BalanceStand"]}) # type: ignore[no-any-return] def standup_normal(self) -> bool: self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["StandUp"]}) @@ -278,17 +289,17 @@ def standup_normal(self) -> bool: return True @rpc - def standup(self): + def standup(self) -> bool: if self.mode == "ai": return self.standup_ai() else: return self.standup_normal() @rpc - def liedown(self): - return self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["StandDown"]}) + def liedown(self) -> bool: + return self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["StandDown"]}) # type: ignore[no-any-return] - async def handstand(self): + async def handstand(self): # type: ignore[no-untyped-def] return self.publish_request( RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["Standup"], "parameter": {"data": True}}, @@ -296,7 +307,7 @@ async def handstand(self): @rpc def color(self, color: VUI_COLOR = VUI_COLOR.RED, colortime: int = 60) -> bool: - return self.publish_request( + return self.publish_request( # type: ignore[no-any-return] RTC_TOPIC["VUI"], { "api_id": 1001, @@ -312,12 +323,14 @@ def raw_video_stream(self) -> Observable[VideoMessage]: subject: Subject[VideoMessage] = Subject() stop_event = threading.Event() - async def accept_track(track: MediaStreamTrack) -> VideoMessage: + from aiortc import MediaStreamTrack # type: ignore[import-untyped] + + async def accept_track(track: MediaStreamTrack) -> None: while True: if stop_event.is_set(): return frame = await track.recv() - serializable_frame = SerializableVideoFrame.from_av_frame(frame) + serializable_frame = SerializableVideoFrame.from_av_frame(frame) # type: ignore[no-untyped-call] subject.on_next(serializable_frame) self.conn.video.add_track_callback(accept_track) @@ -340,7 +353,7 @@ def switch_video_channel_off() -> None: return subject.pipe(ops.finally_action(stop)) - def get_video_stream(self, fps: int = 30) -> Observable[VideoMessage]: + def get_video_stream(self, fps: int = 30) -> Observable[Image]: """Get the video stream from the robot's camera. Implements the AbstractRobot interface method. @@ -352,18 +365,9 @@ def get_video_stream(self, fps: int = 30) -> Observable[VideoMessage]: Returns: Observable: An observable stream of video frames or None if video is not available. """ - try: - print("Starting WebRTC video stream...") - stream = self.video_stream() - if stream is None: - print("Warning: Video stream is not available") - return stream - - except Exception as e: - print(f"Error getting video stream: {e}") - return None + return self.video_stream() # type: ignore[no-any-return] - def stop(self) -> bool: + def stop(self) -> bool: # type: ignore[no-redef] """Stop the robot's movement. Returns: @@ -373,8 +377,7 @@ def stop(self) -> bool: if self.stop_timer: self.stop_timer.cancel() self.stop_timer = None - - return self.move(Twist()) + return True def disconnect(self) -> None: """Disconnect from the robot and clean up resources.""" diff --git a/dimos/robot/unitree/connection/g1.py b/dimos/robot/unitree/connection/g1.py new file mode 100644 index 0000000000..1e15809146 --- /dev/null +++ b/dimos/robot/unitree/connection/g1.py @@ -0,0 +1,102 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Any + +from reactivex.disposable import Disposable + +from dimos import spec +from dimos.core import DimosCluster, In, Module, rpc +from dimos.core.global_config import GlobalConfig +from dimos.msgs.geometry_msgs import Twist +from dimos.robot.unitree.connection.connection import UnitreeWebRTCConnection +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class G1Connection(Module): + cmd_vel: In[Twist] + ip: str | None + connection_type: str | None = None + _global_config: GlobalConfig + + connection: UnitreeWebRTCConnection | None + + def __init__( + self, + ip: str | None = None, + connection_type: str | None = None, + global_config: GlobalConfig | None = None, + *args: Any, + **kwargs: Any, + ) -> None: + self._global_config = global_config or GlobalConfig() + self.ip = ip if ip is not None else self._global_config.robot_ip + self.connection_type = connection_type or self._global_config.unitree_connection_type + self.connection = None + super().__init__(*args, **kwargs) + + @rpc + def start(self) -> None: + super().start() + + match self.connection_type: + case "webrtc": + assert self.ip is not None, "IP address must be provided" + self.connection = UnitreeWebRTCConnection(self.ip) + case "replay": + raise ValueError("Replay connection not implemented for G1 robot") + case "mujoco": + raise ValueError( + "This module does not support simulation, use G1SimConnection instead" + ) + case _: + raise ValueError(f"Unknown connection type: {self.connection_type}") + + assert self.connection is not None + self.connection.start() + + self._disposables.add(Disposable(self.cmd_vel.subscribe(self.move))) + + @rpc + def stop(self) -> None: + assert self.connection is not None + self.connection.stop() + super().stop() + + @rpc + def move(self, twist: Twist, duration: float = 0.0) -> None: + assert self.connection is not None + self.connection.move(twist, duration) + + @rpc + def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: + logger.info(f"Publishing request to topic: {topic} with data: {data}") + assert self.connection is not None + return self.connection.publish_request(topic, data) # type: ignore[no-any-return] + + +g1_connection = G1Connection.blueprint + + +def deploy(dimos: DimosCluster, ip: str, local_planner: spec.LocalPlanner) -> G1Connection: + connection = dimos.deploy(G1Connection, ip) # type: ignore[attr-defined] + connection.cmd_vel.connect(local_planner.cmd_vel) + connection.start() + return connection # type: ignore[no-any-return] + + +__all__ = ["G1Connection", "deploy", "g1_connection"] diff --git a/dimos/robot/unitree/connection/g1sim.py b/dimos/robot/unitree/connection/g1sim.py new file mode 100644 index 0000000000..d72e7d17f6 --- /dev/null +++ b/dimos/robot/unitree/connection/g1sim.py @@ -0,0 +1,128 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import time +from typing import TYPE_CHECKING, Any + +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig +from dimos.msgs.geometry_msgs import ( + PoseStamped, + Quaternion, + Transform, + Twist, + Vector3, +) +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry as SimOdometry +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection + +logger = setup_logger() + + +class G1SimConnection(Module): + cmd_vel: In[Twist] + lidar: Out[LidarMessage] + odom: Out[PoseStamped] + ip: str | None + _global_config: GlobalConfig + + def __init__( + self, + ip: str | None = None, + global_config: GlobalConfig | None = None, + *args: Any, + **kwargs: Any, + ) -> None: + self._global_config = global_config or GlobalConfig() + self.ip = ip if ip is not None else self._global_config.robot_ip + self.connection: MujocoConnection | None = None + super().__init__(*args, **kwargs) + + @rpc + def start(self) -> None: + super().start() + + from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection + + self.connection = MujocoConnection(self._global_config) + assert self.connection is not None + self.connection.start() + + self._disposables.add(Disposable(self.cmd_vel.subscribe(self.move))) + self._disposables.add(self.connection.odom_stream().subscribe(self._publish_sim_odom)) + self._disposables.add(self.connection.lidar_stream().subscribe(self.lidar.publish)) + + @rpc + def stop(self) -> None: + assert self.connection is not None + self.connection.stop() + super().stop() + + def _publish_tf(self, msg: PoseStamped) -> None: + self.odom.publish(msg) + + self.tf.publish(Transform.from_pose("base_link", msg)) + + # Publish camera_link transform + camera_link = Transform( + translation=Vector3(0.3, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ts=time.time(), + ) + + map_to_world = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="map", + child_frame_id="world", + ts=time.time(), + ) + + self.tf.publish(camera_link, map_to_world) + + def _publish_sim_odom(self, msg: SimOdometry) -> None: + self._publish_tf( + PoseStamped( + ts=msg.ts, + frame_id=msg.frame_id, + position=msg.position, + orientation=msg.orientation, + ) + ) + + @rpc + def move(self, twist: Twist, duration: float = 0.0) -> None: + assert self.connection is not None + self.connection.move(twist, duration) + + @rpc + def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: + logger.info(f"Publishing request to topic: {topic} with data: {data}") + assert self.connection is not None + return self.connection.publish_request(topic, data) + + +g1_sim_connection = G1SimConnection.blueprint + + +__all__ = ["G1SimConnection", "g1_sim_connection"] diff --git a/dimos/robot/unitree/connection/go2.py b/dimos/robot/unitree/connection/go2.py new file mode 100644 index 0000000000..34f81e2bbf --- /dev/null +++ b/dimos/robot/unitree/connection/go2.py @@ -0,0 +1,393 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from pathlib import Path +from threading import Thread +import time +from typing import Any, Protocol + +from reactivex.disposable import Disposable +from reactivex.observable import Observable +import rerun as rr +import rerun.blueprint as rrb + +from dimos import spec +from dimos.core import DimosCluster, In, LCMTransport, Module, Out, pSHMTransport, rpc +from dimos.core.global_config import GlobalConfig +from dimos.dashboard.rerun_init import connect_rerun +from dimos.msgs.geometry_msgs import ( + PoseStamped, + Quaternion, + Transform, + Twist, + Vector3, +) +from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 +from dimos.robot.unitree.connection.connection import UnitreeWebRTCConnection +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.utils.data import get_data +from dimos.utils.decorators.decorators import simple_mcache +from dimos.utils.logging_config import setup_logger +from dimos.utils.testing import TimedSensorReplay, TimedSensorStorage + +logger = setup_logger(level=logging.INFO) + +# URDF path for Go2 robot +_GO2_URDF = Path(__file__).parent.parent / "go2" / "go2.urdf" + + +class Go2ConnectionProtocol(Protocol): + """Protocol defining the interface for Go2 robot connections.""" + + def start(self) -> None: ... + def stop(self) -> None: ... + def lidar_stream(self) -> Observable: ... # type: ignore[type-arg] + def odom_stream(self) -> Observable: ... # type: ignore[type-arg] + def video_stream(self) -> Observable: ... # type: ignore[type-arg] + def move(self, twist: Twist, duration: float = 0.0) -> bool: ... + def standup(self) -> bool: ... + def liedown(self) -> bool: ... + def publish_request(self, topic: str, data: dict) -> dict: ... # type: ignore[type-arg] + + +def _camera_info_static() -> CameraInfo: + fx, fy, cx, cy = (819.553492, 820.646595, 625.284099, 336.808987) + width, height = (1280, 720) + + return CameraInfo( + frame_id="camera_optical", + height=height, + width=width, + distortion_model="plumb_bob", + D=[0.0, 0.0, 0.0, 0.0, 0.0], + K=[fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0], + R=[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], + P=[fx, 0.0, cx, 0.0, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0], + binning_x=0, + binning_y=0, + ) + + +class ReplayConnection(UnitreeWebRTCConnection): + dir_name = "unitree_go2_bigoffice" + + # we don't want UnitreeWebRTCConnection to init + def __init__( # type: ignore[no-untyped-def] + self, + **kwargs, + ) -> None: + get_data(self.dir_name) + self.replay_config = { + "loop": kwargs.get("loop"), + "seek": kwargs.get("seek"), + "duration": kwargs.get("duration"), + } + + def connect(self) -> None: + pass + + def start(self) -> None: + pass + + def standup(self) -> bool: + return True + + def liedown(self) -> bool: + return True + + @simple_mcache + def lidar_stream(self): # type: ignore[no-untyped-def] + lidar_store = TimedSensorReplay(f"{self.dir_name}/lidar") # type: ignore[var-annotated] + return lidar_store.stream(**self.replay_config) # type: ignore[arg-type] + + @simple_mcache + def odom_stream(self): # type: ignore[no-untyped-def] + odom_store = TimedSensorReplay(f"{self.dir_name}/odom") # type: ignore[var-annotated] + return odom_store.stream(**self.replay_config) # type: ignore[arg-type] + + # we don't have raw video stream in the data set + @simple_mcache + def video_stream(self): # type: ignore[no-untyped-def] + video_store = TimedSensorReplay(f"{self.dir_name}/video") # type: ignore[var-annotated] + return video_store.stream(**self.replay_config) # type: ignore[arg-type] + + def move(self, twist: Twist, duration: float = 0.0) -> bool: + return True + + def publish_request(self, topic: str, data: dict): # type: ignore[no-untyped-def, type-arg] + """Fake publish request for testing.""" + return {"status": "ok", "message": "Fake publish"} + + +class GO2Connection(Module, spec.Camera, spec.Pointcloud): + cmd_vel: In[Twist] + pointcloud: Out[PointCloud2] + odom: Out[PoseStamped] + lidar: Out[LidarMessage] + color_image: Out[Image] + camera_info: Out[CameraInfo] + + connection: Go2ConnectionProtocol + camera_info_static: CameraInfo = _camera_info_static() + _global_config: GlobalConfig + _camera_info_thread: Thread | None = None + + @classmethod + def rerun_views(cls): # type: ignore[no-untyped-def] + """Return Rerun view blueprints for GO2 camera visualization.""" + return [ + rrb.Spatial2DView( + name="Camera", + origin="world/robot/camera/rgb", + ), + ] + + def __init__( # type: ignore[no-untyped-def] + self, + ip: str | None = None, + global_config: GlobalConfig | None = None, + *args, + **kwargs, + ) -> None: + self._global_config = global_config or GlobalConfig() + + ip = ip if ip is not None else self._global_config.robot_ip + + connection_type = self._global_config.unitree_connection_type + + if ip in ["fake", "mock", "replay"] or connection_type == "replay": + self.connection = ReplayConnection() + elif ip == "mujoco" or connection_type == "mujoco": + from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection + + self.connection = MujocoConnection(self._global_config) + else: + assert ip is not None, "IP address must be provided" + self.connection = UnitreeWebRTCConnection(ip) + + Module.__init__(self, *args, **kwargs) + + @rpc + def record(self, recording_name: str) -> None: + lidar_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/lidar") # type: ignore[type-arg] + lidar_store.save_stream(self.connection.lidar_stream()).subscribe(lambda x: x) # type: ignore[arg-type] + + odom_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/odom") # type: ignore[type-arg] + odom_store.save_stream(self.connection.odom_stream()).subscribe(lambda x: x) # type: ignore[arg-type] + + video_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/video") # type: ignore[type-arg] + video_store.save_stream(self.connection.video_stream()).subscribe(lambda x: x) # type: ignore[arg-type] + + @rpc + def start(self) -> None: + super().start() + + self.connection.start() + + # Initialize Rerun world frame and load URDF (only if Rerun backend) + if self._global_config.viewer_backend.startswith("rerun"): + self._init_rerun_world() + + def onimage(image: Image) -> None: + self.color_image.publish(image) + rr.log("world/robot/camera/rgb", image.to_rerun()) + + self._disposables.add(self.connection.lidar_stream().subscribe(self.lidar.publish)) + self._disposables.add(self.connection.odom_stream().subscribe(self._publish_tf)) + self._disposables.add(self.connection.video_stream().subscribe(onimage)) + self._disposables.add(Disposable(self.cmd_vel.subscribe(self.move))) + + self._camera_info_thread = Thread( + target=self.publish_camera_info, + daemon=True, + ) + self._camera_info_thread.start() + + self.standup() + # self.record("go2_bigoffice") + + def _init_rerun_world(self) -> None: + """Set up Rerun world frame, load URDF, and static assets. + + Does NOT compose blueprint - that's handled by ModuleBlueprintSet.build(). + """ + connect_rerun(global_config=self._global_config) + + # Set up world coordinate system AND register it as a named frame + # This is KEY - it connects entity paths to the named frame system + rr.log( + "world", + rr.ViewCoordinates.RIGHT_HAND_Z_UP, + rr.CoordinateFrame("world"), # type: ignore[attr-defined] + static=True, + ) + + # Bridge the named frame "world" to the implicit frame hierarchy "tf#/world" + # This connects TF named frames to entity path hierarchy + rr.log( + "world", + rr.Transform3D( + parent_frame="world", # type: ignore[call-arg] + child_frame="tf#/world", # type: ignore[call-arg] + ), + static=True, + ) + + # Load robot URDF + if _GO2_URDF.exists(): + rr.log_file_from_path( + str(_GO2_URDF), + entity_path_prefix="world/robot", + static=True, + ) + logger.info(f"Loaded URDF from {_GO2_URDF}") + + # Log static camera pinhole (for frustum) + rr.log("world/robot/camera", _camera_info_static().to_rerun(), static=True) + + @rpc + def stop(self) -> None: + self.liedown() + + if self.connection: + self.connection.stop() + + if self._camera_info_thread and self._camera_info_thread.is_alive(): + self._camera_info_thread.join(timeout=1.0) + + super().stop() + + @classmethod + def _odom_to_tf(cls, odom: PoseStamped) -> list[Transform]: + camera_link = Transform( + translation=Vector3(0.3, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ts=odom.ts, + ) + + camera_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), + frame_id="camera_link", + child_frame_id="camera_optical", + ts=odom.ts, + ) + + return [ + Transform.from_pose("base_link", odom), + camera_link, + camera_optical, + ] + + def _publish_tf(self, msg: PoseStamped) -> None: + transforms = self._odom_to_tf(msg) + self.tf.publish(*transforms) + if self.odom.transport: + self.odom.publish(msg) + + # Log to Rerun: robot pose (relative to parent entity "world") + rr.log( + "world/robot", + rr.Transform3D( + translation=[msg.x, msg.y, msg.z], + rotation=rr.Quaternion( + xyzw=[ + msg.orientation.x, + msg.orientation.y, + msg.orientation.z, + msg.orientation.w, + ] + ), + ), + ) + # Log axes as a child entity for visualization + rr.log("world/robot/axes", rr.TransformAxes3D(0.5)) # type: ignore[attr-defined] + + # Log camera transform (compose base_link -> camera_link -> camera_optical) + # transforms[1] is camera_link, transforms[2] is camera_optical + cam_tf = transforms[1] + transforms[2] # compose transforms + rr.log( + "world/robot/camera", + rr.Transform3D( + translation=[cam_tf.translation.x, cam_tf.translation.y, cam_tf.translation.z], + rotation=rr.Quaternion( + xyzw=[ + cam_tf.rotation.x, + cam_tf.rotation.y, + cam_tf.rotation.z, + cam_tf.rotation.w, + ] + ), + ), + ) + + def publish_camera_info(self) -> None: + while True: + self.camera_info.publish(_camera_info_static()) + time.sleep(1.0) + + @rpc + def move(self, twist: Twist, duration: float = 0.0) -> bool: + """Send movement command to robot.""" + return self.connection.move(twist, duration) + + @rpc + def standup(self) -> bool: + """Make the robot stand up.""" + return self.connection.standup() + + @rpc + def liedown(self) -> bool: + """Make the robot lie down.""" + return self.connection.liedown() + + @rpc + def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: + """Publish a request to the WebRTC connection. + Args: + topic: The RTC topic to publish to + data: The data dictionary to publish + Returns: + The result of the publish request + """ + return self.connection.publish_request(topic, data) + + +go2_connection = GO2Connection.blueprint + + +def deploy(dimos: DimosCluster, ip: str, prefix: str = "") -> GO2Connection: + from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE + + connection = dimos.deploy(GO2Connection, ip) # type: ignore[attr-defined] + + connection.pointcloud.transport = pSHMTransport( + f"{prefix}/lidar", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + connection.color_image.transport = pSHMTransport( + f"{prefix}/image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + + connection.cmd_vel.transport = LCMTransport(f"{prefix}/cmd_vel", Twist) + + connection.camera_info.transport = LCMTransport(f"{prefix}/camera_info", CameraInfo) + connection.start() + + return connection # type: ignore[no-any-return] + + +__all__ = ["GO2Connection", "deploy", "go2_connection"] diff --git a/dimos/robot/unitree/g1/g1agent.py b/dimos/robot/unitree/g1/g1agent.py new file mode 100644 index 0000000000..a95a905b7d --- /dev/null +++ b/dimos/robot/unitree/g1/g1agent.py @@ -0,0 +1,48 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos import agents +from dimos.agents.skills.navigation import NavigationSkillContainer +from dimos.core import DimosCluster +from dimos.perception import spatial_perception +from dimos.robot.unitree.g1 import g1detector + + +def deploy(dimos: DimosCluster, ip: str): # type: ignore[no-untyped-def] + g1 = g1detector.deploy(dimos, ip) + + nav = g1.get("nav") + camera = g1.get("camera") + detector3d = g1.get("detector3d") + connection = g1.get("connection") + + spatialmem = spatial_perception.deploy(dimos, camera) + + navskills = dimos.deploy( # type: ignore[attr-defined] + NavigationSkillContainer, + spatialmem, + nav, + detector3d, + ) + navskills.start() + + agent = agents.deploy( # type: ignore[attr-defined] + dimos, + "You are controling a humanoid robot", + skill_containers=[connection, nav, camera, spatialmem, navskills], + ) + agent.run_implicit_skill("current_position") + agent.run_implicit_skill("video_stream") + + return {"agent": agent, "spatialmem": spatialmem, **g1} diff --git a/dimos/robot/unitree/g1/g1detector.py b/dimos/robot/unitree/g1/g1detector.py new file mode 100644 index 0000000000..55986eb087 --- /dev/null +++ b/dimos/robot/unitree/g1/g1detector.py @@ -0,0 +1,41 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core import DimosCluster +from dimos.perception.detection import module3D, moduleDB +from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector +from dimos.robot.unitree.g1 import g1zed + + +def deploy(dimos: DimosCluster, ip: str): # type: ignore[no-untyped-def] + g1 = g1zed.deploy(dimos, ip) + + nav = g1.get("nav") + camera = g1.get("camera") + + person_detector = module3D.deploy( + dimos, + camera=camera, + lidar=nav, + detector=YoloPersonDetector, + ) + + detector3d = moduleDB.deploy( # type: ignore[attr-defined] + dimos, + camera=camera, + lidar=nav, + filter=lambda det: det.class_id != 0, + ) + + return {"person_detector": person_detector, "detector3d": detector3d, **g1} diff --git a/dimos/robot/unitree/g1/g1zed.py b/dimos/robot/unitree/g1/g1zed.py new file mode 100644 index 0000000000..930de0d944 --- /dev/null +++ b/dimos/robot/unitree/g1/g1zed.py @@ -0,0 +1,90 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TypedDict, cast + +from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE +from dimos.core import DimosCluster, LCMTransport, pSHMTransport +from dimos.hardware.sensors.camera import zed +from dimos.hardware.sensors.camera.module import CameraModule +from dimos.hardware.sensors.camera.webcam import Webcam +from dimos.msgs.geometry_msgs import ( + Quaternion, + Transform, + Vector3, +) +from dimos.msgs.sensor_msgs import CameraInfo +from dimos.navigation import rosnav +from dimos.navigation.rosnav import ROSNav +from dimos.robot import foxglove_bridge +from dimos.robot.unitree.connection import g1 +from dimos.robot.unitree.connection.g1 import G1Connection +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class G1ZedDeployResult(TypedDict): + nav: ROSNav + connection: G1Connection + camera: CameraModule + camerainfo: CameraInfo + + +def deploy_g1_monozed(dimos: DimosCluster) -> CameraModule: + camera = cast( + "CameraModule", + dimos.deploy( # type: ignore[attr-defined] + CameraModule, + frequency=4.0, + transform=Transform( + translation=Vector3(0.05, 0.0, 0.0), + rotation=Quaternion.from_euler(Vector3(0.0, 0.0, 0.0)), + frame_id="sensor", + child_frame_id="camera_link", + ), + hardware=lambda: Webcam( + camera_index=0, + frequency=5, + stereo_slice="left", + camera_info=zed.CameraInfo.SingleWebcam, + ), + ), + ) + + camera.color_image.transport = pSHMTransport( + "/image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + camera.camera_info.transport = LCMTransport("/camera_info", CameraInfo) + camera.start() + return camera + + +def deploy(dimos: DimosCluster, ip: str): # type: ignore[no-untyped-def] + nav = rosnav.deploy( # type: ignore[call-arg] + dimos, + sensor_to_base_link_transform=Transform( + frame_id="sensor", child_frame_id="base_link", translation=Vector3(0.0, 0.0, 1.5) + ), + ) + connection = g1.deploy(dimos, ip, nav) + zedcam = deploy_g1_monozed(dimos) + + foxglove_bridge.deploy(dimos) + + return { + "nav": nav, + "connection": connection, + "camera": zedcam, + } diff --git a/dimos/robot/unitree/go2/go2.py b/dimos/robot/unitree/go2/go2.py new file mode 100644 index 0000000000..d2e7e74674 --- /dev/null +++ b/dimos/robot/unitree/go2/go2.py @@ -0,0 +1,37 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from dimos.core import DimosCluster +from dimos.robot import foxglove_bridge +from dimos.robot.unitree.connection import go2 +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(level=logging.INFO) + + +def deploy(dimos: DimosCluster, ip: str): # type: ignore[no-untyped-def] + connection = go2.deploy(dimos, ip) + foxglove_bridge.deploy(dimos) + + # detector = moduleDB.deploy( + # dimos, + # camera=connection, + # lidar=connection, + # ) + + # agent = agents.deploy(dimos) + # agent.register_skills(detector) + return connection diff --git a/dimos/robot/unitree/go2/go2.urdf b/dimos/robot/unitree/go2/go2.urdf new file mode 100644 index 0000000000..4e67e9ca8e --- /dev/null +++ b/dimos/robot/unitree/go2/go2.urdf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/dimos/robot/unitree/run.py b/dimos/robot/unitree/run.py new file mode 100644 index 0000000000..5b17ad7a9d --- /dev/null +++ b/dimos/robot/unitree/run.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Centralized runner for modular Unitree robot deployment scripts. + +Usage: + python run.py g1agent --ip 192.168.1.100 + python run.py g1/g1zed --ip $ROBOT_IP + python run.py go2/go2.py --ip $ROBOT_IP + python run.py connection/g1.py --ip $ROBOT_IP +""" + +import argparse +import importlib +import os +import sys + +from dotenv import load_dotenv + +from dimos.core import start, wait_exit + + +def main() -> None: + load_dotenv() + + parser = argparse.ArgumentParser(description="Unitree Robot Modular Deployment Runner") + parser.add_argument( + "module", + help="Module name/path to run (e.g., g1agent, g1/g1zed, go2/go2.py)", + ) + parser.add_argument( + "--ip", + default=os.getenv("ROBOT_IP"), + help="Robot IP address (default: ROBOT_IP from .env)", + ) + parser.add_argument( + "--workers", + type=int, + default=8, + help="Number of worker threads for DimosCluster (default: 8)", + ) + + args = parser.parse_args() + + # Validate IP address + if not args.ip: + print("ERROR: Robot IP address not provided") + print("Please provide --ip or set ROBOT_IP in .env") + sys.exit(1) + + # Parse the module path + module_path = args.module + + # Remove .py extension if present + if module_path.endswith(".py"): + module_path = module_path[:-3] + + # Convert path separators to dots for import + module_path = module_path.replace("/", ".") + + # Import the module + try: + # Build the full import path + full_module_path = f"dimos.robot.unitree.{module_path}" + print(f"Importing module: {full_module_path}") + module = importlib.import_module(full_module_path) + except ImportError: + # Try as a relative import from the unitree package + try: + module = importlib.import_module(f".{module_path}", package="dimos.robot.unitree") + except ImportError as e2: + import traceback + + traceback.print_exc() + + print(f"\nERROR: Could not import module '{args.module}'") + print("Tried importing as:") + print(f" 1. {full_module_path}") + print(" 2. Relative import from dimos.robot.unitree") + print("Make sure the module exists in dimos/robot/unitree/") + print(f"Import error: {e2}") + + sys.exit(1) + + # Verify deploy function exists + if not hasattr(module, "deploy"): + print(f"ERROR: Module '{args.module}' does not have a 'deploy' function") + sys.exit(1) + + print(f"Running {args.module}.deploy() with IP {args.ip}") + + # Run the standard deployment pattern + dimos = start(args.workers) + try: + module.deploy(dimos, args.ip) + wait_exit() + finally: + dimos.close_all() # type: ignore[attr-defined] + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/unitree/unitree_go2.py b/dimos/robot/unitree/unitree_go2.py deleted file mode 100644 index a8e28dd80a..0000000000 --- a/dimos/robot/unitree/unitree_go2.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import multiprocessing -import os - -import numpy as np -from reactivex.disposable import CompositeDisposable -from reactivex.scheduler import ThreadPoolScheduler - -from dimos.perception.object_tracker import ObjectTrackingStream -from dimos.perception.person_tracker import PersonTrackingStream -from dimos.robot.global_planner.planner import AstarPlanner -from dimos.robot.local_planner.local_planner import navigate_path_local -from dimos.robot.local_planner.vfh_local_planner import VFHPurePursuitPlanner -from dimos.robot.robot import Robot -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.skills import AbstractRobotSkill, SkillLibrary -from dimos.types.costmap import Costmap -from dimos.types.robot_capabilities import RobotCapability -from dimos.types.vector import Vector -from dimos.utils.logging_config import setup_logger - -# Set up logging -logger = setup_logger("dimos.robot.unitree.unitree_go2", level=logging.DEBUG) - -# UnitreeGo2 Print Colors (Magenta) -UNITREE_GO2_PRINT_COLOR = "\033[35m" -UNITREE_GO2_RESET_COLOR = "\033[0m" - - -class UnitreeGo2(Robot): - """Unitree Go2 robot implementation using ROS2 control interface. - - This class extends the base Robot class to provide specific functionality - for the Unitree Go2 quadruped robot using ROS2 for communication and control. - """ - - def __init__( - self, - video_provider=None, - output_dir: str = os.path.join(os.getcwd(), "assets", "output"), - skill_library: SkillLibrary = None, - robot_capabilities: list[RobotCapability] | None = None, - spatial_memory_collection: str = "spatial_memory", - new_memory: bool = False, - disable_video_stream: bool = False, - mock_connection: bool = False, - enable_perception: bool = True, - ) -> None: - """Initialize UnitreeGo2 robot with ROS control interface. - - Args: - video_provider: Provider for video streams - output_dir: Directory for output files - skill_library: Library of robot skills - robot_capabilities: List of robot capabilities - spatial_memory_collection: Collection name for spatial memory - new_memory: Whether to create new memory collection - disable_video_stream: Whether to disable video streaming - mock_connection: Whether to use mock connection for testing - enable_perception: Whether to enable perception streams and spatial memory - """ - # Create ROS control interface - ros_control = UnitreeROSControl( - node_name="unitree_go2", - video_provider=video_provider, - disable_video_stream=disable_video_stream, - mock_connection=mock_connection, - ) - - # Initialize skill library if not provided - if skill_library is None: - skill_library = MyUnitreeSkills() - - # Initialize base robot with connection interface - super().__init__( - connection_interface=ros_control, - output_dir=output_dir, - skill_library=skill_library, - capabilities=robot_capabilities - or [ - RobotCapability.LOCOMOTION, - RobotCapability.VISION, - RobotCapability.AUDIO, - ], - spatial_memory_collection=spatial_memory_collection, - new_memory=new_memory, - enable_perception=enable_perception, - ) - - if self.skill_library is not None: - for skill in self.skill_library: - if isinstance(skill, AbstractRobotSkill): - self.skill_library.create_instance(skill.__name__, robot=self) - if isinstance(self.skill_library, MyUnitreeSkills): - self.skill_library._robot = self - self.skill_library.init() - self.skill_library.initialize_skills() - - # Camera stuff - self.camera_intrinsics = [819.553492, 820.646595, 625.284099, 336.808987] - self.camera_pitch = np.deg2rad(0) # negative for downward pitch - self.camera_height = 0.44 # meters - - # Initialize UnitreeGo2-specific attributes - self.disposables = CompositeDisposable() - self.main_stream_obs = None - - # Initialize thread pool scheduler - self.optimal_thread_count = multiprocessing.cpu_count() - self.thread_pool_scheduler = ThreadPoolScheduler(self.optimal_thread_count // 2) - - # Initialize visual servoing if enabled - if not disable_video_stream: - self.video_stream_ros = self.get_video_stream(fps=8) - if enable_perception: - self.person_tracker = PersonTrackingStream( - camera_intrinsics=self.camera_intrinsics, - camera_pitch=self.camera_pitch, - camera_height=self.camera_height, - ) - self.object_tracker = ObjectTrackingStream( - camera_intrinsics=self.camera_intrinsics, - camera_pitch=self.camera_pitch, - camera_height=self.camera_height, - ) - person_tracking_stream = self.person_tracker.create_stream(self.video_stream_ros) - object_tracking_stream = self.object_tracker.create_stream(self.video_stream_ros) - - self.person_tracking_stream = person_tracking_stream - self.object_tracking_stream = object_tracking_stream - else: - # Video stream is available but perception tracking is disabled - self.person_tracker = None - self.object_tracker = None - self.person_tracking_stream = None - self.object_tracking_stream = None - else: - # Video stream is disabled - self.video_stream_ros = None - self.person_tracker = None - self.object_tracker = None - self.person_tracking_stream = None - self.object_tracking_stream = None - - # Initialize the local planner and create BEV visualization stream - # Note: These features require ROS-specific methods that may not be available on all connection interfaces - if hasattr(self.connection_interface, "topic_latest") and hasattr( - self.connection_interface, "transform_euler" - ): - self.local_planner = VFHPurePursuitPlanner( - get_costmap=self.connection_interface.topic_latest( - "/local_costmap/costmap", Costmap - ), - transform=self.connection_interface, - move_vel_control=self.connection_interface.move_vel_control, - robot_width=0.36, # Unitree Go2 width in meters - robot_length=0.6, # Unitree Go2 length in meters - max_linear_vel=0.5, - lookahead_distance=2.0, - visualization_size=500, # 500x500 pixel visualization - ) - - self.global_planner = AstarPlanner( - conservativism=20, # how close to obstacles robot is allowed to path plan - set_local_nav=lambda path, stop_event=None, goal_theta=None: navigate_path_local( - self, path, timeout=120.0, goal_theta=goal_theta, stop_event=stop_event - ), - get_costmap=self.connection_interface.topic_latest("map", Costmap), - get_robot_pos=lambda: self.connection_interface.transform_euler_pos("base_link"), - ) - - # Create the visualization stream at 5Hz - self.local_planner_viz_stream = self.local_planner.create_stream(frequency_hz=5.0) - else: - self.local_planner = None - self.global_planner = None - self.local_planner_viz_stream = None - - def get_skills(self) -> SkillLibrary | None: - return self.skill_library - - def get_pose(self) -> dict: - """ - Get the current pose (position and rotation) of the robot in the map frame. - - Returns: - Dictionary containing: - - position: Vector (x, y, z) - - rotation: Vector (roll, pitch, yaw) in radians - """ - position_tuple, orientation_tuple = self.connection_interface.get_pose_odom_transform() - position = Vector(position_tuple[0], position_tuple[1], position_tuple[2]) - rotation = Vector(orientation_tuple[0], orientation_tuple[1], orientation_tuple[2]) - return {"position": position, "rotation": rotation} diff --git a/dimos/robot/unitree/unitree_ros_control.py b/dimos/robot/unitree/unitree_ros_control.py deleted file mode 100644 index 8ab46f5cdc..0000000000 --- a/dimos/robot/unitree/unitree_ros_control.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from go2_interfaces.msg import IMU, Go2State -from sensor_msgs.msg import CameraInfo, CompressedImage, Image -from unitree_go.msg import WebRtcReq - -from dimos.robot.ros_control import RobotMode, ROSControl -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.robot.unitree.unitree_ros_control") - - -class UnitreeROSControl(ROSControl): - """Hardware interface for Unitree Go2 robot using ROS2""" - - # ROS Camera Topics - CAMERA_TOPICS = { - "raw": {"topic": "camera/image_raw", "type": Image}, - "compressed": {"topic": "camera/compressed", "type": CompressedImage}, - "info": {"topic": "camera/camera_info", "type": CameraInfo}, - } - # Hard coded ROS Message types and Topic names for Unitree Go2 - DEFAULT_STATE_MSG_TYPE = Go2State - DEFAULT_IMU_MSG_TYPE = IMU - DEFAULT_WEBRTC_MSG_TYPE = WebRtcReq - DEFAULT_STATE_TOPIC = "go2_states" - DEFAULT_IMU_TOPIC = "imu" - DEFAULT_WEBRTC_TOPIC = "webrtc_req" - DEFAULT_CMD_VEL_TOPIC = "cmd_vel_out" - DEFAULT_POSE_TOPIC = "pose_cmd" - DEFAULT_ODOM_TOPIC = "odom" - DEFAULT_COSTMAP_TOPIC = "local_costmap/costmap" - DEFAULT_MAX_LINEAR_VELOCITY = 1.0 - DEFAULT_MAX_ANGULAR_VELOCITY = 2.0 - - # Hard coded WebRTC API parameters for Unitree Go2 - DEFAULT_WEBRTC_API_TOPIC = "rt/api/sport/request" - - def __init__( - self, - node_name: str = "unitree_hardware_interface", - state_topic: str | None = None, - imu_topic: str | None = None, - webrtc_topic: str | None = None, - webrtc_api_topic: str | None = None, - move_vel_topic: str | None = None, - pose_topic: str | None = None, - odom_topic: str | None = None, - costmap_topic: str | None = None, - state_msg_type: type | None = None, - imu_msg_type: type | None = None, - webrtc_msg_type: type | None = None, - max_linear_velocity: float | None = None, - max_angular_velocity: float | None = None, - use_raw: bool = False, - debug: bool = False, - disable_video_stream: bool = False, - mock_connection: bool = False, - ) -> None: - """ - Initialize Unitree ROS control interface with default values for Unitree Go2 - - Args: - node_name: Name for the ROS node - state_topic: ROS Topic name for robot state (defaults to DEFAULT_STATE_TOPIC) - imu_topic: ROS Topic name for IMU data (defaults to DEFAULT_IMU_TOPIC) - webrtc_topic: ROS Topic for WebRTC commands (defaults to DEFAULT_WEBRTC_TOPIC) - cmd_vel_topic: ROS Topic for direct movement velocity commands (defaults to DEFAULT_CMD_VEL_TOPIC) - pose_topic: ROS Topic for pose commands (defaults to DEFAULT_POSE_TOPIC) - odom_topic: ROS Topic for odometry data (defaults to DEFAULT_ODOM_TOPIC) - costmap_topic: ROS Topic for local costmap data (defaults to DEFAULT_COSTMAP_TOPIC) - state_msg_type: ROS Message type for state data (defaults to DEFAULT_STATE_MSG_TYPE) - imu_msg_type: ROS message type for IMU data (defaults to DEFAULT_IMU_MSG_TYPE) - webrtc_msg_type: ROS message type for webrtc data (defaults to DEFAULT_WEBRTC_MSG_TYPE) - max_linear_velocity: Maximum linear velocity in m/s (defaults to DEFAULT_MAX_LINEAR_VELOCITY) - max_angular_velocity: Maximum angular velocity in rad/s (defaults to DEFAULT_MAX_ANGULAR_VELOCITY) - use_raw: Whether to use raw camera topics (defaults to False) - debug: Whether to enable debug logging - disable_video_stream: Whether to run without video stream for testing. - mock_connection: Whether to run without active ActionClient servers for testing. - """ - - logger.info("Initializing Unitree ROS control interface") - # Select which camera topics to use - active_camera_topics = None - if not disable_video_stream: - active_camera_topics = {"main": self.CAMERA_TOPICS["raw" if use_raw else "compressed"]} - - # Use default values if not provided - state_topic = state_topic or self.DEFAULT_STATE_TOPIC - imu_topic = imu_topic or self.DEFAULT_IMU_TOPIC - webrtc_topic = webrtc_topic or self.DEFAULT_WEBRTC_TOPIC - move_vel_topic = move_vel_topic or self.DEFAULT_CMD_VEL_TOPIC - pose_topic = pose_topic or self.DEFAULT_POSE_TOPIC - odom_topic = odom_topic or self.DEFAULT_ODOM_TOPIC - costmap_topic = costmap_topic or self.DEFAULT_COSTMAP_TOPIC - webrtc_api_topic = webrtc_api_topic or self.DEFAULT_WEBRTC_API_TOPIC - state_msg_type = state_msg_type or self.DEFAULT_STATE_MSG_TYPE - imu_msg_type = imu_msg_type or self.DEFAULT_IMU_MSG_TYPE - webrtc_msg_type = webrtc_msg_type or self.DEFAULT_WEBRTC_MSG_TYPE - max_linear_velocity = max_linear_velocity or self.DEFAULT_MAX_LINEAR_VELOCITY - max_angular_velocity = max_angular_velocity or self.DEFAULT_MAX_ANGULAR_VELOCITY - - super().__init__( - node_name=node_name, - camera_topics=active_camera_topics, - mock_connection=mock_connection, - state_topic=state_topic, - imu_topic=imu_topic, - state_msg_type=state_msg_type, - imu_msg_type=imu_msg_type, - webrtc_msg_type=webrtc_msg_type, - webrtc_topic=webrtc_topic, - webrtc_api_topic=webrtc_api_topic, - move_vel_topic=move_vel_topic, - pose_topic=pose_topic, - odom_topic=odom_topic, - costmap_topic=costmap_topic, - max_linear_velocity=max_linear_velocity, - max_angular_velocity=max_angular_velocity, - debug=debug, - ) - - # Unitree-specific RobotMode State update conditons - def _update_mode(self, msg: Go2State) -> None: - """ - Implementation of abstract method to update robot mode - - Logic: - - If progress is 0 and mode is 1, then state is IDLE - - If progress is 1 OR mode is NOT equal to 1, then state is MOVING - """ - # Direct access to protected instance variables from the parent class - mode = msg.mode - progress = msg.progress - - if progress == 0 and mode == 1: - self._mode = RobotMode.IDLE - logger.debug("Robot mode set to IDLE (progress=0, mode=1)") - elif progress == 1 or mode != 1: - self._mode = RobotMode.MOVING - logger.debug(f"Robot mode set to MOVING (progress={progress}, mode={mode})") - else: - self._mode = RobotMode.UNKNOWN - logger.debug(f"Robot mode set to UNKNOWN (progress={progress}, mode={mode})") diff --git a/dimos/robot/unitree/unitree_skills.py b/dimos/robot/unitree/unitree_skills.py deleted file mode 100644 index 04946d5ff7..0000000000 --- a/dimos/robot/unitree/unitree_skills.py +++ /dev/null @@ -1,315 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING - -from pydantic import Field - -if TYPE_CHECKING: - from dimos.robot.robot import MockRobot, Robot -else: - Robot = "Robot" - MockRobot = "MockRobot" - -from dimos.skills.skills import AbstractRobotSkill, AbstractSkill, SkillLibrary -from dimos.types.constants import Colors -from dimos.types.vector import Vector - -# Module-level constant for Unitree ROS control definitions -UNITREE_ROS_CONTROLS: list[tuple[str, int, str]] = [ - ("Damp", 1001, "Lowers the robot to the ground fully."), - ( - "BalanceStand", - 1002, - "Activates a mode that maintains the robot in a balanced standing position.", - ), - ( - "StandUp", - 1004, - "Commands the robot to transition from a sitting or prone position to a standing posture.", - ), - ( - "StandDown", - 1005, - "Instructs the robot to move from a standing position to a sitting or prone posture.", - ), - ( - "RecoveryStand", - 1006, - "Recovers the robot to a state from which it can take more commands. Useful to run after multiple dynamic commands like front flips.", - ), - # ( - # "Euler", - # 1007, - # "Adjusts the robot's orientation using Euler angles, providing precise control over its rotation.", - # ), - # ("Move", 1008, "Move the robot using velocity commands."), # Intentionally omitted - ("Sit", 1009, "Commands the robot to sit down from a standing or moving stance."), - # ( - # "RiseSit", - # 1010, - # "Commands the robot to rise back to a standing position from a sitting posture.", - # ), - # ( - # "SwitchGait", - # 1011, - # "Switches the robot's walking pattern or style dynamically, suitable for different terrains or speeds.", - # ), - # ("Trigger", 1012, "Triggers a specific action or custom routine programmed into the robot."), - # ( - # "BodyHeight", - # 1013, - # "Adjusts the height of the robot's body from the ground, useful for navigating various obstacles.", - # ), - # ( - # "FootRaiseHeight", - # 1014, - # "Controls how high the robot lifts its feet during movement, which can be adjusted for different surfaces.", - # ), - ( - "SpeedLevel", - 1015, - "Sets or adjusts the speed at which the robot moves, with various levels available for different operational needs.", - ), - ( - "ShakeHand", - 1016, - "Performs a greeting action, which could involve a wave or other friendly gesture.", - ), - ("Stretch", 1017, "Engages the robot in a stretching routine."), - # ( - # "TrajectoryFollow", - # 1018, - # "Directs the robot to follow a predefined trajectory, which could involve complex paths or maneuvers.", - # ), - # ( - # "ContinuousGait", - # 1019, - # "Enables a mode for continuous walking or running, ideal for long-distance travel.", - # ), - ("Content", 1020, "To display or trigger when the robot is happy."), - ("Wallow", 1021, "The robot falls onto its back and rolls around."), - ( - "Dance1", - 1022, - "Performs a predefined dance routine 1, programmed for entertainment or demonstration.", - ), - ("Dance2", 1023, "Performs another variant of a predefined dance routine 2."), - # ("GetBodyHeight", 1024, "Retrieves the current height of the robot's body from the ground."), - # ( - # "GetFootRaiseHeight", - # 1025, - # "Retrieves the current height at which the robot's feet are being raised during movement.", - # ), - # ("GetSpeedLevel", 1026, "Returns the current speed level at which the robot is operating."), - # ( - # "SwitchJoystick", - # 1027, - # "Toggles the control mode to joystick input, allowing for manual direction of the robot's movements.", - # ), - ( - "Pose", - 1028, - "Directs the robot to take a specific pose or stance, which could be used for tasks or performances.", - ), - ( - "Scrape", - 1029, - "Robot falls to its hind legs and makes scraping motions with its front legs.", - ), - ("FrontFlip", 1030, "Executes a front flip, a complex and dynamic maneuver."), - ("FrontJump", 1031, "Commands the robot to perform a forward jump."), - ( - "FrontPounce", - 1032, - "Initiates a pouncing movement forward, mimicking animal-like pouncing behavior.", - ), - # ("WiggleHips", 1033, "Causes the robot to wiggle its hips."), - # ( - # "GetState", - # 1034, - # "Retrieves the current operational state of the robot, including status reports or diagnostic information.", - # ), - # ( - # "EconomicGait", - # 1035, - # "Engages a more energy-efficient walking or running mode to conserve battery life.", - # ), - # ("FingerHeart", 1036, "Performs a finger heart gesture while on its hind legs."), - # ( - # "Handstand", - # 1301, - # "Commands the robot to perform a handstand, demonstrating balance and control.", - # ), - # ( - # "CrossStep", - # 1302, - # "Engages the robot in a cross-stepping routine, useful for complex locomotion or dance moves.", - # ), - # ( - # "OnesidedStep", - # 1303, - # "Commands the robot to perform a stepping motion that predominantly uses one side.", - # ), - # ( - # "Bound", - # 1304, - # "Initiates a bounding motion, similar to a light, repetitive hopping or leaping.", - # ), - # ( - # "LeadFollow", - # 1045, - # "Engages follow-the-leader behavior, where the robot follows a designated leader or follows a signal.", - # ), - # ("LeftFlip", 1042, "Executes a flip towards the left side."), - # ("RightFlip", 1043, "Performs a flip towards the right side."), - # ("Backflip", 1044, "Executes a backflip, a complex and dynamic maneuver."), -] - -# region MyUnitreeSkills - - -class MyUnitreeSkills(SkillLibrary): - """My Unitree Skills.""" - - _robot: Robot | None = None - - @classmethod - def register_skills(cls, skill_classes: AbstractSkill | list[AbstractSkill]) -> None: - """Add multiple skill classes as class attributes. - - Args: - skill_classes: List of skill classes to add - """ - if isinstance(skill_classes, list): - for skill_class in skill_classes: - setattr(cls, skill_class.__name__, skill_class) - else: - setattr(cls, skill_classes.__name__, skill_classes) - - def __init__(self, robot: Robot | None = None) -> None: - super().__init__() - self._robot: Robot = None - - # Add dynamic skills to this class - self.register_skills(self.create_skills_live()) - - if robot is not None: - self._robot = robot - self.initialize_skills() - - def initialize_skills(self) -> None: - # Create the skills and add them to the list of skills - self.register_skills(self.create_skills_live()) - - # Provide the robot instance to each skill - for skill_class in self: - print( - f"{Colors.GREEN_PRINT_COLOR}Creating instance for skill: {skill_class}{Colors.RESET_COLOR}" - ) - self.create_instance(skill_class.__name__, robot=self._robot) - - # Refresh the class skills - self.refresh_class_skills() - - def create_skills_live(self) -> list[AbstractRobotSkill]: - # ================================================ - # Procedurally created skills - # ================================================ - class BaseUnitreeSkill(AbstractRobotSkill): - """Base skill for dynamic skill creation.""" - - def __call__(self): - string = f"{Colors.GREEN_PRINT_COLOR}This is a base skill, created for the specific skill: {self._app_id}{Colors.RESET_COLOR}" - print(string) - super().__call__() - if self._app_id is None: - raise RuntimeError( - f"{Colors.RED_PRINT_COLOR}" - f"No App ID provided to {self.__class__.__name__} Skill" - f"{Colors.RESET_COLOR}" - ) - else: - self._robot.webrtc_req(api_id=self._app_id) - string = f"{Colors.GREEN_PRINT_COLOR}{self.__class__.__name__} was successful: id={self._app_id}{Colors.RESET_COLOR}" - print(string) - return string - - skills_classes = [] - for name, app_id, description in UNITREE_ROS_CONTROLS: - skill_class = type( - name, # Name of the class - (BaseUnitreeSkill,), # Base classes - {"__doc__": description, "_app_id": app_id}, - ) - skills_classes.append(skill_class) - - return skills_classes - - # region Class-based Skills - - class Move(AbstractRobotSkill): - """Move the robot using direct velocity commands. Determine duration required based on user distance instructions.""" - - x: float = Field(..., description="Forward velocity (m/s).") - y: float = Field(default=0.0, description="Left/right velocity (m/s)") - yaw: float = Field(default=0.0, description="Rotational velocity (rad/s)") - duration: float = Field(default=0.0, description="How long to move (seconds).") - - def __call__(self): - super().__call__() - return self._robot.move(Vector(self.x, self.y, self.yaw), duration=self.duration) - - class Reverse(AbstractRobotSkill): - """Reverse the robot using direct velocity commands. Determine duration required based on user distance instructions.""" - - x: float = Field(..., description="Backward velocity (m/s). Positive values move backward.") - y: float = Field(default=0.0, description="Left/right velocity (m/s)") - yaw: float = Field(default=0.0, description="Rotational velocity (rad/s)") - duration: float = Field(default=0.0, description="How long to move (seconds).") - - def __call__(self): - super().__call__() - # Use move with negative x for backward movement - return self._robot.move(Vector(-self.x, self.y, self.yaw), duration=self.duration) - - class SpinLeft(AbstractRobotSkill): - """Spin the robot left using degree commands.""" - - degrees: float = Field(..., description="Distance to spin left in degrees") - - def __call__(self): - super().__call__() - return self._robot.spin(degrees=self.degrees) # Spinning left is positive degrees - - class SpinRight(AbstractRobotSkill): - """Spin the robot right using degree commands.""" - - degrees: float = Field(..., description="Distance to spin right in degrees") - - def __call__(self): - super().__call__() - return self._robot.spin(degrees=-self.degrees) # Spinning right is negative degrees - - class Wait(AbstractSkill): - """Wait for a specified amount of time.""" - - seconds: float = Field(..., description="Seconds to wait") - - def __call__(self) -> str: - time.sleep(self.seconds) - return f"Wait completed with length={self.seconds}s" diff --git a/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py b/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py new file mode 100644 index 0000000000..b2c2aabccb --- /dev/null +++ b/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py @@ -0,0 +1,53 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core.blueprints import autoconnect +from dimos.core.core import rpc +from dimos.core.module import Module +from dimos.core.stream import In, Out + + +class Data1: + pass + + +class Data2: + pass + + +class ModuleA(Module): + shared_data: Out[Data1] + + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + + +class ModuleB(Module): + shared_data: In[Data2] + + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + + +blueprint = autoconnect(ModuleA.blueprint(), ModuleB.blueprint()) diff --git a/dimos/robot/unitree_webrtc/demo_remapping.py b/dimos/robot/unitree_webrtc/demo_remapping.py deleted file mode 100644 index a0b594f95a..0000000000 --- a/dimos/robot/unitree_webrtc/demo_remapping.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.transport import LCMTransport -from dimos.msgs.sensor_msgs import Image -from dimos.robot.unitree_webrtc.unitree_go2 import ConnectionModule -from dimos.robot.unitree_webrtc.unitree_go2_blueprints import standard - -remapping = standard.remappings( - [ - (ConnectionModule, "color_image", "rgb_image"), - ] -) - -remapping_and_transport = remapping.transports( - { - ("rgb_image", Image): LCMTransport("/go2/color_image", Image), - } -) diff --git a/dimos/robot/unitree_webrtc/depth_module.py b/dimos/robot/unitree_webrtc/depth_module.py index 9e9b57b24b..b040fbb63f 100644 --- a/dimos/robot/unitree_webrtc/depth_module.py +++ b/dimos/robot/unitree_webrtc/depth_module.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ from dimos.msgs.sensor_msgs import Image, ImageFormat from dimos.utils.logging_config import setup_logger -logger = setup_logger(__name__) +logger = setup_logger() class DepthModule(Module): @@ -41,13 +41,13 @@ class DepthModule(Module): """ # LCM inputs - color_image: In[Image] = None - camera_info: In[CameraInfo] = None + color_image: In[Image] + camera_info: In[CameraInfo] # LCM outputs - depth_image: Out[Image] = None + depth_image: Out[Image] - def __init__( + def __init__( # type: ignore[no-untyped-def] self, gt_depth_scale: float = 0.5, global_config: GlobalConfig | None = None, @@ -79,7 +79,7 @@ def __init__( self._stop_processing = threading.Event() if global_config: - if global_config.use_simulation: + if global_config.simulation: self.gt_depth_scale = 1.0 @rpc @@ -129,12 +129,12 @@ def _on_camera_info(self, msg: CameraInfo) -> None: cx = K[2] cy = K[5] - self.camera_intrinsics = [fx, fy, cx, cy] + self.camera_intrinsics = [fx, fy, cx, cy] # type: ignore[assignment] # Initialize Metric3D with camera intrinsics from dimos.models.depth.metric3d import Metric3D - self.metric3d = Metric3D(camera_intrinsics=self.camera_intrinsics) + self.metric3d = Metric3D(camera_intrinsics=self.camera_intrinsics) # type: ignore[assignment] self._camera_info_received = True logger.info( @@ -150,7 +150,7 @@ def _on_video(self, msg: Image) -> None: return # Simply store the latest frame - processing happens in main loop - self._latest_frame = msg + self._latest_frame = msg # type: ignore[assignment] logger.debug( f"Received video frame: format={msg.format}, shape={msg.data.shape if hasattr(msg.data, 'shape') else 'unknown'}" ) @@ -186,7 +186,7 @@ def _main_processing_loop(self) -> None: logger.info("Main processing loop stopped") - def _process_depth(self, img_array: np.ndarray) -> None: + def _process_depth(self, img_array: np.ndarray) -> None: # type: ignore[type-arg] """Process depth estimation using Metric3D.""" if self._cannot_process_depth: self._last_depth = None diff --git a/dimos/robot/unitree_webrtc/g1_joystick_module.py b/dimos/robot/unitree_webrtc/g1_joystick_module.py deleted file mode 100644 index 2c6a5e64e5..0000000000 --- a/dimos/robot/unitree_webrtc/g1_joystick_module.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Pygame Joystick Module for testing G1 humanoid control.""" - -import os -import threading - -# Force X11 driver to avoid OpenGL threading issues -os.environ["SDL_VIDEODRIVER"] = "x11" - -from dimos.core import Module, Out, rpc -from dimos.msgs.geometry_msgs import Twist, Vector3 - - -class G1JoystickModule(Module): - """Pygame-based joystick control module for G1 humanoid testing. - - Outputs standard Twist messages on /cmd_vel for velocity control. - Simplified version without mode switching since G1 handles that differently. - """ - - twist_out: Out[Twist] = None # Standard velocity commands - - def __init__(self, *args, **kwargs) -> None: - Module.__init__(self, *args, **kwargs) - self.pygame_ready = False - self.running = False - - @rpc - def start(self) -> bool: - """Initialize pygame and start control loop.""" - super().start() - - try: - import pygame - except ImportError: - print("ERROR: pygame not installed. Install with: pip install pygame") - return False - - self.keys_held = set() - self.pygame_ready = True - self.running = True - - # Start pygame loop in background thread - self._thread = threading.Thread(target=self._pygame_loop, daemon=True) - self._thread.start() - - return True - - @rpc - def stop(self) -> None: - super().stop() - - self.running = False - self.pygame_ready = False - - stop_twist = Twist() - stop_twist.linear = Vector3(0, 0, 0) - stop_twist.angular = Vector3(0, 0, 0) - - self._thread.join(2) - - self.twist_out.publish(stop_twist) - - def _pygame_loop(self) -> None: - """Main pygame event loop - ALL pygame operations happen here.""" - import pygame - - pygame.init() - self.screen = pygame.display.set_mode((500, 400), pygame.SWSURFACE) - pygame.display.set_caption("G1 Humanoid Joystick Control") - self.clock = pygame.time.Clock() - self.font = pygame.font.Font(None, 24) - - print("G1 JoystickModule started - Focus pygame window to control") - print("Controls:") - print(" WS = Forward/Back") - print(" AD = Turn Left/Right") - print(" Space = Emergency Stop") - print(" ESC = Quit") - - while self.running and self.pygame_ready: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - self.running = False - elif event.type == pygame.KEYDOWN: - self.keys_held.add(event.key) - - if event.key == pygame.K_SPACE: - # Emergency stop - clear all keys and send zero twist - self.keys_held.clear() - stop_twist = Twist() - stop_twist.linear = Vector3(0, 0, 0) - stop_twist.angular = Vector3(0, 0, 0) - self.twist_out.publish(stop_twist) - print("EMERGENCY STOP!") - elif event.key == pygame.K_ESCAPE: - # ESC quits - self.running = False - - elif event.type == pygame.KEYUP: - self.keys_held.discard(event.key) - - # Generate Twist message from held keys - twist = Twist() - twist.linear = Vector3(0, 0, 0) - twist.angular = Vector3(0, 0, 0) - - # Forward/backward (W/S) - if pygame.K_w in self.keys_held: - twist.linear.x = 0.5 - if pygame.K_s in self.keys_held: - twist.linear.x = -0.5 - - # Turning (A/D) - if pygame.K_a in self.keys_held: - twist.angular.z = 0.5 - if pygame.K_d in self.keys_held: - twist.angular.z = -0.5 - - # Always publish twist at 50Hz - self.twist_out.publish(twist) - - self._update_display(twist) - - # Maintain 50Hz rate - self.clock.tick(50) - - pygame.quit() - print("G1 JoystickModule stopped") - - def _update_display(self, twist) -> None: - """Update pygame window with current status.""" - import pygame - - self.screen.fill((30, 30, 30)) - - y_pos = 20 - - texts = [ - "G1 Humanoid Control", - "", - f"Linear X (Forward/Back): {twist.linear.x:+.2f} m/s", - f"Angular Z (Turn L/R): {twist.angular.z:+.2f} rad/s", - "", - "Keys: " + ", ".join([pygame.key.name(k).upper() for k in self.keys_held if k < 256]), - ] - - for text in texts: - if text: - color = (0, 255, 255) if text == "G1 Humanoid Control" else (255, 255, 255) - surf = self.font.render(text, True, color) - self.screen.blit(surf, (20, y_pos)) - y_pos += 30 - - if twist.linear.x != 0 or twist.linear.y != 0 or twist.angular.z != 0: - pygame.draw.circle(self.screen, (255, 0, 0), (450, 30), 15) # Red = moving - else: - pygame.draw.circle(self.screen, (0, 255, 0), (450, 30), 15) # Green = stopped - - y_pos = 300 - help_texts = ["WS: Move | AD: Turn", "Space: E-Stop | ESC: Quit"] - for text in help_texts: - surf = self.font.render(text, True, (150, 150, 150)) - self.screen.blit(surf, (20, y_pos)) - y_pos += 25 - - pygame.display.flip() diff --git a/dimos/robot/unitree_webrtc/g1_run.py b/dimos/robot/unitree_webrtc/g1_run.py deleted file mode 100644 index b8c0bc77c7..0000000000 --- a/dimos/robot/unitree_webrtc/g1_run.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Run script for Unitree G1 humanoid robot with Claude agent integration. -Provides interaction capabilities with natural language interface and ZED vision. -""" - -import argparse -import os -import sys -import time - -from dotenv import load_dotenv -import reactivex as rx -import reactivex.operators as ops - -from dimos.agents.claude_agent import ClaudeAgent -from dimos.robot.unitree_webrtc.unitree_g1 import UnitreeG1 -from dimos.robot.unitree_webrtc.unitree_skills import MyUnitreeSkills -from dimos.skills.kill_skill import KillSkill -from dimos.skills.navigation import GetPose -from dimos.utils.logging_config import setup_logger -from dimos.web.robot_web_interface import RobotWebInterface - -logger = setup_logger("dimos.robot.unitree_webrtc.g1_run") - -# Load environment variables -load_dotenv() - -# System prompt - loaded from prompt.txt -SYSTEM_PROMPT_PATH = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), - "assets/agent/prompt.txt", -) - - -def main(): - """Main entry point.""" - # Parse command line arguments - parser = argparse.ArgumentParser(description="Unitree G1 Robot with Claude Agent") - parser.add_argument("--replay", type=str, help="Path to recording to replay") - parser.add_argument("--record", type=str, help="Path to save recording") - args = parser.parse_args() - - print("\n" + "=" * 60) - print("Unitree G1 Humanoid Robot with Claude Agent") - print("=" * 60) - print("\nThis system integrates:") - print(" - Unitree G1 humanoid robot") - print(" - ZED camera for stereo vision and depth") - print(" - WebRTC communication for robot control") - print(" - Claude AI for natural language understanding") - print(" - Web interface with text and voice input") - - if args.replay: - print(f"\nREPLAY MODE: Replaying from {args.replay}") - elif args.record: - print(f"\nRECORDING MODE: Recording to {args.record}") - - print("\nStarting system...\n") - - # Check for API key - if not os.getenv("ANTHROPIC_API_KEY"): - print("WARNING: ANTHROPIC_API_KEY not found in environment") - print("Please set your API key in .env file or environment") - sys.exit(1) - - # Check for robot IP (not needed in replay mode) - robot_ip = os.getenv("ROBOT_IP") - if not robot_ip and not args.replay: - print("ERROR: ROBOT_IP not found in environment") - print("Please set the robot IP address in .env file") - sys.exit(1) - - # Load system prompt - try: - with open(SYSTEM_PROMPT_PATH) as f: - system_prompt = f.read() - except FileNotFoundError: - logger.error(f"System prompt file not found at {SYSTEM_PROMPT_PATH}") - sys.exit(1) - - logger.info("Starting Unitree G1 Robot with Agent") - - # Create robot instance with recording/replay support - robot = UnitreeG1( - ip=robot_ip or "0.0.0.0", # Dummy IP for replay mode - recording_path=args.record, - replay_path=args.replay, - ) - robot.start() - time.sleep(3) - - try: - logger.info("Robot initialized successfully") - - # Set up minimal skill library for G1 with robot_type="g1" - skills = MyUnitreeSkills(robot=robot, robot_type="g1") - skills.add(KillSkill) - skills.add(GetPose) - - # Create skill instances - skills.create_instance("KillSkill", robot=robot, skill_library=skills) - skills.create_instance("GetPose", robot=robot) - - logger.info(f"Skills registered: {[skill.__name__ for skill in skills.get_class_skills()]}") - - # Set up streams for agent and web interface - agent_response_subject = rx.subject.Subject() - agent_response_stream = agent_response_subject.pipe(ops.share()) - audio_subject = rx.subject.Subject() - - # Set up streams for web interface - text_streams = { - "agent_responses": agent_response_stream, - } - - # Create web interface - try: - web_interface = RobotWebInterface( - port=5555, text_streams=text_streams, audio_subject=audio_subject - ) - logger.info("Web interface created successfully") - except Exception as e: - logger.error(f"Failed to create web interface: {e}") - raise - - # Create Claude agent with minimal configuration - agent = ClaudeAgent( - dev_name="unitree_g1_agent", - input_query_stream=web_interface.query_stream, # Text input from web - skills=skills, - system_query=system_prompt, - model_name="claude-3-5-haiku-latest", - thinking_budget_tokens=0, - max_output_tokens_per_request=8192, - ) - - # Subscribe to agent responses - agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) - - logger.info("=" * 60) - logger.info("Unitree G1 Agent Ready!") - logger.info("Web interface available at: http://localhost:5555") - logger.info("You can:") - logger.info(" - Type commands in the web interface") - logger.info(" - Use voice commands") - logger.info(" - Ask the robot to move or perform actions") - logger.info(" - Ask the robot to describe what it sees") - logger.info("=" * 60) - - # Run web interface (this blocks) - web_interface.run() - - except KeyboardInterrupt: - logger.info("Keyboard interrupt received") - except Exception as e: - logger.error(f"Error running robot: {e}") - import traceback - - traceback.print_exc() - finally: - logger.info("Shutting down...") - logger.info("Shutdown complete") - - -if __name__ == "__main__": - main() diff --git a/dimos/robot/unitree_webrtc/keyboard_teleop.py b/dimos/robot/unitree_webrtc/keyboard_teleop.py new file mode 100644 index 0000000000..8e0d987127 --- /dev/null +++ b/dimos/robot/unitree_webrtc/keyboard_teleop.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import threading + +import pygame + +from dimos.core import Module, Out, rpc +from dimos.msgs.geometry_msgs import Twist, Vector3 + +# Force X11 driver to avoid OpenGL threading issues +os.environ["SDL_VIDEODRIVER"] = "x11" + + +class KeyboardTeleop(Module): + """Pygame-based keyboard control module. + + Outputs standard Twist messages on /cmd_vel for velocity control. + """ + + cmd_vel: Out[Twist] # Standard velocity commands + + _stop_event: threading.Event + _keys_held: set[int] | None = None + _thread: threading.Thread | None = None + _screen: pygame.Surface | None = None + _clock: pygame.time.Clock | None = None + _font: pygame.font.Font | None = None + + def __init__(self) -> None: + super().__init__() + self._stop_event = threading.Event() + + @rpc + def start(self) -> bool: + super().start() + + self._keys_held = set() + self._stop_event.clear() + + self._thread = threading.Thread(target=self._pygame_loop, daemon=True) + self._thread.start() + + return True + + @rpc + def stop(self) -> None: + stop_twist = Twist() + stop_twist.linear = Vector3(0, 0, 0) + stop_twist.angular = Vector3(0, 0, 0) + self.cmd_vel.publish(stop_twist) + + self._stop_event.set() + + if self._thread is None: + raise RuntimeError("Cannot stop: thread was never started") + self._thread.join(2) + + super().stop() + + def _pygame_loop(self) -> None: + if self._keys_held is None: + raise RuntimeError("_keys_held not initialized") + + pygame.init() + self._screen = pygame.display.set_mode((500, 400), pygame.SWSURFACE) + pygame.display.set_caption("Keyboard Teleop") + self._clock = pygame.time.Clock() + self._font = pygame.font.Font(None, 24) + + while not self._stop_event.is_set(): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self._stop_event.set() + elif event.type == pygame.KEYDOWN: + self._keys_held.add(event.key) + + if event.key == pygame.K_SPACE: + # Emergency stop - clear all keys and send zero twist + self._keys_held.clear() + stop_twist = Twist() + stop_twist.linear = Vector3(0, 0, 0) + stop_twist.angular = Vector3(0, 0, 0) + self.cmd_vel.publish(stop_twist) + print("EMERGENCY STOP!") + elif event.key == pygame.K_ESCAPE: + # ESC quits + self._stop_event.set() + + elif event.type == pygame.KEYUP: + self._keys_held.discard(event.key) + + # Generate Twist message from held keys + twist = Twist() + twist.linear = Vector3(0, 0, 0) + twist.angular = Vector3(0, 0, 0) + + # Forward/backward (W/S) + if pygame.K_w in self._keys_held: + twist.linear.x = 0.5 + if pygame.K_s in self._keys_held: + twist.linear.x = -0.5 + + # Strafe left/right (Q/E) + if pygame.K_q in self._keys_held: + twist.linear.y = 0.5 + if pygame.K_e in self._keys_held: + twist.linear.y = -0.5 + + # Turning (A/D) + if pygame.K_a in self._keys_held: + twist.angular.z = 0.8 + if pygame.K_d in self._keys_held: + twist.angular.z = -0.8 + + # Apply speed modifiers (Shift = 2x, Ctrl = 0.5x) + speed_multiplier = 1.0 + if pygame.K_LSHIFT in self._keys_held or pygame.K_RSHIFT in self._keys_held: + speed_multiplier = 2.0 + elif pygame.K_LCTRL in self._keys_held or pygame.K_RCTRL in self._keys_held: + speed_multiplier = 0.5 + + twist.linear.x *= speed_multiplier + twist.linear.y *= speed_multiplier + twist.angular.z *= speed_multiplier + + # Always publish twist at 50Hz + self.cmd_vel.publish(twist) + + self._update_display(twist) + + # Maintain 50Hz rate + if self._clock is None: + raise RuntimeError("_clock not initialized") + self._clock.tick(50) + + pygame.quit() + + def _update_display(self, twist: Twist) -> None: + if self._screen is None or self._font is None or self._keys_held is None: + raise RuntimeError("Not initialized correctly") + + self._screen.fill((30, 30, 30)) + + y_pos = 20 + + # Determine active speed multiplier + speed_mult_text = "" + if pygame.K_LSHIFT in self._keys_held or pygame.K_RSHIFT in self._keys_held: + speed_mult_text = " [BOOST 2x]" + elif pygame.K_LCTRL in self._keys_held or pygame.K_RCTRL in self._keys_held: + speed_mult_text = " [SLOW 0.5x]" + + texts = [ + "Keyboard Teleop" + speed_mult_text, + "", + f"Linear X (Forward/Back): {twist.linear.x:+.2f} m/s", + f"Linear Y (Strafe L/R): {twist.linear.y:+.2f} m/s", + f"Angular Z (Turn L/R): {twist.angular.z:+.2f} rad/s", + "", + "Keys: " + ", ".join([pygame.key.name(k).upper() for k in self._keys_held if k < 256]), + ] + + for text in texts: + if text: + color = (0, 255, 255) if text.startswith("Keyboard Teleop") else (255, 255, 255) + surf = self._font.render(text, True, color) + self._screen.blit(surf, (20, y_pos)) + y_pos += 30 + + if twist.linear.x != 0 or twist.linear.y != 0 or twist.angular.z != 0: + pygame.draw.circle(self._screen, (255, 0, 0), (450, 30), 15) # Red = moving + else: + pygame.draw.circle(self._screen, (0, 255, 0), (450, 30), 15) # Green = stopped + + y_pos = 280 + help_texts = [ + "WS: Move | AD: Turn | QE: Strafe", + "Shift: Boost | Ctrl: Slow", + "Space: E-Stop | ESC: Quit", + ] + for text in help_texts: + surf = self._font.render(text, True, (150, 150, 150)) + self._screen.blit(surf, (20, y_pos)) + y_pos += 25 + + pygame.display.flip() + + +keyboard_teleop = KeyboardTeleop.blueprint + +__all__ = ["KeyboardTeleop", "keyboard_teleop"] diff --git a/dimos/robot/unitree_webrtc/modular/__init__.py b/dimos/robot/unitree_webrtc/modular/__init__.py deleted file mode 100644 index d823cd796e..0000000000 --- a/dimos/robot/unitree_webrtc/modular/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from dimos.robot.unitree_webrtc.modular.connection_module import deploy_connection -from dimos.robot.unitree_webrtc.modular.navigation import deploy_navigation diff --git a/dimos/robot/unitree_webrtc/modular/connection_module.py b/dimos/robot/unitree_webrtc/modular/connection_module.py deleted file mode 100644 index bad9af22a1..0000000000 --- a/dimos/robot/unitree_webrtc/modular/connection_module.py +++ /dev/null @@ -1,338 +0,0 @@ -#!/usr/bin/env python3 - -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from dataclasses import dataclass -import functools -import logging -import os -import queue -import warnings - -from dimos_lcm.sensor_msgs import CameraInfo -import reactivex as rx -from reactivex import operators as ops -from reactivex.observable import Observable - -from dimos.agents2 import Output, Reducer, Stream, skill -from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE -from dimos.core import DimosCluster, In, LCMTransport, Module, ModuleConfig, Out, pSHMTransport, rpc -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 -from dimos.msgs.sensor_msgs.Image import Image -from dimos.msgs.std_msgs import Header -from dimos.robot.unitree_webrtc.connection import UnitreeWebRTCConnection -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger -from dimos.utils.testing import TimedSensorReplay, TimedSensorStorage - -logger = setup_logger("dimos.robot.unitree_webrtc.unitree_go2", level=logging.INFO) - -# Suppress verbose loggers -logging.getLogger("aiortc.codecs.h264").setLevel(logging.ERROR) -logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) -logging.getLogger("websockets.server").setLevel(logging.ERROR) -logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) -logging.getLogger("asyncio").setLevel(logging.ERROR) -logging.getLogger("root").setLevel(logging.WARNING) - - -# Suppress warnings -warnings.filterwarnings("ignore", message="coroutine.*was never awaited") -warnings.filterwarnings("ignore", message="H264Decoder.*failed to decode") - -image_resize_factor = 1 -originalwidth, originalheight = (1280, 720) - - -class FakeRTC(UnitreeWebRTCConnection): - dir_name = "unitree_go2_office_walk2" - - # we don't want UnitreeWebRTCConnection to init - def __init__( - self, - **kwargs, - ) -> None: - get_data(self.dir_name) - self.replay_config = { - "loop": kwargs.get("loop"), - "seek": kwargs.get("seek"), - "duration": kwargs.get("duration"), - } - - def connect(self) -> None: - pass - - def start(self) -> None: - pass - - def standup(self) -> None: - print("standup suppressed") - - def liedown(self) -> None: - print("liedown suppressed") - - @functools.cache - def lidar_stream(self): - print("lidar stream start") - lidar_store = TimedSensorReplay(f"{self.dir_name}/lidar") - return lidar_store.stream(**self.replay_config) - - @functools.cache - def odom_stream(self): - print("odom stream start") - odom_store = TimedSensorReplay(f"{self.dir_name}/odom") - return odom_store.stream(**self.replay_config) - - # we don't have raw video stream in the data set - @functools.cache - def video_stream(self): - print("video stream start") - video_store = TimedSensorReplay(f"{self.dir_name}/video") - - return video_store.stream(**self.replay_config) - - def move(self, vector: Twist, duration: float = 0.0) -> None: - pass - - def publish_request(self, topic: str, data: dict): - """Fake publish request for testing.""" - return {"status": "ok", "message": "Fake publish"} - - -@dataclass -class ConnectionModuleConfig(ModuleConfig): - ip: str | None = None - connection_type: str = "fake" # or "fake" or "mujoco" - loop: bool = False # For fake connection - speed: float = 1.0 # For fake connection - - -class ConnectionModule(Module): - camera_info: Out[CameraInfo] = None - odom: Out[PoseStamped] = None - lidar: Out[LidarMessage] = None - video: Out[Image] = None - movecmd: In[Twist] = None - - connection = None - - default_config = ConnectionModuleConfig - - # mega temporary, skill should have a limit decorator for number of - # parallel calls - video_running: bool = False - - def __init__(self, connection_type: str = "webrtc", *args, **kwargs) -> None: - self.connection_config = kwargs - self.connection_type = connection_type - Module.__init__(self, *args, **kwargs) - - @skill(stream=Stream.passive, output=Output.image, reducer=Reducer.latest) - def video_stream_tool(self) -> Image: - """implicit video stream skill, don't call this directly""" - if self.video_running: - return "video stream already running" - self.video_running = True - _queue = queue.Queue(maxsize=1) - self.connection.video_stream().subscribe(_queue.put) - - yield from iter(_queue.get, None) - - @rpc - def record(self, recording_name: str) -> None: - lidar_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/lidar") - lidar_store.save_stream(self.connection.lidar_stream()).subscribe(lambda x: x) - - odom_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/odom") - odom_store.save_stream(self.connection.odom_stream()).subscribe(lambda x: x) - - video_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/video") - video_store.save_stream(self.connection.video_stream()).subscribe(lambda x: x) - - @rpc - def start(self): - """Start the connection and subscribe to sensor streams.""" - - super().start() - - match self.connection_type: - case "webrtc": - self.connection = UnitreeWebRTCConnection(**self.connection_config) - case "fake": - self.connection = FakeRTC(**self.connection_config, seek=12.0) - case "mujoco": - from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection - - self.connection = MujocoConnection(**self.connection_config) - self.connection.start() - case _: - raise ValueError(f"Unknown connection type: {self.connection_type}") - - unsub = self.connection.odom_stream().subscribe( - lambda odom: self._publish_tf(odom) and self.odom.publish(odom) - ) - self._disposables.add(unsub) - - # Connect sensor streams to outputs - unsub = self.connection.lidar_stream().subscribe(self.lidar.publish) - self._disposables.add(unsub) - - # self.connection.lidar_stream().subscribe(lambda lidar: print("LIDAR", lidar.ts)) - # self.connection.video_stream().subscribe(lambda video: print("IMAGE", video.ts)) - # self.connection.odom_stream().subscribe(lambda odom: print("ODOM", odom.ts)) - - def resize(image: Image) -> Image: - return image.resize( - int(originalwidth / image_resize_factor), int(originalheight / image_resize_factor) - ) - - unsub = self.connection.video_stream().subscribe(self.video.publish) - self._disposables.add(unsub) - unsub = self.camera_info_stream().subscribe(self.camera_info.publish) - self._disposables.add(unsub) - unsub = self.movecmd.subscribe(self.connection.move) - self._disposables.add(unsub) - - @rpc - def stop(self) -> None: - if self.connection: - self.connection.stop() - - super().stop() - - @classmethod - def _odom_to_tf(cls, odom: PoseStamped) -> list[Transform]: - camera_link = Transform( - translation=Vector3(0.3, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ts=odom.ts, - ) - - camera_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), - frame_id="camera_link", - child_frame_id="camera_optical", - ts=odom.ts, - ) - - sensor = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="world", - child_frame_id="sensor", - ts=odom.ts, - ) - - return [ - Transform.from_pose("base_link", odom), - camera_link, - camera_optical, - sensor, - ] - - def _publish_tf(self, msg) -> None: - self.odom.publish(msg) - self.tf.publish(*self._odom_to_tf(msg)) - - @rpc - def publish_request(self, topic: str, data: dict): - """Publish a request to the WebRTC connection. - Args: - topic: The RTC topic to publish to - data: The data dictionary to publish - Returns: - The result of the publish request - """ - return self.connection.publish_request(topic, data) - - @classmethod - def _camera_info(cls) -> Out[CameraInfo]: - fx, fy, cx, cy = list( - map( - lambda x: int(x / image_resize_factor), - [819.553492, 820.646595, 625.284099, 336.808987], - ) - ) - width, height = tuple( - map( - lambda x: int(x / image_resize_factor), - [originalwidth, originalheight], - ) - ) - - # Camera matrix K (3x3) - K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] - - # No distortion coefficients for now - D = [0.0, 0.0, 0.0, 0.0, 0.0] - - # Identity rotation matrix - R = [1, 0, 0, 0, 1, 0, 0, 0, 1] - - # Projection matrix P (3x4) - P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0] - - base_msg = { - "D_length": len(D), - "height": height, - "width": width, - "distortion_model": "plumb_bob", - "D": D, - "K": K, - "R": R, - "P": P, - "binning_x": 0, - "binning_y": 0, - } - - return CameraInfo(**base_msg, header=Header("camera_optical")) - - @functools.cache - def camera_info_stream(self) -> Observable[CameraInfo]: - return rx.interval(1).pipe(ops.map(lambda _: self._camera_info())) - - -def deploy_connection(dimos: DimosCluster, **kwargs): - foxglove_bridge = dimos.deploy(FoxgloveBridge) - foxglove_bridge.start() - - connection = dimos.deploy( - ConnectionModule, - ip=os.getenv("ROBOT_IP"), - connection_type=os.getenv("CONNECTION_TYPE", "fake"), - **kwargs, - ) - - connection.odom.transport = LCMTransport("/odom", PoseStamped) - - connection.video.transport = pSHMTransport( - "/image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - - connection.lidar.transport = pSHMTransport( - "/lidar", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - - connection.video.transport = LCMTransport("/image", Image) - connection.lidar.transport = LCMTransport("/lidar", LidarMessage) - connection.movecmd.transport = LCMTransport("/cmd_vel", Twist) - connection.camera_info.transport = LCMTransport("/camera_info", CameraInfo) - - return connection diff --git a/dimos/robot/unitree_webrtc/modular/detect.py b/dimos/robot/unitree_webrtc/modular/detect.py index 46f561b109..2a266ef820 100644 --- a/dimos/robot/unitree_webrtc/modular/detect.py +++ b/dimos/robot/unitree_webrtc/modular/detect.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ def camera_info() -> CameraInfo: ) -def transform_chain(odom_frame: Odometry) -> list: +def transform_chain(odom_frame: Odometry) -> list: # type: ignore[type-arg] from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 from dimos.protocol.tf import TF @@ -97,10 +97,10 @@ def transform_chain(odom_frame: Odometry) -> list: camera_optical, ) - return tf + return tf # type: ignore[return-value] -def broadcast( +def broadcast( # type: ignore[no-untyped-def] timestamp: float, lidar_frame: LidarMessage, video_frame: Image, @@ -108,15 +108,17 @@ def broadcast( detections, annotations, ) -> None: - from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations + from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + ImageAnnotations, + ) from dimos.core import LCMTransport from dimos.msgs.geometry_msgs import PoseStamped - lidar_transport = LCMTransport("/lidar", LidarMessage) - odom_transport = LCMTransport("/odom", PoseStamped) - video_transport = LCMTransport("/image", Image) - camera_info_transport = LCMTransport("/camera_info", CameraInfo) + lidar_transport = LCMTransport("/lidar", LidarMessage) # type: ignore[var-annotated] + odom_transport = LCMTransport("/odom", PoseStamped) # type: ignore[var-annotated] + video_transport = LCMTransport("/image", Image) # type: ignore[var-annotated] + camera_info_transport = LCMTransport("/camera_info", CameraInfo) # type: ignore[var-annotated] lidar_transport.broadcast(None, lidar_frame) video_transport.broadcast(None, video_frame) @@ -129,13 +131,16 @@ def broadcast( print(video_frame) print(odom_frame) video_transport = LCMTransport("/image", Image) - annotations_transport = LCMTransport("/annotations", ImageAnnotations) + annotations_transport = LCMTransport("/annotations", ImageAnnotations) # type: ignore[var-annotated] annotations_transport.broadcast(None, annotations) -def process_data(): +def process_data(): # type: ignore[no-untyped-def] from dimos.msgs.sensor_msgs import Image - from dimos.perception.detection.module2D import Detection2DModule, build_imageannotations + from dimos.perception.detection.module2D import ( # type: ignore[attr-defined] + Detection2DModule, + build_imageannotations, + ) from dimos.robot.unitree_webrtc.type.lidar import LidarMessage from dimos.robot.unitree_webrtc.type.odometry import Odometry from dimos.utils.data import get_data @@ -152,11 +157,11 @@ def attach_frame_id(image: Image) -> Image: return image lidar_frame = lidar_store.find_closest(target, tolerance=1) - video_frame = attach_frame_id(video_store.find_closest(target, tolerance=1)) + video_frame = attach_frame_id(video_store.find_closest(target, tolerance=1)) # type: ignore[arg-type] odom_frame = odom_store.find_closest(target, tolerance=1) detector = Detection2DModule() - detections = detector.detect(video_frame) + detections = detector.detect(video_frame) # type: ignore[attr-defined] annotations = build_imageannotations(detections) data = (target, lidar_frame, video_frame, odom_frame, detections, annotations) @@ -173,7 +178,7 @@ def main() -> None: data = pickle.load(file) except FileNotFoundError: print("Processing data and creating pickle file...") - data = process_data() + data = process_data() # type: ignore[no-untyped-call] broadcast(*data) diff --git a/dimos/robot/unitree_webrtc/modular/ivan_unitree.py b/dimos/robot/unitree_webrtc/modular/ivan_unitree.py deleted file mode 100644 index e7a2bcabc8..0000000000 --- a/dimos/robot/unitree_webrtc/modular/ivan_unitree.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import time - -from dimos.agents2.spec import Model, Provider -from dimos.core import LCMTransport, start - -# from dimos.msgs.detection2d import Detection2DArray -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.msgs.sensor_msgs import Image -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.module2D import Detection2DModule -from dimos.perception.detection.reid import ReidModule -from dimos.protocol.pubsub import lcm -from dimos.robot.foxglove_bridge import FoxgloveBridge -from dimos.robot.unitree_webrtc.modular import deploy_connection -from dimos.robot.unitree_webrtc.modular.connection_module import ConnectionModule -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.robot.unitree_webrtc.unitree_go2", level=logging.INFO) - - -def detection_unitree() -> None: - dimos = start(8) - connection = deploy_connection(dimos) - - def goto(pose) -> bool: - print("NAVIGATION REQUESTED:", pose) - return True - - detector = dimos.deploy( - Detection2DModule, - # goto=goto, - camera_info=ConnectionModule._camera_info(), - ) - - detector.image.connect(connection.video) - # detector.pointcloud.connect(mapper.global_map) - # detector.pointcloud.connect(connection.lidar) - - detector.annotations.transport = LCMTransport("/annotations", ImageAnnotations) - detector.detections.transport = LCMTransport("/detections", Detection2DArray) - - # detector.detected_pointcloud_0.transport = LCMTransport("/detected/pointcloud/0", PointCloud2) - # detector.detected_pointcloud_1.transport = LCMTransport("/detected/pointcloud/1", PointCloud2) - # detector.detected_pointcloud_2.transport = LCMTransport("/detected/pointcloud/2", PointCloud2) - - detector.detected_image_0.transport = LCMTransport("/detected/image/0", Image) - detector.detected_image_1.transport = LCMTransport("/detected/image/1", Image) - detector.detected_image_2.transport = LCMTransport("/detected/image/2", Image) - # detector.scene_update.transport = LCMTransport("/scene_update", SceneUpdate) - - # reidModule = dimos.deploy(ReidModule) - - # reidModule.image.connect(connection.video) - # reidModule.detections.connect(detector.detections) - # reidModule.annotations.transport = LCMTransport("/reid/annotations", ImageAnnotations) - - # nav = deploy_navigation(dimos, connection) - - # person_tracker = dimos.deploy(PersonTracker, cameraInfo=ConnectionModule._camera_info()) - # person_tracker.image.connect(connection.video) - # person_tracker.detections.connect(detector.detections) - # person_tracker.target.transport = LCMTransport("/goal_request", PoseStamped) - - reid = dimos.deploy(ReidModule) - - reid.image.connect(connection.video) - reid.detections.connect(detector.detections) - reid.annotations.transport = LCMTransport("/reid/annotations", ImageAnnotations) - - detector.start() - # person_tracker.start() - connection.start() - reid.start() - - from dimos.agents2 import Agent - from dimos.agents2.cli.human import HumanInput - - agent = Agent( - system_prompt="You are a helpful assistant for controlling a Unitree Go2 robot.", - model=Model.GPT_4O, # Could add CLAUDE models to enum - provider=Provider.OPENAI, # Would need ANTHROPIC provider - ) - - human_input = dimos.deploy(HumanInput) - agent.register_skills(human_input) - # agent.register_skills(connection) - agent.register_skills(detector) - - bridge = FoxgloveBridge( - shm_channels=[ - "/image#sensor_msgs.Image", - "/lidar#sensor_msgs.PointCloud2", - ] - ) - # bridge = FoxgloveBridge() - time.sleep(1) - bridge.start() - - # agent.run_implicit_skill("video_stream_tool") - # agent.run_implicit_skill("human") - - # agent.start() - # agent.loop_thread() - - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - connection.stop() - logger.info("Shutting down...") - - -def main() -> None: - lcm.autoconf() - detection_unitree() - - -if __name__ == "__main__": - main() diff --git a/dimos/robot/unitree_webrtc/modular/navigation.py b/dimos/robot/unitree_webrtc/modular/navigation.py deleted file mode 100644 index 9aa03d104e..0000000000 --- a/dimos/robot/unitree_webrtc/modular/navigation.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.std_msgs import Bool, String - -from dimos.core import LCMTransport -from dimos.msgs.geometry_msgs import PoseStamped, Twist -from dimos.msgs.nav_msgs import OccupancyGrid, Path -from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator -from dimos.navigation.frontier_exploration import WavefrontFrontierExplorer -from dimos.navigation.global_planner import AstarPlanner -from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.map import Map -from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule - - -def deploy_navigation(dimos, connection): - mapper = dimos.deploy(Map, voxel_size=0.5, cost_resolution=0.05, global_publish_interval=2.5) - mapper.lidar.connect(connection.lidar) - mapper.global_map.transport = LCMTransport("/global_map", LidarMessage) - mapper.global_costmap.transport = LCMTransport("/global_costmap", OccupancyGrid) - mapper.local_costmap.transport = LCMTransport("/local_costmap", OccupancyGrid) - - """Deploy and configure navigation modules.""" - global_planner = dimos.deploy(AstarPlanner) - local_planner = dimos.deploy(HolonomicLocalPlanner) - navigator = dimos.deploy( - BehaviorTreeNavigator, - reset_local_planner=local_planner.reset, - check_goal_reached=local_planner.is_goal_reached, - ) - frontier_explorer = dimos.deploy(WavefrontFrontierExplorer) - - navigator.goal.transport = LCMTransport("/navigation_goal", PoseStamped) - navigator.goal_request.transport = LCMTransport("/goal_request", PoseStamped) - navigator.goal_reached.transport = LCMTransport("/goal_reached", Bool) - navigator.navigation_state.transport = LCMTransport("/navigation_state", String) - navigator.global_costmap.transport = LCMTransport("/global_costmap", OccupancyGrid) - global_planner.path.transport = LCMTransport("/global_path", Path) - local_planner.cmd_vel.transport = LCMTransport("/cmd_vel", Twist) - frontier_explorer.goal_request.transport = LCMTransport("/goal_request", PoseStamped) - frontier_explorer.goal_reached.transport = LCMTransport("/goal_reached", Bool) - frontier_explorer.explore_cmd.transport = LCMTransport("/explore_cmd", Bool) - frontier_explorer.stop_explore_cmd.transport = LCMTransport("/stop_explore_cmd", Bool) - - global_planner.target.connect(navigator.goal) - - global_planner.global_costmap.connect(mapper.global_costmap) - global_planner.odom.connect(connection.odom) - - local_planner.path.connect(global_planner.path) - local_planner.local_costmap.connect(mapper.local_costmap) - local_planner.odom.connect(connection.odom) - - connection.movecmd.connect(local_planner.cmd_vel) - - navigator.odom.connect(connection.odom) - - frontier_explorer.costmap.connect(mapper.global_costmap) - frontier_explorer.odometry.connect(connection.odom) - websocket_vis = dimos.deploy(WebsocketVisModule, port=7779) - websocket_vis.click_goal.transport = LCMTransport("/goal_request", PoseStamped) - - websocket_vis.robot_pose.connect(connection.odom) - websocket_vis.path.connect(global_planner.path) - websocket_vis.global_costmap.connect(mapper.global_costmap) - - mapper.start() - global_planner.start() - local_planner.start() - navigator.start() - websocket_vis.start() - - return { - "mapper": mapper, - "global_planner": global_planner, - "local_planner": local_planner, - "navigator": navigator, - "frontier_explorer": frontier_explorer, - "websocket_vis": websocket_vis, - } diff --git a/dimos/robot/unitree_webrtc/mujoco_connection.py b/dimos/robot/unitree_webrtc/mujoco_connection.py index b68097ea33..586f4d0ea7 100644 --- a/dimos/robot/unitree_webrtc/mujoco_connection.py +++ b/dimos/robot/unitree_webrtc/mujoco_connection.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,51 +15,122 @@ # limitations under the License. -import atexit +import base64 +from collections.abc import Callable import functools -import logging +import json +import pickle +import subprocess +import sys import threading import time +from typing import Any, TypeVar +import numpy as np +from numpy.typing import NDArray from reactivex import Observable +from reactivex.abc import ObserverBase, SchedulerBase +from reactivex.disposable import Disposable -from dimos.mapping.types import LatLon -from dimos.msgs.geometry_msgs import Twist +from dimos.core.global_config import GlobalConfig +from dimos.msgs.geometry_msgs import Quaternion, Twist, Vector3 from dimos.msgs.sensor_msgs import Image +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.simulation.mujoco.constants import LAUNCHER_PATH, LIDAR_FPS, VIDEO_FPS +from dimos.simulation.mujoco.shared_memory import ShmWriter from dimos.utils.data import get_data +from dimos.utils.logging_config import setup_logger -LIDAR_FREQUENCY = 10 ODOM_FREQUENCY = 50 -VIDEO_FREQUENCY = 30 -logger = logging.getLogger(__name__) +logger = setup_logger() + +T = TypeVar("T") class MujocoConnection: - def __init__(self, *args, **kwargs) -> None: + """MuJoCo simulator connection that runs in a separate subprocess.""" + + def __init__(self, global_config: GlobalConfig) -> None: try: - from dimos.simulation.mujoco.mujoco import MujocoThread + import mujoco except ImportError: raise ImportError("'mujoco' is not installed. Use `pip install -e .[sim]`") + + # Pre-download the mujoco_sim data. get_data("mujoco_sim") - self.mujoco_thread = MujocoThread() + + # Trigger the download of the mujoco_menajerie package. This is so it + # doesn't trigger in the mujoco process where it can time out. + import mujoco_playground + + self.global_config = global_config + self.process: subprocess.Popen[bytes] | None = None + self.shm_data: ShmWriter | None = None + self._last_video_seq = 0 + self._last_odom_seq = 0 + self._last_lidar_seq = 0 + self._stop_timer: threading.Timer | None = None + self._stream_threads: list[threading.Thread] = [] self._stop_events: list[threading.Event] = [] self._is_cleaned_up = False - # Register cleanup on exit - atexit.register(self.stop) - def start(self) -> None: - self.mujoco_thread.start() + self.shm_data = ShmWriter() + + config_pickle = base64.b64encode(pickle.dumps(self.global_config)).decode("ascii") + shm_names_json = json.dumps(self.shm_data.shm.to_names()) + + # Launch the subprocess + try: + # mjpython must be used macOS (because of launch_passive inside mujoco_process.py) + executable = sys.executable if sys.platform != "darwin" else "mjpython" + self.process = subprocess.Popen( + [executable, str(LAUNCHER_PATH), config_pickle, shm_names_json], + ) + + except Exception as e: + self.shm_data.cleanup() + raise RuntimeError(f"Failed to start MuJoCo subprocess: {e}") from e + + # Wait for process to be ready + ready_timeout = 300.0 + start_time = time.time() + assert self.process is not None + while time.time() - start_time < ready_timeout: + if self.process.poll() is not None: + exit_code = self.process.returncode + self.stop() + raise RuntimeError(f"MuJoCo process failed to start (exit code {exit_code})") + if self.shm_data.is_ready(): + logger.info("MuJoCo process started successfully") + return + time.sleep(0.1) + + # Timeout + self.stop() + raise RuntimeError("MuJoCo process failed to start (timeout)") def stop(self) -> None: - """Clean up all resources. Can be called multiple times safely.""" if self._is_cleaned_up: return self._is_cleaned_up = True + # clean up open file descriptors + if self.process: + if self.process.stderr: + self.process.stderr.close() + if self.process.stdout: + self.process.stdout.close() + + # Cancel any pending timers + if self._stop_timer: + self._stop_timer.cancel() + self._stop_timer = None + # Stop all stream threads for stop_event in self._stop_events: stop_event.set() @@ -71,135 +142,97 @@ def stop(self) -> None: if thread.is_alive(): logger.warning(f"Stream thread {thread.name} did not stop gracefully") - # Clean up the MuJoCo thread - if hasattr(self, "mujoco_thread") and self.mujoco_thread: - self.mujoco_thread.cleanup() - - # Clear references - self._stream_threads.clear() - self._stop_events.clear() - - # Clear cached methods to prevent memory leaks - if hasattr(self, "lidar_stream"): - self.lidar_stream.cache_clear() - if hasattr(self, "odom_stream"): - self.odom_stream.cache_clear() - if hasattr(self, "video_stream"): - self.video_stream.cache_clear() - - def standup(self) -> None: - print("standup supressed") - - def liedown(self) -> None: - print("liedown supressed") + # Signal subprocess to stop + if self.shm_data: + self.shm_data.signal_stop() - @functools.cache - def lidar_stream(self): - def on_subscribe(observer, scheduler): - if self._is_cleaned_up: - observer.on_completed() - return lambda: None - - stop_event = threading.Event() - self._stop_events.append(stop_event) - - def run() -> None: + # Wait for process to finish + if self.process: + try: + self.process.terminate() try: - while not stop_event.is_set() and not self._is_cleaned_up: - lidar_to_publish = self.mujoco_thread.get_lidar_message() - - if lidar_to_publish: - observer.on_next(lidar_to_publish) - - time.sleep(1 / LIDAR_FREQUENCY) - except Exception as e: - logger.error(f"Lidar stream error: {e}") - finally: - observer.on_completed() - - thread = threading.Thread(target=run, daemon=True) - self._stream_threads.append(thread) - thread.start() + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + logger.warning("MuJoCo process did not stop gracefully, killing") + self.process.kill() + self.process.wait(timeout=2) + except Exception as e: + logger.error(f"Error stopping MuJoCo process: {e}") - def dispose() -> None: - stop_event.set() + self.process = None - return dispose - - return Observable(on_subscribe) + # Clean up shared memory + if self.shm_data: + self.shm_data.cleanup() + self.shm_data = None - @functools.cache - def odom_stream(self): - def on_subscribe(observer, scheduler): - if self._is_cleaned_up: - observer.on_completed() - return lambda: None + # Clear references + self._stream_threads.clear() + self._stop_events.clear() - stop_event = threading.Event() - self._stop_events.append(stop_event) + self.lidar_stream.cache_clear() + self.odom_stream.cache_clear() + self.video_stream.cache_clear() - def run() -> None: - try: - while not stop_event.is_set() and not self._is_cleaned_up: - odom_to_publish = self.mujoco_thread.get_odom_message() - if odom_to_publish: - observer.on_next(odom_to_publish) + def standup(self) -> bool: + return True - time.sleep(1 / ODOM_FREQUENCY) - except Exception as e: - logger.error(f"Odom stream error: {e}") - finally: - observer.on_completed() + def liedown(self) -> bool: + return True - thread = threading.Thread(target=run, daemon=True) - self._stream_threads.append(thread) - thread.start() + def get_video_frame(self) -> NDArray[Any] | None: + if self.shm_data is None: + return None - def dispose() -> None: - stop_event.set() + frame, seq = self.shm_data.read_video() + if seq > self._last_video_seq: + self._last_video_seq = seq + return frame - return dispose + return None - return Observable(on_subscribe) + def get_odom_message(self) -> Odometry | None: + if self.shm_data is None: + return None - @functools.cache - def gps_stream(self): - def on_subscribe(observer, scheduler): - if self._is_cleaned_up: - observer.on_completed() - return lambda: None + odom_data, seq = self.shm_data.read_odom() + if seq > self._last_odom_seq and odom_data is not None: + self._last_odom_seq = seq + pos, quat_wxyz, timestamp = odom_data - stop_event = threading.Event() - self._stop_events.append(stop_event) + # Convert quaternion from (w,x,y,z) to (x,y,z,w) for ROS/Dimos + orientation = Quaternion(quat_wxyz[1], quat_wxyz[2], quat_wxyz[3], quat_wxyz[0]) - def run() -> None: - lat = 37.78092426217621 - lon = -122.40682866540769 - try: - while not stop_event.is_set() and not self._is_cleaned_up: - observer.on_next(LatLon(lat=lat, lon=lon)) - lat += 0.00001 - time.sleep(1) - finally: - observer.on_completed() + return Odometry( + position=Vector3(pos[0], pos[1], pos[2]), + orientation=orientation, + ts=timestamp, + frame_id="world", + ) - thread = threading.Thread(target=run, daemon=True) - self._stream_threads.append(thread) - thread.start() + return None - def dispose() -> None: - stop_event.set() + def get_lidar_message(self) -> LidarMessage | None: + if self.shm_data is None: + return None - return dispose + lidar_msg, seq = self.shm_data.read_lidar() + if seq > self._last_lidar_seq and lidar_msg is not None: + self._last_lidar_seq = seq + return lidar_msg - return Observable(on_subscribe) + return None - @functools.cache - def video_stream(self): - def on_subscribe(observer, scheduler): + def _create_stream( + self, + getter: Callable[[], T | None], + frequency: float, + stream_name: str, + ) -> Observable[T]: + def on_subscribe(observer: ObserverBase[T], _scheduler: SchedulerBase | None) -> Disposable: if self._is_cleaned_up: observer.on_completed() - return lambda: None + return Disposable(lambda: None) stop_event = threading.Event() self._stop_events.append(stop_event) @@ -207,13 +240,12 @@ def on_subscribe(observer, scheduler): def run() -> None: try: while not stop_event.is_set() and not self._is_cleaned_up: - with self.mujoco_thread.pixels_lock: - if self.mujoco_thread.shared_pixels is not None: - img = Image.from_numpy(self.mujoco_thread.shared_pixels.copy()) - observer.on_next(img) - time.sleep(1 / VIDEO_FREQUENCY) + data = getter() + if data is not None: + observer.on_next(data) + time.sleep(1 / frequency) except Exception as e: - logger.error(f"Video stream error: {e}") + logger.error(f"{stream_name} stream error: {e}") finally: observer.on_completed() @@ -224,13 +256,50 @@ def run() -> None: def dispose() -> None: stop_event.set() - return dispose + return Disposable(dispose) return Observable(on_subscribe) - def move(self, twist: Twist, duration: float = 0.0) -> None: - if not self._is_cleaned_up: - self.mujoco_thread.move(twist, duration) + @functools.cache + def lidar_stream(self) -> Observable[LidarMessage]: + return self._create_stream(self.get_lidar_message, LIDAR_FPS, "Lidar") - def publish_request(self, topic: str, data: dict) -> None: - pass + @functools.cache + def odom_stream(self) -> Observable[Odometry]: + return self._create_stream(self.get_odom_message, ODOM_FREQUENCY, "Odom") + + @functools.cache + def video_stream(self) -> Observable[Image]: + def get_video_as_image() -> Image | None: + frame = self.get_video_frame() + return Image.from_numpy(frame) if frame is not None else None + + return self._create_stream(get_video_as_image, VIDEO_FPS, "Video") + + def move(self, twist: Twist, duration: float = 0.0) -> bool: + if self._is_cleaned_up or self.shm_data is None: + return True + + linear = np.array([twist.linear.x, twist.linear.y, twist.linear.z], dtype=np.float32) + angular = np.array([twist.angular.x, twist.angular.y, twist.angular.z], dtype=np.float32) + self.shm_data.write_command(linear, angular) + + if duration > 0: + if self._stop_timer: + self._stop_timer.cancel() + + def stop_movement() -> None: + if self.shm_data: + self.shm_data.write_command( + np.zeros(3, dtype=np.float32), np.zeros(3, dtype=np.float32) + ) + self._stop_timer = None + + self._stop_timer = threading.Timer(duration, stop_movement) + self._stop_timer.daemon = True + self._stop_timer.start() + return True + + def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: + print(f"publishing request, topic={topic}, data={data}") + return {} diff --git a/dimos/robot/unitree_webrtc/params/front_camera_720.yaml b/dimos/robot/unitree_webrtc/params/front_camera_720.yaml index eb09710667..0030d5fc6c 100644 --- a/dimos/robot/unitree_webrtc/params/front_camera_720.yaml +++ b/dimos/robot/unitree_webrtc/params/front_camera_720.yaml @@ -23,4 +23,4 @@ projection_matrix: cols: 4 data: [651.42609, 0. , 633.16224, 0. , 0. , 804.93951, 373.8537 , 0. , - 0. , 0. , 1. , 0. ] \ No newline at end of file + 0. , 0. , 1. , 0. ] diff --git a/dimos/robot/unitree_webrtc/params/sim_camera.yaml b/dimos/robot/unitree_webrtc/params/sim_camera.yaml index 8fc1574953..6a5ac3e6d8 100644 --- a/dimos/robot/unitree_webrtc/params/sim_camera.yaml +++ b/dimos/robot/unitree_webrtc/params/sim_camera.yaml @@ -23,4 +23,4 @@ projection_matrix: cols: 4 data: [277., 0. , 160. , 0. , 0. , 277., 120. , 0. , - 0. , 0. , 1. , 0. ] \ No newline at end of file + 0. , 0. , 1. , 0. ] diff --git a/dimos/robot/unitree_webrtc/rosnav.py b/dimos/robot/unitree_webrtc/rosnav.py index bd91fafb90..3244ecfd05 100644 --- a/dimos/robot/unitree_webrtc/rosnav.py +++ b/dimos/robot/unitree_webrtc/rosnav.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,16 +22,17 @@ from dimos.msgs.std_msgs.Bool import Bool from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.robot.unitree_webrtc.nav_bot", level=logging.INFO) +logger = setup_logger(level=logging.INFO) +# TODO: Remove, deprecated class NavigationModule(Module): - goal_pose: Out[PoseStamped] = None - goal_reached: In[Bool] = None - cancel_goal: Out[Bool] = None - joy: Out[Joy] = None + goal_pose: Out[PoseStamped] + goal_reached: In[Bool] + cancel_goal: Out[Bool] + joy: Out[Joy] - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] """Initialize NavigationModule.""" Module.__init__(self, *args, **kwargs) self.goal_reach = None @@ -45,7 +46,7 @@ def start(self) -> None: def _on_goal_reached(self, msg: Bool) -> None: """Handle goal reached status messages.""" - self.goal_reach = msg.data + self.goal_reach = msg.data # type: ignore[assignment] def _set_autonomy_mode(self) -> None: """ diff --git a/dimos/robot/unitree_webrtc/test_unitree_go2_integration.py b/dimos/robot/unitree_webrtc/test_unitree_go2_integration.py deleted file mode 100644 index 7acdfc1980..0000000000 --- a/dimos/robot/unitree_webrtc/test_unitree_go2_integration.py +++ /dev/null @@ -1,200 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio - -import pytest - -from dimos import core -from dimos.core import Module, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Twist, Vector3 -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.msgs.sensor_msgs import Image -from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator -from dimos.navigation.frontier_exploration import WavefrontFrontierExplorer -from dimos.navigation.global_planner import AstarPlanner -from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner -from dimos.protocol import pubsub -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.map import Map -from dimos.robot.unitree_webrtc.unitree_go2 import ConnectionModule -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_unitree_go2_integration") - -pubsub.lcm.autoconf() - - -class MovementControlModule(Module): - """Simple module to send movement commands for testing.""" - - movecmd: Out[Twist] = None - - def __init__(self) -> None: - super().__init__() - self.commands_sent = [] - - @rpc - def send_move_command(self, x: float, y: float, yaw: float) -> None: - """Send a movement command.""" - cmd = Twist(linear=Vector3(x, y, 0.0), angular=Vector3(0.0, 0.0, yaw)) - self.movecmd.publish(cmd) - self.commands_sent.append(cmd) - logger.info(f"Sent move command: x={x}, y={y}, yaw={yaw}") - - @rpc - def get_command_count(self) -> int: - """Get number of commands sent.""" - return len(self.commands_sent) - - -@pytest.mark.module -class TestUnitreeGo2CoreModules: - @pytest.mark.asyncio - async def test_unitree_go2_navigation_stack(self) -> None: - """Test UnitreeGo2 core navigation modules without perception/visualization.""" - - # Start Dask - dimos = core.start(4) - - try: - # Deploy ConnectionModule with playback mode (uses test data) - connection = dimos.deploy( - ConnectionModule, - ip="127.0.0.1", # IP doesn't matter for playback - playback=True, # Enable playback mode - ) - - # Configure LCM transports - connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) - connection.odom.transport = core.LCMTransport("/odom", PoseStamped) - connection.video.transport = core.LCMTransport("/video", Image) - - # Deploy Map module - mapper = dimos.deploy(Map, voxel_size=0.5, global_publish_interval=2.5) - mapper.global_map.transport = core.LCMTransport("/global_map", LidarMessage) - mapper.global_costmap.transport = core.LCMTransport("/global_costmap", OccupancyGrid) - mapper.local_costmap.transport = core.LCMTransport("/local_costmap", OccupancyGrid) - mapper.lidar.connect(connection.lidar) - - # Deploy navigation stack - global_planner = dimos.deploy(AstarPlanner) - local_planner = dimos.deploy(HolonomicLocalPlanner) - navigator = dimos.deploy(BehaviorTreeNavigator, local_planner=local_planner) - - # Set up transports first - from dimos_lcm.std_msgs import Bool - - from dimos.msgs.nav_msgs import Path - - navigator.goal.transport = core.LCMTransport("/navigation_goal", PoseStamped) - navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) - navigator.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) - navigator.global_costmap.transport = core.LCMTransport("/global_costmap", OccupancyGrid) - global_planner.path.transport = core.LCMTransport("/global_path", Path) - local_planner.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - - # Configure navigation connections - global_planner.target.connect(navigator.goal) - global_planner.global_costmap.connect(mapper.global_costmap) - global_planner.odom.connect(connection.odom) - - local_planner.path.connect(global_planner.path) - local_planner.local_costmap.connect(mapper.local_costmap) - local_planner.odom.connect(connection.odom) - - connection.movecmd.connect(local_planner.cmd_vel) - navigator.odom.connect(connection.odom) - - # Deploy movement control module for testing - movement = dimos.deploy(MovementControlModule) - movement.movecmd.transport = core.LCMTransport("/test_move", Twist) - connection.movecmd.connect(movement.movecmd) - - # Start all modules - connection.start() - mapper.start() - global_planner.start() - local_planner.start() - navigator.start() - - logger.info("All core modules started") - - # Wait for initialization - await asyncio.sleep(3) - - # Test movement commands - movement.send_move_command(0.5, 0.0, 0.0) - await asyncio.sleep(0.5) - - movement.send_move_command(0.0, 0.0, 0.3) - await asyncio.sleep(0.5) - - movement.send_move_command(0.0, 0.0, 0.0) - await asyncio.sleep(0.5) - - # Check commands were sent - cmd_count = movement.get_command_count() - assert cmd_count == 3, f"Expected 3 commands, got {cmd_count}" - logger.info(f"Successfully sent {cmd_count} movement commands") - - # Test navigation - target_pose = PoseStamped( - frame_id="world", - position=Vector3(2.0, 1.0, 0.0), - orientation=Quaternion(0, 0, 0, 1), - ) - - # Set navigation goal (non-blocking) - try: - navigator.set_goal(target_pose) - logger.info("Navigation goal set") - except Exception as e: - logger.warning(f"Navigation goal setting failed: {e}") - - await asyncio.sleep(2) - - # Cancel navigation - navigator.cancel_goal() - logger.info("Navigation cancelled") - - # Test frontier exploration - frontier_explorer = dimos.deploy(WavefrontFrontierExplorer) - frontier_explorer.costmap.connect(mapper.global_costmap) - frontier_explorer.odometry.connect(connection.odom) - frontier_explorer.goal_request.transport = core.LCMTransport( - "/frontier_goal", PoseStamped - ) - frontier_explorer.goal_reached.transport = core.LCMTransport("/frontier_reached", Bool) - frontier_explorer.start() - - # Try to start exploration - result = frontier_explorer.explore() - logger.info(f"Exploration started: {result}") - - await asyncio.sleep(2) - - # Stop exploration - frontier_explorer.stop_exploration() - logger.info("Exploration stopped") - - logger.info("All core navigation tests passed!") - - finally: - dimos.close() - logger.info("Closed Dask cluster") - - -if __name__ == "__main__": - pytest.main(["-v", "-s", __file__]) diff --git a/dimos/robot/unitree_webrtc/testing/helpers.py b/dimos/robot/unitree_webrtc/testing/helpers.py index 5159deab4c..aaf188dbc3 100644 --- a/dimos/robot/unitree_webrtc/testing/helpers.py +++ b/dimos/robot/unitree_webrtc/testing/helpers.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import time from typing import Any, Protocol -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] from reactivex.observable import Observable color1 = [1, 0.706, 0] @@ -50,13 +50,13 @@ def benchmark(calls: int, targetf: Callable[[], int | None]) -> float: class ReturnsDrawable(Protocol): - def o3d_geometry(self) -> O3dDrawable: ... + def o3d_geometry(self) -> O3dDrawable: ... # type: ignore[valid-type] Drawable = O3dDrawable | ReturnsDrawable -def show3d(*components: Iterable[Drawable], title: str = "open3d") -> o3d.visualization.Visualizer: +def show3d(*components: Iterable[Drawable], title: str = "open3d") -> o3d.visualization.Visualizer: # type: ignore[valid-type] vis = o3d.visualization.Visualizer() vis.create_window(window_name=title) for component in components: @@ -99,7 +99,7 @@ def show3d_stream( q: queue.Queue[Any] = queue.Queue() stop_flag = threading.Event() - def on_next(geometry: O3dDrawable) -> None: + def on_next(geometry: O3dDrawable) -> None: # type: ignore[valid-type] q.put(geometry) def on_error(e: Exception) -> None: @@ -116,9 +116,9 @@ def on_completed() -> None: on_completed=on_completed, ) - def geom(geometry: Drawable) -> O3dDrawable: + def geom(geometry: Drawable) -> O3dDrawable: # type: ignore[valid-type] """Extracts the Open3D geometry from the given object.""" - return geometry.o3d_geometry if hasattr(geometry, "o3d_geometry") else geometry + return geometry.o3d_geometry if hasattr(geometry, "o3d_geometry") else geometry # type: ignore[attr-defined, no-any-return] # Wait for the first geometry first_geometry = None diff --git a/dimos/robot/unitree_webrtc/testing/mock.py b/dimos/robot/unitree_webrtc/testing/mock.py index 20eb357cc0..34ca390842 100644 --- a/dimos/robot/unitree_webrtc/testing/mock.py +++ b/dimos/robot/unitree_webrtc/testing/mock.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ def iterate(self) -> Iterator[LidarMessage]: filename = os.path.splitext(basename)[0] yield self.load_one(filename) - def stream(self, rate_hz: float = 10.0): + def stream(self, rate_hz: float = 10.0): # type: ignore[no-untyped-def] sleep_time = 1.0 / rate_hz return from_iterable(self.iterate()).pipe( @@ -67,14 +67,14 @@ def stream(self, rate_hz: float = 10.0): ops.map(lambda x: x[0] if isinstance(x, tuple) else x), ) - def save_stream(self, observable: Observable[LidarMessage]): - return observable.pipe(ops.map(lambda frame: self.save_one(frame))) + def save_stream(self, observable: Observable[LidarMessage]): # type: ignore[no-untyped-def] + return observable.pipe(ops.map(lambda frame: self.save_one(frame))) # type: ignore[no-untyped-call] - def save(self, *frames): - [self.save_one(frame) for frame in frames] + def save(self, *frames): # type: ignore[no-untyped-def] + [self.save_one(frame) for frame in frames] # type: ignore[no-untyped-call] return self.cnt - def save_one(self, frame): + def save_one(self, frame): # type: ignore[no-untyped-def] file_name = f"/lidar_data_{self.cnt:03d}.pickle" full_path = self.root + file_name diff --git a/dimos/robot/unitree_webrtc/testing/multimock.py b/dimos/robot/unitree_webrtc/testing/multimock.py deleted file mode 100644 index eab10e14bb..0000000000 --- a/dimos/robot/unitree_webrtc/testing/multimock.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Multimock – lightweight persistence & replay helper built on RxPy. - -A directory of pickle files acts as a tiny append-only log of (timestamp, data) -pairs. You can: - • save() / consume(): append new frames - • iterate(): read them back lazily - • interval_stream(): emit at a fixed cadence - • stream(): replay with original timing (optionally scaled) - -The implementation keeps memory usage constant by relying on reactive -operators instead of pre-materialising lists. Timing is reproduced via -`rx.timer`, and drift is avoided with `concat_map`. -""" - -from __future__ import annotations - -import glob -import os -import pickle -import time -from typing import TYPE_CHECKING, Any, Generic, TypeVar - -from reactivex import from_iterable, interval, operators as ops - -from dimos.robot.unitree_webrtc.type.timeseries import TEvent, Timeseries -from dimos.utils.threadpool import get_scheduler - -if TYPE_CHECKING: - import builtins - from collections.abc import Iterator - - from reactivex.observable import Observable - from reactivex.scheduler import ThreadPoolScheduler - -T = TypeVar("T") - - -class Multimock(Generic[T], Timeseries[TEvent[T]]): - """Persist frames as pickle files and replay them with RxPy.""" - - def __init__(self, root: str = "office", file_prefix: str = "msg") -> None: - current_dir = os.path.dirname(os.path.abspath(__file__)) - self.root = os.path.join(current_dir, f"multimockdata/{root}") - self.file_prefix = file_prefix - - os.makedirs(self.root, exist_ok=True) - self.cnt: int = 0 - - def save(self, *frames: Any) -> int: - """Persist one or more frames; returns the new counter value.""" - for frame in frames: - self.save_one(frame) - return self.cnt - - def save_one(self, frame: Any) -> int: - """Persist a single frame and return the running count.""" - file_name = f"/{self.file_prefix}_{self.cnt:03d}.pickle" - full_path = os.path.join(self.root, file_name.lstrip("/")) - self.cnt += 1 - - if os.path.isfile(full_path): - raise FileExistsError(f"file {full_path} exists") - - # Optional convinience magic to extract raw messages from advanced types - # trying to deprecate for now - # if hasattr(frame, "raw_msg"): - # frame = frame.raw_msg # type: ignore[attr-defined] - - with open(full_path, "wb") as f: - pickle.dump([time.time(), frame], f) - - return self.cnt - - def load(self, *names: int | str) -> builtins.list[tuple[float, T]]: - """Load multiple items by name or index.""" - return list(map(self.load_one, names)) - - def load_one(self, name: int | str) -> TEvent[T]: - """Load a single item by name or index.""" - if isinstance(name, int): - file_name = f"/{self.file_prefix}_{name:03d}.pickle" - else: - file_name = f"/{name}.pickle" - - full_path = os.path.join(self.root, file_name.lstrip("/")) - - with open(full_path, "rb") as f: - timestamp, data = pickle.load(f) - - return TEvent(timestamp, data) - - def iterate(self) -> Iterator[TEvent[T]]: - """Yield all persisted TEvent(timestamp, data) pairs lazily in order.""" - pattern = os.path.join(self.root, f"{self.file_prefix}_*.pickle") - for file_path in sorted(glob.glob(pattern)): - with open(file_path, "rb") as f: - timestamp, data = pickle.load(f) - yield TEvent(timestamp, data) - - def list(self) -> builtins.list[TEvent[T]]: - return list(self.iterate()) - - def interval_stream(self, rate_hz: float = 10.0) -> Observable[T]: - """Emit frames at a fixed rate, ignoring recorded timing.""" - sleep_time = 1.0 / rate_hz - return from_iterable(self.iterate()).pipe( - ops.zip(interval(sleep_time)), - ops.map(lambda pair: pair[1]), # keep only the frame - ) - - def stream( - self, - replay_speed: float = 1.0, - scheduler: ThreadPoolScheduler | None = None, - ) -> Observable[T]: - def _generator(): - prev_ts: float | None = None - for event in self.iterate(): - if prev_ts is not None: - delay = (event.ts - prev_ts).total_seconds() / replay_speed - time.sleep(delay) - prev_ts = event.ts - yield event.data - - return from_iterable(_generator(), scheduler=scheduler or get_scheduler()) - - def consume(self, observable: Observable[Any]) -> Observable[int]: - """Side-effect: save every frame that passes through.""" - return observable.pipe(ops.map(self.save_one)) - - def __iter__(self) -> Iterator[TEvent[T]]: - """Allow iteration over the Multimock instance to yield TEvent(timestamp, data) pairs.""" - return self.iterate() diff --git a/dimos/robot/unitree_webrtc/testing/test_actors.py b/dimos/robot/unitree_webrtc/testing/test_actors.py index 4612f45a79..7e79ca24cc 100644 --- a/dimos/robot/unitree_webrtc/testing/test_actors.py +++ b/dimos/robot/unitree_webrtc/testing/test_actors.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/robot/unitree_webrtc/testing/test_mock.py b/dimos/robot/unitree_webrtc/testing/test_mock.py index 73eeef05ba..0765894409 100644 --- a/dimos/robot/unitree_webrtc/testing/test_mock.py +++ b/dimos/robot/unitree_webrtc/testing/test_mock.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/robot/unitree_webrtc/testing/test_tooling.py b/dimos/robot/unitree_webrtc/testing/test_tooling.py index 38a3dba593..456d600879 100644 --- a/dimos/robot/unitree_webrtc/testing/test_tooling.py +++ b/dimos/robot/unitree_webrtc/testing/test_tooling.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,49 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys import time -from dotenv import load_dotenv import pytest from dimos.robot.unitree_webrtc.type.lidar import LidarMessage from dimos.robot.unitree_webrtc.type.odometry import Odometry from dimos.utils.reactive import backpressure -from dimos.utils.testing import TimedSensorReplay, TimedSensorStorage - - -@pytest.mark.tool -def test_record_all() -> None: - from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 - - load_dotenv() - robot = UnitreeGo2(ip=os.getenv("ROBOT_IP"), mode="ai") - - print("Robot is standing up...") - - robot.standup() - - lidar_store = TimedSensorStorage("unitree/lidar") - odom_store = TimedSensorStorage("unitree/odom") - video_store = TimedSensorStorage("unitree/video") - - lidar_store.save_stream(robot.raw_lidar_stream()).subscribe(print) - odom_store.save_stream(robot.raw_odom_stream()).subscribe(print) - video_store.save_stream(robot.video_stream()).subscribe(print) - - print("Recording, CTRL+C to kill") - - try: - while True: - time.sleep(0.1) - - except KeyboardInterrupt: - print("Robot is lying down...") - robot.liedown() - print("Exit") - sys.exit(0) +from dimos.utils.testing import TimedSensorReplay @pytest.mark.tool diff --git a/dimos/robot/unitree_webrtc/type/lidar.py b/dimos/robot/unitree_webrtc/type/lidar.py index ea1c9fe7e2..b598373a09 100644 --- a/dimos/robot/unitree_webrtc/type/lidar.py +++ b/dimos/robot/unitree_webrtc/type/lidar.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ from typing import TypedDict import numpy as np -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] from dimos.msgs.geometry_msgs import Vector3 from dimos.msgs.sensor_msgs import PointCloud2 @@ -24,7 +24,7 @@ class RawLidarPoints(TypedDict): - points: np.ndarray # Shape (N, 3) array of 3D points [x, y, z] + points: np.ndarray # type: ignore[type-arg] # Shape (N, 3) array of 3D points [x, y, z] class RawLidarData(TypedDict): @@ -53,18 +53,18 @@ class LidarMessage(PointCloud2): raw_msg: RawLidarMsg | None # _costmap: Optional[Costmap] = None # TODO: Fix after costmap migration - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__( pointcloud=kwargs.get("pointcloud"), ts=kwargs.get("ts"), frame_id="world", ) - self.origin = kwargs.get("origin") + self.origin = kwargs.get("origin") # type: ignore[assignment] self.resolution = kwargs.get("resolution", 0.05) @classmethod - def from_msg(cls: "LidarMessage", raw_message: RawLidarMsg, **kwargs) -> "LidarMessage": + def from_msg(cls: type["LidarMessage"], raw_message: RawLidarMsg, **kwargs) -> "LidarMessage": # type: ignore[no-untyped-def] data = raw_message["data"] points = data["data"]["points"] pointcloud = o3d.geometry.PointCloud() @@ -89,11 +89,11 @@ def from_msg(cls: "LidarMessage", raw_message: RawLidarMsg, **kwargs) -> "LidarM def __repr__(self) -> str: return f"LidarMessage(ts={to_human_readable(self.ts)}, origin={self.origin}, resolution={self.resolution}, {self.pointcloud})" - def __iadd__(self, other: "LidarMessage") -> "LidarMessage": + def __iadd__(self, other: "LidarMessage") -> "LidarMessage": # type: ignore[override] self.pointcloud += other.pointcloud return self - def __add__(self, other: "LidarMessage") -> "LidarMessage": + def __add__(self, other: "LidarMessage") -> "LidarMessage": # type: ignore[override] # Determine which message is more recent if self.ts >= other.ts: ts = self.ts @@ -105,7 +105,7 @@ def __add__(self, other: "LidarMessage") -> "LidarMessage": resolution = other.resolution # Return a new LidarMessage with combined data - return LidarMessage( + return LidarMessage( # type: ignore[attr-defined, no-any-return] ts=ts, origin=origin, resolution=resolution, @@ -113,7 +113,7 @@ def __add__(self, other: "LidarMessage") -> "LidarMessage": ).estimate_normals() @property - def o3d_geometry(self): + def o3d_geometry(self): # type: ignore[no-untyped-def] return self.pointcloud # TODO: Fix after costmap migration diff --git a/dimos/robot/unitree_webrtc/type/lowstate.py b/dimos/robot/unitree_webrtc/type/lowstate.py index c50504135c..3e7926424a 100644 --- a/dimos/robot/unitree_webrtc/type/lowstate.py +++ b/dimos/robot/unitree_webrtc/type/lowstate.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/robot/unitree_webrtc/type/map.py b/dimos/robot/unitree_webrtc/type/map.py index ea02ae47d0..3bc1e61aef 100644 --- a/dimos/robot/unitree_webrtc/type/map.py +++ b/dimos/robot/unitree_webrtc/type/map.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,35 +12,41 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path import time +from typing import Any -import numpy as np -import open3d as o3d +import open3d as o3d # type: ignore[import-untyped] from reactivex import interval from reactivex.disposable import Disposable -from dimos.core import In, Module, Out, rpc +from dimos.core import DimosCluster, In, LCMTransport, Module, Out, rpc from dimos.core.global_config import GlobalConfig +from dimos.mapping.pointclouds.accumulators.general import GeneralPointCloudAccumulator +from dimos.mapping.pointclouds.accumulators.protocol import PointCloudAccumulator +from dimos.mapping.pointclouds.occupancy import general_occupancy from dimos.msgs.nav_msgs import OccupancyGrid from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.robot.unitree.connection.go2 import Go2ConnectionProtocol from dimos.robot.unitree_webrtc.type.lidar import LidarMessage class Map(Module): - lidar: In[LidarMessage] = None - global_map: Out[LidarMessage] = None - global_costmap: Out[OccupancyGrid] = None - local_costmap: Out[OccupancyGrid] = None + lidar: In[LidarMessage] + global_map: Out[LidarMessage] + global_costmap: Out[OccupancyGrid] - pointcloud: o3d.geometry.PointCloud = o3d.geometry.PointCloud() + _point_cloud_accumulator: PointCloudAccumulator + _global_config: GlobalConfig + _preloaded_occupancy: OccupancyGrid | None = None - def __init__( + def __init__( # type: ignore[no-untyped-def] self, voxel_size: float = 0.05, cost_resolution: float = 0.05, global_publish_interval: float | None = None, - min_height: float = 0.15, - max_height: float = 0.6, + min_height: float = 0.10, + max_height: float = 0.5, global_config: GlobalConfig | None = None, **kwargs, ) -> None: @@ -49,10 +55,13 @@ def __init__( self.global_publish_interval = global_publish_interval self.min_height = min_height self.max_height = max_height + self._global_config = global_config or GlobalConfig() + self._point_cloud_accumulator = GeneralPointCloudAccumulator( + self.voxel_size, self._global_config + ) - if global_config: - if global_config.use_simulation: - self.min_height = 0.3 + if self._global_config.simulation: + self.min_height = 0.3 super().__init__(**kwargs) @@ -60,25 +69,10 @@ def __init__( def start(self) -> None: super().start() - unsub = self.lidar.subscribe(self.add_frame) - self._disposables.add(Disposable(unsub)) - - def publish(_) -> None: - self.global_map.publish(self.to_lidar_message()) - - # temporary, not sure if it belogs in mapper - # used only for visualizations, not for any algo - occupancygrid = OccupancyGrid.from_pointcloud( - self.to_lidar_message(), - resolution=self.cost_resolution, - min_height=self.min_height, - max_height=self.max_height, - ) - - self.global_costmap.publish(occupancygrid) + self._disposables.add(Disposable(self.lidar.subscribe(self.add_frame))) if self.global_publish_interval is not None: - unsub = interval(self.global_publish_interval).subscribe(publish) + unsub = interval(self.global_publish_interval).subscribe(self._publish) self._disposables.add(unsub) @rpc @@ -87,87 +81,57 @@ def stop(self) -> None: def to_PointCloud2(self) -> PointCloud2: return PointCloud2( - pointcloud=self.pointcloud, + pointcloud=self._point_cloud_accumulator.get_point_cloud(), ts=time.time(), ) def to_lidar_message(self) -> LidarMessage: return LidarMessage( - pointcloud=self.pointcloud, + pointcloud=self._point_cloud_accumulator.get_point_cloud(), origin=[0.0, 0.0, 0.0], resolution=self.voxel_size, ts=time.time(), ) + # TODO: Why is this RPC? @rpc - def add_frame(self, frame: LidarMessage) -> "Map": - """Voxelise *frame* and splice it into the running map.""" - new_pct = frame.pointcloud.voxel_down_sample(voxel_size=self.voxel_size) - - # Skip for empty pointclouds. - if len(new_pct.points) == 0: - return self - - self.pointcloud = splice_cylinder(self.pointcloud, new_pct, shrink=0.5) - local_costmap = OccupancyGrid.from_pointcloud( - frame, - resolution=self.cost_resolution, - min_height=0.15, - max_height=0.6, - ).gradient(max_distance=0.25) - self.local_costmap.publish(local_costmap) + def add_frame(self, frame: LidarMessage) -> None: + self._point_cloud_accumulator.add(frame.pointcloud) @property def o3d_geometry(self) -> o3d.geometry.PointCloud: - return self.pointcloud - + return self._point_cloud_accumulator.get_point_cloud() -def splice_sphere( - map_pcd: o3d.geometry.PointCloud, - patch_pcd: o3d.geometry.PointCloud, - shrink: float = 0.95, -) -> o3d.geometry.PointCloud: - center = patch_pcd.get_center() - radius = np.linalg.norm(np.asarray(patch_pcd.points) - center, axis=1).max() * shrink - dists = np.linalg.norm(np.asarray(map_pcd.points) - center, axis=1) - victims = np.nonzero(dists < radius)[0] - survivors = map_pcd.select_by_index(victims, invert=True) - return survivors + patch_pcd + def _publish(self, _: Any) -> None: + self.global_map.publish(self.to_lidar_message()) + occupancygrid = general_occupancy( + self.to_lidar_message(), + resolution=self.cost_resolution, + min_height=self.min_height, + max_height=self.max_height, + ) -def splice_cylinder( - map_pcd: o3d.geometry.PointCloud, - patch_pcd: o3d.geometry.PointCloud, - axis: int = 2, - shrink: float = 0.95, -) -> o3d.geometry.PointCloud: - center = patch_pcd.get_center() - patch_pts = np.asarray(patch_pcd.points) - - # Axes perpendicular to cylinder - axes = [0, 1, 2] - axes.remove(axis) - - planar_dists = np.linalg.norm(patch_pts[:, axes] - center[axes], axis=1) - radius = planar_dists.max() * shrink - - axis_min = (patch_pts[:, axis].min() - center[axis]) * shrink + center[axis] - axis_max = (patch_pts[:, axis].max() - center[axis]) * shrink + center[axis] + # When debugging occupancy navigation, load a predefined occupancy grid. + if self._global_config.mujoco_global_costmap_from_occupancy: + if self._preloaded_occupancy is None: + path = Path(self._global_config.mujoco_global_costmap_from_occupancy) + self._preloaded_occupancy = OccupancyGrid.from_path(path) + occupancygrid = self._preloaded_occupancy - map_pts = np.asarray(map_pcd.points) - planar_dists_map = np.linalg.norm(map_pts[:, axes] - center[axes], axis=1) + self.global_costmap.publish(occupancygrid) - victims = np.nonzero( - (planar_dists_map < radius) - & (map_pts[:, axis] >= axis_min) - & (map_pts[:, axis] <= axis_max) - )[0] - survivors = map_pcd.select_by_index(victims, invert=True) - return survivors + patch_pcd +mapper = Map.blueprint -mapper = Map.blueprint +def deploy(dimos: DimosCluster, connection: Go2ConnectionProtocol): # type: ignore[no-untyped-def] + mapper = dimos.deploy(Map, global_publish_interval=1.0) # type: ignore[attr-defined] + mapper.global_map.transport = LCMTransport("/global_map", LidarMessage) + mapper.global_costmap.transport = LCMTransport("/global_costmap", OccupancyGrid) + mapper.lidar.connect(connection.pointcloud) # type: ignore[attr-defined] + mapper.start() + return mapper __all__ = ["Map", "mapper"] diff --git a/dimos/robot/unitree_webrtc/type/odometry.py b/dimos/robot/unitree_webrtc/type/odometry.py index 52a8544fbc..9f0b400691 100644 --- a/dimos/robot/unitree_webrtc/type/odometry.py +++ b/dimos/robot/unitree_webrtc/type/odometry.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,13 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import time from typing import Literal, TypedDict from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 from dimos.robot.unitree_webrtc.type.timeseries import ( Timestamped, ) +from dimos.types.timestamped import to_timestamp raw_odometry_msg_sample = { "type": "msg", @@ -71,11 +71,11 @@ class RawOdometryMessage(TypedDict): data: OdometryData -class Odometry(PoseStamped, Timestamped): +class Odometry(PoseStamped, Timestamped): # type: ignore[misc] name = "geometry_msgs.PoseStamped" - def __init__(self, frame_id: str = "base_link", *args, **kwargs) -> None: - super().__init__(frame_id=frame_id, *args, **kwargs) + def __init__(self, frame_id: str = "base_link", *args, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(frame_id=frame_id, *args, **kwargs) # type: ignore[misc] @classmethod def from_msg(cls, msg: RawOdometryMessage) -> "Odometry": @@ -95,10 +95,7 @@ def from_msg(cls, msg: RawOdometryMessage) -> "Odometry": pose["orientation"].get("w"), ) - # ts = to_timestamp(msg["data"]["header"]["stamp"]) - # lidar / video timestamps are not available from the robot - # so we are deferring to local time for everything - ts = time.time() + ts = to_timestamp(msg["data"]["header"]["stamp"]) return Odometry(position=pos, orientation=rot, ts=ts, frame_id="world") def __repr__(self) -> str: diff --git a/dimos/robot/unitree_webrtc/type/test_lidar.py b/dimos/robot/unitree_webrtc/type/test_lidar.py index 93435e8e4b..0ad918409b 100644 --- a/dimos/robot/unitree_webrtc/type/test_lidar.py +++ b/dimos/robot/unitree_webrtc/type/test_lidar.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/robot/unitree_webrtc/type/test_map.py b/dimos/robot/unitree_webrtc/type/test_map.py index 12ee8f832d..2f8afbc743 100644 --- a/dimos/robot/unitree_webrtc/type/test_map.py +++ b/dimos/robot/unitree_webrtc/type/test_map.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,10 +14,11 @@ import pytest +from dimos.mapping.pointclouds.accumulators.general import _splice_cylinder from dimos.robot.unitree_webrtc.testing.helpers import show3d from dimos.robot.unitree_webrtc.testing.mock import Mock from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.map import Map, splice_sphere +from dimos.robot.unitree_webrtc.type.map import Map from dimos.utils.testing import SensorReplay @@ -48,7 +49,7 @@ def test_reconstruction_with_realtime_vis() -> None: for frame in mock.iterate(): map.add_frame(frame) - show3d(map.pointcloud, title="Reconstructed Map").run() + show3d(map.o3d_geometry, title="Reconstructed Map").run() @pytest.mark.vis @@ -56,7 +57,7 @@ def test_splice_vis() -> None: mock = Mock("test") target = mock.load("a") insert = mock.load("b") - show3d(splice_sphere(target.pointcloud, insert.pointcloud, shrink=0.7)).run() + show3d(_splice_cylinder(target.pointcloud, insert.pointcloud, shrink=0.7)).run() @pytest.mark.vis @@ -69,32 +70,35 @@ def test_robot_vis() -> None: for frame in mock.iterate(): map.add_frame(frame) - show3d(map.pointcloud, title="global dynamic map test").run() + show3d(map.o3d_geometry, title="global dynamic map test").run() -def test_robot_mapping() -> None: - lidar_replay = SensorReplay("office_lidar", autocast=LidarMessage.from_msg) +@pytest.fixture +def map_(): map = Map(voxel_size=0.5) + yield map + map.stop() + + +def test_robot_mapping(map_) -> None: + lidar_replay = SensorReplay("office_lidar", autocast=LidarMessage.from_msg) # Mock the output streams to avoid publishing errors class MockStream: def publish(self, msg) -> None: pass # Do nothing - map.local_costmap = MockStream() - map.global_costmap = MockStream() - map.global_map = MockStream() + map_.global_costmap = MockStream() + map_.global_map = MockStream() # Process all frames from replay for frame in lidar_replay.iterate(): - map.add_frame(frame) + map_.add_frame(frame) # Check the built map - global_map = map.to_lidar_message() + global_map = map_.to_lidar_message() pointcloud = global_map.pointcloud # Verify map has points assert len(pointcloud.points) > 0 print(f"Map contains {len(pointcloud.points)} points") - - map._close_module() diff --git a/dimos/robot/unitree_webrtc/type/test_odometry.py b/dimos/robot/unitree_webrtc/type/test_odometry.py index b1a251b254..e277455cdd 100644 --- a/dimos/robot/unitree_webrtc/type/test_odometry.py +++ b/dimos/robot/unitree_webrtc/type/test_odometry.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,15 +15,12 @@ from __future__ import annotations from operator import add, sub -import os -import threading -from dotenv import load_dotenv import pytest import reactivex.operators as ops from dimos.robot.unitree_webrtc.type.odometry import Odometry -from dimos.utils.testing import SensorReplay, SensorStorage +from dimos.utils.testing import SensorReplay _EXPECTED_TOTAL_RAD = -4.05212 @@ -82,27 +79,3 @@ def test_total_rotation_travel_rxpy() -> None: ) assert total_rad == pytest.approx(4.05, abs=0.01) - - -# data collection tool -@pytest.mark.tool -def test_store_odometry_stream() -> None: - from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 - - load_dotenv() - - robot = UnitreeGo2(ip=os.getenv("ROBOT_IP"), mode="ai") - robot.standup() - - storage = SensorStorage("raw_odometry_rotate_walk") - storage.save_stream(robot.raw_odom_stream()) - - shutdown = threading.Event() - - try: - while not shutdown.wait(0.1): - pass - except KeyboardInterrupt: - shutdown.set() - finally: - robot.liedown() diff --git a/dimos/robot/unitree_webrtc/type/test_timeseries.py b/dimos/robot/unitree_webrtc/type/test_timeseries.py index b7c955933d..2c7606d9f2 100644 --- a/dimos/robot/unitree_webrtc/type/test_timeseries.py +++ b/dimos/robot/unitree_webrtc/type/test_timeseries.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/robot/unitree_webrtc/type/timeseries.py b/dimos/robot/unitree_webrtc/type/timeseries.py index a85afba93f..b75a41b932 100644 --- a/dimos/robot/unitree_webrtc/type/timeseries.py +++ b/dimos/robot/unitree_webrtc/type/timeseries.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -91,17 +91,17 @@ def __iter__(self) -> Iterable[EVENT]: ... @property def start_time(self) -> datetime: """Return the timestamp of the earliest event, assuming the data is sorted.""" - return next(iter(self)).ts + return next(iter(self)).ts # type: ignore[call-overload, no-any-return, type-var] @property def end_time(self) -> datetime: """Return the timestamp of the latest event, assuming the data is sorted.""" - return next(reversed(list(self))).ts + return next(reversed(list(self))).ts # type: ignore[call-overload, no-any-return] @property def frequency(self) -> float: """Calculate the frequency of events in Hz.""" - return len(list(self)) / (self.duration().total_seconds() or 1) + return len(list(self)) / (self.duration().total_seconds() or 1) # type: ignore[call-overload] def time_range(self) -> tuple[datetime, datetime]: """Return (earliest_ts, latest_ts). Empty input ⇒ ValueError.""" @@ -121,7 +121,7 @@ def closest_to(self, timestamp: EpochLike) -> EVENT: closest = None min_dist = float("inf") - for event in self: + for event in self: # type: ignore[attr-defined] dist = abs(event.ts - target_ts) if dist > min_dist: break @@ -130,11 +130,11 @@ def closest_to(self, timestamp: EpochLike) -> EVENT: closest = event print(f"closest: {closest}") - return closest + return closest # type: ignore[return-value] def __repr__(self) -> str: """Return a string representation of the Timeseries.""" - return f"Timeseries(date={self.start_time.strftime('%Y-%m-%d')}, start={self.start_time.strftime('%H:%M:%S')}, end={self.end_time.strftime('%H:%M:%S')}, duration={self.duration()}, events={len(list(self))}, freq={self.frequency:.2f}Hz)" + return f"Timeseries(date={self.start_time.strftime('%Y-%m-%d')}, start={self.start_time.strftime('%H:%M:%S')}, end={self.end_time.strftime('%H:%M:%S')}, duration={self.duration()}, events={len(list(self))}, freq={self.frequency:.2f}Hz)" # type: ignore[call-overload] def __str__(self) -> str: """Return a string representation of the Timeseries.""" diff --git a/dimos/robot/unitree_webrtc/type/vector.py b/dimos/robot/unitree_webrtc/type/vector.py index be00e3403c..58438c0a98 100644 --- a/dimos/robot/unitree_webrtc/type/vector.py +++ b/dimos/robot/unitree_webrtc/type/vector.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -89,7 +89,7 @@ def __getitem__(self, idx: int) -> float: return float(self._data[idx]) def __iter__(self) -> Iterable[float]: - return iter(self._data) + return iter(self._data) # type: ignore[no-any-return] def __repr__(self) -> str: components = ",".join(f"{x:.6g}" for x in self._data) @@ -114,7 +114,7 @@ def getArrow() -> str: return f"{getArrow()} Vector {self.__repr__()}" - def serialize(self) -> dict: + def serialize(self) -> dict: # type: ignore[type-arg] """Serialize the vector to a dictionary.""" return {"type": "vector", "c": self._data.tolist()} @@ -231,12 +231,6 @@ def project(self: T, onto: Union["Vector", Iterable[float]]) -> T: scalar_projection = np.dot(self._data, onto_data) / onto_length_sq return self.__class__(scalar_projection * onto_data) - # this is here to test ros_observable_topic - # doesn't happen irl afaik that we want a vector from ros message - @classmethod - def from_msg(cls: type[T], msg: Any) -> T: - return cls(*msg) - @classmethod def zeros(cls: type[T], dim: int) -> T: """Create a zero vector of given dimension.""" diff --git a/dimos/robot/unitree_webrtc/unitree_b1/README.md b/dimos/robot/unitree_webrtc/unitree_b1/README.md index 8616fc286a..f59e6a57ae 100644 --- a/dimos/robot/unitree_webrtc/unitree_b1/README.md +++ b/dimos/robot/unitree_webrtc/unitree_b1/README.md @@ -44,7 +44,7 @@ The B1 robot runs Ubuntu with the following requirements: ```bash # Edit the CMakeLists.txt in the unitree_legged_sdk_B1 directory vim CMakeLists.txt - + # Add this line with the other add_executable statements: add_executable(joystick_server example/joystick_server_udp.cpp) target_link_libraries(joystick_server ${EXTRA_LIBS})``` @@ -144,7 +144,7 @@ This prints commands instead of sending UDP packets - useful for development. - **Mode safety**: Movement only allowed in WALK mode - **Graceful shutdown**: Sends stop commands on exit -### Server Side +### Server Side - **Packet timeout**: Robot stops if no packets for 100ms - **Continuous monitoring**: Checks timeout before every control update - **Safe defaults**: Starts in IDLE mode @@ -173,13 +173,13 @@ External Machine (Client) B1 Robot (Server) This is because the onboard hardware (mini PC, jetson, etc.) needs to connect to both the B1 wifi AP network to send cmd_vel messages over UDP, as well as the network running dimensional -Plug in wireless adapter +Plug in wireless adapter ```bash nmcli device status nmcli device wifi list ifname *DEVICE_NAME* # Connect to b1 network nmcli device wifi connect "Unitree_B1-251" password "00000000" ifname *DEVICE_NAME* -# Verify connection +# Verify connection nmcli connection show --active ``` diff --git a/dimos/robot/unitree_webrtc/unitree_b1/__init__.py b/dimos/robot/unitree_webrtc/unitree_b1/__init__.py index e6e5a0f04a..db85984070 100644 --- a/dimos/robot/unitree_webrtc/unitree_b1/__init__.py +++ b/dimos/robot/unitree_webrtc/unitree_b1/__init__.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. """Unitree B1 robot module.""" diff --git a/dimos/robot/unitree_webrtc/unitree_b1/b1_command.py b/dimos/robot/unitree_webrtc/unitree_b1/b1_command.py index 82545fa2c6..3fa57043d1 100644 --- a/dimos/robot/unitree_webrtc/unitree_b1/b1_command.py +++ b/dimos/robot/unitree_webrtc/unitree_b1/b1_command.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. """Internal B1 command structure for UDP communication.""" @@ -39,7 +39,7 @@ class B1Command(BaseModel): ) # Control mode (uint8): 0=idle, 1=stand, 2=walk, 6=recovery @classmethod - def from_twist(cls, twist, mode: int = 2): + def from_twist(cls, twist, mode: int = 2): # type: ignore[no-untyped-def] """Create B1Command from standard ROS Twist message. This is the key integration point for navigation and planning. diff --git a/dimos/robot/unitree_webrtc/unitree_b1/connection.py b/dimos/robot/unitree_webrtc/unitree_b1/connection.py index 73285b4d76..f0cb5317e6 100644 --- a/dimos/robot/unitree_webrtc/unitree_b1/connection.py +++ b/dimos/robot/unitree_webrtc/unitree_b1/connection.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. """B1 Connection Module that accepts standard Twist commands and converts to UDP packets.""" @@ -33,7 +33,7 @@ from .b1_command import B1Command # Setup logger with DEBUG level for troubleshooting -logger = setup_logger("dimos.robot.unitree_webrtc.unitree_b1.connection", level=logging.DEBUG) +logger = setup_logger(level=logging.DEBUG) class RobotMode: @@ -52,13 +52,13 @@ class B1ConnectionModule(Module): internally converts to B1Command format, and sends UDP packets at 50Hz. """ - cmd_vel: In[TwistStamped] = None # Timestamped velocity commands from ROS - mode_cmd: In[Int32] = None # Mode changes - odom_in: In[Odometry] = None # External odometry from ROS SLAM/lidar + cmd_vel: In[TwistStamped] # Timestamped velocity commands from ROS + mode_cmd: In[Int32] # Mode changes + odom_in: In[Odometry] # External odometry from ROS SLAM/lidar - odom_pose: Out[PoseStamped] = None # Converted pose for internal use + odom_pose: Out[PoseStamped] # Converted pose for internal use - def __init__( + def __init__( # type: ignore[no-untyped-def] self, ip: str = "192.168.12.1", port: int = 9090, test_mode: bool = False, *args, **kwargs ) -> None: """Initialize B1 connection module. @@ -95,7 +95,7 @@ def start(self) -> None: # Setup UDP socket (unless in test mode) if not self.test_mode: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # type: ignore[assignment] logger.info(f"B1 Connection started - UDP to {self.ip}:{self.port} at 50Hz") else: logger.info(f"[TEST MODE] B1 Connection started - would send to {self.ip}:{self.port}") @@ -116,12 +116,12 @@ def start(self) -> None: self.watchdog_running = True # Start 50Hz sending thread - self.send_thread = threading.Thread(target=self._send_loop, daemon=True) - self.send_thread.start() + self.send_thread = threading.Thread(target=self._send_loop, daemon=True) # type: ignore[assignment] + self.send_thread.start() # type: ignore[attr-defined] # Start watchdog thread - self.watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True) - self.watchdog_thread.start() + self.watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True) # type: ignore[assignment] + self.watchdog_thread.start() # type: ignore[attr-defined] @rpc def stop(self) -> None: @@ -359,9 +359,9 @@ def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> bool: class MockB1ConnectionModule(B1ConnectionModule): """Test connection module that prints commands instead of sending UDP.""" - def __init__(self, ip: str = "127.0.0.1", port: int = 9090, *args, **kwargs) -> None: + def __init__(self, ip: str = "127.0.0.1", port: int = 9090, *args, **kwargs) -> None: # type: ignore[no-untyped-def] """Initialize test connection without creating socket.""" - super().__init__(ip, port, test_mode=True, *args, **kwargs) + super().__init__(ip, port, test_mode=True, *args, **kwargs) # type: ignore[misc] def _send_loop(self) -> None: """Override to provide better test output with timeout detection.""" diff --git a/dimos/robot/unitree_webrtc/unitree_b1/joystick_module.py b/dimos/robot/unitree_webrtc/unitree_b1/joystick_module.py index 9c3c09861c..3aef29122a 100644 --- a/dimos/robot/unitree_webrtc/unitree_b1/joystick_module.py +++ b/dimos/robot/unitree_webrtc/unitree_b1/joystick_module.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. """Pygame Joystick Module for testing B1 control via LCM.""" @@ -37,10 +37,10 @@ class JoystickModule(Module): This allows testing the same interface that navigation will use. """ - twist_out: Out[TwistStamped] = None # Timestamped velocity commands - mode_out: Out[Int32] = None # Mode changes + twist_out: Out[TwistStamped] # Timestamped velocity commands + mode_out: Out[Int32] # Mode changes - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] Module.__init__(self, *args, **kwargs) self.pygame_ready = False self.running = False @@ -58,7 +58,7 @@ def start(self) -> bool: print("ERROR: pygame not installed. Install with: pip install pygame") return False - self.keys_held = set() + self.keys_held = set() # type: ignore[var-annotated] self.pygame_ready = True self.running = True @@ -224,7 +224,7 @@ def _pygame_loop(self) -> None: pygame.quit() print("JoystickModule stopped") - def _update_display(self, twist) -> None: + def _update_display(self, twist) -> None: # type: ignore[no-untyped-def] """Update pygame window with current status.""" import pygame diff --git a/dimos/robot/unitree_webrtc/unitree_b1/joystick_server_udp.cpp b/dimos/robot/unitree_webrtc/unitree_b1/joystick_server_udp.cpp index 56e2b29412..e86e999b8d 100644 --- a/dimos/robot/unitree_webrtc/unitree_b1/joystick_server_udp.cpp +++ b/dimos/robot/unitree_webrtc/unitree_b1/joystick_server_udp.cpp @@ -31,7 +31,7 @@ struct NetworkJoystickCmd { class JoystickServer { public: - JoystickServer(uint8_t level, int server_port) : + JoystickServer(uint8_t level, int server_port) : safe(LeggedType::B1), udp(level, 8090, "192.168.123.220", 8082), server_port_(server_port), @@ -57,28 +57,28 @@ class JoystickServer { UDP udp; HighCmd cmd = {0}; HighState state = {0}; - + NetworkJoystickCmd joystick_cmd_; std::mutex cmd_mutex_; - + int server_port_; int server_socket_; bool running_; std::thread server_thread_; - + // Client tracking for debug struct sockaddr_in last_client_addr_; bool has_client_ = false; - + // SAFETY: Timeout tracking std::chrono::steady_clock::time_point last_packet_time_; const int PACKET_TIMEOUT_MS = 100; // Stop if no packet for 100ms - + float dt = 0.002; - + // Control parameters const float MAX_FORWARD_SPEED = 0.2f; // m/s - const float MAX_SIDE_SPEED = 0.2f; // m/s + const float MAX_SIDE_SPEED = 0.2f; // m/s const float MAX_YAW_SPEED = 0.2f; // rad/s const float MAX_BODY_HEIGHT = 0.1f; // m const float MAX_EULER_ANGLE = 0.3f; // rad @@ -87,13 +87,13 @@ class JoystickServer { void JoystickServer::Start() { running_ = true; - + // Start network server thread server_thread_ = std::thread(&JoystickServer::NetworkServerThread, this); - + // Initialize environment InitEnvironment(); - + // Start control loops LoopFunc loop_control("control_loop", dt, boost::bind(&JoystickServer::RobotControl, this)); LoopFunc loop_udpSend("udp_send", dt, 3, boost::bind(&JoystickServer::UDPSend, this)); @@ -107,7 +107,7 @@ void JoystickServer::Start() { std::cout << "Timeout protection: " << PACKET_TIMEOUT_MS << "ms" << std::endl; std::cout << "Expected packet size: 19 bytes" << std::endl; std::cout << "Robot control loops started" << std::endl; - + // Keep running while (running_) { sleep(1); @@ -129,41 +129,41 @@ void JoystickServer::NetworkServerThread() { std::cerr << "Failed to create UDP socket" << std::endl; return; } - + // Allow socket reuse int opt = 1; setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - + // Bind socket struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(server_port_); - + if (bind(server_socket_, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { std::cerr << "Failed to bind UDP socket to port " << server_port_ << std::endl; close(server_socket_); return; } - + std::cout << "UDP server listening on port " << server_port_ << std::endl; std::cout << "Waiting for joystick packets..." << std::endl; - + NetworkJoystickCmd net_cmd; struct sockaddr_in client_addr; socklen_t client_len; - + while (running_) { client_len = sizeof(client_addr); - + // Receive UDP datagram (blocks until packet arrives) - ssize_t bytes = recvfrom(server_socket_, &net_cmd, sizeof(net_cmd), + ssize_t bytes = recvfrom(server_socket_, &net_cmd, sizeof(net_cmd), 0, (struct sockaddr*)&client_addr, &client_len); - + if (bytes == 19) { // Perfect packet size from Python client if (!has_client_) { - std::cout << "Client connected from " << inet_ntoa(client_addr.sin_addr) + std::cout << "Client connected from " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl; has_client_ = true; last_client_addr_ = client_addr; @@ -172,7 +172,7 @@ void JoystickServer::NetworkServerThread() { } else if (bytes == sizeof(NetworkJoystickCmd)) { // C++ client with padding (20 bytes) if (!has_client_) { - std::cout << "C++ Client connected from " << inet_ntoa(client_addr.sin_addr) + std::cout << "C++ Client connected from " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl; has_client_ = true; last_client_addr_ = client_addr; @@ -182,7 +182,7 @@ void JoystickServer::NetworkServerThread() { // Wrong packet size - ignore but log static int error_count = 0; if (error_count++ < 5) { // Only log first 5 errors - std::cerr << "Ignored packet with wrong size: " << bytes + std::cerr << "Ignored packet with wrong size: " << bytes << " bytes (expected 19)" << std::endl; } } @@ -193,10 +193,10 @@ void JoystickServer::NetworkServerThread() { void JoystickServer::ParseJoystickCommand(const NetworkJoystickCmd& net_cmd) { std::lock_guard lock(cmd_mutex_); joystick_cmd_ = net_cmd; - + // SAFETY: Update timestamp for timeout tracking last_packet_time_ = std::chrono::steady_clock::now(); - + // Apply deadzone to analog sticks if (fabs(joystick_cmd_.lx) < DEADZONE) joystick_cmd_.lx = 0; if (fabs(joystick_cmd_.ly) < DEADZONE) joystick_cmd_.ly = 0; @@ -208,16 +208,16 @@ void JoystickServer::CheckTimeout() { auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast( now - last_packet_time_).count(); - + static bool timeout_printed = false; - + if (elapsed > PACKET_TIMEOUT_MS) { joystick_cmd_.lx = 0; joystick_cmd_.ly = 0; joystick_cmd_.rx = 0; joystick_cmd_.ry = 0; joystick_cmd_.buttons = 0; - + if (!timeout_printed) { std::cout << "SAFETY: Packet timeout - stopping movement!" << std::endl; timeout_printed = true; @@ -241,7 +241,7 @@ void JoystickServer::UDPSend() { void JoystickServer::RobotControl() { udp.GetRecv(state); - + // SAFETY: Check for packet timeout NetworkJoystickCmd current_cmd; { @@ -249,7 +249,7 @@ void JoystickServer::RobotControl() { CheckTimeout(); // This may zero movement if timeout current_cmd = joystick_cmd_; } - + cmd.mode = 0; cmd.gaitType = 0; cmd.speedLevel = 0; @@ -262,35 +262,35 @@ void JoystickServer::RobotControl() { cmd.velocity[1] = 0.0f; cmd.yawSpeed = 0.0f; cmd.reserve = 0; - + // Set mode from joystick cmd.mode = current_cmd.mode; - + // Map joystick to robot control based on mode switch (current_cmd.mode) { case 0: // Idle // Robot stops break; - + case 1: // Force stand with body control // Left stick controls body height and yaw cmd.bodyHeight = current_cmd.ly * MAX_BODY_HEIGHT; cmd.euler[2] = current_cmd.lx * MAX_EULER_ANGLE; - + // Right stick controls pitch and roll cmd.euler[1] = current_cmd.ry * MAX_EULER_ANGLE; cmd.euler[0] = current_cmd.rx * MAX_EULER_ANGLE; break; - + case 2: // Walk mode cmd.velocity[0] = std::clamp(current_cmd.ly * MAX_FORWARD_SPEED, -MAX_FORWARD_SPEED, MAX_FORWARD_SPEED); cmd.yawSpeed = std::clamp(-current_cmd.lx * MAX_YAW_SPEED, -MAX_YAW_SPEED, MAX_YAW_SPEED); cmd.velocity[1] = std::clamp(-current_cmd.rx * MAX_SIDE_SPEED, -MAX_SIDE_SPEED, MAX_SIDE_SPEED); - + // Check button states for gait type if (current_cmd.buttons & 0x0001) { // Button A cmd.gaitType = 0; // Trot - } else if (current_cmd.buttons & 0x0002) { // Button B + } else if (current_cmd.buttons & 0x0002) { // Button B cmd.gaitType = 1; // Trot running } else if (current_cmd.buttons & 0x0004) { // Button X cmd.gaitType = 2; // Climb mode @@ -298,30 +298,30 @@ void JoystickServer::RobotControl() { cmd.gaitType = 3; // Trot obstacle } break; - + case 5: // Damping mode case 6: // Recovery stand up break; - + default: cmd.mode = 0; // Default to idle for safety break; } - + // Debug output static int counter = 0; if (counter++ % 500 == 0) { // Print every second auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast( now - last_packet_time_).count(); - - std::cout << "Mode: " << (int)cmd.mode + + std::cout << "Mode: " << (int)cmd.mode << " Vel: [" << cmd.velocity[0] << ", " << cmd.velocity[1] << "]" << " Yaw: " << cmd.yawSpeed << " Last packet: " << elapsed << "ms ago" << " IMU: " << state.imu.rpy[2] << std::endl; } - + udp.SetSend(cmd); } @@ -338,11 +338,11 @@ void signal_handler(int sig) { int main(int argc, char* argv[]) { int port = 9090; // Default port - + if (argc > 1) { port = atoi(argv[1]); } - + std::cout << "UDP Unitree B1 Joystick Control Server" << std::endl; std::cout << "Communication level: HIGH-level" << std::endl; std::cout << "Protocol: UDP (datagram)" << std::endl; @@ -352,15 +352,15 @@ int main(int argc, char* argv[]) { std::cout << "WARNING: Make sure the robot is standing on the ground." << std::endl; std::cout << "Press Enter to continue..." << std::endl; std::cin.ignore(); - + JoystickServer server(HIGHLEVEL, port); g_server = &server; - + // Set up signal handler signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); - + server.Start(); - + return 0; -} \ No newline at end of file +} diff --git a/dimos/robot/unitree_webrtc/unitree_b1/test_connection.py b/dimos/robot/unitree_webrtc/unitree_b1/test_connection.py index 49421c85e0..e43a3124dc 100644 --- a/dimos/robot/unitree_webrtc/unitree_b1/test_connection.py +++ b/dimos/robot/unitree_webrtc/unitree_b1/test_connection.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. """Comprehensive tests for Unitree B1 connection module Timer implementation.""" diff --git a/dimos/robot/unitree_webrtc/unitree_b1/unitree_b1.py b/dimos/robot/unitree_webrtc/unitree_b1/unitree_b1.py index 04390c2e9e..ff608c2b1f 100644 --- a/dimos/robot/unitree_webrtc/unitree_b1/unitree_b1.py +++ b/dimos/robot/unitree_webrtc/unitree_b1/unitree_b1.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. """ Unitree B1 quadruped robot with simplified UDP control. @@ -42,18 +42,20 @@ # Handle ROS imports for environments where ROS is not available like CI try: - from geometry_msgs.msg import TwistStamped as ROSTwistStamped - from nav_msgs.msg import Odometry as ROSOdometry - from tf2_msgs.msg import TFMessage as ROSTFMessage + from geometry_msgs.msg import ( # type: ignore[attr-defined] + TwistStamped as ROSTwistStamped, + ) + from nav_msgs.msg import Odometry as ROSOdometry # type: ignore[attr-defined] + from tf2_msgs.msg import TFMessage as ROSTFMessage # type: ignore[attr-defined] ROS_AVAILABLE = True except ImportError: - ROSTwistStamped = None - ROSOdometry = None - ROSTFMessage = None + ROSTwistStamped = None # type: ignore[assignment, misc] + ROSOdometry = None # type: ignore[assignment, misc] + ROSTFMessage = None # type: ignore[assignment, misc] ROS_AVAILABLE = False -logger = setup_logger("dimos.robot.unitree_webrtc.unitree_b1", level=logging.INFO) +logger = setup_logger(level=logging.INFO) class UnitreeB1(Robot, Resource): @@ -110,28 +112,28 @@ def start(self) -> None: logger.info("Deploying connection module...") if self.test_mode: - self.connection = self._dimos.deploy(MockB1ConnectionModule, self.ip, self.port) + self.connection = self._dimos.deploy(MockB1ConnectionModule, self.ip, self.port) # type: ignore[assignment] else: - self.connection = self._dimos.deploy(B1ConnectionModule, self.ip, self.port) + self.connection = self._dimos.deploy(B1ConnectionModule, self.ip, self.port) # type: ignore[assignment] # Configure LCM transports for connection (matching G1 pattern) - self.connection.cmd_vel.transport = core.LCMTransport("/cmd_vel", TwistStamped) - self.connection.mode_cmd.transport = core.LCMTransport("/b1/mode", Int32) - self.connection.odom_in.transport = core.LCMTransport("/state_estimation", Odometry) - self.connection.odom_pose.transport = core.LCMTransport("/odom", PoseStamped) + self.connection.cmd_vel.transport = core.LCMTransport("/cmd_vel", TwistStamped) # type: ignore[attr-defined] + self.connection.mode_cmd.transport = core.LCMTransport("/b1/mode", Int32) # type: ignore[attr-defined] + self.connection.odom_in.transport = core.LCMTransport("/state_estimation", Odometry) # type: ignore[attr-defined] + self.connection.odom_pose.transport = core.LCMTransport("/odom", PoseStamped) # type: ignore[attr-defined] # Deploy joystick move_vel control if self.enable_joystick: from dimos.robot.unitree_webrtc.unitree_b1.joystick_module import JoystickModule - self.joystick = self._dimos.deploy(JoystickModule) - self.joystick.twist_out.transport = core.LCMTransport("/cmd_vel", TwistStamped) - self.joystick.mode_out.transport = core.LCMTransport("/b1/mode", Int32) + self.joystick = self._dimos.deploy(JoystickModule) # type: ignore[assignment] + self.joystick.twist_out.transport = core.LCMTransport("/cmd_vel", TwistStamped) # type: ignore[attr-defined] + self.joystick.mode_out.transport = core.LCMTransport("/b1/mode", Int32) # type: ignore[attr-defined] logger.info("Joystick module deployed - pygame window will open") self._dimos.start_all_modules() - self.connection.idle() # Start in IDLE mode for safety + self.connection.idle() # type: ignore[attr-defined] # Start in IDLE mode for safety logger.info("B1 started in IDLE mode (safety)") # Deploy ROS bridge if enabled (matching G1 pattern) @@ -151,24 +153,24 @@ def stop(self) -> None: def _deploy_ros_bridge(self) -> None: """Deploy and configure ROS bridge (matching G1 implementation).""" - self.ros_bridge = ROSBridge("b1_ros_bridge") + self.ros_bridge = ROSBridge("b1_ros_bridge") # type: ignore[assignment] # Add /cmd_vel topic from ROS to DIMOS - self.ros_bridge.add_topic( + self.ros_bridge.add_topic( # type: ignore[attr-defined] "/cmd_vel", TwistStamped, ROSTwistStamped, direction=BridgeDirection.ROS_TO_DIMOS ) # Add /state_estimation topic from ROS to DIMOS (external odometry) - self.ros_bridge.add_topic( + self.ros_bridge.add_topic( # type: ignore[attr-defined] "/state_estimation", Odometry, ROSOdometry, direction=BridgeDirection.ROS_TO_DIMOS ) # Add /tf topic from ROS to DIMOS - self.ros_bridge.add_topic( + self.ros_bridge.add_topic( # type: ignore[attr-defined] "/tf", TFMessage, ROSTFMessage, direction=BridgeDirection.ROS_TO_DIMOS ) - self.ros_bridge.start() + self.ros_bridge.start() # type: ignore[attr-defined] logger.info("ROS bridge deployed: /cmd_vel, /state_estimation, /tf (ROS → DIMOS)") @@ -221,7 +223,7 @@ def main() -> None: args = parser.parse_args() - robot = UnitreeB1( + robot = UnitreeB1( # type: ignore[abstract] ip=args.ip, port=args.port, output_dir=args.output_dir, diff --git a/dimos/robot/unitree_webrtc/unitree_g1.py b/dimos/robot/unitree_webrtc/unitree_g1.py deleted file mode 100644 index fc148c54c3..0000000000 --- a/dimos/robot/unitree_webrtc/unitree_g1.py +++ /dev/null @@ -1,549 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Unitree G1 humanoid robot. -Minimal implementation using WebRTC connection for robot control. -""" - -import logging -import os -import time - -from dimos_lcm.foxglove_msgs import SceneUpdate -from geometry_msgs.msg import PoseStamped as ROSPoseStamped, TwistStamped as ROSTwistStamped -from nav_msgs.msg import Odometry as ROSOdometry -from reactivex.disposable import Disposable -from sensor_msgs.msg import Joy as ROSJoy, PointCloud2 as ROSPointCloud2 -from tf2_msgs.msg import TFMessage as ROSTFMessage - -from dimos import core -from dimos.agents2 import Agent -from dimos.agents2.cli.human import HumanInput -from dimos.agents2.skills.ros_navigation import RosNavigation -from dimos.agents2.spec import Model, Provider -from dimos.core import In, Module, Out, rpc -from dimos.core.module_coordinator import ModuleCoordinator -from dimos.core.resource import Resource -from dimos.hardware.camera import zed -from dimos.hardware.camera.module import CameraModule -from dimos.hardware.camera.webcam import Webcam -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.msgs.geometry_msgs import ( - PoseStamped, - Quaternion, - Transform, - Twist, - TwistStamped, - Vector3, -) -from dimos.msgs.nav_msgs.Odometry import Odometry -from dimos.msgs.sensor_msgs import CameraInfo, Image, Joy, PointCloud2 -from dimos.msgs.std_msgs.Bool import Bool -from dimos.msgs.tf2_msgs.TFMessage import TFMessage -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.moduleDB import ObjectDBModule -from dimos.perception.spatial_perception import SpatialMemory -from dimos.protocol import pubsub -from dimos.protocol.pubsub.lcmpubsub import LCM -from dimos.robot.foxglove_bridge import FoxgloveBridge -from dimos.robot.robot import Robot -from dimos.robot.ros_bridge import BridgeDirection, ROSBridge -from dimos.robot.unitree_webrtc.connection import UnitreeWebRTCConnection -from dimos.robot.unitree_webrtc.rosnav import NavigationModule -from dimos.robot.unitree_webrtc.unitree_g1_skill_container import UnitreeG1SkillContainer -from dimos.robot.unitree_webrtc.unitree_skills import MyUnitreeSkills -from dimos.skills.skills import SkillLibrary -from dimos.types.robot_capabilities import RobotCapability -from dimos.utils.logging_config import setup_logger -from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule - -logger = setup_logger("dimos.robot.unitree_webrtc.unitree_g1", level=logging.INFO) - -# Suppress verbose loggers -logging.getLogger("aiortc.codecs.h264").setLevel(logging.ERROR) -logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) -logging.getLogger("websockets.server").setLevel(logging.ERROR) -logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) -logging.getLogger("asyncio").setLevel(logging.ERROR) - - -class G1ConnectionModule(Module): - """Simplified connection module for G1 - uses WebRTC for control.""" - - movecmd: In[TwistStamped] = None - odom_in: In[Odometry] = None - - odom_pose: Out[PoseStamped] = None - ip: str - connection_type: str = "webrtc" - - def __init__( - self, ip: str | None = None, connection_type: str = "webrtc", *args, **kwargs - ) -> None: - self.ip = ip - self.connection_type = connection_type - self.connection = None - Module.__init__(self, *args, **kwargs) - - @rpc - def start(self) -> None: - """Start the connection and subscribe to sensor streams.""" - - super().start() - - # Use the exact same UnitreeWebRTCConnection as Go2 - self.connection = UnitreeWebRTCConnection(self.ip) - self.connection.start() - unsub = self.movecmd.subscribe(self.move) - self._disposables.add(Disposable(unsub)) - unsub = self.odom_in.subscribe(self._publish_odom_pose) - self._disposables.add(Disposable(unsub)) - - @rpc - def stop(self) -> None: - self.connection.stop() - super().stop() - - def _publish_odom_pose(self, msg: Odometry) -> None: - self.odom_pose.publish( - PoseStamped( - ts=msg.ts, - frame_id=msg.frame_id, - position=msg.pose.pose.position, - orientation=msg.pose.orientation, - ) - ) - - @rpc - def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> None: - """Send movement command to robot.""" - twist = Twist(linear=twist_stamped.linear, angular=twist_stamped.angular) - self.connection.move(twist, duration) - - @rpc - def publish_request(self, topic: str, data: dict): - """Forward WebRTC publish requests to connection.""" - return self.connection.publish_request(topic, data) - - -class UnitreeG1(Robot, Resource): - """Unitree G1 humanoid robot.""" - - def __init__( - self, - ip: str, - output_dir: str | None = None, - websocket_port: int = 7779, - skill_library: SkillLibrary | None = None, - recording_path: str | None = None, - replay_path: str | None = None, - enable_joystick: bool = False, - enable_connection: bool = True, - enable_ros_bridge: bool = True, - enable_perception: bool = False, - enable_camera: bool = False, - ) -> None: - """Initialize the G1 robot. - - Args: - ip: Robot IP address - output_dir: Directory for saving outputs - websocket_port: Port for web visualization - skill_library: Skill library instance - recording_path: Path to save recordings (if recording) - replay_path: Path to replay recordings from (if replaying) - enable_joystick: Enable pygame joystick control - enable_connection: Enable robot connection module - enable_ros_bridge: Enable ROS bridge - enable_camera: Enable web camera module - """ - super().__init__() - self.ip = ip - self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") - self.recording_path = recording_path - self.replay_path = replay_path - self.enable_joystick = enable_joystick - self.enable_connection = enable_connection - self.enable_ros_bridge = enable_ros_bridge - self.enable_perception = enable_perception - self.enable_camera = enable_camera - self.websocket_port = websocket_port - self.lcm = LCM() - - # Initialize skill library with G1 robot type - if skill_library is None: - from dimos.robot.unitree_webrtc.unitree_skills import MyUnitreeSkills - - skill_library = MyUnitreeSkills(robot_type="g1") - self.skill_library = skill_library - - # Set robot capabilities - self.capabilities = [RobotCapability.LOCOMOTION] - - # Module references - self._dimos = ModuleCoordinator(n=4) - self.connection = None - self.websocket_vis = None - self.foxglove_bridge = None - self.spatial_memory_module = None - self.joystick = None - self.ros_bridge = None - self.camera = None - self._ros_nav = None - self._setup_directories() - - def _setup_directories(self) -> None: - """Setup directories for spatial memory storage.""" - os.makedirs(self.output_dir, exist_ok=True) - logger.info(f"Robot outputs will be saved to: {self.output_dir}") - - # Initialize memory directories - self.memory_dir = os.path.join(self.output_dir, "memory") - os.makedirs(self.memory_dir, exist_ok=True) - - # Initialize spatial memory properties - self.spatial_memory_dir = os.path.join(self.memory_dir, "spatial_memory") - self.spatial_memory_collection = "spatial_memory" - self.db_path = os.path.join(self.spatial_memory_dir, "chromadb_data") - self.visual_memory_path = os.path.join(self.spatial_memory_dir, "visual_memory.pkl") - - # Create spatial memory directories - os.makedirs(self.spatial_memory_dir, exist_ok=True) - os.makedirs(self.db_path, exist_ok=True) - - def _deploy_detection(self, goto) -> None: - detection = self._dimos.deploy( - ObjectDBModule, goto=goto, camera_info=zed.CameraInfo.SingleWebcam - ) - - detection.image.connect(self.camera.image) - detection.pointcloud.transport = core.LCMTransport("/map", PointCloud2) - - detection.annotations.transport = core.LCMTransport("/annotations", ImageAnnotations) - detection.detections.transport = core.LCMTransport("/detections", Detection2DArray) - - detection.scene_update.transport = core.LCMTransport("/scene_update", SceneUpdate) - detection.target.transport = core.LCMTransport("/target", PoseStamped) - detection.detected_pointcloud_0.transport = core.LCMTransport( - "/detected/pointcloud/0", PointCloud2 - ) - detection.detected_pointcloud_1.transport = core.LCMTransport( - "/detected/pointcloud/1", PointCloud2 - ) - detection.detected_pointcloud_2.transport = core.LCMTransport( - "/detected/pointcloud/2", PointCloud2 - ) - - detection.detected_image_0.transport = core.LCMTransport("/detected/image/0", Image) - detection.detected_image_1.transport = core.LCMTransport("/detected/image/1", Image) - detection.detected_image_2.transport = core.LCMTransport("/detected/image/2", Image) - - self.detection = detection - - def start(self) -> None: - self.lcm.start() - self._dimos.start() - - if self.enable_connection: - self._deploy_connection() - - self._deploy_visualization() - - if self.enable_joystick: - self._deploy_joystick() - - if self.enable_ros_bridge: - self._deploy_ros_bridge() - - self.nav = self._dimos.deploy(NavigationModule) - self.nav.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) - self.nav.goal_pose.transport = core.LCMTransport("/goal_pose", PoseStamped) - self.nav.cancel_goal.transport = core.LCMTransport("/cancel_goal", Bool) - self.nav.joy.transport = core.LCMTransport("/joy", Joy) - self.nav.start() - - self._deploy_camera() - self._deploy_detection(self.nav.go_to) - - if self.enable_perception: - self._deploy_perception() - - self.lcm.start() - - # Setup agent with G1 skills - logger.info("Setting up agent with G1 skills...") - - agent = Agent( - system_prompt="You are a helpful assistant controlling a Unitree G1 humanoid robot. You can control the robot's arms, movement modes, and navigation.", - model=Model.GPT_4O, - provider=Provider.OPENAI, - ) - - # Register G1-specific skill container - g1_skills = UnitreeG1SkillContainer(robot=self) - agent.register_skills(g1_skills) - - human_input = self._dimos.deploy(HumanInput) - agent.register_skills(human_input) - - if self.enable_perception: - agent.register_skills(self.detection) - - # Register ROS navigation - self._ros_nav = RosNavigation(self) - self._ros_nav.start() - agent.register_skills(self._ros_nav) - - agent.run_implicit_skill("human") - agent.start() - - # For logging - skills = [tool.name for tool in agent.get_tools()] - logger.info(f"Agent configured with {len(skills)} skills: {', '.join(skills)}") - - agent.loop_thread() - - logger.info("UnitreeG1 initialized and started") - logger.info(f"WebSocket visualization available at http://localhost:{self.websocket_port}") - self._start_modules() - - def stop(self) -> None: - self._dimos.stop() - if self._ros_nav: - self._ros_nav.stop() - self.lcm.stop() - - def _deploy_connection(self) -> None: - """Deploy and configure the connection module.""" - self.connection = self._dimos.deploy(G1ConnectionModule, self.ip) - - # Configure LCM transports - self.connection.movecmd.transport = core.LCMTransport("/cmd_vel", TwistStamped) - self.connection.odom_in.transport = core.LCMTransport("/state_estimation", Odometry) - self.connection.odom_pose.transport = core.LCMTransport("/odom", PoseStamped) - - def _deploy_camera(self) -> None: - """Deploy and configure a standard webcam module.""" - logger.info("Deploying standard webcam module...") - - self.camera = self._dimos.deploy( - CameraModule, - transform=Transform( - translation=Vector3(0.05, 0.0, 0.0), - rotation=Quaternion.from_euler(Vector3(0.0, 0.2, 0.0)), - frame_id="sensor", - child_frame_id="camera_link", - ), - hardware=lambda: Webcam( - camera_index=0, - frequency=15, - stereo_slice="left", - camera_info=zed.CameraInfo.SingleWebcam, - ), - ) - - self.camera.image.transport = core.LCMTransport("/image", Image) - self.camera.camera_info.transport = core.LCMTransport("/camera_info", CameraInfo) - logger.info("Webcam module configured") - - def _deploy_visualization(self) -> None: - """Deploy and configure visualization modules.""" - # Deploy WebSocket visualization module - self.websocket_vis = self._dimos.deploy(WebsocketVisModule, port=self.websocket_port) - self.websocket_vis.movecmd_stamped.transport = core.LCMTransport("/cmd_vel", TwistStamped) - - # Note: robot_pose connection removed since odom was removed from G1ConnectionModule - - # Deploy Foxglove bridge - self.foxglove_bridge = FoxgloveBridge( - shm_channels=[ - "/zed/color_image#sensor_msgs.Image", - "/zed/depth_image#sensor_msgs.Image", - ] - ) - self.foxglove_bridge.start() - - def _deploy_perception(self) -> None: - self.spatial_memory_module = self._dimos.deploy( - SpatialMemory, - collection_name=self.spatial_memory_collection, - db_path=self.db_path, - visual_memory_path=self.visual_memory_path, - output_dir=self.spatial_memory_dir, - ) - - self.spatial_memory_module.color_image.connect(self.camera.image) - self.spatial_memory_module.odom.transport = core.LCMTransport("/odom", PoseStamped) - - logger.info("Spatial memory module deployed and connected") - - def _deploy_joystick(self) -> None: - """Deploy joystick control module.""" - from dimos.robot.unitree_webrtc.g1_joystick_module import G1JoystickModule - - logger.info("Deploying G1 joystick module...") - self.joystick = self._dimos.deploy(G1JoystickModule) - self.joystick.twist_out.transport = core.LCMTransport("/cmd_vel", Twist) - logger.info("Joystick module deployed - pygame window will open") - - def _deploy_ros_bridge(self) -> None: - """Deploy and configure ROS bridge.""" - self.ros_bridge = ROSBridge("g1_ros_bridge") - - # Add /cmd_vel topic from ROS to DIMOS - self.ros_bridge.add_topic( - "/cmd_vel", TwistStamped, ROSTwistStamped, direction=BridgeDirection.ROS_TO_DIMOS - ) - - # Add /state_estimation topic from ROS to DIMOS - self.ros_bridge.add_topic( - "/state_estimation", Odometry, ROSOdometry, direction=BridgeDirection.ROS_TO_DIMOS - ) - - # Add /tf topic from ROS to DIMOS - self.ros_bridge.add_topic( - "/tf", TFMessage, ROSTFMessage, direction=BridgeDirection.ROS_TO_DIMOS - ) - - from std_msgs.msg import Bool as ROSBool - - from dimos.msgs.std_msgs import Bool - - # Navigation control topics from autonomy stack - self.ros_bridge.add_topic( - "/goal_pose", PoseStamped, ROSPoseStamped, direction=BridgeDirection.DIMOS_TO_ROS - ) - self.ros_bridge.add_topic( - "/cancel_goal", Bool, ROSBool, direction=BridgeDirection.DIMOS_TO_ROS - ) - self.ros_bridge.add_topic( - "/goal_reached", Bool, ROSBool, direction=BridgeDirection.ROS_TO_DIMOS - ) - - self.ros_bridge.add_topic("/joy", Joy, ROSJoy, direction=BridgeDirection.DIMOS_TO_ROS) - - self.ros_bridge.add_topic( - "/registered_scan", - PointCloud2, - ROSPointCloud2, - direction=BridgeDirection.ROS_TO_DIMOS, - remap_topic="/map", - ) - - self.ros_bridge.start() - - logger.info( - "ROS bridge deployed: /cmd_vel, /state_estimation, /tf, /registered_scan (ROS → DIMOS)" - ) - - def _start_modules(self) -> None: - """Start all deployed modules.""" - self._dimos.start_all_modules() - - # Initialize skills after connection is established - if self.skill_library is not None: - for skill in self.skill_library: - if hasattr(skill, "__name__"): - self.skill_library.create_instance(skill.__name__, robot=self) - if isinstance(self.skill_library, MyUnitreeSkills): - self.skill_library._robot = self - self.skill_library.init() - self.skill_library.initialize_skills() - - def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> None: - """Send movement command to robot.""" - self.connection.move(twist_stamped, duration) - - def get_odom(self) -> PoseStamped: - """Get the robot's odometry.""" - # Note: odom functionality removed from G1ConnectionModule - return None - - @property - def spatial_memory(self) -> SpatialMemory | None: - return self.spatial_memory_module - - -def main() -> None: - """Main entry point for testing.""" - import argparse - import os - - from dotenv import load_dotenv - - load_dotenv() - - parser = argparse.ArgumentParser(description="Unitree G1 Humanoid Robot Control") - parser.add_argument("--ip", default=os.getenv("ROBOT_IP"), help="Robot IP address") - parser.add_argument("--joystick", action="store_true", help="Enable pygame joystick control") - parser.add_argument("--camera", action="store_true", help="Enable usb camera module") - parser.add_argument("--output-dir", help="Output directory for logs/data") - parser.add_argument("--record", help="Path to save recording") - parser.add_argument("--replay", help="Path to replay recording from") - - args = parser.parse_args() - - pubsub.lcm.autoconf() - - robot = UnitreeG1( - ip=args.ip, - output_dir=args.output_dir, - recording_path=args.record, - replay_path=args.replay, - enable_joystick=args.joystick, - enable_camera=args.camera, - enable_connection=os.getenv("ROBOT_IP") is not None, - enable_ros_bridge=True, - enable_perception=True, - ) - robot.start() - - # time.sleep(7) - # print("Starting navigation...") - # print( - # robot.nav.go_to( - # PoseStamped( - # ts=time.time(), - # frame_id="map", - # position=Vector3(0.0, 0.0, 0.03), - # orientation=Quaternion(0, 0, 0, 0), - # ), - # timeout=10, - # ), - # ) - try: - if args.joystick: - print("\n" + "=" * 50) - print("G1 HUMANOID JOYSTICK CONTROL") - print("=" * 50) - print("Focus the pygame window to control") - print("Keys:") - print(" WASD = Forward/Back/Strafe") - print(" QE = Turn Left/Right") - print(" Space = Emergency Stop") - print(" ESC = Quit pygame (then Ctrl+C to exit)") - print("=" * 50 + "\n") - - logger.info("G1 robot running. Press Ctrl+C to stop.") - while True: - time.sleep(1) - except KeyboardInterrupt: - logger.info("Shutting down...") - robot.stop() - - -if __name__ == "__main__": - main() diff --git a/dimos/robot/unitree_webrtc/unitree_g1_blueprints.py b/dimos/robot/unitree_webrtc/unitree_g1_blueprints.py new file mode 100644 index 0000000000..3c11d32f0a --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_g1_blueprints.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Blueprint configurations for Unitree G1 humanoid robot. + +This module provides pre-configured blueprints for various G1 robot setups, +from basic teleoperation to full autonomous agent configurations. +""" + +from dimos_lcm.foxglove_msgs import SceneUpdate +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + ImageAnnotations, +) +from dimos_lcm.sensor_msgs import CameraInfo + +from dimos.agents.agent import llm_agent +from dimos.agents.cli.human import human_input +from dimos.agents.skills.navigation import navigation_skill +from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE +from dimos.core.blueprints import autoconnect +from dimos.core.transport import LCMTransport, pSHMTransport +from dimos.hardware.sensors.camera import zed +from dimos.hardware.sensors.camera.module import camera_module # type: ignore[attr-defined] +from dimos.hardware.sensors.camera.webcam import Webcam +from dimos.mapping.costmapper import cost_mapper +from dimos.mapping.voxels import voxel_mapper +from dimos.msgs.geometry_msgs import ( + PoseStamped, + Quaternion, + Transform, + Twist, + Vector3, +) +from dimos.msgs.nav_msgs import Odometry, Path +from dimos.msgs.sensor_msgs import Image, PointCloud2 +from dimos.msgs.std_msgs import Bool +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.navigation.frontier_exploration import wavefront_frontier_explorer +from dimos.navigation.replanning_a_star.module import replanning_a_star_planner +from dimos.navigation.rosnav import ros_nav +from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector +from dimos.perception.detection.module3D import Detection3DModule, detection3d_module +from dimos.perception.detection.moduleDB import ObjectDBModule, detectionDB_module +from dimos.perception.detection.person_tracker import PersonTracker, person_tracker_module +from dimos.perception.object_tracker import object_tracking +from dimos.perception.spatial_perception import spatial_memory +from dimos.robot.foxglove_bridge import foxglove_bridge +from dimos.robot.unitree.connection.g1 import g1_connection +from dimos.robot.unitree.connection.g1sim import g1_sim_connection +from dimos.robot.unitree_webrtc.keyboard_teleop import keyboard_teleop +from dimos.robot.unitree_webrtc.unitree_g1_skill_container import g1_skills +from dimos.utils.monitoring import utilization +from dimos.web.websocket_vis.websocket_vis_module import websocket_vis + +_basic_no_nav = ( + autoconnect( + camera_module( + transform=Transform( + translation=Vector3(0.05, 0.0, 0.0), + rotation=Quaternion.from_euler(Vector3(0.0, 0.2, 0.0)), + frame_id="sensor", + child_frame_id="camera_link", + ), + hardware=lambda: Webcam( + camera_index=0, + frequency=15, + stereo_slice="left", + camera_info=zed.CameraInfo.SingleWebcam, + ), + ), + voxel_mapper(voxel_size=0.1), + cost_mapper(), + wavefront_frontier_explorer(), + # Visualization + websocket_vis(), + foxglove_bridge(), + ) + .global_config(n_dask_workers=4, robot_model="unitree_g1") + .transports( + { + # G1 uses Twist for movement commands + ("cmd_vel", Twist): LCMTransport("/cmd_vel", Twist), + # State estimation from ROS + ("state_estimation", Odometry): LCMTransport("/state_estimation", Odometry), + # Odometry output from ROSNavigationModule + ("odom", PoseStamped): LCMTransport("/odom", PoseStamped), + # Navigation module topics from nav_bot + ("goal_req", PoseStamped): LCMTransport("/goal_req", PoseStamped), + ("goal_active", PoseStamped): LCMTransport("/goal_active", PoseStamped), + ("path_active", Path): LCMTransport("/path_active", Path), + ("pointcloud", PointCloud2): LCMTransport("/lidar", PointCloud2), + ("global_pointcloud", PointCloud2): LCMTransport("/map", PointCloud2), + # Original navigation topics for backwards compatibility + ("goal_pose", PoseStamped): LCMTransport("/goal_pose", PoseStamped), + ("goal_reached", Bool): LCMTransport("/goal_reached", Bool), + ("cancel_goal", Bool): LCMTransport("/cancel_goal", Bool), + # Camera topics (if camera module is added) + ("color_image", Image): LCMTransport("/g1/color_image", Image), + ("camera_info", CameraInfo): LCMTransport("/g1/camera_info", CameraInfo), + } + ) +) + +basic_ros = autoconnect( + _basic_no_nav, + g1_connection(), + ros_nav(), +) + +basic_sim = autoconnect( + _basic_no_nav, + g1_sim_connection(), + replanning_a_star_planner(), +) + +_perception_and_memory = autoconnect( + spatial_memory(), + object_tracking(frame_id="camera_link"), + utilization(), +) + +standard = autoconnect( + basic_ros, + _perception_and_memory, +).global_config(n_dask_workers=8) + +standard_sim = autoconnect( + basic_sim, + _perception_and_memory, +).global_config(n_dask_workers=8) + +# Optimized configuration using shared memory for images +standard_with_shm = autoconnect( + standard.transports( + { + ("color_image", Image): pSHMTransport( + "/g1/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ), + } + ), + foxglove_bridge( + shm_channels=[ + "/g1/color_image#sensor_msgs.Image", + ] + ), +) + +_agentic_skills = autoconnect( + llm_agent(), + human_input(), + navigation_skill(), + g1_skills(), +) + +# Full agentic configuration with LLM and skills +agentic = autoconnect( + standard, + _agentic_skills, +) + +agentic_sim = autoconnect( + standard_sim, + _agentic_skills, +) + +# Configuration with joystick control for teleoperation +with_joystick = autoconnect( + basic_ros, + keyboard_teleop(), # Pygame-based joystick control +) + +# Detection configuration with person tracking and 3D detection +detection = ( + autoconnect( + basic_ros, + # Person detection modules with YOLO + detection3d_module( + camera_info=zed.CameraInfo.SingleWebcam, + detector=YoloPersonDetector, + ), + detectionDB_module( + camera_info=zed.CameraInfo.SingleWebcam, + filter=lambda det: det.class_id == 0, # Filter for person class only + ), + person_tracker_module( + cameraInfo=zed.CameraInfo.SingleWebcam, + ), + ) + .global_config(n_dask_workers=8) + .remappings( + [ + # Connect detection modules to camera and lidar + (Detection3DModule, "image", "color_image"), + (Detection3DModule, "pointcloud", "pointcloud"), + (ObjectDBModule, "image", "color_image"), + (ObjectDBModule, "pointcloud", "pointcloud"), + (PersonTracker, "image", "color_image"), + (PersonTracker, "detections", "detections_2d"), + ] + ) + .transports( + { + # Detection 3D module outputs + ("detections", Detection3DModule): LCMTransport( + "/detector3d/detections", Detection2DArray + ), + ("annotations", Detection3DModule): LCMTransport( + "/detector3d/annotations", ImageAnnotations + ), + ("scene_update", Detection3DModule): LCMTransport( + "/detector3d/scene_update", SceneUpdate + ), + ("detected_pointcloud_0", Detection3DModule): LCMTransport( + "/detector3d/pointcloud/0", PointCloud2 + ), + ("detected_pointcloud_1", Detection3DModule): LCMTransport( + "/detector3d/pointcloud/1", PointCloud2 + ), + ("detected_pointcloud_2", Detection3DModule): LCMTransport( + "/detector3d/pointcloud/2", PointCloud2 + ), + ("detected_image_0", Detection3DModule): LCMTransport("/detector3d/image/0", Image), + ("detected_image_1", Detection3DModule): LCMTransport("/detector3d/image/1", Image), + ("detected_image_2", Detection3DModule): LCMTransport("/detector3d/image/2", Image), + # Detection DB module outputs + ("detections", ObjectDBModule): LCMTransport( + "/detectorDB/detections", Detection2DArray + ), + ("annotations", ObjectDBModule): LCMTransport( + "/detectorDB/annotations", ImageAnnotations + ), + ("scene_update", ObjectDBModule): LCMTransport("/detectorDB/scene_update", SceneUpdate), + ("detected_pointcloud_0", ObjectDBModule): LCMTransport( + "/detectorDB/pointcloud/0", PointCloud2 + ), + ("detected_pointcloud_1", ObjectDBModule): LCMTransport( + "/detectorDB/pointcloud/1", PointCloud2 + ), + ("detected_pointcloud_2", ObjectDBModule): LCMTransport( + "/detectorDB/pointcloud/2", PointCloud2 + ), + ("detected_image_0", ObjectDBModule): LCMTransport("/detectorDB/image/0", Image), + ("detected_image_1", ObjectDBModule): LCMTransport("/detectorDB/image/1", Image), + ("detected_image_2", ObjectDBModule): LCMTransport("/detectorDB/image/2", Image), + # Person tracker outputs + ("target", PersonTracker): LCMTransport("/person_tracker/target", PoseStamped), + } + ) +) + +# Full featured configuration with everything +full_featured = autoconnect( + standard_with_shm, + _agentic_skills, + keyboard_teleop(), +) diff --git a/dimos/robot/unitree_webrtc/unitree_g1_skill_container.py b/dimos/robot/unitree_webrtc/unitree_g1_skill_container.py index 170b577c21..99b028b4d9 100644 --- a/dimos/robot/unitree_webrtc/unitree_g1_skill_container.py +++ b/dimos/robot/unitree_webrtc/unitree_g1_skill_container.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,25 +13,19 @@ # limitations under the License. """ -Unitree G1 skill container for the new agents2 framework. +Unitree G1 skill container for the new agents framework. Dynamically generates skills for G1 humanoid robot including arm controls and movement modes. """ -from __future__ import annotations - -from typing import TYPE_CHECKING +import difflib from dimos.core.core import rpc -from dimos.msgs.geometry_msgs import TwistStamped, Vector3 +from dimos.core.skill_module import SkillModule +from dimos.msgs.geometry_msgs import Twist, Vector3 from dimos.protocol.skill.skill import skill -from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer from dimos.utils.logging_config import setup_logger -if TYPE_CHECKING: - from dimos.robot.unitree_webrtc.unitree_g1 import UnitreeG1 - from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 - -logger = setup_logger("dimos.robot.unitree_webrtc.unitree_g1_skill_container") +logger = setup_logger() # G1 Arm Actions - all use api_id 7106 on topic "rt/api/arm/request" G1_ARM_CONTROLS = [ @@ -58,25 +52,20 @@ ("RunMode", 801, "Switch to running mode."), ] +_ARM_COMMANDS: dict[str, tuple[int, str]] = { + name: (id_, description) for name, id_, description in G1_ARM_CONTROLS +} -class UnitreeG1SkillContainer(UnitreeSkillContainer): - """Container for Unitree G1 humanoid robot skills. - - Inherits all Go2 skills and adds G1-specific arm controls and movement modes. - """ +_MODE_COMMANDS: dict[str, tuple[int, str]] = { + name: (id_, description) for name, id_, description in G1_MODE_CONTROLS +} - def __init__(self, robot: UnitreeG1 | UnitreeGo2 | None = None) -> None: - """Initialize the skill container with robot reference. - - Args: - robot: The UnitreeG1 or UnitreeGo2 robot instance - """ - # Initialize parent class to get all base Unitree skills - super().__init__(robot) - # Add G1-specific skills on top - self._generate_arm_skills() - self._generate_mode_skills() +class UnitreeG1SkillContainer(SkillModule): + rpc_calls: list[str] = [ + "G1ConnectionModule.move", + "G1ConnectionModule.publish_request", + ] @rpc def start(self) -> None: @@ -86,146 +75,87 @@ def start(self) -> None: def stop(self) -> None: super().stop() - def _generate_arm_skills(self) -> None: - """Dynamically generate arm control skills from G1_ARM_CONTROLS list.""" - logger.info(f"Generating {len(G1_ARM_CONTROLS)} G1 arm control skills") - - for name, data_value, description in G1_ARM_CONTROLS: - skill_name = self._convert_to_snake_case(name) - self._create_arm_skill(skill_name, data_value, description, name) - - def _generate_mode_skills(self) -> None: - """Dynamically generate movement mode skills from G1_MODE_CONTROLS list.""" - logger.info(f"Generating {len(G1_MODE_CONTROLS)} G1 movement mode skills") - - for name, data_value, description in G1_MODE_CONTROLS: - skill_name = self._convert_to_snake_case(name) - self._create_mode_skill(skill_name, data_value, description, name) + @skill() + def move(self, x: float, y: float = 0.0, yaw: float = 0.0, duration: float = 0.0) -> str: + """Move the robot using direct velocity commands. Determine duration required based on user distance instructions. - def _create_arm_skill( - self, skill_name: str, data_value: int, description: str, original_name: str - ) -> None: - """Create a dynamic arm control skill method with the @skill decorator. + Example call: + args = { "x": 0.5, "y": 0.0, "yaw": 0.0, "duration": 2.0 } + move(**args) Args: - skill_name: Snake_case name for the method - data_value: The arm action data value - description: Human-readable description - original_name: Original CamelCase name for display + x: Forward velocity (m/s) + y: Left/right velocity (m/s) + yaw: Rotational velocity (rad/s) + duration: How long to move (seconds) """ - def dynamic_skill_func(self) -> str: - """Dynamic arm skill function.""" - return self._execute_arm_command(data_value, original_name) - - # Set the function's metadata - dynamic_skill_func.__name__ = skill_name - dynamic_skill_func.__doc__ = description - - # Apply the @skill decorator - decorated_skill = skill()(dynamic_skill_func) - - # Bind the method to the instance - bound_method = decorated_skill.__get__(self, self.__class__) + move_rpc = self.get_rpc_calls("G1ConnectionModule.move") + twist = Twist(linear=Vector3(x, y, 0), angular=Vector3(0, 0, yaw)) + move_rpc(twist, duration=duration) + return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" - # Add it as an attribute - setattr(self, skill_name, bound_method) + @skill() + def execute_arm_command(self, command_name: str) -> str: + return self._execute_g1_command(_ARM_COMMANDS, 7106, command_name) - logger.debug(f"Generated arm skill: {skill_name} (data={data_value})") + @skill() + def execute_mode_command(self, command_name: str) -> str: + return self._execute_g1_command(_MODE_COMMANDS, 7101, command_name) - def _create_mode_skill( - self, skill_name: str, data_value: int, description: str, original_name: str - ) -> None: - """Create a dynamic movement mode skill method with the @skill decorator. + def _execute_g1_command( + self, command_dict: dict[str, tuple[int, str]], api_id: int, command_name: str + ) -> str: + publish_request_rpc = self.get_rpc_calls("G1ConnectionModule.publish_request") - Args: - skill_name: Snake_case name for the method - data_value: The mode data value - description: Human-readable description - original_name: Original CamelCase name for display - """ + if command_name not in command_dict: + suggestions = difflib.get_close_matches( + command_name, command_dict.keys(), n=3, cutoff=0.6 + ) + return f"There's no '{command_name}' command. Did you mean: {suggestions}" - def dynamic_skill_func(self) -> str: - """Dynamic mode skill function.""" - return self._execute_mode_command(data_value, original_name) + id_, _ = command_dict[command_name] - # Set the function's metadata - dynamic_skill_func.__name__ = skill_name - dynamic_skill_func.__doc__ = description + try: + publish_request_rpc( + "rt/api/sport/request", {"api_id": api_id, "parameter": {"data": id_}} + ) + return f"'{command_name}' command executed successfully." + except Exception as e: + logger.error(f"Failed to execute {command_name}: {e}") + return "Failed to execute the command." - # Apply the @skill decorator - decorated_skill = skill()(dynamic_skill_func) - # Bind the method to the instance - bound_method = decorated_skill.__get__(self, self.__class__) +_arm_commands = "\n".join( + [f'- "{name}": {description}' for name, (_, description) in _ARM_COMMANDS.items()] +) - # Add it as an attribute - setattr(self, skill_name, bound_method) +UnitreeG1SkillContainer.execute_arm_command.__doc__ = f"""Execute a Unitree G1 arm command. - logger.debug(f"Generated mode skill: {skill_name} (data={data_value})") +Example usage: - # ========== Override Skills for G1 ========== + execute_arm_command("ArmHeart") - @skill() - def move(self, x: float, y: float = 0.0, yaw: float = 0.0, duration: float = 0.0) -> str: - """Move the robot using direct velocity commands (G1 version with TwistStamped). +Here are all the command names and what they do. - Args: - x: Forward velocity (m/s) - y: Left/right velocity (m/s) - yaw: Rotational velocity (rad/s) - duration: How long to move (seconds) - """ - if self._robot is None: - return "Error: Robot not connected" +{_arm_commands} +""" - # G1 uses TwistStamped instead of Twist - twist_stamped = TwistStamped(linear=Vector3(x, y, 0), angular=Vector3(0, 0, yaw)) - self._robot.move(twist_stamped, duration=duration) - return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" +_mode_commands = "\n".join( + [f'- "{name}": {description}' for name, (_, description) in _MODE_COMMANDS.items()] +) - # ========== Helper Methods ========== +UnitreeG1SkillContainer.execute_mode_command.__doc__ = f"""Execute a Unitree G1 mode command. - def _execute_arm_command(self, data_value: int, name: str) -> str: - """Execute an arm command through WebRTC interface. +Example usage: - Args: - data_value: The arm action data value - name: Human-readable name of the command - """ - if self._robot is None: - return f"Error: Robot not connected (cannot execute {name})" + execute_mode_command("RunMode") - try: - self._robot.connection.publish_request( - "rt/api/arm/request", {"api_id": 7106, "parameter": {"data": data_value}} - ) - message = f"G1 arm action {name} executed successfully (data={data_value})" - logger.info(message) - return message - except Exception as e: - error_msg = f"Failed to execute G1 arm action {name}: {e}" - logger.error(error_msg) - return error_msg +Here are all the command names and what they do. - def _execute_mode_command(self, data_value: int, name: str) -> str: - """Execute a movement mode command through WebRTC interface. +{_mode_commands} +""" - Args: - data_value: The mode data value - name: Human-readable name of the command - """ - if self._robot is None: - return f"Error: Robot not connected (cannot execute {name})" +g1_skills = UnitreeG1SkillContainer.blueprint - try: - self._robot.connection.publish_request( - "rt/api/sport/request", {"api_id": 7101, "parameter": {"data": data_value}} - ) - message = f"G1 mode {name} activated successfully (data={data_value})" - logger.info(message) - return message - except Exception as e: - error_msg = f"Failed to execute G1 mode {name}: {e}" - logger.error(error_msg) - return error_msg +__all__ = ["UnitreeG1SkillContainer", "g1_skills"] diff --git a/dimos/robot/unitree_webrtc/unitree_go2.py b/dimos/robot/unitree_webrtc/unitree_go2.py deleted file mode 100644 index b91433ead8..0000000000 --- a/dimos/robot/unitree_webrtc/unitree_go2.py +++ /dev/null @@ -1,705 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import functools -import logging -import os -import time -import warnings - -from dimos_lcm.sensor_msgs import CameraInfo -from dimos_lcm.std_msgs import Bool, String -from reactivex import Observable -from reactivex.disposable import CompositeDisposable - -from dimos import core -from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE -from dimos.core import In, Module, Out, rpc -from dimos.core.global_config import GlobalConfig -from dimos.core.module_coordinator import ModuleCoordinator -from dimos.core.resource import Resource -from dimos.mapping.types import LatLon -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 -from dimos.msgs.nav_msgs import OccupancyGrid, Path -from dimos.msgs.sensor_msgs import Image -from dimos.msgs.std_msgs import Header -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.navigation.bbox_navigation import BBoxNavigationModule -from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator, NavigatorState -from dimos.navigation.frontier_exploration import WavefrontFrontierExplorer -from dimos.navigation.global_planner import AstarPlanner -from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner -from dimos.perception.common.utils import ( - load_camera_info, - load_camera_info_opencv, - rectify_image, -) -from dimos.perception.object_tracker_2d import ObjectTracker2D -from dimos.perception.spatial_perception import SpatialMemory -from dimos.protocol import pubsub -from dimos.protocol.pubsub.lcmpubsub import LCM -from dimos.protocol.tf import TF -from dimos.robot.foxglove_bridge import FoxgloveBridge -from dimos.robot.robot import UnitreeRobot -from dimos.robot.unitree_webrtc.connection import UnitreeWebRTCConnection -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.map import Map -from dimos.robot.unitree_webrtc.type.odometry import Odometry -from dimos.robot.unitree_webrtc.unitree_skills import MyUnitreeSkills -from dimos.skills.skills import AbstractRobotSkill, SkillLibrary -from dimos.types.robot_capabilities import RobotCapability -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger -from dimos.utils.monitoring import UtilizationModule -from dimos.utils.testing import TimedSensorReplay -from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule - -logger = setup_logger(__file__, level=logging.INFO) - -# Suppress verbose loggers -logging.getLogger("aiortc.codecs.h264").setLevel(logging.ERROR) -logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) -logging.getLogger("websockets.server").setLevel(logging.ERROR) -logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) -logging.getLogger("asyncio").setLevel(logging.ERROR) -logging.getLogger("root").setLevel(logging.WARNING) - -# Suppress warnings -warnings.filterwarnings("ignore", message="coroutine.*was never awaited") -warnings.filterwarnings("ignore", message="H264Decoder.*failed to decode") - - -class ReplayRTC(Resource): - """Replay WebRTC connection for testing with recorded data.""" - - def __init__(self, *args, **kwargs) -> None: - get_data("unitree_office_walk") # Preload data for testing - - def start(self) -> None: - pass - - def stop(self) -> None: - pass - - def standup(self) -> None: - print("standup suppressed") - - def liedown(self) -> None: - print("liedown suppressed") - - @functools.cache - def lidar_stream(self): - print("lidar stream start") - lidar_store = TimedSensorReplay("unitree_office_walk/lidar", autocast=LidarMessage.from_msg) - return lidar_store.stream() - - @functools.cache - def odom_stream(self): - print("odom stream start") - odom_store = TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - return odom_store.stream() - - @functools.cache - def video_stream(self): - print("video stream start") - video_store = TimedSensorReplay( - "unitree_office_walk/video", autocast=lambda x: Image.from_numpy(x).to_rgb() - ) - return video_store.stream() - - def move(self, twist: Twist, duration: float = 0.0) -> None: - pass - - def publish_request(self, topic: str, data: dict): - """Fake publish request for testing.""" - return {"status": "ok", "message": "Fake publish"} - - -class ConnectionModule(Module): - """Module that handles robot sensor data, movement commands, and camera information.""" - - cmd_vel: In[Twist] = None - odom: Out[PoseStamped] = None - gps_location: Out[LatLon] = None - lidar: Out[LidarMessage] = None - color_image: Out[Image] = None - camera_info: Out[CameraInfo] = None - camera_pose: Out[PoseStamped] = None - ip: str - connection_type: str = "webrtc" - - _odom: PoseStamped = None - _lidar: LidarMessage = None - _last_image: Image = None - - def __init__( - self, - ip: str | None = None, - connection_type: str | None = None, - rectify_image: bool = True, - global_config: GlobalConfig | None = None, - *args, - **kwargs, - ) -> None: - cfg = global_config or GlobalConfig() - self.ip = ip if ip is not None else cfg.robot_ip - self.connection_type = connection_type or cfg.unitree_connection_type - self.rectify_image = not cfg.use_simulation - self.tf = TF() - self.connection = None - - # Load camera parameters from YAML - base_dir = os.path.dirname(os.path.abspath(__file__)) - - # Use sim camera parameters for mujoco, real camera for others - if connection_type == "mujoco": - camera_params_path = os.path.join(base_dir, "params", "sim_camera.yaml") - else: - camera_params_path = os.path.join(base_dir, "params", "front_camera_720.yaml") - - self.lcm_camera_info = load_camera_info(camera_params_path, frame_id="camera_link") - - # Load OpenCV matrices for rectification if enabled - if rectify_image: - self.camera_matrix, self.dist_coeffs = load_camera_info_opencv(camera_params_path) - self.lcm_camera_info.D = [0.0] * len( - self.lcm_camera_info.D - ) # zero out distortion coefficients for rectification - else: - self.camera_matrix = None - self.dist_coeffs = None - - Module.__init__(self, *args, **kwargs) - - @rpc - def start(self) -> None: - """Start the connection and subscribe to sensor streams.""" - super().start() - - match self.connection_type: - case "webrtc": - self.connection = UnitreeWebRTCConnection(self.ip) - case "replay": - self.connection = ReplayRTC(self.ip) - case "mujoco": - from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection - - self.connection = MujocoConnection() - case _: - raise ValueError(f"Unknown connection type: {self.connection_type}") - - self.connection.start() - - # Connect sensor streams to outputs - unsub = self.connection.lidar_stream().subscribe(self.lidar.publish) - self._disposables.add(unsub) - - unsub = self.connection.odom_stream().subscribe(self._publish_tf) - self._disposables.add(unsub) - - if self.connection_type == "mujoco": - unsub = self.connection.gps_stream().subscribe(self._publish_gps_location) - self._disposables.add(unsub) - - unsub = self.connection.video_stream().subscribe(self._on_video) - self._disposables.add(unsub) - - unsub = self.cmd_vel.subscribe(self.move) - self._disposables.add(unsub) - - @rpc - def stop(self) -> None: - if self.connection: - self.connection.stop() - super().stop() - - def _on_video(self, msg: Image) -> None: - """Handle incoming video frames and publish synchronized camera data.""" - # Apply rectification if enabled - if self.rectify_image: - rectified_msg = rectify_image(msg, self.camera_matrix, self.dist_coeffs) - self._last_image = rectified_msg - self.color_image.publish(rectified_msg) - else: - self._last_image = msg - self.color_image.publish(msg) - - # Publish camera info and pose synchronized with video - timestamp = msg.ts if msg.ts else time.time() - self._publish_camera_info(timestamp) - self._publish_camera_pose(timestamp) - - def _publish_gps_location(self, msg: LatLon) -> None: - self.gps_location.publish(msg) - - def _publish_tf(self, msg) -> None: - self._odom = msg - self.odom.publish(msg) - self.tf.publish(Transform.from_pose("base_link", msg)) - camera_link = Transform( - translation=Vector3(0.3, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ts=time.time(), - ) - self.tf.publish(camera_link) - - def _publish_camera_info(self, timestamp: float) -> None: - header = Header(timestamp, "camera_link") - self.lcm_camera_info.header = header - self.camera_info.publish(self.lcm_camera_info) - - def _publish_camera_pose(self, timestamp: float) -> None: - """Publish camera pose from TF lookup.""" - try: - # Look up transform from world to camera_link - transform = self.tf.get( - parent_frame="world", - child_frame="camera_link", - time_point=timestamp, - time_tolerance=1.0, - ) - - if transform: - pose_msg = PoseStamped( - ts=timestamp, - frame_id="camera_link", - position=transform.translation, - orientation=transform.rotation, - ) - self.camera_pose.publish(pose_msg) - else: - logger.debug("Could not find transform from world to camera_link") - - except Exception as e: - logger.error(f"Error publishing camera pose: {e}") - - @rpc - def get_odom(self) -> PoseStamped | None: - """Get the robot's odometry. - - Returns: - The robot's odometry - """ - return self._odom - - @rpc - def move(self, twist: Twist, duration: float = 0.0) -> None: - """Send movement command to robot.""" - self.connection.move(twist, duration) - - @rpc - def standup(self): - """Make the robot stand up.""" - return self.connection.standup() - - @rpc - def liedown(self): - """Make the robot lie down.""" - return self.connection.liedown() - - @rpc - def publish_request(self, topic: str, data: dict): - """Publish a request to the WebRTC connection. - Args: - topic: The RTC topic to publish to - data: The data dictionary to publish - Returns: - The result of the publish request - """ - return self.connection.publish_request(topic, data) - - -connection = ConnectionModule.blueprint - - -class UnitreeGo2(UnitreeRobot, Resource): - """Full Unitree Go2 robot with navigation and perception capabilities.""" - - _dimos: ModuleCoordinator - _disposables: CompositeDisposable = CompositeDisposable() - - def __init__( - self, - ip: str | None, - output_dir: str | None = None, - websocket_port: int = 7779, - skill_library: SkillLibrary | None = None, - connection_type: str | None = "webrtc", - ) -> None: - """Initialize the robot system. - - Args: - ip: Robot IP address (or None for replay connection) - output_dir: Directory for saving outputs (default: assets/output) - websocket_port: Port for web visualization - skill_library: Skill library instance - connection_type: webrtc, replay, or mujoco - """ - super().__init__() - self._dimos = ModuleCoordinator(n=8, memory_limit="8GiB") - self.ip = ip - self.connection_type = connection_type or "webrtc" - if ip is None and self.connection_type == "webrtc": - self.connection_type = "replay" # Auto-enable playback if no IP provided - self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") - self.websocket_port = websocket_port - self.lcm = LCM() - - # Initialize skill library - if skill_library is None: - skill_library = MyUnitreeSkills() - self.skill_library = skill_library - - # Set capabilities - self.capabilities = [RobotCapability.LOCOMOTION, RobotCapability.VISION] - - self.connection = None - self.mapper = None - self.global_planner = None - self.local_planner = None - self.navigator = None - self.frontier_explorer = None - self.websocket_vis = None - self.foxglove_bridge = None - self.spatial_memory_module = None - self.object_tracker = None - self.utilization_module = None - - self._setup_directories() - - def _setup_directories(self) -> None: - """Setup directories for spatial memory storage.""" - os.makedirs(self.output_dir, exist_ok=True) - logger.info(f"Robot outputs will be saved to: {self.output_dir}") - - # Initialize memory directories - self.memory_dir = os.path.join(self.output_dir, "memory") - os.makedirs(self.memory_dir, exist_ok=True) - - # Initialize spatial memory properties - self.spatial_memory_dir = os.path.join(self.memory_dir, "spatial_memory") - self.spatial_memory_collection = "spatial_memory" - self.db_path = os.path.join(self.spatial_memory_dir, "chromadb_data") - self.visual_memory_path = os.path.join(self.spatial_memory_dir, "visual_memory.pkl") - - # Create spatial memory directories - os.makedirs(self.spatial_memory_dir, exist_ok=True) - os.makedirs(self.db_path, exist_ok=True) - - def start(self) -> None: - self.lcm.start() - self._dimos.start() - - self._deploy_connection() - self._deploy_mapping() - self._deploy_navigation() - self._deploy_visualization() - self._deploy_foxglove_bridge() - self._deploy_perception() - self._deploy_camera() - - self._start_modules() - logger.info("UnitreeGo2 initialized and started") - - def stop(self) -> None: - if self.foxglove_bridge: - self.foxglove_bridge.stop() - self._disposables.dispose() - self._dimos.stop() - self.lcm.stop() - - def _deploy_connection(self) -> None: - """Deploy and configure the connection module.""" - self.connection = self._dimos.deploy( - ConnectionModule, self.ip, connection_type=self.connection_type - ) - - self.connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) - self.connection.odom.transport = core.LCMTransport("/odom", PoseStamped) - self.connection.gps_location.transport = core.pLCMTransport("/gps_location") - self.connection.color_image.transport = core.pSHMTransport( - "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - self.connection.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - self.connection.camera_info.transport = core.LCMTransport("/go2/camera_info", CameraInfo) - self.connection.camera_pose.transport = core.LCMTransport("/go2/camera_pose", PoseStamped) - - def _deploy_mapping(self) -> None: - """Deploy and configure the mapping module.""" - min_height = 0.3 if self.connection_type == "mujoco" else 0.15 - self.mapper = self._dimos.deploy( - Map, voxel_size=0.5, global_publish_interval=2.5, min_height=min_height - ) - - self.mapper.global_map.transport = core.LCMTransport("/global_map", LidarMessage) - self.mapper.global_costmap.transport = core.LCMTransport("/global_costmap", OccupancyGrid) - self.mapper.local_costmap.transport = core.LCMTransport("/local_costmap", OccupancyGrid) - - self.mapper.lidar.connect(self.connection.lidar) - - def _deploy_navigation(self) -> None: - """Deploy and configure navigation modules.""" - self.global_planner = self._dimos.deploy(AstarPlanner) - self.local_planner = self._dimos.deploy(HolonomicLocalPlanner) - self.navigator = self._dimos.deploy( - BehaviorTreeNavigator, - reset_local_planner=self.local_planner.reset, - check_goal_reached=self.local_planner.is_goal_reached, - ) - self.frontier_explorer = self._dimos.deploy(WavefrontFrontierExplorer) - - self.navigator.target.transport = core.LCMTransport("/navigation_goal", PoseStamped) - self.navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) - self.navigator.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) - self.navigator.navigation_state.transport = core.LCMTransport("/navigation_state", String) - self.navigator.global_costmap.transport = core.LCMTransport( - "/global_costmap", OccupancyGrid - ) - self.global_planner.path.transport = core.LCMTransport("/global_path", Path) - self.local_planner.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - self.frontier_explorer.goal_request.transport = core.LCMTransport( - "/goal_request", PoseStamped - ) - self.frontier_explorer.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) - self.frontier_explorer.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) - self.frontier_explorer.stop_explore_cmd.transport = core.LCMTransport( - "/stop_explore_cmd", Bool - ) - - self.global_planner.target.connect(self.navigator.target) - - self.global_planner.global_costmap.connect(self.mapper.global_costmap) - self.global_planner.odom.connect(self.connection.odom) - - self.local_planner.path.connect(self.global_planner.path) - self.local_planner.local_costmap.connect(self.mapper.local_costmap) - self.local_planner.odom.connect(self.connection.odom) - - self.connection.cmd_vel.connect(self.local_planner.cmd_vel) - - self.navigator.odom.connect(self.connection.odom) - - self.frontier_explorer.global_costmap.connect(self.mapper.global_costmap) - self.frontier_explorer.odom.connect(self.connection.odom) - - def _deploy_visualization(self) -> None: - """Deploy and configure visualization modules.""" - self.websocket_vis = self._dimos.deploy(WebsocketVisModule, port=self.websocket_port) - self.websocket_vis.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) - self.websocket_vis.gps_goal.transport = core.pLCMTransport("/gps_goal") - self.websocket_vis.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) - self.websocket_vis.stop_explore_cmd.transport = core.LCMTransport("/stop_explore_cmd", Bool) - self.websocket_vis.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - - self.websocket_vis.odom.connect(self.connection.odom) - self.websocket_vis.gps_location.connect(self.connection.gps_location) - self.websocket_vis.path.connect(self.global_planner.path) - self.websocket_vis.global_costmap.connect(self.mapper.global_costmap) - - def _deploy_foxglove_bridge(self) -> None: - self.foxglove_bridge = FoxgloveBridge( - shm_channels=[ - "/go2/color_image#sensor_msgs.Image", - "/go2/tracked_overlay#sensor_msgs.Image", - ] - ) - self.foxglove_bridge.start() - - def _deploy_perception(self) -> None: - """Deploy and configure perception modules.""" - # Deploy spatial memory - self.spatial_memory_module = self._dimos.deploy( - SpatialMemory, - collection_name=self.spatial_memory_collection, - db_path=self.db_path, - visual_memory_path=self.visual_memory_path, - output_dir=self.spatial_memory_dir, - ) - - self.spatial_memory_module.color_image.transport = core.pSHMTransport( - "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - self.spatial_memory_module.odom.transport = core.LCMTransport( - "/go2/camera_pose", PoseStamped - ) - - logger.info("Spatial memory module deployed and connected") - - # Deploy 2D object tracker - self.object_tracker = self._dimos.deploy( - ObjectTracker2D, - frame_id="camera_link", - ) - - # Deploy bbox navigation module - self.bbox_navigator = self._dimos.deploy(BBoxNavigationModule, goal_distance=1.0) - - self.utilization_module = self._dimos.deploy(UtilizationModule) - - # Set up transports for object tracker - self.object_tracker.detection2darray.transport = core.LCMTransport( - "/go2/detection2d", Detection2DArray - ) - self.object_tracker.tracked_overlay.transport = core.pSHMTransport( - "/go2/tracked_overlay", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - - # Set up transports for bbox navigator - self.bbox_navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) - - logger.info("Object tracker and bbox navigator modules deployed") - - def _deploy_camera(self) -> None: - """Deploy and configure the camera module.""" - # Connect object tracker inputs - if self.object_tracker: - self.object_tracker.color_image.connect(self.connection.color_image) - logger.info("Object tracker connected to camera") - - # Connect bbox navigator inputs - if self.bbox_navigator: - self.bbox_navigator.detection2d.connect(self.object_tracker.detection2darray) - self.bbox_navigator.camera_info.connect(self.connection.camera_info) - self.bbox_navigator.goal_request.connect(self.navigator.goal_request) - logger.info("BBox navigator connected") - - def _start_modules(self) -> None: - """Start all deployed modules in the correct order.""" - self._dimos.start_all_modules() - - # Initialize skills after connection is established - if self.skill_library is not None: - for skill in self.skill_library: - if isinstance(skill, AbstractRobotSkill): - self.skill_library.create_instance(skill.__name__, robot=self) - if isinstance(self.skill_library, MyUnitreeSkills): - self.skill_library._robot = self - self.skill_library.init() - self.skill_library.initialize_skills() - - def move(self, twist: Twist, duration: float = 0.0) -> None: - """Send movement command to robot.""" - self.connection.move(twist, duration) - - def explore(self) -> bool: - """Start autonomous frontier exploration. - - Returns: - True if exploration started successfully - """ - return self.frontier_explorer.explore() - - def navigate_to(self, pose: PoseStamped, blocking: bool = True) -> bool: - """Navigate to a target pose. - - Args: - pose: Target pose to navigate to - blocking: If True, block until goal is reached. If False, return immediately. - - Returns: - If blocking=True: True if navigation was successful, False otherwise - If blocking=False: True if goal was accepted, False otherwise - """ - - logger.info( - f"Navigating to pose: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" - ) - self.navigator.set_goal(pose) - time.sleep(1.0) - - if blocking: - while self.navigator.get_state() == NavigatorState.FOLLOWING_PATH: - time.sleep(0.25) - - time.sleep(1.0) - if not self.navigator.is_goal_reached(): - logger.info("Navigation was cancelled or failed") - return False - else: - logger.info("Navigation goal reached") - return True - - return True - - def stop_exploration(self) -> bool: - """Stop autonomous exploration. - - Returns: - True if exploration was stopped - """ - self.navigator.cancel_goal() - return self.frontier_explorer.stop_exploration() - - def is_exploration_active(self) -> bool: - return self.frontier_explorer.is_exploration_active() - - def cancel_navigation(self) -> bool: - """Cancel the current navigation goal. - - Returns: - True if goal was cancelled - """ - return self.navigator.cancel_goal() - - @property - def spatial_memory(self) -> SpatialMemory | None: - """Get the robot's spatial memory module. - - Returns: - SpatialMemory module instance or None if perception is disabled - """ - return self.spatial_memory_module - - @functools.cached_property - def gps_position_stream(self) -> Observable[LatLon]: - return self.connection.gps_location.transport.pure_observable() - - def get_odom(self) -> PoseStamped: - """Get the robot's odometry. - - Returns: - The robot's odometry - """ - return self.connection.get_odom() - - -def main() -> None: - """Main entry point.""" - ip = os.getenv("ROBOT_IP") - connection_type = os.getenv("CONNECTION_TYPE", "webrtc") - - pubsub.lcm.autoconf() - - robot = UnitreeGo2(ip=ip, websocket_port=7779, connection_type=connection_type) - robot.start() - - try: - while True: - time.sleep(0.1) - except KeyboardInterrupt: - pass - finally: - robot.stop() - - -if __name__ == "__main__": - main() - - -__all__ = ["ConnectionModule", "ReplayRTC", "UnitreeGo2", "connection"] diff --git a/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py b/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py index b74756cf84..7629644ed6 100644 --- a/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py +++ b/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,115 +14,187 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos_lcm.sensor_msgs import CameraInfo +import platform -from dimos.agents2.agent import llm_agent -from dimos.agents2.cli.human import human_input -from dimos.agents2.skills.navigation import navigation_skill -from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE, DEFAULT_CAPACITY_DEPTH_IMAGE +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( # type: ignore[import-untyped] + ImageAnnotations, +) + +from dimos.agents.agent import llm_agent +from dimos.agents.cli.human import human_input +from dimos.agents.cli.web import web_input +from dimos.agents.ollama_agent import ollama_installed +from dimos.agents.skills.navigation import navigation_skill +from dimos.agents.skills.speak_skill import speak_skill +from dimos.agents.spec import Provider +from dimos.agents.vlm_agent import vlm_agent +from dimos.agents.vlm_stream_tester import vlm_stream_tester +from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE from dimos.core.blueprints import autoconnect from dimos.core.transport import JpegLcmTransport, JpegShmTransport, LCMTransport, pSHMTransport -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Image -from dimos.navigation.bt_navigator.navigator import ( - behavior_tree_navigator, -) +from dimos.dashboard.tf_rerun_module import tf_rerun +from dimos.mapping.costmapper import cost_mapper +from dimos.mapping.voxels import voxel_mapper +from dimos.msgs.sensor_msgs import Image, PointCloud2 +from dimos.msgs.vision_msgs import Detection2DArray from dimos.navigation.frontier_exploration import ( wavefront_frontier_explorer, ) -from dimos.navigation.global_planner import astar_planner -from dimos.navigation.local_planner.holonomic_local_planner import ( - holonomic_local_planner, +from dimos.navigation.replanning_a_star.module import ( + replanning_a_star_planner, ) -from dimos.perception.object_tracker import object_tracking +from dimos.perception.detection.moduleDB import ObjectDBModule, detectionDB_module from dimos.perception.spatial_perception import spatial_memory +from dimos.protocol.mcp.mcp import MCPModule from dimos.robot.foxglove_bridge import foxglove_bridge -from dimos.robot.unitree_webrtc.depth_module import depth_module -from dimos.robot.unitree_webrtc.type.map import mapper -from dimos.robot.unitree_webrtc.unitree_go2 import connection +from dimos.robot.unitree.connection.go2 import GO2Connection, go2_connection +from dimos.robot.unitree_webrtc.unitree_skill_container import unitree_skills from dimos.utils.monitoring import utilization from dimos.web.websocket_vis.websocket_vis_module import websocket_vis -basic = ( - autoconnect( - connection(), - mapper(voxel_size=0.5, global_publish_interval=2.5), - astar_planner(), - holonomic_local_planner(), - behavior_tree_navigator(), - wavefront_frontier_explorer(), - websocket_vis(), - foxglove_bridge(), - ) - .global_config(n_dask_workers=4) - .transports( - # These are kept the same so that we don't have to change foxglove configs. - # Although we probably should. - { - ("color_image", Image): LCMTransport("/go2/color_image", Image), - ("camera_pose", PoseStamped): LCMTransport("/go2/camera_pose", PoseStamped), - ("camera_info", CameraInfo): LCMTransport("/go2/camera_info", CameraInfo), - } - ) +# Mac has some issue with high bandwidth UDP +# +# so we use pSHMTransport for color_image +# (Could we adress this on the system config layer? Is this fixable on mac?) +mac = autoconnect( + foxglove_bridge( + shm_channels=[ + "/color_image#sensor_msgs.Image", + ] + ), +).transports( + { + ("color_image", Image): pSHMTransport( + "color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ), + } ) -standard = ( + +linux = autoconnect(foxglove_bridge()) + +basic = autoconnect( + go2_connection(), + linux if platform.system() == "Linux" else mac, + websocket_vis(), + tf_rerun(), # Auto-visualize all TF transforms in Rerun +).global_config(n_dask_workers=4, robot_model="unitree_go2") + +nav = autoconnect( + basic, + voxel_mapper(voxel_size=0.1), + cost_mapper(), + replanning_a_star_planner(), + wavefront_frontier_explorer(), +).global_config(n_dask_workers=6, robot_model="unitree_go2") + +detection = ( autoconnect( - basic, - spatial_memory(), - object_tracking(frame_id="camera_link"), - depth_module(), - utilization(), + nav, + detectionDB_module( + camera_info=GO2Connection.camera_info_static, + ), ) - .global_config(n_dask_workers=8) - .transports( - { - ("depth_image", Image): LCMTransport("/go2/depth_image", Image), - } + .remappings( + [ + (ObjectDBModule, "pointcloud", "global_map"), + ] ) -) - -standard_with_shm = autoconnect( - standard.transports( + .transports( { - ("color_image", Image): pSHMTransport( - "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + # Detection 3D module outputs + ("detections", ObjectDBModule): LCMTransport( + "/detector3d/detections", Detection2DArray ), - ("depth_image", Image): pSHMTransport( - "/go2/depth_image", default_capacity=DEFAULT_CAPACITY_DEPTH_IMAGE + ("annotations", ObjectDBModule): LCMTransport( + "/detector3d/annotations", ImageAnnotations ), + # ("scene_update", ObjectDBModule): LCMTransport( + # "/detector3d/scene_update", SceneUpdate + # ), + ("detected_pointcloud_0", ObjectDBModule): LCMTransport( + "/detector3d/pointcloud/0", PointCloud2 + ), + ("detected_pointcloud_1", ObjectDBModule): LCMTransport( + "/detector3d/pointcloud/1", PointCloud2 + ), + ("detected_pointcloud_2", ObjectDBModule): LCMTransport( + "/detector3d/pointcloud/2", PointCloud2 + ), + ("detected_image_0", ObjectDBModule): LCMTransport("/detector3d/image/0", Image), + ("detected_image_1", ObjectDBModule): LCMTransport("/detector3d/image/1", Image), + ("detected_image_2", ObjectDBModule): LCMTransport("/detector3d/image/2", Image), } - ), - foxglove_bridge( - shm_channels=[ - "/go2/color_image#sensor_msgs.Image", - "/go2/depth_image#sensor_msgs.Image", - ] - ), + ) ) -standard_with_jpeglcm = standard.transports( + +spatial = autoconnect( + nav, + spatial_memory(), + utilization(), +).global_config(n_dask_workers=8) + +with_jpeglcm = nav.transports( { - ("color_image", Image): JpegLcmTransport("/go2/color_image", Image), + ("color_image", Image): JpegLcmTransport("/color_image", Image), } ) -standard_with_jpegshm = autoconnect( - standard.transports( +with_jpegshm = autoconnect( + nav.transports( { - ("color_image", Image): JpegShmTransport("/go2/color_image", quality=75), + ("color_image", Image): JpegShmTransport("/color_image", quality=75), } ), foxglove_bridge( jpeg_shm_channels=[ - "/go2/color_image#sensor_msgs.Image", + "/color_image#sensor_msgs.Image", ] ), ) -agentic = autoconnect( - standard, - llm_agent(), +_common_agentic = autoconnect( human_input(), navigation_skill(), + unitree_skills(), + web_input(), + speak_skill(), +) + +agentic = autoconnect( + spatial, + llm_agent(), + _common_agentic, +) + +agentic_mcp = autoconnect( + agentic, + MCPModule.blueprint(), +) + +agentic_ollama = autoconnect( + spatial, + llm_agent( + model="qwen3:8b", + provider=Provider.OLLAMA, # type: ignore[attr-defined] + ), + _common_agentic, +).requirements( + ollama_installed, +) + +agentic_huggingface = autoconnect( + spatial, + llm_agent( + model="Qwen/Qwen2.5-1.5B-Instruct", + provider=Provider.HUGGINGFACE, # type: ignore[attr-defined] + ), + _common_agentic, +) + +vlm_stream_test = autoconnect( + basic, + vlm_agent(), + vlm_stream_tester(), ) diff --git a/dimos/robot/unitree_webrtc/unitree_skill_container.py b/dimos/robot/unitree_webrtc/unitree_skill_container.py index e6179adcbb..c3dea43424 100644 --- a/dimos/robot/unitree_webrtc/unitree_skill_container.py +++ b/dimos/robot/unitree_webrtc/unitree_skill_container.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,136 +12,125 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Unitree skill container for the new agents2 framework. -Dynamically generates skills from UNITREE_WEBRTC_CONTROLS list. -""" - from __future__ import annotations import datetime +import difflib +import math import time -from typing import TYPE_CHECKING -from go2_webrtc_driver.constants import RTC_TOPIC +from unitree_webrtc_connect.constants import RTC_TOPIC -from dimos.core import Module from dimos.core.core import rpc -from dimos.msgs.geometry_msgs import Twist, Vector3 +from dimos.core.skill_module import SkillModule +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 +from dimos.navigation.base import NavigationState from dimos.protocol.skill.skill import skill from dimos.protocol.skill.type import Reducer, Stream from dimos.robot.unitree_webrtc.unitree_skills import UNITREE_WEBRTC_CONTROLS from dimos.utils.logging_config import setup_logger -if TYPE_CHECKING: - from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 +logger = setup_logger() -logger = setup_logger("dimos.robot.unitree_webrtc.unitree_skill_container") +_UNITREE_COMMANDS = { + name: (id_, description) + for name, id_, description in UNITREE_WEBRTC_CONTROLS + if name not in ["Reverse", "Spin"] +} -class UnitreeSkillContainer(Module): - """Container for Unitree Go2 robot skills using the new framework.""" - - def __init__(self, robot: UnitreeGo2 | None = None) -> None: - """Initialize the skill container with robot reference. - Args: - robot: The UnitreeGo2 robot instance - """ - super().__init__() - self._robot = robot +class UnitreeSkillContainer(SkillModule): + """Container for Unitree Go2 robot skills using the new framework.""" - # Dynamically generate skills from UNITREE_WEBRTC_CONTROLS - self._generate_unitree_skills() + rpc_calls: list[str] = [ + "NavigationInterface.set_goal", + "NavigationInterface.get_state", + "NavigationInterface.is_goal_reached", + "NavigationInterface.cancel_goal", + "GO2Connection.publish_request", + ] @rpc def start(self) -> None: super().start() + # Initialize TF early so it can start receiving transforms. + _ = self.tf @rpc def stop(self) -> None: - # TODO: Do I need to clean up dynamic skills? super().stop() - def _generate_unitree_skills(self) -> None: - """Dynamically generate skills from the UNITREE_WEBRTC_CONTROLS list.""" - logger.info(f"Generating {len(UNITREE_WEBRTC_CONTROLS)} dynamic Unitree skills") - - for name, api_id, description in UNITREE_WEBRTC_CONTROLS: - if name not in ["Reverse", "Spin"]: # Exclude reverse and spin as in original - # Convert CamelCase to snake_case for method name - skill_name = self._convert_to_snake_case(name) - self._create_dynamic_skill(skill_name, api_id, description, name) - - def _convert_to_snake_case(self, name: str) -> str: - """Convert CamelCase to snake_case. - - Examples: - StandUp -> stand_up - RecoveryStand -> recovery_stand - FrontFlip -> front_flip - """ - result = [] - for i, char in enumerate(name): - if i > 0 and char.isupper(): - result.append("_") - result.append(char.lower()) - return "".join(result) - - def _create_dynamic_skill( - self, skill_name: str, api_id: int, description: str, original_name: str - ) -> None: - """Create a dynamic skill method with the @skill decorator. - - Args: - skill_name: Snake_case name for the method - api_id: The API command ID - description: Human-readable description - original_name: Original CamelCase name for display - """ - - # Define the skill function - def dynamic_skill_func(self) -> str: - """Dynamic skill function.""" - return self._execute_sport_command(api_id, original_name) - - # Set the function's metadata - dynamic_skill_func.__name__ = skill_name - dynamic_skill_func.__doc__ = description - - # Apply the @skill decorator - decorated_skill = skill()(dynamic_skill_func) + @skill() + def relative_move(self, forward: float = 0.0, left: float = 0.0, degrees: float = 0.0) -> str: + """Move the robot relative to its current position. - # Bind the method to the instance - bound_method = decorated_skill.__get__(self, self.__class__) + The `degrees` arguments refers to the rotation the robot should be at the end, relative to its current rotation. - # Add it as an attribute - setattr(self, skill_name, bound_method) + Example calls: - logger.debug(f"Generated skill: {skill_name} (API ID: {api_id})") + # Move to a point that's 2 meters forward and 1 to the right. + relative_move(forward=2, left=-1, degrees=0) - # ========== Explicit Skills ========== + # Move back 1 meter, while still facing the same direction. + relative_move(forward=-1, left=0, degrees=0) - @skill() - def move(self, x: float, y: float = 0.0, yaw: float = 0.0, duration: float = 0.0) -> str: - """Move the robot using direct velocity commands. Determine duration required based on user distance instructions. + # Rotate 90 degrees to the right (in place) + relative_move(forward=0, left=0, degrees=-90) - Example call: - args = { "x": 0.5, "y": 0.0, "yaw": 0.0, "duration": 2.0 } - move(**args) - - Args: - x: Forward velocity (m/s) - y: Left/right velocity (m/s) - yaw: Rotational velocity (rad/s) - duration: How long to move (seconds) + # Move 3 meters left, and face that direction + relative_move(forward=0, left=3, degrees=90) """ - if self._robot is None: - return "Error: Robot not connected" + forward, left, degrees = float(forward), float(left), float(degrees) - twist = Twist(linear=Vector3(x, y, 0), angular=Vector3(0, 0, yaw)) - self._robot.move(twist, duration=duration) - return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" + tf = self.tf.get("world", "base_link") + if tf is None: + return "Failed to get the position of the robot." + + try: + set_goal_rpc, get_state_rpc, is_goal_reached_rpc = self.get_rpc_calls( + "NavigationInterface.set_goal", + "NavigationInterface.get_state", + "NavigationInterface.is_goal_reached", + ) + except Exception: + logger.error("Navigation module not connected properly") + return "Failed to connect to navigation module." + + # TODO: Improve this. This is not a nice way to do it. I should + # subscribe to arrival/cancellation events instead. + + set_goal_rpc(self._generate_new_goal(tf.to_pose(), forward, left, degrees)) + + time.sleep(1.0) + + start_time = time.monotonic() + timeout = 100.0 + while get_state_rpc() == NavigationState.FOLLOWING_PATH: + if time.monotonic() - start_time > timeout: + return "Navigation timed out" + time.sleep(0.1) + + time.sleep(1.0) + + if not is_goal_reached_rpc(): + return "Navigation was cancelled or failed" + else: + return "Navigation goal reached" + + def _generate_new_goal( + self, current_pose: PoseStamped, forward: float, left: float, degrees: float + ) -> PoseStamped: + local_offset = Vector3(forward, left, 0) + global_offset = current_pose.orientation.rotate_vector(local_offset) + goal_position = current_pose.position + global_offset + + current_euler = current_pose.orientation.to_euler() + goal_yaw = current_euler.yaw + math.radians(degrees) + goal_euler = Vector3(current_euler.roll, current_euler.pitch, goal_yaw) + goal_orientation = Quaternion.from_euler(goal_euler) + + return PoseStamped(position=goal_position, orientation=goal_orientation) @skill() def wait(self, seconds: float) -> str: @@ -153,8 +142,8 @@ def wait(self, seconds: float) -> str: time.sleep(seconds) return f"Wait completed with length={seconds}s" - @skill(stream=Stream.passive, reducer=Reducer.latest) - def current_time(self): + @skill(stream=Stream.passive, reducer=Reducer.latest, hide_skill=True) # type: ignore[arg-type] + def current_time(self): # type: ignore[no-untyped-def] """Provides current time implicitly, don't call this skill directly.""" print("Starting current_time skill") while True: @@ -162,28 +151,45 @@ def current_time(self): time.sleep(1) @skill() - def speak(self, text: str) -> str: - """Speak text out loud through the robot's speakers.""" - return f"This is being said aloud: {text}" - - # ========== Helper Methods ========== + def execute_sport_command(self, command_name: str) -> str: + try: + publish_request = self.get_rpc_calls("GO2Connection.publish_request") + except Exception: + logger.error("GO2Connection not connected properly") + return "Failed to connect to GO2Connection." - def _execute_sport_command(self, api_id: int, name: str) -> str: - """Execute a sport command through WebRTC interface. + if command_name not in _UNITREE_COMMANDS: + suggestions = difflib.get_close_matches( + command_name, _UNITREE_COMMANDS.keys(), n=3, cutoff=0.6 + ) + return f"There's no '{command_name}' command. Did you mean: {suggestions}" - Args: - api_id: The API command ID - name: Human-readable name of the command - """ - if self._robot is None: - return f"Error: Robot not connected (cannot execute {name})" + id_, _ = _UNITREE_COMMANDS[command_name] try: - self._robot.connection.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": api_id}) - message = f"{name} command executed successfully (id={api_id})" - logger.info(message) - return message + publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": id_}) + return f"'{command_name}' command executed successfully." except Exception as e: - error_msg = f"Failed to execute {name}: {e}" - logger.error(error_msg) - return error_msg + logger.error(f"Failed to execute {command_name}: {e}") + return "Failed to execute the command." + + +_commands = "\n".join( + [f'- "{name}": {description}' for name, (_, description) in _UNITREE_COMMANDS.items()] +) + +UnitreeSkillContainer.execute_sport_command.__doc__ = f"""Execute a Unitree sport command. + +Example usage: + + execute_sport_command("FrontPounce") + +Here are all the command names and what they do. + +{_commands} +""" + + +unitree_skills = UnitreeSkillContainer.blueprint + +__all__ = ["UnitreeSkillContainer", "unitree_skills"] diff --git a/dimos/robot/unitree_webrtc/unitree_skills.py b/dimos/robot/unitree_webrtc/unitree_skills.py index 2bba4caa53..05e01f63fb 100644 --- a/dimos/robot/unitree_webrtc/unitree_skills.py +++ b/dimos/robot/unitree_webrtc/unitree_skills.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,12 +20,12 @@ from pydantic import Field if TYPE_CHECKING: - from dimos.robot.robot import MockRobot, Robot + from dimos.robot.robot import MockRobot, Robot # type: ignore[attr-defined] else: Robot = "Robot" MockRobot = "MockRobot" -from go2_webrtc_driver.constants import RTC_TOPIC +from unitree_webrtc_connect.constants import RTC_TOPIC from dimos.msgs.geometry_msgs import Twist, Vector3 from dimos.skills.skills import AbstractRobotSkill, AbstractSkill, SkillLibrary @@ -220,7 +220,7 @@ def __init__(self, robot: Robot | None = None, robot_type: str = "go2") -> None: robot_type: Type of robot ("go2" or "g1"), defaults to "go2" """ super().__init__() - self._robot: Robot = None + self._robot: Robot = None # type: ignore[assignment] self.robot_type = robot_type.lower() if self.robot_type not in ["go2", "g1"]: @@ -228,7 +228,7 @@ def __init__(self, robot: Robot | None = None, robot_type: str = "go2") -> None: # Add dynamic skills to this class based on robot type dynamic_skills = self.create_skills_live() - self.register_skills(dynamic_skills) + self.register_skills(dynamic_skills) # type: ignore[arg-type] @classmethod def register_skills(cls, skill_classes: AbstractSkill | list[AbstractSkill]) -> None: @@ -242,11 +242,11 @@ def register_skills(cls, skill_classes: AbstractSkill | list[AbstractSkill]) -> for skill_class in skill_classes: # Add to the class as a skill - setattr(cls, skill_class.__name__, skill_class) + setattr(cls, skill_class.__name__, skill_class) # type: ignore[attr-defined] def initialize_skills(self) -> None: for skill_class in self.get_class_skills(): - self.create_instance(skill_class.__name__, robot=self._robot) + self.create_instance(skill_class.__name__, robot=self._robot) # type: ignore[attr-defined] # Refresh the class skills self.refresh_class_skills() @@ -259,13 +259,13 @@ class BaseUnitreeSkill(AbstractRobotSkill): """Base skill for dynamic skill creation.""" def __call__(self) -> str: - super().__call__() + super().__call__() # type: ignore[no-untyped-call] # For Go2: Simple api_id based call if hasattr(self, "_app_id"): string = f"{Colors.GREEN_PRINT_COLOR}Executing Go2 skill: {self.__class__.__name__} with api_id={self._app_id}{Colors.RESET_COLOR}" print(string) - self._robot.connection.publish_request( + self._robot.connection.publish_request( # type: ignore[attr-defined] RTC_TOPIC["SPORT_MOD"], {"api_id": self._app_id} ) return f"{self.__class__.__name__} executed successfully" @@ -274,9 +274,9 @@ def __call__(self) -> str: elif hasattr(self, "_data_value"): string = f"{Colors.GREEN_PRINT_COLOR}Executing G1 skill: {self.__class__.__name__} with data={self._data_value}{Colors.RESET_COLOR}" print(string) - self._robot.connection.publish_request( - self._topic, - {"api_id": self._api_id, "parameter": {"data": self._data_value}}, + self._robot.connection.publish_request( # type: ignore[attr-defined] + self._topic, # type: ignore[attr-defined] + {"api_id": self._api_id, "parameter": {"data": self._data_value}}, # type: ignore[attr-defined] ) return f"{self.__class__.__name__} executed successfully" else: @@ -323,7 +323,7 @@ def __call__(self) -> str: ) skills_classes.append(skill_class) - return skills_classes + return skills_classes # type: ignore[return-value] # region Class-based Skills @@ -336,7 +336,7 @@ class Move(AbstractRobotSkill): duration: float = Field(default=0.0, description="How long to move (seconds).") def __call__(self) -> str: - self._robot.move( + self._robot.move( # type: ignore[attr-defined] Twist(linear=Vector3(self.x, self.y, 0.0), angular=Vector3(0.0, 0.0, self.yaw)), duration=self.duration, ) diff --git a/dimos/robot/utils/robot_debugger.py b/dimos/robot/utils/robot_debugger.py index b3cfb195ce..c7f3cd7291 100644 --- a/dimos/robot/utils/robot_debugger.py +++ b/dimos/robot/utils/robot_debugger.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,11 +17,11 @@ from dimos.core.resource import Resource from dimos.utils.logging_config import setup_logger -logger = setup_logger(__file__) +logger = setup_logger() class RobotDebugger(Resource): - def __init__(self, robot) -> None: + def __init__(self, robot) -> None: # type: ignore[no-untyped-def] self._robot = robot self._threaded_server = None @@ -30,8 +30,8 @@ def start(self) -> None: return try: - import rpyc - from rpyc.utils.server import ThreadedServer + import rpyc # type: ignore[import-not-found] + from rpyc.utils.server import ThreadedServer # type: ignore[import-not-found] except ImportError: return @@ -41,8 +41,8 @@ def start(self) -> None: robot = self._robot - class RobotService(rpyc.Service): - def exposed_robot(self): + class RobotService(rpyc.Service): # type: ignore[misc] + def exposed_robot(self): # type: ignore[no-untyped-def] return robot self._threaded_server = ThreadedServer( @@ -52,7 +52,7 @@ def exposed_robot(self): "allow_all_attrs": True, }, ) - self._threaded_server.start() + self._threaded_server.start() # type: ignore[attr-defined] def stop(self) -> None: if self._threaded_server: diff --git a/dimos/rxpy_backpressure/LICENSE.txt b/dimos/rxpy_backpressure/LICENSE.txt new file mode 100644 index 0000000000..8e1d704dc7 --- /dev/null +++ b/dimos/rxpy_backpressure/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Mark Haynes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dimos/rxpy_backpressure/__init__.py b/dimos/rxpy_backpressure/__init__.py new file mode 100644 index 0000000000..ff3b1f37c0 --- /dev/null +++ b/dimos/rxpy_backpressure/__init__.py @@ -0,0 +1,3 @@ +from dimos.rxpy_backpressure.backpressure import BackPressure + +__all__ = [BackPressure] diff --git a/dimos/rxpy_backpressure/backpressure.py b/dimos/rxpy_backpressure/backpressure.py new file mode 100644 index 0000000000..bf84fa95bd --- /dev/null +++ b/dimos/rxpy_backpressure/backpressure.py @@ -0,0 +1,29 @@ +# Copyright (c) rxpy_backpressure +from dimos.rxpy_backpressure.drop import ( + wrap_observer_with_buffer_strategy, + wrap_observer_with_drop_strategy, +) +from dimos.rxpy_backpressure.latest import wrap_observer_with_latest_strategy + + +class BackPressure: + """ + Latest strategy will remember the next most recent message to process and will call the observer with it when + the observer has finished processing its current message. + """ + + LATEST = wrap_observer_with_latest_strategy + + """ + Drop strategy accepts a cache size, the strategy will remember the most recent messages and remove older + messages from the cache. The strategy guarantees that the oldest messages in the cache are passed to the + observer first. + :param cache_size: int = 10 is default + """ + DROP = wrap_observer_with_drop_strategy + + """ + Buffer strategy has a unbounded cache and will pass all messages to its consumer in the order it received them + beware of Memory leaks due to a build up of messages. + """ + BUFFER = wrap_observer_with_buffer_strategy diff --git a/dimos/rxpy_backpressure/drop.py b/dimos/rxpy_backpressure/drop.py new file mode 100644 index 0000000000..6273042f42 --- /dev/null +++ b/dimos/rxpy_backpressure/drop.py @@ -0,0 +1,67 @@ +# Copyright (c) rxpy_backpressure +from typing import Any + +from dimos.rxpy_backpressure.function_runner import thread_function_runner +from dimos.rxpy_backpressure.locks import BooleanLock, Lock +from dimos.rxpy_backpressure.observer import Observer + + +class DropBackPressureStrategy(Observer): + def __init__(self, wrapped_observer: Observer, cache_size: int): + self.wrapped_observer: Observer = wrapped_observer + self.__function_runner = thread_function_runner + self.__lock: Lock = BooleanLock() + self.__cache_size: int | None = cache_size + self.__message_cache: list = [] + self.__error_cache: list = [] + + def on_next(self, message): + if self.__lock.is_locked(): + self.__update_cache(self.__message_cache, message) + else: + self.__lock.lock() + self.__function_runner(self, self.__on_next, message) + + @staticmethod + def __on_next(self, message: any): + self.wrapped_observer.on_next(message) + if len(self.__message_cache) > 0: + self.__function_runner(self, self.__on_next, self.__message_cache.pop(0)) + else: + self.__lock.unlock() + + def on_error(self, error: any): + if self.__lock.is_locked(): + self.__update_cache(self.__error_cache, error) + else: + self.__lock.lock() + self.__function_runner(self, self.__on_error, error) + + @staticmethod + def __on_error(self, error: any): + self.wrapped_observer.on_error(error) + if len(self.__error_cache) > 0: + self.__function_runner(self, self.__on_error, self.__error_cache.pop(0)) + else: + self.__lock.unlock() + + def __update_cache(self, cache: list, item: Any): + if self.__cache_size is None or len(cache) < self.__cache_size: + cache.append(item) + else: + cache.pop(0) + cache.append(item) + + def on_completed(self): + self.wrapped_observer.on_completed() + + def is_locked(self): + return self.__lock.is_locked() + + +def wrap_observer_with_drop_strategy(observer: Observer, cache_size: int = 10) -> Observer: + return DropBackPressureStrategy(observer, cache_size=cache_size) + + +def wrap_observer_with_buffer_strategy(observer: Observer) -> Observer: + return DropBackPressureStrategy(observer, cache_size=None) diff --git a/dimos/rxpy_backpressure/function_runner.py b/dimos/rxpy_backpressure/function_runner.py new file mode 100644 index 0000000000..7779016d41 --- /dev/null +++ b/dimos/rxpy_backpressure/function_runner.py @@ -0,0 +1,6 @@ +# Copyright (c) rxpy_backpressure +from threading import Thread + + +def thread_function_runner(self, func, message): + Thread(target=func, args=(self, message)).start() diff --git a/dimos/rxpy_backpressure/latest.py b/dimos/rxpy_backpressure/latest.py new file mode 100644 index 0000000000..73a4ebc8d9 --- /dev/null +++ b/dimos/rxpy_backpressure/latest.py @@ -0,0 +1,57 @@ +# Copyright (c) rxpy_backpressure +from typing import Optional + +from dimos.rxpy_backpressure.function_runner import thread_function_runner +from dimos.rxpy_backpressure.locks import BooleanLock, Lock +from dimos.rxpy_backpressure.observer import Observer + + +class LatestBackPressureStrategy(Observer): + def __init__(self, wrapped_observer: Observer): + self.wrapped_observer: Observer = wrapped_observer + self.__function_runner = thread_function_runner + self.__lock: Lock = BooleanLock() + self.__message_cache: Optional = None + self.__error_cache: Optional = None + + def on_next(self, message): + if self.__lock.is_locked(): + self.__message_cache = message + else: + self.__lock.lock() + self.__function_runner(self, self.__on_next, message) + + @staticmethod + def __on_next(self, message: any): + self.wrapped_observer.on_next(message) + if self.__message_cache is not None: + self.__function_runner(self, self.__on_next, self.__message_cache) + self.__message_cache = None + else: + self.__lock.unlock() + + def on_error(self, error: any): + if self.__lock.is_locked(): + self.__error_cache = error + else: + self.__lock.lock() + self.__function_runner(self, self.__on_error, error) + + @staticmethod + def __on_error(self, error: any): + self.wrapped_observer.on_error(error) + if self.__error_cache: + self.__function_runner(self, self.__on_error, self.__error_cache) + self.__error_cache = None + else: + self.__lock.unlock() + + def on_completed(self): + self.wrapped_observer.on_completed() + + def is_locked(self): + return self.__lock.is_locked() + + +def wrap_observer_with_latest_strategy(observer: Observer) -> Observer: + return LatestBackPressureStrategy(observer) diff --git a/dimos/rxpy_backpressure/locks.py b/dimos/rxpy_backpressure/locks.py new file mode 100644 index 0000000000..62c58c25b2 --- /dev/null +++ b/dimos/rxpy_backpressure/locks.py @@ -0,0 +1,30 @@ +# Copyright (c) rxpy_backpressure +from abc import abstractmethod + + +class Lock: + @abstractmethod + def is_locked(self) -> bool: + return NotImplemented + + @abstractmethod + def unlock(self): + return NotImplemented + + @abstractmethod + def lock(self): + return NotImplemented + + +class BooleanLock(Lock): + def __init__(self): + self.locked: bool = False + + def is_locked(self) -> bool: + return self.locked + + def unlock(self): + self.locked = False + + def lock(self): + self.locked = True diff --git a/dimos/rxpy_backpressure/observer.py b/dimos/rxpy_backpressure/observer.py new file mode 100644 index 0000000000..7cf023c04f --- /dev/null +++ b/dimos/rxpy_backpressure/observer.py @@ -0,0 +1,18 @@ +# Copyright (c) rxpy_backpressure +from abc import ABCMeta, abstractmethod + + +class Observer: + __metaclass__ = ABCMeta + + @abstractmethod + def on_next(self, value): + return NotImplemented + + @abstractmethod + def on_error(self, error): + return NotImplemented + + @abstractmethod + def on_completed(self): + return NotImplemented diff --git a/dimos/simulation/README.md b/dimos/simulation/README.md index 7304e45bf4..95d8b4cda1 100644 --- a/dimos/simulation/README.md +++ b/dimos/simulation/README.md @@ -91,8 +91,8 @@ This will: ## Viewing the Stream -The camera stream will be available at: +The camera stream will be available at: - RTSP: `rtsp://localhost:8554/stream` or `rtsp://:8554/stream` -You can view it using VLC or any RTSP-capable player. \ No newline at end of file +You can view it using VLC or any RTSP-capable player. diff --git a/dimos/simulation/__init__.py b/dimos/simulation/__init__.py index 2b77f47097..1a68191a36 100644 --- a/dimos/simulation/__init__.py +++ b/dimos/simulation/__init__.py @@ -2,14 +2,14 @@ try: from .isaac import IsaacSimulator, IsaacStream except ImportError: - IsaacSimulator = None # type: ignore - IsaacStream = None # type: ignore + IsaacSimulator = None # type: ignore[assignment, misc] + IsaacStream = None # type: ignore[assignment, misc] # Try to import Genesis components try: from .genesis import GenesisSimulator, GenesisStream except ImportError: - GenesisSimulator = None # type: ignore - GenesisStream = None # type: ignore + GenesisSimulator = None # type: ignore[assignment, misc] + GenesisStream = None # type: ignore[assignment, misc] __all__ = ["GenesisSimulator", "GenesisStream", "IsaacSimulator", "IsaacStream"] diff --git a/dimos/simulation/base/simulator_base.py b/dimos/simulation/base/simulator_base.py index 777893d74c..59e366a1d3 100644 --- a/dimos/simulation/base/simulator_base.py +++ b/dimos/simulation/base/simulator_base.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ def __init__( self, headless: bool = True, open_usd: str | None = None, # Keep for Isaac compatibility - entities: list[dict[str, str | dict]] | None = None, # Add for Genesis + entities: list[dict[str, str | dict]] | None = None, # type: ignore[type-arg] # Add for Genesis ) -> None: """Initialize the simulator. @@ -37,11 +37,11 @@ def __init__( self.stage = None @abstractmethod - def get_stage(self): + def get_stage(self): # type: ignore[no-untyped-def] """Get the current stage/scene.""" pass @abstractmethod - def close(self): + def close(self): # type: ignore[no-untyped-def] """Close the simulation.""" pass diff --git a/dimos/simulation/base/stream_base.py b/dimos/simulation/base/stream_base.py index 1fb0e86add..9f8898439e 100644 --- a/dimos/simulation/base/stream_base.py +++ b/dimos/simulation/base/stream_base.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ class StreamBase(ABC): """Base class for simulation streaming.""" @abstractmethod - def __init__( + def __init__( # type: ignore[no-untyped-def] self, simulator, width: int = 1920, @@ -61,12 +61,12 @@ def __init__( self.proc = None @abstractmethod - def _load_stage(self, usd_path: str | Path): + def _load_stage(self, usd_path: str | Path): # type: ignore[no-untyped-def] """Load stage from file.""" pass @abstractmethod - def _setup_camera(self): + def _setup_camera(self): # type: ignore[no-untyped-def] """Setup and validate camera.""" pass @@ -98,19 +98,19 @@ def _setup_ffmpeg(self) -> None: self.transport, self.rtsp_url, ] - self.proc = subprocess.Popen(command, stdin=subprocess.PIPE) + self.proc = subprocess.Popen(command, stdin=subprocess.PIPE) # type: ignore[assignment] @abstractmethod - def _setup_annotator(self): + def _setup_annotator(self): # type: ignore[no-untyped-def] """Setup annotator.""" pass @abstractmethod - def stream(self): + def stream(self): # type: ignore[no-untyped-def] """Start streaming.""" pass @abstractmethod - def cleanup(self): + def cleanup(self): # type: ignore[no-untyped-def] """Cleanup resources.""" pass diff --git a/dimos/simulation/genesis/simulator.py b/dimos/simulation/genesis/simulator.py index f3a73be08b..4e679dcfa3 100644 --- a/dimos/simulation/genesis/simulator.py +++ b/dimos/simulation/genesis/simulator.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # limitations under the License. -import genesis as gs # type: ignore +import genesis as gs # type: ignore[import-not-found] from ..base.simulator_base import SimulatorBase @@ -25,7 +25,7 @@ def __init__( self, headless: bool = True, open_usd: str | None = None, # Keep for compatibility - entities: list[dict[str, str | dict]] | None = None, + entities: list[dict[str, str | dict]] | None = None, # type: ignore[type-arg] ) -> None: """Initialize the Genesis simulation. @@ -74,10 +74,10 @@ def __init__( # Don't build scene yet - let stream add camera first self.is_built = False - def _load_entities(self, entities: list[dict[str, str | dict]]): + def _load_entities(self, entities: list[dict[str, str | dict]]): # type: ignore[no-untyped-def, type-arg] """Load multiple entities into the scene.""" for entity in entities: - entity_type = entity.get("type", "").lower() + entity_type = entity.get("type", "").lower() # type: ignore[union-attr] path = entity.get("path", "") params = entity.get("params", {}) @@ -107,7 +107,7 @@ def _load_entities(self, entities: list[dict[str, str | dict]]): print(f"[Genesis] Added MJCF model from {path}") elif entity_type == "primitive": - shape_type = params.pop("shape", "plane") + shape_type = params.pop("shape", "plane") # type: ignore[union-attr] if shape_type == "plane": morph = gs.morphs.Plane(**params) elif shape_type == "box": @@ -133,7 +133,7 @@ def _load_entities(self, entities: list[dict[str, str | dict]]): except Exception as e: print(f"[Warning] Failed to load entity {entity}: {e!s}") - def add_entity(self, entity_type: str, path: str = "", **params) -> None: + def add_entity(self, entity_type: str, path: str = "", **params) -> None: # type: ignore[no-untyped-def] """Add a single entity to the scene. Args: @@ -143,7 +143,7 @@ def add_entity(self, entity_type: str, path: str = "", **params) -> None: """ self._load_entities([{"type": entity_type, "path": path, "params": params}]) - def get_stage(self): + def get_stage(self): # type: ignore[no-untyped-def] """Get the current stage/scene.""" return self.scene diff --git a/dimos/simulation/genesis/stream.py b/dimos/simulation/genesis/stream.py index d24b254b38..0d3bcc6832 100644 --- a/dimos/simulation/genesis/stream.py +++ b/dimos/simulation/genesis/stream.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ class GenesisStream(StreamBase): """Genesis stream implementation.""" - def __init__( + def __init__( # type: ignore[no-untyped-def] self, simulator, width: int = 1920, @@ -110,8 +110,8 @@ def stream(self) -> None: frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # Write to FFmpeg - self.proc.stdin.write(frame.tobytes()) - self.proc.stdin.flush() + self.proc.stdin.write(frame.tobytes()) # type: ignore[attr-defined] + self.proc.stdin.flush() # type: ignore[attr-defined] # Log metrics frame_time = time.time() - frame_start @@ -134,8 +134,8 @@ def cleanup(self) -> None: """Cleanup resources.""" print("[Cleanup] Stopping FFmpeg process...") if hasattr(self, "proc"): - self.proc.stdin.close() - self.proc.wait() + self.proc.stdin.close() # type: ignore[attr-defined] + self.proc.wait() # type: ignore[attr-defined] print("[Cleanup] Closing simulation...") try: self.simulator.close() diff --git a/dimos/simulation/isaac/simulator.py b/dimos/simulation/isaac/simulator.py index 0d49b9145e..1b524e1cb5 100644 --- a/dimos/simulation/isaac/simulator.py +++ b/dimos/simulation/isaac/simulator.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # limitations under the License. -from isaacsim import SimulationApp +from isaacsim import SimulationApp # type: ignore[import-not-found] from ..base.simulator_base import SimulatorBase @@ -25,15 +25,15 @@ def __init__( self, headless: bool = True, open_usd: str | None = None, - entities: list[dict[str, str | dict]] | None = None, # Add but ignore + entities: list[dict[str, str | dict]] | None = None, # type: ignore[type-arg] # Add but ignore ) -> None: """Initialize the Isaac Sim simulation.""" super().__init__(headless, open_usd) self.app = SimulationApp({"headless": headless, "open_usd": open_usd}) - def get_stage(self): + def get_stage(self): # type: ignore[no-untyped-def] """Get the current USD stage.""" - import omni.usd + import omni.usd # type: ignore[import-not-found] self.stage = omni.usd.get_context().get_stage() return self.stage diff --git a/dimos/simulation/isaac/stream.py b/dimos/simulation/isaac/stream.py index eb85ba8815..e927c4bad4 100644 --- a/dimos/simulation/isaac/stream.py +++ b/dimos/simulation/isaac/stream.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ class IsaacStream(StreamBase): """Isaac Sim stream implementation.""" - def __init__( + def __init__( # type: ignore[no-untyped-def] self, simulator, width: int = 1920, @@ -49,20 +49,20 @@ def __init__( ) # Import omni.replicator after SimulationApp initialization - import omni.replicator.core as rep + import omni.replicator.core as rep # type: ignore[import-not-found] self.rep = rep # Initialize components if usd_path: self._load_stage(usd_path) - self._setup_camera() + self._setup_camera() # type: ignore[no-untyped-call] self._setup_ffmpeg() self._setup_annotator() - def _load_stage(self, usd_path: str | Path): + def _load_stage(self, usd_path: str | Path): # type: ignore[no-untyped-def] """Load USD stage from file.""" - import omni.usd + import omni.usd # type: ignore[import-not-found] abs_path = str(Path(usd_path).resolve()) omni.usd.get_context().open_stage(abs_path) @@ -70,7 +70,7 @@ def _load_stage(self, usd_path: str | Path): if not self.stage: raise RuntimeError(f"Failed to load stage: {abs_path}") - def _setup_camera(self): + def _setup_camera(self): # type: ignore[no-untyped-def] """Setup and validate camera.""" self.stage = self.simulator.get_stage() camera_prim = self.stage.GetPrimAtPath(self.camera_path) @@ -106,8 +106,8 @@ def stream(self) -> None: frame = cv2.cvtColor(frame, cv2.COLOR_RGBA2BGR) # Write to FFmpeg - self.proc.stdin.write(frame.tobytes()) - self.proc.stdin.flush() + self.proc.stdin.write(frame.tobytes()) # type: ignore[attr-defined] + self.proc.stdin.flush() # type: ignore[attr-defined] # Log metrics frame_time = time.time() - frame_start @@ -130,8 +130,8 @@ def cleanup(self) -> None: """Cleanup resources.""" print("[Cleanup] Stopping FFmpeg process...") if hasattr(self, "proc"): - self.proc.stdin.close() - self.proc.wait() + self.proc.stdin.close() # type: ignore[attr-defined] + self.proc.wait() # type: ignore[attr-defined] print("[Cleanup] Closing simulation...") self.simulator.close() print("[Cleanup] Successfully cleaned up resources") diff --git a/dimos/simulation/mujoco/constants.py b/dimos/simulation/mujoco/constants.py new file mode 100644 index 0000000000..aca916a372 --- /dev/null +++ b/dimos/simulation/mujoco/constants.py @@ -0,0 +1,34 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +# Video/Camera constants +VIDEO_WIDTH = 320 +VIDEO_HEIGHT = 240 +DEPTH_CAMERA_FOV = 160 + +# Depth camera range/filtering constants +MAX_RANGE = 3 +MIN_RANGE = 0.2 +MAX_HEIGHT = 1.2 + +# Lidar constants +LIDAR_RESOLUTION = 0.05 + +# Simulation timing constants +VIDEO_FPS = 20 +LIDAR_FPS = 2 + +LAUNCHER_PATH = Path(__file__).parent / "mujoco_process.py" diff --git a/dimos/simulation/mujoco/depth_camera.py b/dimos/simulation/mujoco/depth_camera.py index bb7cc34047..486b740ffd 100644 --- a/dimos/simulation/mujoco/depth_camera.py +++ b/dimos/simulation/mujoco/depth_camera.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,21 +15,21 @@ # limitations under the License. import math +from typing import Any import numpy as np -import open3d as o3d +from numpy.typing import NDArray +import open3d as o3d # type: ignore[import-untyped] -MAX_RANGE = 3 -MIN_RANGE = 0.2 -MAX_HEIGHT = 1.2 +from dimos.simulation.mujoco.constants import MAX_HEIGHT, MAX_RANGE, MIN_RANGE def depth_image_to_point_cloud( - depth_image: np.ndarray, - camera_pos: np.ndarray, - camera_mat: np.ndarray, + depth_image: NDArray[Any], + camera_pos: NDArray[Any], + camera_mat: NDArray[Any], fov_degrees: float = 120, -) -> np.ndarray: +) -> NDArray[Any]: """ Convert a depth image from a camera to a 3D point cloud using perspective projection. @@ -61,7 +61,7 @@ def depth_image_to_point_cloud( o3d_cloud = o3d.geometry.PointCloud.create_from_depth_image(o3d_depth, cam_intrinsics) # Convert Open3D point cloud to numpy array - camera_points = np.asarray(o3d_cloud.points) + camera_points: NDArray[Any] = np.asarray(o3d_cloud.points) if camera_points.size == 0: return np.array([]).reshape(0, 3) @@ -83,6 +83,6 @@ def depth_image_to_point_cloud( return np.array([]).reshape(0, 3) # Transform to world coordinates - world_points = (camera_mat @ camera_points.T).T + camera_pos + world_points: NDArray[Any] = (camera_mat @ camera_points.T).T + camera_pos return world_points diff --git a/dimos/simulation/mujoco/input_controller.py b/dimos/simulation/mujoco/input_controller.py new file mode 100644 index 0000000000..9ebe7ed98a --- /dev/null +++ b/dimos/simulation/mujoco/input_controller.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Protocol + +from numpy.typing import NDArray + + +class InputController(Protocol): + """A protocol for input devices to control the robot.""" + + def get_command(self) -> NDArray[Any]: ... + def stop(self) -> None: ... diff --git a/dimos/simulation/mujoco/model.py b/dimos/simulation/mujoco/model.py index 12d97181b2..de533521da 100644 --- a/dimos/simulation/mujoco/model.py +++ b/dimos/simulation/mujoco/model.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,54 +15,106 @@ # limitations under the License. +from pathlib import Path +import xml.etree.ElementTree as ET + from etils import epath import mujoco from mujoco_playground._src import mjx_env import numpy as np -from dimos.simulation.mujoco.policy import OnnxController -from dimos.simulation.mujoco.types import InputController +from dimos.core.global_config import GlobalConfig +from dimos.mapping.occupancy.extrude_occupancy import generate_mujoco_scene +from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid +from dimos.simulation.mujoco.input_controller import InputController +from dimos.simulation.mujoco.policy import G1OnnxController, Go1OnnxController, OnnxController +from dimos.utils.data import get_data + -_HERE = epath.Path(__file__).parent +def _get_data_dir() -> epath.Path: + return epath.Path(str(get_data("mujoco_sim"))) def get_assets() -> dict[str, bytes]: + data_dir = _get_data_dir() # Assets used from https://sketchfab.com/3d-models/mersus-office-8714be387bcd406898b2615f7dae3a47 # Created by Ryan Cassidy and Coleman Costello assets: dict[str, bytes] = {} - assets_path = _HERE / "../../../data/mujoco_sim/go1" - mjx_env.update_assets(assets, assets_path, "*.xml") - mjx_env.update_assets(assets, assets_path / "assets") - path = mjx_env.MENAGERIE_PATH / "unitree_go1" - mjx_env.update_assets(assets, path, "*.xml") - mjx_env.update_assets(assets, path / "assets") + mjx_env.update_assets(assets, data_dir, "*.xml") + mjx_env.update_assets(assets, data_dir / "scene_office1/textures", "*.png") + mjx_env.update_assets(assets, data_dir / "scene_office1/office_split", "*.obj") + mjx_env.update_assets(assets, mjx_env.MENAGERIE_PATH / "unitree_go1" / "assets") + mjx_env.update_assets(assets, mjx_env.MENAGERIE_PATH / "unitree_g1" / "assets") return assets -def load_model(input_device: InputController, model=None, data=None): +def load_model( + input_device: InputController, robot: str, scene_xml: str +) -> tuple[mujoco.MjModel, mujoco.MjData]: mujoco.set_mjcb_control(None) - model = mujoco.MjModel.from_xml_path( - (_HERE / "../../../data/mujoco_sim/go1/robot.xml").as_posix(), - assets=get_assets(), - ) + xml_string = get_model_xml(robot, scene_xml) + model = mujoco.MjModel.from_xml_string(xml_string, assets=get_assets()) data = mujoco.MjData(model) mujoco.mj_resetDataKeyframe(model, data, 0) + match robot: + case "unitree_g1": + sim_dt = 0.002 + case _: + sim_dt = 0.005 + ctrl_dt = 0.02 - sim_dt = 0.01 n_substeps = round(ctrl_dt / sim_dt) model.opt.timestep = sim_dt - policy = OnnxController( - policy_path=(_HERE / "../../../data/mujoco_sim/go1/go1_policy.onnx").as_posix(), - default_angles=np.array(model.keyframe("home").qpos[7:]), - n_substeps=n_substeps, - action_scale=0.5, - input_controller=input_device, - ) + params = { + "policy_path": (_get_data_dir() / f"{robot}_policy.onnx").as_posix(), + "default_angles": np.array(model.keyframe("home").qpos[7:]), + "n_substeps": n_substeps, + "action_scale": 0.5, + "input_controller": input_device, + "ctrl_dt": ctrl_dt, + } + + match robot: + case "unitree_go1": + policy: OnnxController = Go1OnnxController(**params) + case "unitree_g1": + policy = G1OnnxController(**params, drift_compensation=[-0.18, 0.0, -0.09]) + case _: + raise ValueError(f"Unknown robot policy: {robot}") mujoco.set_mjcb_control(policy.get_control) return model, data + + +def get_model_xml(robot: str, scene_xml: str) -> str: + root = ET.fromstring(scene_xml) + root.set("model", f"{robot}_scene") + root.insert(0, ET.Element("include", file=f"{robot}.xml")) + + # Ensure visual/map element exists with znear and zfar + visual = root.find("visual") + if visual is None: + visual = ET.SubElement(root, "visual") + map_elem = visual.find("map") + if map_elem is None: + map_elem = ET.SubElement(visual, "map") + map_elem.set("znear", "0.01") + map_elem.set("zfar", "10000") + + return ET.tostring(root, encoding="unicode") + + +def load_scene_xml(config: GlobalConfig) -> str: + if config.mujoco_room_from_occupancy: + path = Path(config.mujoco_room_from_occupancy) + return generate_mujoco_scene(OccupancyGrid.from_path(path)) + + mujoco_room = config.mujoco_room or "office1" + xml_file = (_get_data_dir() / f"scene_{mujoco_room}.xml").as_posix() + with open(xml_file) as f: + return f.read() diff --git a/dimos/simulation/mujoco/mujoco.py b/dimos/simulation/mujoco/mujoco.py deleted file mode 100644 index 5e867a26d1..0000000000 --- a/dimos/simulation/mujoco/mujoco.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import atexit -import logging -import threading -import time - -import mujoco -from mujoco import viewer -import numpy as np -import open3d as o3d - -from dimos.msgs.geometry_msgs import Quaternion, Twist, Vector3 -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.odometry import Odometry -from dimos.simulation.mujoco.depth_camera import depth_image_to_point_cloud -from dimos.simulation.mujoco.model import load_model - -LIDAR_RESOLUTION = 0.05 -DEPTH_CAMERA_FOV = 160 -STEPS_PER_FRAME = 2 -VIDEO_FPS = 20 -LIDAR_FPS = 4 - -logger = logging.getLogger(__name__) - - -class MujocoThread(threading.Thread): - def __init__(self) -> None: - super().__init__(daemon=True) - self.shared_pixels = None - self.pixels_lock = threading.RLock() - self.shared_depth_front = None - self.depth_lock_front = threading.RLock() - self.shared_depth_left = None - self.depth_left_lock = threading.RLock() - self.shared_depth_right = None - self.depth_right_lock = threading.RLock() - self.odom_data = None - self.odom_lock = threading.RLock() - self.lidar_lock = threading.RLock() - self.model = None - self.data = None - self._command = np.zeros(3, dtype=np.float32) - self._command_lock = threading.RLock() - self._is_running = True - self._stop_timer: threading.Timer | None = None - self._viewer = None - self._rgb_renderer = None - self._depth_renderer = None - self._depth_left_renderer = None - self._depth_right_renderer = None - self._cleanup_registered = False - - # Register cleanup on exit - atexit.register(self.cleanup) - - def run(self) -> None: - try: - self.run_simulation() - except Exception as e: - logger.error(f"MuJoCo simulation thread error: {e}") - finally: - self._cleanup_resources() - - def run_simulation(self) -> None: - self.model, self.data = load_model(self) - - camera_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_CAMERA, "head_camera") - lidar_camera_id = mujoco.mj_name2id( - self.model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_front_camera" - ) - lidar_left_camera_id = mujoco.mj_name2id( - self.model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_left_camera" - ) - lidar_right_camera_id = mujoco.mj_name2id( - self.model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_right_camera" - ) - - with viewer.launch_passive( - self.model, self.data, show_left_ui=False, show_right_ui=False - ) as m_viewer: - self._viewer = m_viewer - camera_size = (320, 240) - - # Create separate renderers for RGB and depth - self._rgb_renderer = mujoco.Renderer( - self.model, height=camera_size[1], width=camera_size[0] - ) - self._depth_renderer = mujoco.Renderer( - self.model, height=camera_size[1], width=camera_size[0] - ) - # Enable depth rendering only for depth renderer - self._depth_renderer.enable_depth_rendering() - - # Create renderers for left and right depth cameras - self._depth_left_renderer = mujoco.Renderer( - self.model, height=camera_size[1], width=camera_size[0] - ) - self._depth_left_renderer.enable_depth_rendering() - - self._depth_right_renderer = mujoco.Renderer( - self.model, height=camera_size[1], width=camera_size[0] - ) - self._depth_right_renderer.enable_depth_rendering() - - scene_option = mujoco.MjvOption() - - # Timing control variables - last_video_time = 0 - last_lidar_time = 0 - video_interval = 1.0 / VIDEO_FPS - lidar_interval = 1.0 / LIDAR_FPS - - while m_viewer.is_running() and self._is_running: - step_start = time.time() - - for _ in range(STEPS_PER_FRAME): - mujoco.mj_step(self.model, self.data) - - m_viewer.sync() - - # Odometry happens every loop - with self.odom_lock: - # base position - pos = self.data.qpos[0:3] - # base orientation - quat = self.data.qpos[3:7] # (w, x, y, z) - self.odom_data = (pos.copy(), quat.copy()) - - current_time = time.time() - - # Video rendering - if current_time - last_video_time >= video_interval: - self._rgb_renderer.update_scene( - self.data, camera=camera_id, scene_option=scene_option - ) - pixels = self._rgb_renderer.render() - - with self.pixels_lock: - self.shared_pixels = pixels.copy() - - last_video_time = current_time - - # Lidar rendering - if current_time - last_lidar_time >= lidar_interval: - # Render fisheye camera for depth/lidar data - self._depth_renderer.update_scene( - self.data, camera=lidar_camera_id, scene_option=scene_option - ) - # When depth rendering is enabled, render() returns depth as float array in meters - depth = self._depth_renderer.render() - - with self.depth_lock_front: - self.shared_depth_front = depth.copy() - - # Render left depth camera - self._depth_left_renderer.update_scene( - self.data, camera=lidar_left_camera_id, scene_option=scene_option - ) - depth_left = self._depth_left_renderer.render() - - with self.depth_left_lock: - self.shared_depth_left = depth_left.copy() - - # Render right depth camera - self._depth_right_renderer.update_scene( - self.data, camera=lidar_right_camera_id, scene_option=scene_option - ) - depth_right = self._depth_right_renderer.render() - - with self.depth_right_lock: - self.shared_depth_right = depth_right.copy() - - last_lidar_time = current_time - - # Control the simulation speed - time_until_next_step = self.model.opt.timestep - (time.time() - step_start) - if time_until_next_step > 0: - time.sleep(time_until_next_step) - - def _process_depth_camera(self, camera_name: str, depth_data, depth_lock) -> np.ndarray | None: - """Process a single depth camera and return point cloud points.""" - with depth_lock: - if depth_data is None: - return None - - depth_image = depth_data.copy() - camera_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_CAMERA, camera_name) - if camera_id == -1: - return None - - camera_pos = self.data.cam_xpos[camera_id] - camera_mat = self.data.cam_xmat[camera_id].reshape(3, 3) - points = depth_image_to_point_cloud( - depth_image, - camera_pos, - camera_mat, - fov_degrees=DEPTH_CAMERA_FOV, - ) - return points if points.size > 0 else None - - def get_lidar_message(self) -> LidarMessage | None: - all_points = [] - origin = None - - with self.lidar_lock: - if self.model is not None and self.data is not None: - pos = self.data.qpos[0:3] - origin = Vector3(pos[0], pos[1], pos[2]) - - cameras = [ - ("lidar_front_camera", self.shared_depth_front, self.depth_lock_front), - ("lidar_left_camera", self.shared_depth_left, self.depth_left_lock), - ("lidar_right_camera", self.shared_depth_right, self.depth_right_lock), - ] - - for camera_name, depth_data, depth_lock in cameras: - points = self._process_depth_camera(camera_name, depth_data, depth_lock) - if points is not None: - all_points.append(points) - - # Combine all point clouds - if not all_points: - return None - - combined_points = np.vstack(all_points) - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(combined_points) - - # Apply voxel downsampling to remove overlapping points - pcd = pcd.voxel_down_sample(voxel_size=LIDAR_RESOLUTION) - lidar_to_publish = LidarMessage( - pointcloud=pcd, - ts=time.time(), - origin=origin, - resolution=LIDAR_RESOLUTION, - ) - return lidar_to_publish - - def get_odom_message(self) -> Odometry | None: - with self.odom_lock: - if self.odom_data is None: - return None - pos, quat_wxyz = self.odom_data - - # MuJoCo uses (w, x, y, z) for quaternions. - # ROS and Dimos use (x, y, z, w). - orientation = Quaternion(quat_wxyz[1], quat_wxyz[2], quat_wxyz[3], quat_wxyz[0]) - - odom_to_publish = Odometry( - position=Vector3(pos[0], pos[1], pos[2]), - orientation=orientation, - ts=time.time(), - frame_id="world", - ) - return odom_to_publish - - def _stop_move(self) -> None: - with self._command_lock: - self._command = np.zeros(3, dtype=np.float32) - self._stop_timer = None - - def move(self, twist: Twist, duration: float = 0.0) -> None: - if self._stop_timer: - self._stop_timer.cancel() - - with self._command_lock: - self._command = np.array( - [twist.linear.x, twist.linear.y, twist.angular.z], dtype=np.float32 - ) - - if duration > 0: - self._stop_timer = threading.Timer(duration, self._stop_move) - self._stop_timer.daemon = True - self._stop_timer.start() - else: - self._stop_timer = None - - def get_command(self) -> np.ndarray: - with self._command_lock: - return self._command.copy() - - def stop(self) -> None: - """Stop the simulation thread gracefully.""" - self._is_running = False - - # Cancel any pending timers - if self._stop_timer: - self._stop_timer.cancel() - self._stop_timer = None - - # Wait for thread to finish - if self.is_alive(): - self.join(timeout=5.0) - if self.is_alive(): - logger.warning("MuJoCo thread did not stop gracefully within timeout") - - def cleanup(self) -> None: - """Clean up all resources. Can be called multiple times safely.""" - if self._cleanup_registered: - return - self._cleanup_registered = True - - logger.debug("Cleaning up MuJoCo resources") - self.stop() - self._cleanup_resources() - - def _cleanup_resources(self) -> None: - """Internal method to clean up MuJoCo-specific resources.""" - try: - # Cancel any timers - if self._stop_timer: - self._stop_timer.cancel() - self._stop_timer = None - - # Clean up renderers - if self._rgb_renderer is not None: - try: - self._rgb_renderer.close() - except Exception as e: - logger.debug(f"Error closing RGB renderer: {e}") - finally: - self._rgb_renderer = None - - if self._depth_renderer is not None: - try: - self._depth_renderer.close() - except Exception as e: - logger.debug(f"Error closing depth renderer: {e}") - finally: - self._depth_renderer = None - - if self._depth_left_renderer is not None: - try: - self._depth_left_renderer.close() - except Exception as e: - logger.debug(f"Error closing left depth renderer: {e}") - finally: - self._depth_left_renderer = None - - if self._depth_right_renderer is not None: - try: - self._depth_right_renderer.close() - except Exception as e: - logger.debug(f"Error closing right depth renderer: {e}") - finally: - self._depth_right_renderer = None - - # Clear data references - with self.pixels_lock: - self.shared_pixels = None - - with self.depth_lock_front: - self.shared_depth_front = None - - with self.depth_left_lock: - self.shared_depth_left = None - - with self.depth_right_lock: - self.shared_depth_right = None - - with self.odom_lock: - self.odom_data = None - - # Clear model and data - self.model = None - self.data = None - - # Reset MuJoCo control callback - try: - mujoco.set_mjcb_control(None) - except Exception as e: - logger.debug(f"Error resetting MuJoCo control callback: {e}") - - except Exception as e: - logger.error(f"Error during resource cleanup: {e}") - - def __del__(self) -> None: - """Destructor to ensure cleanup on object deletion.""" - try: - self.cleanup() - except Exception: - pass diff --git a/dimos/simulation/mujoco/mujoco_process.py b/dimos/simulation/mujoco/mujoco_process.py new file mode 100755 index 0000000000..2363a8abd3 --- /dev/null +++ b/dimos/simulation/mujoco/mujoco_process.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 + +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import json +import pickle +import signal +import sys +import time +from typing import Any + +import mujoco +from mujoco import viewer +import numpy as np +from numpy.typing import NDArray +import open3d as o3d # type: ignore[import-untyped] + +from dimos.core.global_config import GlobalConfig +from dimos.msgs.geometry_msgs import Vector3 +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.simulation.mujoco.constants import ( + DEPTH_CAMERA_FOV, + LIDAR_FPS, + LIDAR_RESOLUTION, + VIDEO_FPS, + VIDEO_HEIGHT, + VIDEO_WIDTH, +) +from dimos.simulation.mujoco.depth_camera import depth_image_to_point_cloud +from dimos.simulation.mujoco.model import load_model, load_scene_xml +from dimos.simulation.mujoco.shared_memory import ShmReader +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class MockController: + """Controller that reads commands from shared memory.""" + + def __init__(self, shm_interface: ShmReader) -> None: + self.shm = shm_interface + self._command = np.zeros(3, dtype=np.float32) + + def get_command(self) -> NDArray[Any]: + """Get the current movement command.""" + cmd_data = self.shm.read_command() + if cmd_data is not None: + linear, angular = cmd_data + # MuJoCo expects [forward, lateral, rotational] + self._command[0] = linear[0] # forward/backward + self._command[1] = linear[1] # left/right + self._command[2] = angular[2] # rotation + return self._command.copy() + + def stop(self) -> None: + """Stop method to satisfy InputController protocol.""" + pass + + +def _run_simulation(config: GlobalConfig, shm: ShmReader) -> None: + robot_name = config.robot_model or "unitree_go1" + if robot_name == "unitree_go2": + robot_name = "unitree_go1" + + controller = MockController(shm) + model, data = load_model(controller, robot=robot_name, scene_xml=load_scene_xml(config)) + + if model is None or data is None: + raise ValueError("Failed to load MuJoCo model: model or data is None") + + match robot_name: + case "unitree_go1": + z = 0.3 + case "unitree_g1": + z = 0.8 + case _: + z = 0 + + pos = config.mujoco_start_pos_float + + data.qpos[0:3] = [pos[0], pos[1], z] + + mujoco.mj_forward(model, data) + + camera_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "head_camera") + lidar_camera_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_front_camera") + lidar_left_camera_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_left_camera") + lidar_right_camera_id = mujoco.mj_name2id( + model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_right_camera" + ) + + shm.signal_ready() + + with viewer.launch_passive(model, data, show_left_ui=False, show_right_ui=False) as m_viewer: + camera_size = (VIDEO_WIDTH, VIDEO_HEIGHT) + + # Create renderers + rgb_renderer = mujoco.Renderer(model, height=camera_size[1], width=camera_size[0]) + depth_renderer = mujoco.Renderer(model, height=camera_size[1], width=camera_size[0]) + depth_renderer.enable_depth_rendering() + + depth_left_renderer = mujoco.Renderer(model, height=camera_size[1], width=camera_size[0]) + depth_left_renderer.enable_depth_rendering() + + depth_right_renderer = mujoco.Renderer(model, height=camera_size[1], width=camera_size[0]) + depth_right_renderer.enable_depth_rendering() + + scene_option = mujoco.MjvOption() + + # Timing control + last_video_time = 0.0 + last_lidar_time = 0.0 + video_interval = 1.0 / VIDEO_FPS + lidar_interval = 1.0 / LIDAR_FPS + + m_viewer.cam.lookat = config.mujoco_camera_position_float[0:3] + m_viewer.cam.distance = config.mujoco_camera_position_float[3] + m_viewer.cam.azimuth = config.mujoco_camera_position_float[4] + m_viewer.cam.elevation = config.mujoco_camera_position_float[5] + + while m_viewer.is_running() and not shm.should_stop(): + step_start = time.time() + + # Step simulation + for _ in range(config.mujoco_steps_per_frame): + mujoco.mj_step(model, data) + + m_viewer.sync() + + # Always update odometry + pos = data.qpos[0:3].copy() + quat = data.qpos[3:7].copy() # (w, x, y, z) + shm.write_odom(pos, quat, time.time()) + + current_time = time.time() + + # Video rendering + if current_time - last_video_time >= video_interval: + rgb_renderer.update_scene(data, camera=camera_id, scene_option=scene_option) + pixels = rgb_renderer.render() + shm.write_video(pixels) + last_video_time = current_time + + # Lidar/depth rendering + if current_time - last_lidar_time >= lidar_interval: + # Render all depth cameras + depth_renderer.update_scene(data, camera=lidar_camera_id, scene_option=scene_option) + depth_front = depth_renderer.render() + + depth_left_renderer.update_scene( + data, camera=lidar_left_camera_id, scene_option=scene_option + ) + depth_left = depth_left_renderer.render() + + depth_right_renderer.update_scene( + data, camera=lidar_right_camera_id, scene_option=scene_option + ) + depth_right = depth_right_renderer.render() + + shm.write_depth(depth_front, depth_left, depth_right) + + # Process depth images into lidar message + all_points = [] + cameras_data = [ + ( + depth_front, + data.cam_xpos[lidar_camera_id], + data.cam_xmat[lidar_camera_id].reshape(3, 3), + ), + ( + depth_left, + data.cam_xpos[lidar_left_camera_id], + data.cam_xmat[lidar_left_camera_id].reshape(3, 3), + ), + ( + depth_right, + data.cam_xpos[lidar_right_camera_id], + data.cam_xmat[lidar_right_camera_id].reshape(3, 3), + ), + ] + + for depth_image, camera_pos, camera_mat in cameras_data: + points = depth_image_to_point_cloud( + depth_image, camera_pos, camera_mat, fov_degrees=DEPTH_CAMERA_FOV + ) + if points.size > 0: + all_points.append(points) + + if all_points: + combined_points = np.vstack(all_points) + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(combined_points) + pcd = pcd.voxel_down_sample(voxel_size=LIDAR_RESOLUTION) + + lidar_msg = LidarMessage( + pointcloud=pcd, + ts=time.time(), + origin=Vector3(pos[0], pos[1], pos[2]), + resolution=LIDAR_RESOLUTION, + ) + shm.write_lidar(lidar_msg) + + last_lidar_time = current_time + + # Control simulation speed + time_until_next_step = model.opt.timestep - (time.time() - step_start) + if time_until_next_step > 0: + time.sleep(time_until_next_step) + + +if __name__ == "__main__": + + def signal_handler(_signum: int, _frame: Any) -> None: + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + global_config = pickle.loads(base64.b64decode(sys.argv[1])) + shm_names = json.loads(sys.argv[2]) + + shm = ShmReader(shm_names) + try: + _run_simulation(global_config, shm) + finally: + shm.cleanup() diff --git a/dimos/simulation/mujoco/policy.py b/dimos/simulation/mujoco/policy.py index 2ea974f0be..00491b4379 100644 --- a/dimos/simulation/mujoco/policy.py +++ b/dimos/simulation/mujoco/policy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,23 +15,26 @@ # limitations under the License. +from abc import ABC, abstractmethod +from typing import Any + import mujoco import numpy as np -import onnxruntime as rt - -from dimos.simulation.mujoco.types import InputController +import onnxruntime as rt # type: ignore[import-untyped] +from dimos.simulation.mujoco.input_controller import InputController -class OnnxController: - """ONNX controller for the Go-1 robot.""" +class OnnxController(ABC): def __init__( self, policy_path: str, - default_angles: np.ndarray, + default_angles: np.ndarray[Any, Any], n_substeps: int, action_scale: float, input_controller: InputController, + ctrl_dt: float | None = None, + drift_compensation: list[float] | None = None, ) -> None: self._output_names = ["continuous_actions"] self._policy = rt.InferenceSession(policy_path, providers=["CPUExecutionProvider"]) @@ -44,7 +47,28 @@ def __init__( self._n_substeps = n_substeps self._input_controller = input_controller - def get_obs(self, model, data) -> np.ndarray: + self._drift_compensation = np.array(drift_compensation or [0.0, 0.0, 0.0], dtype=np.float32) + + @abstractmethod + def get_obs(self, model: mujoco.MjModel, data: mujoco.MjData) -> np.ndarray[Any, Any]: + pass + + def get_control(self, model: mujoco.MjModel, data: mujoco.MjData) -> None: + self._counter += 1 + if self._counter % self._n_substeps == 0: + obs = self.get_obs(model, data) + onnx_input = {"obs": obs.reshape(1, -1)} + onnx_pred = self._policy.run(self._output_names, onnx_input)[0][0] + self._last_action = onnx_pred.copy() + data.ctrl[:] = onnx_pred * self._action_scale + self._default_angles + self._post_control_update() + + def _post_control_update(self) -> None: # noqa: B027 + pass + + +class Go1OnnxController(OnnxController): + def get_obs(self, model: mujoco.MjModel, data: mujoco.MjData) -> np.ndarray[Any, Any]: linvel = data.sensor("local_linvel").data gyro = data.sensor("gyro").data imu_xmat = data.site_xmat[model.site("imu").id].reshape(3, 3) @@ -64,11 +88,60 @@ def get_obs(self, model, data) -> np.ndarray: ) return obs.astype(np.float32) - def get_control(self, model: mujoco.MjModel, data: mujoco.MjData) -> None: - self._counter += 1 - if self._counter % self._n_substeps == 0: - obs = self.get_obs(model, data) - onnx_input = {"obs": obs.reshape(1, -1)} - onnx_pred = self._policy.run(self._output_names, onnx_input)[0][0] - self._last_action = onnx_pred.copy() - data.ctrl[:] = onnx_pred * self._action_scale + self._default_angles + +class G1OnnxController(OnnxController): + def __init__( + self, + policy_path: str, + default_angles: np.ndarray[Any, Any], + ctrl_dt: float, + n_substeps: int, + action_scale: float, + input_controller: InputController, + drift_compensation: list[float] | None = None, + ) -> None: + super().__init__( + policy_path, + default_angles, + n_substeps, + action_scale, + input_controller, + ctrl_dt, + drift_compensation, + ) + + self._phase = np.array([0.0, np.pi]) + self._gait_freq = 1.5 + self._phase_dt = 2 * np.pi * self._gait_freq * ctrl_dt + + def get_obs(self, model: mujoco.MjModel, data: mujoco.MjData) -> np.ndarray[Any, Any]: + linvel = data.sensor("local_linvel_pelvis").data + gyro = data.sensor("gyro_pelvis").data + imu_xmat = data.site_xmat[model.site("imu_in_pelvis").id].reshape(3, 3) + gravity = imu_xmat.T @ np.array([0, 0, -1]) + joint_angles = data.qpos[7:] - self._default_angles + joint_velocities = data.qvel[6:] + phase = np.concatenate([np.cos(self._phase), np.sin(self._phase)]) + command = self._input_controller.get_command() + command[0] = command[0] * 2 + command[1] = command[1] * 2 + command[0] += self._drift_compensation[0] + command[1] += self._drift_compensation[1] + command[2] += self._drift_compensation[2] + obs = np.hstack( + [ + linvel, + gyro, + gravity, + command, + joint_angles, + joint_velocities, + self._last_action, + phase, + ] + ) + return obs.astype(np.float32) + + def _post_control_update(self) -> None: + phase_tp1 = self._phase + self._phase_dt + self._phase = np.fmod(phase_tp1 + np.pi, 2 * np.pi) - np.pi diff --git a/dimos/simulation/mujoco/shared_memory.py b/dimos/simulation/mujoco/shared_memory.py new file mode 100644 index 0000000000..4c22062233 --- /dev/null +++ b/dimos/simulation/mujoco/shared_memory.py @@ -0,0 +1,286 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from multiprocessing import resource_tracker +from multiprocessing.shared_memory import SharedMemory +import pickle +from typing import Any + +import numpy as np +from numpy.typing import NDArray + +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.simulation.mujoco.constants import VIDEO_HEIGHT, VIDEO_WIDTH +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + +# Video buffer: VIDEO_WIDTH x VIDEO_HEIGHT x 3 RGB +_video_size = VIDEO_WIDTH * VIDEO_HEIGHT * 3 +# Depth buffers: 3 cameras x VIDEO_WIDTH x VIDEO_HEIGHT float32 +_depth_size = VIDEO_WIDTH * VIDEO_HEIGHT * 4 # float32 = 4 bytes +# Odometry buffer: position(3) + quaternion(4) + timestamp(1) = 8 floats +_odom_size = 8 * 8 # 8 float64 values +# Command buffer: linear(3) + angular(3) = 6 floats +_cmd_size = 6 * 4 # 6 float32 values +# Lidar message buffer: for serialized lidar data +_lidar_size = 1024 * 1024 * 4 # 4MB should be enough for point cloud +# Sequence/version numbers for detecting updates +_seq_size = 8 * 8 # 8 int64 values for different data types +# Control buffer: ready flag + stop flag +_control_size = 2 * 4 # 2 int32 values + +_shm_sizes = { + "video": _video_size, + "depth_front": _depth_size, + "depth_left": _depth_size, + "depth_right": _depth_size, + "odom": _odom_size, + "cmd": _cmd_size, + "lidar": _lidar_size, + "lidar_len": 4, + "seq": _seq_size, + "control": _control_size, +} + + +def _unregister(shm: SharedMemory) -> SharedMemory: + try: + resource_tracker.unregister(shm._name, "shared_memory") # type: ignore[attr-defined] + except Exception: + pass + return shm + + +@dataclass(frozen=True) +class ShmSet: + video: SharedMemory + depth_front: SharedMemory + depth_left: SharedMemory + depth_right: SharedMemory + odom: SharedMemory + cmd: SharedMemory + lidar: SharedMemory + lidar_len: SharedMemory + seq: SharedMemory + control: SharedMemory + + @classmethod + def from_names(cls, shm_names: dict[str, str]) -> "ShmSet": + return cls(**{k: _unregister(SharedMemory(name=shm_names[k])) for k in _shm_sizes.keys()}) + + @classmethod + def from_sizes(cls) -> "ShmSet": + return cls( + **{ + k: _unregister(SharedMemory(create=True, size=_shm_sizes[k])) + for k in _shm_sizes.keys() + } + ) + + def to_names(self) -> dict[str, str]: + return {k: getattr(self, k).name for k in _shm_sizes.keys()} + + def as_list(self) -> list[SharedMemory]: + return [getattr(self, k) for k in _shm_sizes.keys()] + + +class ShmReader: + shm: ShmSet + _last_cmd_seq: int + + def __init__(self, shm_names: dict[str, str]) -> None: + self.shm = ShmSet.from_names(shm_names) + self._last_cmd_seq = 0 + + def signal_ready(self) -> None: + control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) + control_array[0] = 1 # ready flag + + def should_stop(self) -> bool: + control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) + return bool(control_array[1] == 1) # stop flag + + def write_video(self, pixels: NDArray[Any]) -> None: + video_array: NDArray[Any] = np.ndarray( + (VIDEO_HEIGHT, VIDEO_WIDTH, 3), dtype=np.uint8, buffer=self.shm.video.buf + ) + video_array[:] = pixels + self._increment_seq(0) + + def write_depth(self, front: NDArray[Any], left: NDArray[Any], right: NDArray[Any]) -> None: + # Front camera + depth_array: NDArray[Any] = np.ndarray( + (VIDEO_HEIGHT, VIDEO_WIDTH), dtype=np.float32, buffer=self.shm.depth_front.buf + ) + depth_array[:] = front + + # Left camera + depth_array = np.ndarray( + (VIDEO_HEIGHT, VIDEO_WIDTH), dtype=np.float32, buffer=self.shm.depth_left.buf + ) + depth_array[:] = left + + # Right camera + depth_array = np.ndarray( + (VIDEO_HEIGHT, VIDEO_WIDTH), dtype=np.float32, buffer=self.shm.depth_right.buf + ) + depth_array[:] = right + + self._increment_seq(1) + + def write_odom(self, pos: NDArray[Any], quat: NDArray[Any], timestamp: float) -> None: + odom_array: NDArray[Any] = np.ndarray((8,), dtype=np.float64, buffer=self.shm.odom.buf) + odom_array[0:3] = pos + odom_array[3:7] = quat + odom_array[7] = timestamp + self._increment_seq(2) + + def write_lidar(self, lidar_msg: LidarMessage) -> None: + data = pickle.dumps(lidar_msg) + data_len = len(data) + + if data_len > self.shm.lidar.size: + logger.error(f"Lidar data too large: {data_len} > {self.shm.lidar.size}") + return + + # Write length + len_array: NDArray[Any] = np.ndarray((1,), dtype=np.uint32, buffer=self.shm.lidar_len.buf) + len_array[0] = data_len + + # Write data + lidar_array: NDArray[Any] = np.ndarray( + (data_len,), dtype=np.uint8, buffer=self.shm.lidar.buf + ) + lidar_array[:] = np.frombuffer(data, dtype=np.uint8) + + self._increment_seq(4) + + def read_command(self) -> tuple[NDArray[Any], NDArray[Any]] | None: + seq = self._get_seq(3) + if seq > self._last_cmd_seq: + self._last_cmd_seq = seq + cmd_array: NDArray[Any] = np.ndarray((6,), dtype=np.float32, buffer=self.shm.cmd.buf) + linear = cmd_array[0:3].copy() + angular = cmd_array[3:6].copy() + return linear, angular + return None + + def _increment_seq(self, index: int) -> None: + seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) + seq_array[index] += 1 + + def _get_seq(self, index: int) -> int: + seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) + return int(seq_array[index]) + + def cleanup(self) -> None: + for shm in self.shm.as_list(): + try: + shm.close() + except Exception: + pass + + +class ShmWriter: + shm: ShmSet + + def __init__(self) -> None: + self.shm = ShmSet.from_sizes() + + seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) + seq_array[:] = 0 + + cmd_array: NDArray[Any] = np.ndarray((6,), dtype=np.float32, buffer=self.shm.cmd.buf) + cmd_array[:] = 0 + + control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) + control_array[:] = 0 # [ready_flag, stop_flag] + + def is_ready(self) -> bool: + control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) + return bool(control_array[0] == 1) + + def signal_stop(self) -> None: + control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) + control_array[1] = 1 # Set stop flag + + def read_video(self) -> tuple[NDArray[Any] | None, int]: + seq = self._get_seq(0) + if seq > 0: + video_array: NDArray[Any] = np.ndarray( + (VIDEO_HEIGHT, VIDEO_WIDTH, 3), dtype=np.uint8, buffer=self.shm.video.buf + ) + return video_array.copy(), seq + return None, 0 + + def read_odom(self) -> tuple[tuple[NDArray[Any], NDArray[Any], float] | None, int]: + seq = self._get_seq(2) + if seq > 0: + odom_array: NDArray[Any] = np.ndarray((8,), dtype=np.float64, buffer=self.shm.odom.buf) + pos = odom_array[0:3].copy() + quat = odom_array[3:7].copy() + timestamp = odom_array[7] + return (pos, quat, timestamp), seq + return None, 0 + + def write_command(self, linear: NDArray[Any], angular: NDArray[Any]) -> None: + cmd_array: NDArray[Any] = np.ndarray((6,), dtype=np.float32, buffer=self.shm.cmd.buf) + cmd_array[0:3] = linear + cmd_array[3:6] = angular + self._increment_seq(3) + + def read_lidar(self) -> tuple[LidarMessage | None, int]: + seq = self._get_seq(4) + if seq > 0: + # Read length + len_array: NDArray[Any] = np.ndarray( + (1,), dtype=np.uint32, buffer=self.shm.lidar_len.buf + ) + data_len = int(len_array[0]) + + if data_len > 0 and data_len <= self.shm.lidar.size: + # Read data + lidar_array: NDArray[Any] = np.ndarray( + (data_len,), dtype=np.uint8, buffer=self.shm.lidar.buf + ) + data = bytes(lidar_array) + + try: + lidar_msg = pickle.loads(data) + return lidar_msg, seq + except Exception as e: + logger.error(f"Failed to deserialize lidar message: {e}") + return None, 0 + + def _increment_seq(self, index: int) -> None: + seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) + seq_array[index] += 1 + + def _get_seq(self, index: int) -> int: + seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) + return int(seq_array[index]) + + def cleanup(self) -> None: + for shm in self.shm.as_list(): + try: + shm.unlink() + except Exception: + pass + + try: + shm.close() + except Exception: + pass diff --git a/dimos/simulation/mujoco/types.py b/dimos/simulation/mujoco/types.py deleted file mode 100644 index 42fd28efd2..0000000000 --- a/dimos/simulation/mujoco/types.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from typing import Protocol - -import numpy as np - - -class InputController(Protocol): - """A protocol for input devices to control the robot.""" - - def get_command(self) -> np.ndarray: ... - def stop(self) -> None: ... diff --git a/dimos/skills/kill_skill.py b/dimos/skills/kill_skill.py index b9d02729f5..f0ca805e6f 100644 --- a/dimos/skills/kill_skill.py +++ b/dimos/skills/kill_skill.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ from dimos.skills.skills import AbstractSkill, SkillLibrary from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.skills.kill_skill") +logger = setup_logger() class KillSkill(AbstractSkill): @@ -38,7 +38,7 @@ class KillSkill(AbstractSkill): skill_name: str = Field(..., description="Name of the skill to terminate") - def __init__(self, skill_library: SkillLibrary | None = None, **data) -> None: + def __init__(self, skill_library: SkillLibrary | None = None, **data) -> None: # type: ignore[no-untyped-def] """ Initialize the kill skill. @@ -49,13 +49,13 @@ def __init__(self, skill_library: SkillLibrary | None = None, **data) -> None: super().__init__(**data) self._skill_library = skill_library - def __call__(self): + def __call__(self): # type: ignore[no-untyped-def] """ Terminate the specified skill. Returns: A message indicating whether the skill was successfully terminated """ - print("running skills", self._skill_library.get_running_skills()) + print("running skills", self._skill_library.get_running_skills()) # type: ignore[union-attr] # Terminate the skill using the skill library - return self._skill_library.terminate_skill(self.skill_name) + return self._skill_library.terminate_skill(self.skill_name) # type: ignore[union-attr] diff --git a/dimos/skills/manipulation/abstract_manipulation_skill.py b/dimos/skills/manipulation/abstract_manipulation_skill.py index e3f6e719fa..e767ad8c8f 100644 --- a/dimos/skills/manipulation/abstract_manipulation_skill.py +++ b/dimos/skills/manipulation/abstract_manipulation_skill.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ class AbstractManipulationSkill(AbstractRobotSkill): This abstract class provides access to the robot's manipulation memory system. """ - def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: + def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: # type: ignore[no-untyped-def] """Initialize the manipulation skill. Args: @@ -34,7 +34,7 @@ def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: """ super().__init__(*args, robot=robot, **kwargs) - if self._robot and not self._robot.manipulation_interface: + if self._robot and not self._robot.manipulation_interface: # type: ignore[attr-defined] raise NotImplementedError( "This robot does not have a manipulation interface implemented" ) @@ -55,4 +55,4 @@ def manipulation_interface(self) -> ManipulationInterface | None: if not self._robot.has_capability(RobotCapability.MANIPULATION): raise RuntimeError("This robot does not have manipulation capabilities") - return self._robot.manipulation_interface + return self._robot.manipulation_interface # type: ignore[attr-defined, no-any-return] diff --git a/dimos/skills/manipulation/force_constraint_skill.py b/dimos/skills/manipulation/force_constraint_skill.py index 72616c32a3..edeac0844e 100644 --- a/dimos/skills/manipulation/force_constraint_skill.py +++ b/dimos/skills/manipulation/force_constraint_skill.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ from pydantic import Field from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill -from dimos.types.manipulation import ForceConstraint, Vector +from dimos.types.manipulation import ForceConstraint, Vector # type: ignore[attr-defined] from dimos.utils.logging_config import setup_logger # Initialize logger -logger = setup_logger("dimos.skills.force_constraint_skill") +logger = setup_logger() class ForceConstraintSkill(AbstractManipulationSkill): @@ -53,7 +53,7 @@ def __call__(self) -> ForceConstraint: # Create force direction vector if provided (convert 2D point to 3D vector with z=0) force_direction_vector = None if self.force_direction: - force_direction_vector = Vector(self.force_direction[0], self.force_direction[1], 0.0) + force_direction_vector = Vector(self.force_direction[0], self.force_direction[1], 0.0) # type: ignore[arg-type] # Create and return the constraint constraint = ForceConstraint( @@ -64,7 +64,7 @@ def __call__(self) -> ForceConstraint: ) # Add constraint to manipulation interface for Agent recall - self.manipulation_interface.add_constraint(constraint) + self.manipulation_interface.add_constraint(constraint) # type: ignore[union-attr] # Log the constraint creation logger.info(f"Generated force constraint: {self.description}") diff --git a/dimos/skills/manipulation/manipulate_skill.py b/dimos/skills/manipulation/manipulate_skill.py index 7905d4f76c..830ddc33e0 100644 --- a/dimos/skills/manipulation/manipulate_skill.py +++ b/dimos/skills/manipulation/manipulate_skill.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ from dimos.utils.logging_config import setup_logger # Initialize logger -logger = setup_logger("dimos.skills.manipulate_skill") +logger = setup_logger() class Manipulate(AbstractManipulationSkill): @@ -80,7 +80,7 @@ def __call__(self) -> dict[str, Any]: task = ManipulationTask( description=self.description, target_object=self.target_object, - target_point=tuple(map(int, self.target_point.strip("()").split(","))), + target_point=tuple(map(int, self.target_point.strip("()").split(","))), # type: ignore[arg-type] constraints=constraint, metadata=metadata, timestamp=timestamp, @@ -89,7 +89,7 @@ def __call__(self) -> dict[str, Any]: ) # Add task to manipulation interface - self.manipulation_interface.add_manipulation_task(task) + self.manipulation_interface.add_manipulation_task(task) # type: ignore[union-attr] # Execute the manipulation result = self._execute_manipulation(task) @@ -106,9 +106,9 @@ def _build_manipulation_metadata(self) -> ManipulationMetadata: Build metadata for the current environment state, including object data and movement tolerances. """ # Get detected objects from the manipulation interface - detected_objects = [] + detected_objects = [] # type: ignore[var-annotated] try: - detected_objects = self.manipulation_interface.get_latest_objects() or [] + detected_objects = self.manipulation_interface.get_latest_objects() or [] # type: ignore[union-attr] except Exception as e: logger.warning(f"Failed to get detected objects: {e}") diff --git a/dimos/skills/manipulation/pick_and_place.py b/dimos/skills/manipulation/pick_and_place.py index 1143ce073a..1d1063edad 100644 --- a/dimos/skills/manipulation/pick_and_place.py +++ b/dimos/skills/manipulation/pick_and_place.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ from dimos.skills.skills import AbstractRobotSkill from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.skills.manipulation.pick_and_place") +logger = setup_logger() def parse_qwen_points_response(response: str) -> tuple[tuple[int, int], tuple[int, int]] | None: @@ -75,7 +75,7 @@ def parse_qwen_points_response(response: str) -> tuple[tuple[int, int], tuple[in def save_debug_image_with_points( - image: np.ndarray, + image: np.ndarray, # type: ignore[type-arg] pick_point: tuple[int, int] | None = None, place_point: tuple[int, int] | None = None, filename_prefix: str = "qwen_debug", @@ -205,7 +205,7 @@ class PickAndPlace(AbstractRobotSkill): "qwen2.5-vl-72b-instruct", description="Qwen model to use for visual queries" ) - def __init__(self, robot=None, **data) -> None: + def __init__(self, robot=None, **data) -> None: # type: ignore[no-untyped-def] """ Initialize the PickAndPlace skill. @@ -215,29 +215,30 @@ def __init__(self, robot=None, **data) -> None: """ super().__init__(robot=robot, **data) - def _get_camera_frame(self) -> np.ndarray | None: + def _get_camera_frame(self) -> np.ndarray | None: # type: ignore[type-arg] """ Get a single RGB frame from the robot's camera. Returns: RGB image as numpy array or None if capture fails """ - if not self._robot or not self._robot.manipulation_interface: + if not self._robot or not self._robot.manipulation_interface: # type: ignore[attr-defined] logger.error("Robot or stereo camera not available") return None try: # Use the RPC call to get a single RGB frame - rgb_frame = self._robot.manipulation_interface.get_single_rgb_frame() + rgb_frame = self._robot.manipulation_interface.get_single_rgb_frame() # type: ignore[attr-defined] if rgb_frame is None: logger.error("Failed to capture RGB frame from camera") - return rgb_frame + return rgb_frame # type: ignore[no-any-return] except Exception as e: logger.error(f"Error getting camera frame: {e}") return None def _query_pick_and_place_points( - self, frame: np.ndarray + self, + frame: np.ndarray, # type: ignore[type-arg] ) -> tuple[tuple[int, int], tuple[int, int]] | None: """ Query Qwen to get both pick and place points in a single query. @@ -270,7 +271,10 @@ def _query_pick_and_place_points( return None def _query_single_point( - self, frame: np.ndarray, query: str, point_type: str + self, + frame: np.ndarray, # type: ignore[type-arg] + query: str, + point_type: str, ) -> tuple[int, int] | None: """ Query Qwen to get a single point location. @@ -323,7 +327,7 @@ def __call__(self) -> dict[str, Any]: Returns: Dictionary with operation results """ - super().__call__() + super().__call__() # type: ignore[no-untyped-call] if not self._robot: error_msg = "No robot instance provided to PickAndPlace skill" @@ -331,7 +335,7 @@ def __call__(self) -> dict[str, Any]: return {"success": False, "error": error_msg} # Register skill as running - skill_library = self._robot.get_skills() + skill_library = self._robot.get_skills() # type: ignore[no-untyped-call] self.register_as_running("PickAndPlace", skill_library) # Get camera frame @@ -365,7 +369,7 @@ def __call__(self) -> dict[str, Any]: # Try single query first for efficiency points = self._query_pick_and_place_points(frame) - pick_point, place_point = points + pick_point, place_point = points # type: ignore[misc] logger.info(f"Pick point: {pick_point}, Place point: {place_point}") @@ -377,7 +381,7 @@ def __call__(self) -> dict[str, Any]: try: if place_point: # Pick and place - result = self._robot.pick_and_place( + result = self._robot.pick_and_place( # type: ignore[attr-defined] pick_x=pick_point[0], pick_y=pick_point[1], place_x=place_point[0], @@ -385,7 +389,7 @@ def __call__(self) -> dict[str, Any]: ) else: # Pick only - result = self._robot.pick_and_place( + result = self._robot.pick_and_place( # type: ignore[attr-defined] pick_x=pick_point[0], pick_y=pick_point[1], place_x=None, place_y=None ) @@ -434,7 +438,7 @@ def stop(self) -> None: # Unregister skill from skill library if self._robot: - skill_library = self._robot.get_skills() + skill_library = self._robot.get_skills() # type: ignore[no-untyped-call] self.unregister_as_running("PickAndPlace", skill_library) logger.info("PickAndPlace skill stopped successfully") diff --git a/dimos/skills/manipulation/rotation_constraint_skill.py b/dimos/skills/manipulation/rotation_constraint_skill.py index ae1bdbb57d..72e6a53716 100644 --- a/dimos/skills/manipulation/rotation_constraint_skill.py +++ b/dimos/skills/manipulation/rotation_constraint_skill.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ from dimos.utils.logging_config import setup_logger # Initialize logger -logger = setup_logger("dimos.skills.rotation_constraint_skill") +logger = setup_logger() class RotationConstraintSkill(AbstractManipulationSkill): @@ -71,25 +71,27 @@ def __call__(self) -> RotationConstraint: values = [0.0, 0.0, 0.0] axis_index = {"roll": 0, "pitch": 1, "yaw": 2}[self.rotation_axis] values[axis_index] = self.start_angle - start_angle_vector = Vector(*values) + start_angle_vector = Vector(*values) # type: ignore[arg-type] end_angle_vector = None if self.end_angle is not None: values = [0.0, 0.0, 0.0] axis_index = {"roll": 0, "pitch": 1, "yaw": 2}[self.rotation_axis] values[axis_index] = self.end_angle - end_angle_vector = Vector(*values) + end_angle_vector = Vector(*values) # type: ignore[arg-type] # Create pivot point vector if provided (convert 2D point to 3D vector with z=0) pivot_point_vector = None if self.pivot_point: - pivot_point_vector = Vector(self.pivot_point[0], self.pivot_point[1], 0.0) + pivot_point_vector = Vector(self.pivot_point[0], self.pivot_point[1], 0.0) # type: ignore[arg-type] # Create secondary pivot point vector if provided secondary_pivot_vector = None if self.secondary_pivot_point: secondary_pivot_vector = Vector( - self.secondary_pivot_point[0], self.secondary_pivot_point[1], 0.0 + self.secondary_pivot_point[0], # type: ignore[arg-type] + self.secondary_pivot_point[1], # type: ignore[arg-type] + 0.0, # type: ignore[arg-type] ) constraint = RotationConstraint( @@ -101,7 +103,7 @@ def __call__(self) -> RotationConstraint: ) # Add constraint to manipulation interface - self.manipulation_interface.add_constraint(constraint) + self.manipulation_interface.add_constraint(constraint) # type: ignore[union-attr] # Log the constraint creation logger.info(f"Generated rotation constraint around {self.rotation_axis} axis") diff --git a/dimos/skills/manipulation/translation_constraint_skill.py b/dimos/skills/manipulation/translation_constraint_skill.py index 6e1808744f..78ea38cfe4 100644 --- a/dimos/skills/manipulation/translation_constraint_skill.py +++ b/dimos/skills/manipulation/translation_constraint_skill.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,11 +17,11 @@ from pydantic import Field from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill -from dimos.types.manipulation import TranslationConstraint, Vector +from dimos.types.manipulation import TranslationConstraint, Vector # type: ignore[attr-defined] from dimos.utils.logging_config import setup_logger # Initialize logger -logger = setup_logger("dimos.skills.translation_constraint_skill") +logger = setup_logger() class TranslationConstraintSkill(AbstractManipulationSkill): @@ -66,22 +66,22 @@ def __call__(self) -> TranslationConstraint: # Create reference point vector if provided (convert 2D point to 3D vector with z=0) reference_point = None if self.reference_point: - reference_point = Vector(self.reference_point[0], self.reference_point[1], 0.0) + reference_point = Vector(self.reference_point[0], self.reference_point[1], 0.0) # type: ignore[arg-type] # Create bounds minimum vector if provided bounds_min = None if self.bounds_min: - bounds_min = Vector(self.bounds_min[0], self.bounds_min[1], 0.0) + bounds_min = Vector(self.bounds_min[0], self.bounds_min[1], 0.0) # type: ignore[arg-type] # Create bounds maximum vector if provided bounds_max = None if self.bounds_max: - bounds_max = Vector(self.bounds_max[0], self.bounds_max[1], 0.0) + bounds_max = Vector(self.bounds_max[0], self.bounds_max[1], 0.0) # type: ignore[arg-type] # Create relative target vector if provided target_point = None if self.target_point: - target_point = Vector(self.target_point[0], self.target_point[1], 0.0) + target_point = Vector(self.target_point[0], self.target_point[1], 0.0) # type: ignore[arg-type] constraint = TranslationConstraint( translation_axis=self.translation_axis, @@ -92,9 +92,9 @@ def __call__(self) -> TranslationConstraint: ) # Add constraint to manipulation interface - self.manipulation_interface.add_constraint(constraint) + self.manipulation_interface.add_constraint(constraint) # type: ignore[union-attr] # Log the constraint creation logger.info(f"Generated translation constraint along {self.translation_axis} axis") - return {"success": True} + return {"success": True} # type: ignore[return-value] diff --git a/dimos/skills/rest/rest.py b/dimos/skills/rest/rest.py index a8b5adfeb9..471a7022df 100644 --- a/dimos/skills/rest/rest.py +++ b/dimos/skills/rest/rest.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ import logging from pydantic import Field -import requests +import requests # type: ignore[import-untyped] from dimos.skills.skills import AbstractSkill @@ -87,7 +87,7 @@ def __call__(self) -> str: logger.debug( f"Request successful. Status: {response.status_code}, Response: {response.text[:100]}..." ) - return response.text # Return text content directly + return response.text # type: ignore[no-any-return] # Return text content directly except requests.exceptions.HTTPError as http_err: logger.error( f"HTTP error occurred: {http_err} - Status Code: {http_err.response.status_code}" diff --git a/dimos/skills/skills.py b/dimos/skills/skills.py index 197c9e2fe0..94f8b3726f 100644 --- a/dimos/skills/skills.py +++ b/dimos/skills/skills.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ class SkillLibrary: def __init__(self) -> None: self.registered_skills: list[AbstractSkill] = [] self.class_skills: list[AbstractSkill] = [] - self._running_skills = {} # {skill_name: (instance, subscription)} + self._running_skills = {} # type: ignore[var-annotated] # {skill_name: (instance, subscription)} self.init() @@ -78,7 +78,7 @@ def get_class_skills(self) -> list[AbstractSkill]: # Skip attributes that can't be accessed or aren't classes continue - return skills + return skills # type: ignore[return-value] def refresh_class_skills(self) -> None: self.class_skills = self.get_class_skills() @@ -99,7 +99,7 @@ def remove(self, skill: AbstractSkill) -> None: def clear(self) -> None: self.registered_skills.clear() - def __iter__(self) -> Iterator: + def __iter__(self) -> Iterator: # type: ignore[type-arg] return iter(self.registered_skills) def __len__(self) -> int: @@ -108,14 +108,14 @@ def __len__(self) -> int: def __contains__(self, skill: AbstractSkill) -> bool: return skill in self.registered_skills - def __getitem__(self, index): + def __getitem__(self, index): # type: ignore[no-untyped-def] return self.registered_skills[index] # ==== Calling a Function ==== - _instances: dict[str, dict] = {} + _instances: dict[str, dict] = {} # type: ignore[type-arg] - def create_instance(self, name: str, **kwargs) -> None: + def create_instance(self, name: str, **kwargs) -> None: # type: ignore[no-untyped-def] # Key based only on the name key = name @@ -123,7 +123,7 @@ def create_instance(self, name: str, **kwargs) -> None: # Instead of creating an instance, store the args for later use self._instances[key] = kwargs - def call(self, name: str, **args): + def call(self, name: str, **args): # type: ignore[no-untyped-def] try: # Get the stored args if available; otherwise, use an empty dict stored_args = self._instances.get(name, {}) @@ -135,7 +135,7 @@ def call(self, name: str, **args): skill_class = getattr(self, name, None) if skill_class is None: for skill in self.get(): - if name == skill.__name__: + if name == skill.__name__: # type: ignore[attr-defined] skill_class = skill break if skill_class is None: @@ -144,7 +144,7 @@ def call(self, name: str, **args): return error_msg # Initialize the instance with the merged arguments - instance = skill_class(**complete_args) + instance = skill_class(**complete_args) # type: ignore[operator] print(f"Instance created and function called for: {name} with args: {complete_args}") # Call the instance directly @@ -162,9 +162,9 @@ def get_tools(self) -> Any: return tools_json def get_list_of_skills_as_json(self, list_of_skills: list[AbstractSkill]) -> list[str]: - return list(map(pydantic_function_tool, list_of_skills)) + return list(map(pydantic_function_tool, list_of_skills)) # type: ignore[arg-type] - def register_running_skill(self, name: str, instance: Any, subscription=None) -> None: + def register_running_skill(self, name: str, instance: Any, subscription=None) -> None: # type: ignore[no-untyped-def] """ Register a running skill with its subscription. @@ -194,7 +194,7 @@ def unregister_running_skill(self, name: str) -> bool: return True return False - def get_running_skills(self): + def get_running_skills(self): # type: ignore[no-untyped-def] """ Get all running skills. @@ -203,7 +203,7 @@ def get_running_skills(self): """ return self._running_skills.copy() - def terminate_skill(self, name: str): + def terminate_skill(self, name: str): # type: ignore[no-untyped-def] """ Terminate a running skill. @@ -256,17 +256,17 @@ def terminate_skill(self, name: str): class AbstractSkill(BaseModel): - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] print("Initializing AbstractSkill Class") super().__init__(*args, **kwargs) - self._instances = {} - self._list_of_skills = [] # Initialize the list of skills + self._instances = {} # type: ignore[var-annotated] + self._list_of_skills = [] # type: ignore[var-annotated] # Initialize the list of skills print(f"Instances: {self._instances}") def clone(self) -> AbstractSkill: return AbstractSkill() - def register_as_running( + def register_as_running( # type: ignore[no-untyped-def] self, name: str, skill_library: SkillLibrary, subscription=None ) -> None: """ @@ -296,15 +296,13 @@ def get_tools(self) -> Any: return tools_json def get_list_of_skills_as_json(self, list_of_skills: list[AbstractSkill]) -> list[str]: - return list(map(pydantic_function_tool, list_of_skills)) + return list(map(pydantic_function_tool, list_of_skills)) # type: ignore[arg-type] # endregion AbstractSkill # region Abstract Robot Skill -from typing import TYPE_CHECKING - if TYPE_CHECKING: from dimos.robot.robot import Robot else: @@ -312,11 +310,11 @@ def get_list_of_skills_as_json(self, list_of_skills: list[AbstractSkill]) -> lis class AbstractRobotSkill(AbstractSkill): - _robot: Robot = None + _robot: Robot = None # type: ignore[assignment] - def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: + def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) - self._robot = robot + self._robot = robot # type: ignore[assignment] print( f"{Colors.BLUE_PRINT_COLOR}Robot Skill Initialized with Robot: {robot}{Colors.RESET_COLOR}" ) @@ -329,7 +327,7 @@ def set_robot(self, robot: Robot) -> None: """ self._robot = robot - def __call__(self): + def __call__(self): # type: ignore[no-untyped-def] if self._robot is None: raise RuntimeError( f"{Colors.RED_PRINT_COLOR}" diff --git a/dimos/skills/speak.py b/dimos/skills/speak.py index a1e3abb078..fc26fd2cd0 100644 --- a/dimos/skills/speak.py +++ b/dimos/skills/speak.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,13 +23,13 @@ from dimos.skills.skills import AbstractSkill from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.skills.speak") +logger = setup_logger() # Global lock to prevent multiple simultaneous audio playbacks _audio_device_lock = threading.RLock() # Global queue for sequential audio processing -_audio_queue = queue.Queue() +_audio_queue = queue.Queue() # type: ignore[var-annotated] _queue_processor_thread = None _queue_running = False @@ -79,27 +79,27 @@ class Speak(AbstractSkill): text: str = Field(..., description="Text to speak") - def __init__(self, tts_node: Any | None = None, **data) -> None: + def __init__(self, tts_node: Any | None = None, **data) -> None: # type: ignore[no-untyped-def] super().__init__(**data) self._tts_node = tts_node self._audio_complete = threading.Event() self._subscription = None - self._subscriptions: list = [] # Track all subscriptions + self._subscriptions: list = [] # type: ignore[type-arg] # Track all subscriptions - def __call__(self): + def __call__(self): # type: ignore[no-untyped-def] if not self._tts_node: logger.error("No TTS node provided to Speak skill") return "Error: No TTS node available" # Create a result queue to get the result back from the audio thread - result_queue = queue.Queue(1) + result_queue = queue.Queue(1) # type: ignore[var-annotated] # Define the speech task to run in the audio queue def speak_task() -> None: try: # Using a lock to ensure exclusive access to audio device with _audio_device_lock: - text_subject = Subject() + text_subject = Subject() # type: ignore[var-annotated] self._audio_complete.clear() self._subscriptions = [] @@ -109,15 +109,15 @@ def on_complete() -> None: self._audio_complete.set() # This function will be called if there's an error - def on_error(error) -> None: + def on_error(error) -> None: # type: ignore[no-untyped-def] logger.error(f"Error in TTS processing: {error}") self._audio_complete.set() # Connect the Subject to the TTS node and keep the subscription - self._tts_node.consume_text(text_subject) + self._tts_node.consume_text(text_subject) # type: ignore[union-attr] # Subscribe to the audio output to know when it's done - self._subscription = self._tts_node.emit_text().subscribe( + self._subscription = self._tts_node.emit_text().subscribe( # type: ignore[union-attr] on_next=lambda text: logger.debug(f"TTS processing: {text}"), on_completed=on_complete, on_error=on_error, diff --git a/dimos/skills/unitree/__init__.py b/dimos/skills/unitree/__init__.py index 8b13789179..e69de29bb2 100644 --- a/dimos/skills/unitree/__init__.py +++ b/dimos/skills/unitree/__init__.py @@ -1 +0,0 @@ - diff --git a/dimos/skills/unitree/unitree_speak.py b/dimos/skills/unitree/unitree_speak.py index 539ca0cd29..84abc3296a 100644 --- a/dimos/skills/unitree/unitree_speak.py +++ b/dimos/skills/unitree/unitree_speak.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,16 +19,16 @@ import tempfile import time -from go2_webrtc_driver.constants import RTC_TOPIC import numpy as np from openai import OpenAI from pydantic import Field -import soundfile as sf +import soundfile as sf # type: ignore[import-untyped] +from unitree_webrtc_connect.constants import RTC_TOPIC from dimos.skills.skills import AbstractRobotSkill from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.skills.unitree.unitree_speak") +logger = setup_logger() # Audio API constants (from go2_webrtc_driver) AUDIO_API = { @@ -58,33 +58,33 @@ class UnitreeSpeak(AbstractRobotSkill): default=False, description="Use megaphone mode for lower latency (experimental)" ) - def __init__(self, **data) -> None: + def __init__(self, **data) -> None: # type: ignore[no-untyped-def] super().__init__(**data) self._openai_client = None - def _get_openai_client(self): + def _get_openai_client(self): # type: ignore[no-untyped-def] if self._openai_client is None: - self._openai_client = OpenAI() + self._openai_client = OpenAI() # type: ignore[assignment] return self._openai_client def _generate_audio(self, text: str) -> bytes: try: - client = self._get_openai_client() + client = self._get_openai_client() # type: ignore[no-untyped-call] response = client.audio.speech.create( model="tts-1", voice=self.voice, input=text, speed=self.speed, response_format="mp3" ) - return response.content + return response.content # type: ignore[no-any-return] except Exception as e: logger.error(f"Error generating audio: {e}") raise - def _webrtc_request(self, api_id: int, parameter: dict | None = None): + def _webrtc_request(self, api_id: int, parameter: dict | None = None): # type: ignore[no-untyped-def, type-arg] if parameter is None: parameter = {} request_data = {"api_id": api_id, "parameter": json.dumps(parameter) if parameter else "{}"} - return self._robot.connection.publish_request(RTC_TOPIC["AUDIO_HUB_REQ"], request_data) + return self._robot.connection.publish_request(RTC_TOPIC["AUDIO_HUB_REQ"], request_data) # type: ignore[attr-defined] def _upload_audio_to_robot(self, audio_data: bytes, filename: str) -> str: try: @@ -123,7 +123,7 @@ def _upload_audio_to_robot(self, audio_data: bytes, filename: str) -> str: for audio in audio_list: if audio.get("CUSTOM_NAME") == filename: - return audio.get("UNIQUE_ID") + return audio.get("UNIQUE_ID") # type: ignore[no-any-return] logger.warning( f"Could not find uploaded audio '{filename}' in list, using filename as UUID" @@ -134,7 +134,7 @@ def _upload_audio_to_robot(self, audio_data: bytes, filename: str) -> str: logger.error(f"Error uploading audio to robot: {e}") raise - def _play_audio_on_robot(self, uuid: str): + def _play_audio_on_robot(self, uuid: str): # type: ignore[no-untyped-def] try: self._webrtc_request(AUDIO_API["SET_PLAY_MODE"], {"play_mode": PLAY_MODES["NO_CYCLE"]}) time.sleep(0.1) @@ -155,7 +155,7 @@ def _stop_audio_playback(self) -> None: except Exception as e: logger.warning(f"Error stopping audio playback: {e}") - def _upload_and_play_megaphone(self, audio_data: bytes, duration: float): + def _upload_and_play_megaphone(self, audio_data: bytes, duration: float): # type: ignore[no-untyped-def] try: logger.debug("Entering megaphone mode") self._webrtc_request(AUDIO_API["ENTER_MEGAPHONE"], {}) @@ -204,7 +204,7 @@ def _upload_and_play_megaphone(self, audio_data: bytes, duration: float): logger.warning(f"Error exiting megaphone mode: {e}") def __call__(self) -> str: - super().__call__() + super().__call__() # type: ignore[no-untyped-call] if not self._robot: logger.error("No robot instance provided to UnitreeSpeak skill") diff --git a/dimos/skills/visual_navigation_skills.py b/dimos/skills/visual_navigation_skills.py index 8064f28cc9..9ce6d34f09 100644 --- a/dimos/skills/visual_navigation_skills.py +++ b/dimos/skills/visual_navigation_skills.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,12 +25,14 @@ from pydantic import Field -from dimos.perception.visual_servoing import VisualServoing +from dimos.perception.visual_servoing import ( # type: ignore[import-not-found, import-untyped] + VisualServoing, +) from dimos.skills.skills import AbstractRobotSkill from dimos.types.vector import Vector from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.skills.visual_navigation", level=logging.DEBUG) +logger = setup_logger(level=logging.DEBUG) class FollowHuman(AbstractRobotSkill): @@ -51,19 +53,19 @@ class FollowHuman(AbstractRobotSkill): None, description="Optional point to start tracking (x,y pixel coordinates)" ) - def __init__(self, robot=None, **data) -> None: + def __init__(self, robot=None, **data) -> None: # type: ignore[no-untyped-def] super().__init__(robot=robot, **data) self._stop_event = threading.Event() self._visual_servoing = None - def __call__(self): + def __call__(self): # type: ignore[no-untyped-def] """ Start following a human using visual servoing. Returns: bool: True if successful, False otherwise """ - super().__call__() + super().__call__() # type: ignore[no-untyped-call] if ( not hasattr(self._robot, "person_tracking_stream") @@ -88,7 +90,7 @@ def __call__(self): start_time = time.time() # Start tracking - track_success = self._visual_servoing.start_tracking( + track_success = self._visual_servoing.start_tracking( # type: ignore[attr-defined] point=self.point, desired_distance=self.distance ) @@ -98,15 +100,15 @@ def __call__(self): # Main follow loop while ( - self._visual_servoing.running + self._visual_servoing.running # type: ignore[attr-defined] and time.time() - start_time < self.timeout and not self._stop_event.is_set() ): - output = self._visual_servoing.updateTracking() + output = self._visual_servoing.updateTracking() # type: ignore[attr-defined] x_vel = output.get("linear_vel") z_vel = output.get("angular_vel") logger.debug(f"Following human: x_vel: {x_vel}, z_vel: {z_vel}") - self._robot.move(Vector(x_vel, 0, z_vel)) + self._robot.move(Vector(x_vel, 0, z_vel)) # type: ignore[arg-type, attr-defined] time.sleep(0.05) # If we completed the full timeout duration, consider it success diff --git a/dimos/spec/__init__.py b/dimos/spec/__init__.py new file mode 100644 index 0000000000..03c1024d12 --- /dev/null +++ b/dimos/spec/__init__.py @@ -0,0 +1,15 @@ +from dimos.spec.control import LocalPlanner +from dimos.spec.map import Global3DMap, GlobalCostmap, GlobalMap +from dimos.spec.nav import Nav +from dimos.spec.perception import Camera, Image, Pointcloud + +__all__ = [ + "Camera", + "Global3DMap", + "GlobalCostmap", + "GlobalMap", + "Image", + "LocalPlanner", + "Nav", + "Pointcloud", +] diff --git a/dimos/spec/control.py b/dimos/spec/control.py new file mode 100644 index 0000000000..e2024c5a09 --- /dev/null +++ b/dimos/spec/control.py @@ -0,0 +1,22 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Protocol + +from dimos.core import Out +from dimos.msgs.geometry_msgs import Twist + + +class LocalPlanner(Protocol): + cmd_vel: Out[Twist] diff --git a/dimos/spec/map.py b/dimos/spec/map.py new file mode 100644 index 0000000000..438b77a7a6 --- /dev/null +++ b/dimos/spec/map.py @@ -0,0 +1,31 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Protocol + +from dimos.core import Out +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.msgs.sensor_msgs import PointCloud2 + + +class Global3DMap(Protocol): + global_pointcloud: Out[PointCloud2] + + +class GlobalMap(Protocol): + global_map: Out[OccupancyGrid] + + +class GlobalCostmap(Protocol): + global_costmap: Out[OccupancyGrid] diff --git a/dimos/spec/nav.py b/dimos/spec/nav.py new file mode 100644 index 0000000000..d1f62c0846 --- /dev/null +++ b/dimos/spec/nav.py @@ -0,0 +1,31 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Protocol + +from dimos.core import In, Out +from dimos.msgs.geometry_msgs import PoseStamped, Twist +from dimos.msgs.nav_msgs import Path + + +class Nav(Protocol): + goal_req: In[PoseStamped] + goal_active: Out[PoseStamped] + path_active: Out[Path] + ctrl: Out[Twist] + + # identity quaternion (Quaternion(0,0,0,1)) represents "no rotation requested" + def navigate_to_target(self, target: PoseStamped) -> None: ... + + def stop_navigating(self) -> None: ... diff --git a/dimos/spec/perception.py b/dimos/spec/perception.py new file mode 100644 index 0000000000..f2d43e1363 --- /dev/null +++ b/dimos/spec/perception.py @@ -0,0 +1,31 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Protocol + +from dimos.core import Out +from dimos.msgs.sensor_msgs import CameraInfo, Image as ImageMsg, PointCloud2 + + +class Image(Protocol): + color_image: Out[ImageMsg] + + +class Camera(Image): + camera_info: Out[CameraInfo] + _camera_info: CameraInfo + + +class Pointcloud(Protocol): + pointcloud: Out[PointCloud2] diff --git a/dimos/stream/audio/base.py b/dimos/stream/audio/base.py index 43c3c13dec..54bd1705a3 100644 --- a/dimos/stream/audio/base.py +++ b/dimos/stream/audio/base.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ class AbstractAudioEmitter(ABC): """Base class for components that emit audio.""" @abstractmethod - def emit_audio(self) -> Observable: + def emit_audio(self) -> Observable: # type: ignore[type-arg] """Create an observable that emits audio frames. Returns: @@ -35,7 +35,7 @@ class AbstractAudioConsumer(ABC): """Base class for components that consume audio.""" @abstractmethod - def consume_audio(self, audio_observable: Observable) -> "AbstractAudioConsumer": + def consume_audio(self, audio_observable: Observable) -> "AbstractAudioConsumer": # type: ignore[type-arg] """Set the audio observable to consume. Args: @@ -60,7 +60,11 @@ class AudioEvent: """Class to represent an audio frame event with metadata.""" def __init__( - self, data: np.ndarray, sample_rate: int, timestamp: float, channels: int = 1 + self, + data: np.ndarray, # type: ignore[type-arg] + sample_rate: int, + timestamp: float, + channels: int = 1, ) -> None: """ Initialize an AudioEvent. diff --git a/dimos/stream/audio/node_key_recorder.py b/dimos/stream/audio/node_key_recorder.py index 5e918bae5c..a6489d0e5a 100644 --- a/dimos/stream/audio/node_key_recorder.py +++ b/dimos/stream/audio/node_key_recorder.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ from dimos.stream.audio.base import AbstractAudioTransform, AudioEvent from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.audio.key_recorder") +logger = setup_logger() class KeyRecorder(AbstractAudioTransform): @@ -51,7 +51,7 @@ def __init__( self.max_recording_time = max_recording_time self.always_subscribe = always_subscribe - self._audio_buffer = [] + self._audio_buffer = [] # type: ignore[var-annotated] self._is_recording = False self._recording_start_time = 0 self._sample_rate = None # Will be updated from incoming audio @@ -59,8 +59,8 @@ def __init__( self._audio_observable = None self._subscription = None - self._output_subject = Subject() # For record-time passthrough - self._recording_subject = ReplaySubject(1) # For full completed recordings + self._output_subject = Subject() # type: ignore[var-annotated] # For record-time passthrough + self._recording_subject = ReplaySubject(1) # type: ignore[var-annotated] # For full completed recordings # Start a thread to monitor for input self._running = True @@ -69,7 +69,7 @@ def __init__( logger.info("Started audio recorder (press any key to start/stop recording)") - def consume_audio(self, audio_observable: Observable) -> "KeyRecorder": + def consume_audio(self, audio_observable: Observable) -> "KeyRecorder": # type: ignore[type-arg] """ Set the audio observable to use when recording. If always_subscribe is True, subscribes immediately. @@ -81,11 +81,11 @@ def consume_audio(self, audio_observable: Observable) -> "KeyRecorder": Returns: Self for method chaining """ - self._audio_observable = audio_observable + self._audio_observable = audio_observable # type: ignore[assignment] # If configured to always subscribe, do it now if self.always_subscribe and not self._subscription: - self._subscription = audio_observable.subscribe( + self._subscription = audio_observable.subscribe( # type: ignore[assignment] on_next=self._process_audio_event, on_error=self._handle_error, on_completed=self._handle_completion, @@ -94,7 +94,7 @@ def consume_audio(self, audio_observable: Observable) -> "KeyRecorder": return self - def emit_audio(self) -> Observable: + def emit_audio(self) -> Observable: # type: ignore[type-arg] """ Create an observable that emits audio events in real-time (pass-through). @@ -103,7 +103,7 @@ def emit_audio(self) -> Observable: """ return self._output_subject - def emit_recording(self) -> Observable: + def emit_recording(self) -> Observable: # type: ignore[type-arg] """ Create an observable that emits combined audio recordings when recording stops. @@ -187,7 +187,7 @@ def _stop_recording(self) -> None: else: logger.warning("No audio was recorded") - def _process_audio_event(self, audio_event) -> None: + def _process_audio_event(self, audio_event) -> None: # type: ignore[no-untyped-def] """Process incoming audio events.""" # Only buffer if recording @@ -215,7 +215,7 @@ def _combine_audio_events(self, audio_events: list[AudioEvent]) -> AudioEvent: """Combine multiple audio events into a single event.""" if not audio_events: logger.warning("Attempted to combine empty audio events list") - return None + return None # type: ignore[return-value] # Filter out any empty events that might cause broadcasting errors valid_events = [ @@ -227,7 +227,7 @@ def _combine_audio_events(self, audio_events: list[AudioEvent]) -> AudioEvent: if not valid_events: logger.warning("No valid audio events to combine") - return None + return None # type: ignore[return-value] first_event = valid_events[0] channels = first_event.channels @@ -239,7 +239,7 @@ def _combine_audio_events(self, audio_events: list[AudioEvent]) -> AudioEvent: # Safety check - if somehow we got no samples if total_samples <= 0: logger.warning(f"Combined audio would have {total_samples} samples - aborting") - return None + return None # type: ignore[return-value] # For multichannel audio, data shape could be (samples,) or (samples, channels) if len(first_event.data.shape) == 1: @@ -278,15 +278,15 @@ def _combine_audio_events(self, audio_events: list[AudioEvent]) -> AudioEvent: if combined_data.size > 0: return AudioEvent( data=combined_data, - sample_rate=self._sample_rate, + sample_rate=self._sample_rate, # type: ignore[arg-type] timestamp=valid_events[0].timestamp, channels=channels, ) else: logger.warning("Failed to create valid combined audio event") - return None + return None # type: ignore[return-value] - def _handle_error(self, error) -> None: + def _handle_error(self, error) -> None: # type: ignore[no-untyped-def] """Handle errors from the observable.""" logger.error(f"Error in audio observable: {error}") diff --git a/dimos/stream/audio/node_microphone.py b/dimos/stream/audio/node_microphone.py index 1f4bf13499..5d6e28dc74 100644 --- a/dimos/stream/audio/node_microphone.py +++ b/dimos/stream/audio/node_microphone.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import numpy as np from reactivex import Observable, create, disposable -import sounddevice as sd +import sounddevice as sd # type: ignore[import-untyped] from dimos.stream.audio.base import ( AbstractAudioEmitter, @@ -26,7 +26,7 @@ ) from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.audio.node_microphone") +logger = setup_logger() class SounddeviceAudioSource(AbstractAudioEmitter): @@ -38,7 +38,7 @@ def __init__( sample_rate: int = 16000, channels: int = 1, block_size: int = 1024, - dtype: np.dtype = np.float32, + dtype: np.dtype = np.float32, # type: ignore[assignment, type-arg] ) -> None: """ Initialize SounddeviceAudioSource. @@ -59,7 +59,7 @@ def __init__( self._stream = None self._running = False - def emit_audio(self) -> Observable: + def emit_audio(self) -> Observable: # type: ignore[type-arg] """ Create an observable that emits audio frames. @@ -67,9 +67,9 @@ def emit_audio(self) -> Observable: Observable emitting AudioEvent objects """ - def on_subscribe(observer, scheduler): + def on_subscribe(observer, scheduler): # type: ignore[no-untyped-def] # Callback function to process audio data - def audio_callback(indata, frames, time_info, status) -> None: + def audio_callback(indata, frames, time_info, status) -> None: # type: ignore[no-untyped-def] if status: logger.warning(f"Audio callback status: {status}") @@ -93,7 +93,7 @@ def audio_callback(indata, frames, time_info, status) -> None: dtype=self.dtype, callback=audio_callback, ) - self._stream.start() + self._stream.start() # type: ignore[attr-defined] self._running = True logger.info( @@ -120,7 +120,7 @@ def dispose() -> None: def get_available_devices(self) -> list[dict[str, Any]]: """Get a list of available audio input devices.""" - return sd.query_devices() + return sd.query_devices() # type: ignore[no-any-return] if __name__ == "__main__": diff --git a/dimos/stream/audio/node_normalizer.py b/dimos/stream/audio/node_normalizer.py index 064fc3cf6c..60a25a0404 100644 --- a/dimos/stream/audio/node_normalizer.py +++ b/dimos/stream/audio/node_normalizer.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ ) from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.stream.audio.node_normalizer") +logger = setup_logger() class AudioNormalizer(AbstractAudioTransform): @@ -46,7 +46,7 @@ def __init__( max_gain: float = 10.0, decay_factor: float = 0.999, adapt_speed: float = 0.05, - volume_func: Callable[[np.ndarray], float] = calculate_peak_volume, + volume_func: Callable[[np.ndarray], float] = calculate_peak_volume, # type: ignore[type-arg] ) -> None: """ Initialize AudioNormalizer. @@ -119,7 +119,7 @@ def _normalize_audio(self, audio_event: AudioEvent) -> AudioEvent: channels=audio_event.channels, ) - def consume_audio(self, audio_observable: Observable) -> "AudioNormalizer": + def consume_audio(self, audio_observable: Observable) -> "AudioNormalizer": # type: ignore[type-arg] """ Set the audio source observable to consume. @@ -129,10 +129,10 @@ def consume_audio(self, audio_observable: Observable) -> "AudioNormalizer": Returns: Self for method chaining """ - self.audio_observable = audio_observable + self.audio_observable = audio_observable # type: ignore[assignment] return self - def emit_audio(self) -> Observable: + def emit_audio(self) -> Observable: # type: ignore[type-arg] """ Create an observable that emits normalized audio frames. @@ -190,7 +190,7 @@ def dispose() -> None: use_mic = True elif arg.startswith("level="): try: - target_level = float(arg.split("=")[1]) + target_level = float(arg.split("=")[1]) # type: ignore[assignment] except ValueError: print(f"Invalid target level: {arg}") sys.exit(1) diff --git a/dimos/stream/audio/node_output.py b/dimos/stream/audio/node_output.py index 3dc93d3757..4b4d407329 100644 --- a/dimos/stream/audio/node_output.py +++ b/dimos/stream/audio/node_output.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,14 +17,14 @@ import numpy as np from reactivex import Observable -import sounddevice as sd +import sounddevice as sd # type: ignore[import-untyped] from dimos.stream.audio.base import ( AbstractAudioTransform, ) from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.stream.audio.node_output") +logger = setup_logger() class SounddeviceAudioOutput(AbstractAudioTransform): @@ -42,7 +42,7 @@ def __init__( sample_rate: int = 16000, channels: int = 1, block_size: int = 1024, - dtype: np.dtype = np.float32, + dtype: np.dtype = np.float32, # type: ignore[assignment, type-arg] ) -> None: """ Initialize SounddeviceAudioOutput. @@ -65,7 +65,7 @@ def __init__( self._subscription = None self.audio_observable = None - def consume_audio(self, audio_observable: Observable) -> "SounddeviceAudioOutput": + def consume_audio(self, audio_observable: Observable) -> "SounddeviceAudioOutput": # type: ignore[type-arg] """ Subscribe to an audio observable and play the audio through the speakers. @@ -75,7 +75,7 @@ def consume_audio(self, audio_observable: Observable) -> "SounddeviceAudioOutput Returns: Self for method chaining """ - self.audio_observable = audio_observable + self.audio_observable = audio_observable # type: ignore[assignment] # Create and start the output stream try: @@ -86,7 +86,7 @@ def consume_audio(self, audio_observable: Observable) -> "SounddeviceAudioOutput blocksize=self.block_size, dtype=self.dtype, ) - self._stream.start() + self._stream.start() # type: ignore[attr-defined] self._running = True logger.info( @@ -99,7 +99,7 @@ def consume_audio(self, audio_observable: Observable) -> "SounddeviceAudioOutput raise e # Subscribe to the observable - self._subscription = audio_observable.subscribe( + self._subscription = audio_observable.subscribe( # type: ignore[assignment] on_next=self._play_audio_event, on_error=self._handle_error, on_completed=self._handle_completion, @@ -107,7 +107,7 @@ def consume_audio(self, audio_observable: Observable) -> "SounddeviceAudioOutput return self - def emit_audio(self) -> Observable: + def emit_audio(self) -> Observable: # type: ignore[type-arg] """ Pass through the audio observable to allow chaining with other components. @@ -133,7 +133,7 @@ def stop(self) -> None: self._stream.close() self._stream = None - def _play_audio_event(self, audio_event) -> None: + def _play_audio_event(self, audio_event) -> None: # type: ignore[no-untyped-def] """Play audio from an AudioEvent.""" if not self._running or not self._stream: return @@ -151,7 +151,7 @@ def _play_audio_event(self, audio_event) -> None: except Exception as e: logger.error(f"Error playing audio: {e}") - def _handle_error(self, error) -> None: + def _handle_error(self, error) -> None: # type: ignore[no-untyped-def] """Handle errors from the observable.""" logger.error(f"Error in audio observable: {error}") @@ -166,7 +166,7 @@ def _handle_completion(self) -> None: def get_available_devices(self) -> list[dict[str, Any]]: """Get a list of available audio output devices.""" - return sd.query_devices() + return sd.query_devices() # type: ignore[no-any-return] if __name__ == "__main__": diff --git a/dimos/stream/audio/node_simulated.py b/dimos/stream/audio/node_simulated.py index 82de718ced..f000f14649 100644 --- a/dimos/stream/audio/node_simulated.py +++ b/dimos/stream/audio/node_simulated.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,16 +18,16 @@ import numpy as np from reactivex import Observable, create, disposable -from dimos.stream.audio.abstract import ( +from dimos.stream.audio.abstract import ( # type: ignore[import-not-found, import-untyped] AbstractAudioEmitter, AudioEvent, ) from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.stream.audio.node_simulated") +logger = setup_logger() -class SimulatedAudioSource(AbstractAudioEmitter): +class SimulatedAudioSource(AbstractAudioEmitter): # type: ignore[misc] """Audio source that generates simulated audio for testing.""" def __init__( @@ -35,7 +35,7 @@ def __init__( sample_rate: int = 16000, frame_length: int = 1024, channels: int = 1, - dtype: np.dtype = np.float32, + dtype: np.dtype = np.float32, # type: ignore[assignment, type-arg] frequency: float = 440.0, # A4 note waveform: str = "sine", # Type of waveform modulation_rate: float = 0.5, # Modulation rate in Hz @@ -71,7 +71,7 @@ def __init__( self._running = False self._thread = None - def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: + def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: # type: ignore[type-arg] """Generate a waveform based on selected type.""" # Generate base time points with phase t = time_points + self.phase @@ -131,9 +131,9 @@ def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: if self.dtype == np.int16: wave = (wave * 32767).astype(np.int16) - return wave + return wave # type: ignore[no-any-return] - def _audio_thread(self, observer, interval: float) -> None: + def _audio_thread(self, observer, interval: float) -> None: # type: ignore[no-untyped-def] """Thread function for simulated audio generation.""" try: sample_index = 0 @@ -171,7 +171,7 @@ def _audio_thread(self, observer, interval: float) -> None: self._running = False observer.on_completed() - def emit_audio(self, fps: int = 30) -> Observable: + def emit_audio(self, fps: int = 30) -> Observable: # type: ignore[type-arg] """ Create an observable that emits simulated audio frames. @@ -182,15 +182,15 @@ def emit_audio(self, fps: int = 30) -> Observable: Observable emitting AudioEvent objects """ - def on_subscribe(observer, scheduler): + def on_subscribe(observer, scheduler): # type: ignore[no-untyped-def] # Calculate interval based on fps interval = 1.0 / fps # Start the audio generation thread - self._thread = threading.Thread( + self._thread = threading.Thread( # type: ignore[assignment] target=self._audio_thread, args=(observer, interval), daemon=True ) - self._thread.start() + self._thread.start() # type: ignore[attr-defined] logger.info( f"Started simulated audio source: {self.sample_rate}Hz, " diff --git a/dimos/stream/audio/node_volume_monitor.py b/dimos/stream/audio/node_volume_monitor.py index e1c5b226a4..894e63d46c 100644 --- a/dimos/stream/audio/node_volume_monitor.py +++ b/dimos/stream/audio/node_volume_monitor.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ from dimos.stream.audio.volume import calculate_peak_volume from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.stream.audio.node_volume_monitor") +logger = setup_logger() class VolumeMonitorNode(AbstractAudioConsumer, AbstractTextEmitter): @@ -35,7 +35,7 @@ def __init__( self, threshold: float = 0.01, bar_length: int = 50, - volume_func: Callable = calculate_peak_volume, + volume_func: Callable = calculate_peak_volume, # type: ignore[type-arg] ) -> None: """ Initialize VolumeMonitorNode. @@ -75,7 +75,7 @@ def create_volume_text(self, volume: float) -> str: activity = "active" if active else "silent" return f"{bar} {percentage:3d}% {activity}" - def consume_audio(self, audio_observable: Observable) -> "VolumeMonitorNode": + def consume_audio(self, audio_observable: Observable) -> "VolumeMonitorNode": # type: ignore[type-arg] """ Set the audio source observable to consume. @@ -85,10 +85,10 @@ def consume_audio(self, audio_observable: Observable) -> "VolumeMonitorNode": Returns: Self for method chaining """ - self.audio_observable = audio_observable + self.audio_observable = audio_observable # type: ignore[assignment] return self - def emit_text(self) -> Observable: + def emit_text(self) -> Observable: # type: ignore[type-arg] """ Create an observable that emits volume text descriptions. @@ -134,10 +134,10 @@ def dispose() -> None: def monitor( - audio_source: Observable, + audio_source: Observable, # type: ignore[type-arg] threshold: float = 0.01, bar_length: int = 50, - volume_func: Callable = calculate_peak_volume, + volume_func: Callable = calculate_peak_volume, # type: ignore[type-arg] ) -> VolumeMonitorNode: """ Create a volume monitor node connected to a text output node. @@ -168,8 +168,8 @@ def monitor( if __name__ == "__main__": - from audio.node_simulated import SimulatedAudioSource - from utils import keepalive + from audio.node_simulated import SimulatedAudioSource # type: ignore[import-not-found] + from utils import keepalive # type: ignore[import-not-found] # Use the monitor function to create and connect the nodes volume_monitor = monitor(SimulatedAudioSource().emit_audio()) diff --git a/dimos/stream/audio/pipelines.py b/dimos/stream/audio/pipelines.py index ceaeb80fac..5685b47bcf 100644 --- a/dimos/stream/audio/pipelines.py +++ b/dimos/stream/audio/pipelines.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ from dimos.stream.audio.tts.node_openai import OpenAITTSNode, Voice -def stt(): +def stt(): # type: ignore[no-untyped-def] # Create microphone source, recorder, and audio output mic = SounddeviceAudioSource() normalizer = AudioNormalizer() @@ -41,7 +41,7 @@ def stt(): return whisper_node -def tts(): +def tts(): # type: ignore[no-untyped-def] tts_node = OpenAITTSNode(speed=1.2, voice=Voice.ONYX) agent_text_printer = TextPrinterNode(prefix="AGENT: ") agent_text_printer.consume_text(tts_node.emit_text()) diff --git a/dimos/stream/audio/stt/node_whisper.py b/dimos/stream/audio/stt/node_whisper.py index 05ec5274c8..e162d150a1 100644 --- a/dimos/stream/audio/stt/node_whisper.py +++ b/dimos/stream/audio/stt/node_whisper.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ from typing import Any from reactivex import Observable, create, disposable -import whisper +import whisper # type: ignore[import-untyped] from dimos.stream.audio.base import ( AbstractAudioConsumer, @@ -25,7 +25,7 @@ from dimos.stream.audio.text.base import AbstractTextEmitter from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.stream.audio.stt.node_whisper") +logger = setup_logger() class WhisperNode(AbstractAudioConsumer, AbstractTextEmitter): @@ -44,7 +44,7 @@ def __init__( self.modelopts = modelopts self.model = whisper.load_model(model) - def consume_audio(self, audio_observable: Observable) -> "WhisperNode": + def consume_audio(self, audio_observable: Observable) -> "WhisperNode": # type: ignore[type-arg] """ Set the audio source observable to consume. @@ -54,10 +54,10 @@ def consume_audio(self, audio_observable: Observable) -> "WhisperNode": Returns: Self for method chaining """ - self.audio_observable = audio_observable + self.audio_observable = audio_observable # type: ignore[assignment] return self - def emit_text(self) -> Observable: + def emit_text(self) -> Observable: # type: ignore[type-arg] """ Create an observable that emits transcribed text from audio. diff --git a/dimos/stream/audio/text/base.py b/dimos/stream/audio/text/base.py index b7305c0bcc..b101121357 100644 --- a/dimos/stream/audio/text/base.py +++ b/dimos/stream/audio/text/base.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ class AbstractTextEmitter(ABC): """Base class for components that emit audio.""" @abstractmethod - def emit_text(self) -> Observable: + def emit_text(self) -> Observable: # type: ignore[type-arg] """Create an observable that emits audio frames. Returns: @@ -34,7 +34,7 @@ class AbstractTextConsumer(ABC): """Base class for components that consume audio.""" @abstractmethod - def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": + def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": # type: ignore[type-arg] """Set the audio observable to consume. Args: diff --git a/dimos/stream/audio/text/node_stdout.py b/dimos/stream/audio/text/node_stdout.py index b0a5fd4ac8..4a25b7b1fa 100644 --- a/dimos/stream/audio/text/node_stdout.py +++ b/dimos/stream/audio/text/node_stdout.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ from dimos.stream.audio.text.base import AbstractTextConsumer from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.stream.audio.text.node_stdout") +logger = setup_logger() class TextPrinterNode(AbstractTextConsumer): @@ -49,7 +49,7 @@ def print_text(self, text: str) -> None: """ print(f"{self.prefix}{text}{self.suffix}", end=self.end, flush=True) - def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": + def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": # type: ignore[type-arg] """ Start processing text from the observable source. @@ -62,7 +62,7 @@ def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": logger.info("Starting text printer") # Subscribe to the text observable - self.subscription = text_observable.subscribe( + self.subscription = text_observable.subscribe( # type: ignore[assignment] on_next=self.print_text, on_error=lambda e: logger.error(f"Error: {e}"), on_completed=lambda: logger.info("Text printer completed"), @@ -77,7 +77,7 @@ def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": from reactivex import Subject # Create a simple text subject that we can push values to - text_subject = Subject() + text_subject = Subject() # type: ignore[var-annotated] # Create and connect the text printer text_printer = TextPrinterNode(prefix="Text: ") diff --git a/dimos/stream/audio/tts/node_openai.py b/dimos/stream/audio/tts/node_openai.py index 211b2b0246..bed1f35682 100644 --- a/dimos/stream/audio/tts/node_openai.py +++ b/dimos/stream/audio/tts/node_openai.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ from openai import OpenAI from reactivex import Observable, Subject -import soundfile as sf +import soundfile as sf # type: ignore[import-untyped] from dimos.stream.audio.base import ( AbstractAudioEmitter, @@ -29,7 +29,7 @@ from dimos.stream.audio.text.base import AbstractTextConsumer, AbstractTextEmitter from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.stream.audio.tts.openai") +logger = setup_logger() class Voice(str, Enum): @@ -78,15 +78,15 @@ def __init__( self.client = OpenAI(api_key=api_key) # Initialize state - self.audio_subject = Subject() - self.text_subject = Subject() + self.audio_subject = Subject() # type: ignore[var-annotated] + self.text_subject = Subject() # type: ignore[var-annotated] self.subscription = None self.processing_thread = None self.is_running = True - self.text_queue = [] + self.text_queue = [] # type: ignore[var-annotated] self.queue_lock = threading.Lock() - def emit_audio(self) -> Observable: + def emit_audio(self) -> Observable: # type: ignore[type-arg] """ Returns an observable that emits audio frames. @@ -95,7 +95,7 @@ def emit_audio(self) -> Observable: """ return self.audio_subject - def emit_text(self) -> Observable: + def emit_text(self) -> Observable: # type: ignore[type-arg] """ Returns an observable that emits the text being spoken. @@ -104,7 +104,7 @@ def emit_text(self) -> Observable: """ return self.text_subject - def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": + def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": # type: ignore[type-arg] """ Start consuming text from the observable source. @@ -117,11 +117,11 @@ def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": logger.info("Starting OpenAITTSNode") # Start the processing thread - self.processing_thread = threading.Thread(target=self._process_queue, daemon=True) - self.processing_thread.start() + self.processing_thread = threading.Thread(target=self._process_queue, daemon=True) # type: ignore[assignment] + self.processing_thread.start() # type: ignore[attr-defined] # Subscribe to the text observable - self.subscription = text_observable.subscribe( + self.subscription = text_observable.subscribe( # type: ignore[assignment] on_next=self._queue_text, on_error=lambda e: logger.error(f"Error in OpenAITTSNode: {e}"), ) @@ -226,7 +226,7 @@ def dispose(self) -> None: from dimos.stream.audio.utils import keepalive # Create a simple text subject that we can push values to - text_subject = Subject() + text_subject = Subject() # type: ignore[var-annotated] tts_node = OpenAITTSNode(voice=Voice.ALLOY) tts_node.consume_text(text_subject) diff --git a/dimos/stream/audio/tts/node_pytts.py b/dimos/stream/audio/tts/node_pytts.py index f1543331ef..e444a22367 100644 --- a/dimos/stream/audio/tts/node_pytts.py +++ b/dimos/stream/audio/tts/node_pytts.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,16 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pyttsx3 +import pyttsx3 # type: ignore[import-not-found] from reactivex import Observable, Subject -from dimos.stream.audio.text.abstract import AbstractTextTransform +from dimos.stream.audio.text.abstract import ( # type: ignore[import-not-found, import-untyped] + AbstractTextTransform, +) from dimos.utils.logging_config import setup_logger -logger = setup_logger(__name__) +logger = setup_logger() -class PyTTSNode(AbstractTextTransform): +class PyTTSNode(AbstractTextTransform): # type: ignore[misc] """ A transform node that passes through text but also speaks it using pyttsx3. @@ -42,10 +44,10 @@ def __init__(self, rate: int = 200, volume: float = 1.0) -> None: self.engine.setProperty("rate", rate) self.engine.setProperty("volume", volume) - self.text_subject = Subject() + self.text_subject = Subject() # type: ignore[var-annotated] self.subscription = None - def emit_text(self) -> Observable: + def emit_text(self) -> Observable: # type: ignore[type-arg] """ Returns an observable that emits text strings passed through this node. @@ -54,7 +56,7 @@ def emit_text(self) -> Observable: """ return self.text_subject - def consume_text(self, text_observable: Observable) -> "AbstractTextTransform": + def consume_text(self, text_observable: Observable) -> "AbstractTextTransform": # type: ignore[type-arg] """ Start processing text from the observable source. @@ -67,7 +69,7 @@ def consume_text(self, text_observable: Observable) -> "AbstractTextTransform": logger.info("Starting PyTTSNode") # Subscribe to the text observable - self.subscription = text_observable.subscribe( + self.subscription = text_observable.subscribe( # type: ignore[assignment] on_next=self.process_text, on_error=lambda e: logger.error(f"Error in PyTTSNode: {e}"), on_completed=lambda: self.on_text_completed(), @@ -108,7 +110,7 @@ def dispose(self) -> None: import time # Create a simple text subject that we can push values to - text_subject = Subject() + text_subject = Subject() # type: ignore[var-annotated] # Create and connect the TTS node tts_node = PyTTSNode(rate=150) diff --git a/dimos/stream/audio/utils.py b/dimos/stream/audio/utils.py index 1a2991467c..c0c3b866d0 100644 --- a/dimos/stream/audio/utils.py +++ b/dimos/stream/audio/utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/stream/audio/volume.py b/dimos/stream/audio/volume.py index bd137172b3..eafb61690b 100644 --- a/dimos/stream/audio/volume.py +++ b/dimos/stream/audio/volume.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import numpy as np -def calculate_rms_volume(audio_data: np.ndarray) -> float: +def calculate_rms_volume(audio_data: np.ndarray) -> float: # type: ignore[type-arg] """ Calculate RMS (Root Mean Square) volume of audio data. @@ -38,10 +38,10 @@ def calculate_rms_volume(audio_data: np.ndarray) -> float: if audio_data.dtype == np.int16: rms = rms / 32768.0 - return rms + return rms # type: ignore[no-any-return] -def calculate_peak_volume(audio_data: np.ndarray) -> float: +def calculate_peak_volume(audio_data: np.ndarray) -> float: # type: ignore[type-arg] """ Calculate peak volume of audio data. @@ -63,7 +63,7 @@ def calculate_peak_volume(audio_data: np.ndarray) -> float: if audio_data.dtype == np.int16: peak = peak / 32768.0 - return peak + return peak # type: ignore[no-any-return] if __name__ == "__main__": @@ -78,7 +78,7 @@ def calculate_peak_volume(audio_data: np.ndarray) -> float: # Create observable and subscribe to get a single frame audio_observable = audio_source.capture_audio_as_observable() - def process_frame(frame) -> None: + def process_frame(frame) -> None: # type: ignore[no-untyped-def] # Calculate and print both RMS and peak volumes rms_vol = calculate_rms_volume(frame.data) peak_vol = calculate_peak_volume(frame.data) @@ -90,7 +90,7 @@ def process_frame(frame) -> None: # Set a flag to track when processing is complete processed = {"done": False} - def process_frame_wrapper(frame) -> None: + def process_frame_wrapper(frame) -> None: # type: ignore[no-untyped-def] # Process the frame process_frame(frame) # Mark as processed diff --git a/dimos/stream/data_provider.py b/dimos/stream/data_provider.py index f931857fda..2a2d18d857 100644 --- a/dimos/stream/data_provider.py +++ b/dimos/stream/data_provider.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,14 +32,14 @@ class AbstractDataProvider(ABC): def __init__(self, dev_name: str = "NA") -> None: self.dev_name = dev_name - self._data_subject = Subject() # Regular Subject, no initial None value + self._data_subject = Subject() # type: ignore[var-annotated] # Regular Subject, no initial None value @property - def data_stream(self) -> Observable: + def data_stream(self) -> Observable: # type: ignore[type-arg] """Get the data stream observable.""" return self._data_subject - def push_data(self, data) -> None: + def push_data(self, data) -> None: # type: ignore[no-untyped-def] """Push new data to the stream.""" self._data_subject.on_next(data) @@ -55,13 +55,13 @@ def __init__(self, dev_name: str = "ros_provider") -> None: super().__init__(dev_name) self.logger = logging.getLogger(dev_name) - def push_data(self, data) -> None: + def push_data(self, data) -> None: # type: ignore[no-untyped-def] """Push new data to the stream.""" print(f"ROSDataProvider pushing data of type: {type(data)}") super().push_data(data) print("Data pushed to subject") - def capture_data_as_observable(self, fps: int | None = None) -> Observable: + def capture_data_as_observable(self, fps: int | None = None) -> Observable: # type: ignore[type-arg] """Get the data stream as an observable. Args: @@ -168,7 +168,7 @@ def start_query_stream( # Zip the timer with the query source so each timer tick emits the next query. query_stream = timer.pipe( ops.zip(query_source), - ops.map(lambda pair: query_template.format(query=pair[1])), + ops.map(lambda pair: query_template.format(query=pair[1])), # type: ignore[index] ops.observe_on(pool_scheduler), # ops.do_action( # on_next=lambda q: self.logger.info(f"Emitting query: {q}"), diff --git a/dimos/stream/frame_processor.py b/dimos/stream/frame_processor.py index fda13ece61..ab18400c88 100644 --- a/dimos/stream/frame_processor.py +++ b/dimos/stream/frame_processor.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,19 +57,19 @@ def __init__( # TODO: Add randomness to jpg folder storage naming. # Will overwrite between sessions. - def to_grayscale(self, frame): + def to_grayscale(self, frame): # type: ignore[no-untyped-def] if frame is None: print("Received None frame for grayscale conversion.") return None return cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - def edge_detection(self, frame): + def edge_detection(self, frame): # type: ignore[no-untyped-def] return cv2.Canny(frame, 100, 200) - def resize(self, frame, scale: float = 0.5): + def resize(self, frame, scale: float = 0.5): # type: ignore[no-untyped-def] return cv2.resize(frame, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA) - def export_to_jpeg(self, frame, save_limit: int = 100, loop: bool = False, suffix: str = ""): + def export_to_jpeg(self, frame, save_limit: int = 100, loop: bool = False, suffix: str = ""): # type: ignore[no-untyped-def] if frame is None: print("Error: Attempted to save a None image.") return None @@ -93,10 +93,10 @@ def export_to_jpeg(self, frame, save_limit: int = 100, loop: bool = False, suffi def compute_optical_flow( self, - acc: tuple[np.ndarray, np.ndarray, float | None], - current_frame: np.ndarray, + acc: tuple[np.ndarray, np.ndarray, float | None], # type: ignore[type-arg] + current_frame: np.ndarray, # type: ignore[type-arg] compute_relevancy: bool = True, - ) -> tuple[np.ndarray, np.ndarray, float | None]: + ) -> tuple[np.ndarray, np.ndarray, float | None]: # type: ignore[type-arg] """Computes optical flow between consecutive frames. Uses the Farneback algorithm to compute dense optical flow between the @@ -128,11 +128,11 @@ def compute_optical_flow( return (current_frame, None, None) # Convert frames to grayscale - gray_current = self.to_grayscale(current_frame) - gray_prev = self.to_grayscale(prev_frame) + gray_current = self.to_grayscale(current_frame) # type: ignore[no-untyped-call] + gray_prev = self.to_grayscale(prev_frame) # type: ignore[no-untyped-call] # Compute optical flow - flow = cv2.calcOpticalFlowFarneback(gray_prev, gray_current, None, 0.5, 3, 15, 3, 5, 1.2, 0) + flow = cv2.calcOpticalFlowFarneback(gray_prev, gray_current, None, 0.5, 3, 15, 3, 5, 1.2, 0) # type: ignore[call-overload] # Relevancy calulation (average magnitude of flow vectors) relevancy = None @@ -141,37 +141,37 @@ def compute_optical_flow( relevancy = np.mean(mag) # Return the current frame as the new previous frame and the processed optical flow, with relevancy score - return (current_frame, flow, relevancy) + return (current_frame, flow, relevancy) # type: ignore[return-value] - def visualize_flow(self, flow): + def visualize_flow(self, flow): # type: ignore[no-untyped-def] if flow is None: return None hsv = np.zeros((flow.shape[0], flow.shape[1], 3), dtype=np.uint8) hsv[..., 1] = 255 mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1]) hsv[..., 0] = ang * 180 / np.pi / 2 - hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX) + hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX) # type: ignore[call-overload] rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) return rgb # ============================== - def process_stream_edge_detection(self, frame_stream): + def process_stream_edge_detection(self, frame_stream): # type: ignore[no-untyped-def] return frame_stream.pipe( ops.map(self.edge_detection), ) - def process_stream_resize(self, frame_stream): + def process_stream_resize(self, frame_stream): # type: ignore[no-untyped-def] return frame_stream.pipe( ops.map(self.resize), ) - def process_stream_to_greyscale(self, frame_stream): + def process_stream_to_greyscale(self, frame_stream): # type: ignore[no-untyped-def] return frame_stream.pipe( ops.map(self.to_grayscale), ) - def process_stream_optical_flow(self, frame_stream: Observable) -> Observable: + def process_stream_optical_flow(self, frame_stream: Observable) -> Observable: # type: ignore[type-arg] """Processes video stream to compute and visualize optical flow. Computes optical flow between consecutive frames and generates a color-coded @@ -203,15 +203,15 @@ def process_stream_optical_flow(self, frame_stream: Observable) -> Observable: """ return frame_stream.pipe( ops.scan( - lambda acc, frame: self.compute_optical_flow(acc, frame, compute_relevancy=False), + lambda acc, frame: self.compute_optical_flow(acc, frame, compute_relevancy=False), # type: ignore[arg-type, return-value] (None, None, None), ), - ops.map(lambda result: result[1]), # Extract flow component + ops.map(lambda result: result[1]), # type: ignore[index] # Extract flow component ops.filter(lambda flow: flow is not None), ops.map(self.visualize_flow), ) - def process_stream_optical_flow_with_relevancy(self, frame_stream: Observable) -> Observable: + def process_stream_optical_flow_with_relevancy(self, frame_stream: Observable) -> Observable: # type: ignore[type-arg] """Processes video stream to compute optical flow with movement relevancy. Applies optical flow computation to each frame and returns both the @@ -247,23 +247,26 @@ def process_stream_optical_flow_with_relevancy(self, frame_stream: Observable) - """ return frame_stream.pipe( ops.scan( - lambda acc, frame: self.compute_optical_flow(acc, frame, compute_relevancy=True), + lambda acc, frame: self.compute_optical_flow(acc, frame, compute_relevancy=True), # type: ignore[arg-type, return-value] (None, None, None), ), # Result is (current_frame, flow, relevancy) - ops.filter(lambda result: result[1] is not None), # Filter out None flows + ops.filter(lambda result: result[1] is not None), # type: ignore[index] # Filter out None flows ops.map( lambda result: ( - self.visualize_flow(result[1]), # Visualized flow - result[2], # Relevancy score + self.visualize_flow(result[1]), # type: ignore[index, no-untyped-call] # Visualized flow + result[2], # type: ignore[index] # Relevancy score ) ), - ops.filter(lambda result: result[0] is not None), # Ensure valid visualization + ops.filter(lambda result: result[0] is not None), # type: ignore[index] # Ensure valid visualization ) def process_stream_with_jpeg_export( - self, frame_stream: Observable, suffix: str = "", loop: bool = False - ) -> Observable: + self, + frame_stream: Observable, # type: ignore[type-arg] + suffix: str = "", + loop: bool = False, + ) -> Observable: # type: ignore[type-arg] """Processes stream by saving frames as JPEGs while passing them through. Saves each frame from the stream as a JPEG file and passes the frame diff --git a/dimos/stream/ros_video_provider.py b/dimos/stream/ros_video_provider.py index 5182ca79f8..cf842aa257 100644 --- a/dimos/stream/ros_video_provider.py +++ b/dimos/stream/ros_video_provider.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -53,11 +53,11 @@ def __init__( """ super().__init__(dev_name, pool_scheduler) self.logger = logging.getLogger(dev_name) - self._subject = Subject() + self._subject = Subject() # type: ignore[var-annotated] self._last_frame_time = None self.logger.info("ROSVideoProvider initialized") - def push_data(self, frame: np.ndarray) -> None: + def push_data(self, frame: np.ndarray) -> None: # type: ignore[type-arg] """Push a new frame into the provider. Args: @@ -74,7 +74,7 @@ def push_data(self, frame: np.ndarray) -> None: self.logger.debug( f"Frame interval: {frame_interval:.3f}s ({1 / frame_interval:.1f} FPS)" ) - self._last_frame_time = current_time + self._last_frame_time = current_time # type: ignore[assignment] self.logger.debug(f"Pushing frame type: {type(frame)}") self._subject.on_next(frame) @@ -83,7 +83,7 @@ def push_data(self, frame: np.ndarray) -> None: self.logger.error(f"Push error: {e}") raise - def capture_video_as_observable(self, fps: int = 30) -> Observable: + def capture_video_as_observable(self, fps: int = 30) -> Observable: # type: ignore[type-arg] """Return an observable of video frames. Args: diff --git a/dimos/stream/rtsp_video_provider.py b/dimos/stream/rtsp_video_provider.py index 3aeb651a4d..fb53e80dd8 100644 --- a/dimos/stream/rtsp_video_provider.py +++ b/dimos/stream/rtsp_video_provider.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import threading import time -import ffmpeg # ffmpeg-python wrapper +import ffmpeg # type: ignore[import-untyped] # ffmpeg-python wrapper import numpy as np import reactivex as rx from reactivex import operators as ops @@ -31,7 +31,7 @@ # Assuming AbstractVideoProvider and exceptions are in the sibling file from .video_provider import AbstractVideoProvider, VideoFrameError, VideoSourceError -logger = setup_logger("dimos.stream.rtsp_video_provider") +logger = setup_logger() class RtspVideoProvider(AbstractVideoProvider): @@ -55,11 +55,11 @@ def __init__( super().__init__(dev_name, pool_scheduler) self.rtsp_url = rtsp_url # Holds the currently active ffmpeg process Popen object - self._ffmpeg_process: subprocess.Popen | None = None + self._ffmpeg_process: subprocess.Popen | None = None # type: ignore[type-arg] # Lock to protect access to the ffmpeg process object self._lock = threading.Lock() - def _get_stream_info(self) -> dict: + def _get_stream_info(self) -> dict: # type: ignore[type-arg] """Probes the RTSP stream to get video dimensions and FPS using ffprobe.""" logger.info(f"({self.dev_name}) Probing RTSP stream.") try: @@ -114,7 +114,7 @@ def _get_stream_info(self) -> dict: logger.info(f"({self.dev_name}) Stream info: {width}x{height} @ {fps:.2f} FPS") return {"width": width, "height": height, "fps": fps} - def _start_ffmpeg_process(self, width: int, height: int) -> subprocess.Popen: + def _start_ffmpeg_process(self, width: int, height: int) -> subprocess.Popen: # type: ignore[type-arg] """Starts the ffmpeg process to capture and decode the stream.""" logger.info(f"({self.dev_name}) Starting ffmpeg process for rtsp stream.") try: @@ -133,7 +133,7 @@ def _start_ffmpeg_process(self, width: int, height: int) -> subprocess.Popen: .run_async(pipe_stdout=True, pipe_stderr=True) # Capture stdout and stderr ) logger.info(f"({self.dev_name}) ffmpeg process started (PID: {process.pid})") - return process + return process # type: ignore[no-any-return] except ffmpeg.Error as e: stderr = e.stderr.decode("utf8", errors="ignore") if e.stderr else "No stderr" msg = f"({self.dev_name}) Failed to start ffmpeg for {self.rtsp_url}: {stderr}" @@ -144,7 +144,7 @@ def _start_ffmpeg_process(self, width: int, height: int) -> subprocess.Popen: logger.error(msg) raise VideoSourceError(msg) from e - def capture_video_as_observable(self, fps: int = 0) -> Observable: + def capture_video_as_observable(self, fps: int = 0) -> Observable: # type: ignore[type-arg] """Creates an observable from the RTSP stream using ffmpeg. The observable attempts to reconnect if the stream drops. @@ -167,9 +167,9 @@ def capture_video_as_observable(self, fps: int = 0) -> Observable: f"({self.dev_name}) The 'fps' argument ({fps}) is currently ignored. Using stream native FPS." ) - def emit_frames(observer, scheduler): + def emit_frames(observer, scheduler): # type: ignore[no-untyped-def] """Function executed by rx.create to emit frames.""" - process: subprocess.Popen | None = None + process: subprocess.Popen | None = None # type: ignore[type-arg] # Event to signal the processing loop should stop (e.g., on dispose) should_stop = threading.Event() @@ -198,7 +198,7 @@ def cleanup_process() -> None: # Ensure we clear the process variable even if wait/kill fails process = None # Also clear the shared class attribute if this was the active process - if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: + if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: # type: ignore[attr-defined] self._ffmpeg_process = None elif process and process.poll() is not None: # Process exists but already terminated @@ -207,7 +207,7 @@ def cleanup_process() -> None: ) process = None # Clear the variable # Clear shared attribute if it matches - if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: + if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: # type: ignore[attr-defined] self._ffmpeg_process = None else: # Process variable is already None or doesn't match _ffmpeg_process @@ -243,7 +243,7 @@ def cleanup_process() -> None: # 3. Frame reading loop while not should_stop.is_set(): # Read exactly one frame's worth of bytes - in_bytes = process.stdout.read(frame_size) + in_bytes = process.stdout.read(frame_size) # type: ignore[union-attr] if len(in_bytes) == 0: # End of stream or process terminated unexpectedly @@ -251,7 +251,7 @@ def cleanup_process() -> None: f"({self.dev_name}) ffmpeg stdout returned 0 bytes. EOF or process terminated." ) process.wait(timeout=0.5) # Allow stderr to flush - stderr_data = process.stderr.read().decode("utf8", errors="ignore") + stderr_data = process.stderr.read().decode("utf8", errors="ignore") # type: ignore[union-attr] exit_code = process.poll() logger.warning( f"({self.dev_name}) ffmpeg process (PID: {process.pid}) exited with code {exit_code}. Stderr: {stderr_data}" diff --git a/dimos/stream/stream_merger.py b/dimos/stream/stream_merger.py index b59c78fa96..645fb86030 100644 --- a/dimos/stream/stream_merger.py +++ b/dimos/stream/stream_merger.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/stream/video_operators.py b/dimos/stream/video_operators.py index d7299f3dce..558972e155 100644 --- a/dimos/stream/video_operators.py +++ b/dimos/stream/video_operators.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import cv2 import numpy as np from reactivex import Observable, Observer, create, operators as ops -import zmq if TYPE_CHECKING: from dimos.stream.frame_processor import FrameProcessor @@ -33,7 +32,7 @@ class VideoOperators: @staticmethod def with_fps_sampling( fps: int = 25, *, sample_interval: timedelta | None = None, use_latest: bool = True - ) -> Callable[[Observable], Observable]: + ) -> Callable[[Observable], Observable]: # type: ignore[type-arg] """Creates an operator that samples frames at a specified rate. Creates a transformation operator that samples frames either by taking @@ -95,7 +94,7 @@ def with_fps_sampling( raise ValueError("FPS must be positive") sample_interval = timedelta(microseconds=int(1_000_000 / fps)) - def _operator(source: Observable) -> Observable: + def _operator(source: Observable) -> Observable: # type: ignore[type-arg] return source.pipe( ops.sample(sample_interval) if use_latest else ops.throttle_first(sample_interval) ) @@ -108,7 +107,7 @@ def with_jpeg_export( save_limit: int = 100, suffix: str = "", loop: bool = False, - ) -> Callable[[Observable], Observable]: + ) -> Callable[[Observable], Observable]: # type: ignore[type-arg] """Creates an operator that saves video frames as JPEG files. Creates a transformation operator that saves each frame from the video @@ -139,7 +138,7 @@ def with_jpeg_export( ... ) """ - def _operator(source: Observable) -> Observable: + def _operator(source: Observable) -> Observable: # type: ignore[type-arg] return source.pipe( ops.map( lambda frame: frame_processor.export_to_jpeg(frame, save_limit, loop, suffix) @@ -149,7 +148,7 @@ def _operator(source: Observable) -> Observable: return _operator @staticmethod - def with_optical_flow_filtering(threshold: float = 1.0) -> Callable[[Observable], Observable]: + def with_optical_flow_filtering(threshold: float = 1.0) -> Callable[[Observable], Observable]: # type: ignore[type-arg] """Creates an operator that filters optical flow frames by relevancy score. Filters a stream of optical flow results (frame, relevancy_score) tuples, @@ -184,57 +183,39 @@ def with_optical_flow_filtering(threshold: float = 1.0) -> Callable[[Observable] None scores are filtered out. """ return lambda source: source.pipe( - ops.filter(lambda result: result[1] is not None), - ops.filter(lambda result: result[1] > threshold), - ops.map(lambda result: result[0]), + ops.filter(lambda result: result[1] is not None), # type: ignore[index] + ops.filter(lambda result: result[1] > threshold), # type: ignore[index] + ops.map(lambda result: result[0]), # type: ignore[index] ) @staticmethod def with_edge_detection( frame_processor: "FrameProcessor", - ) -> Callable[[Observable], Observable]: + ) -> Callable[[Observable], Observable]: # type: ignore[type-arg] return lambda source: source.pipe( - ops.map(lambda frame: frame_processor.edge_detection(frame)) + ops.map(lambda frame: frame_processor.edge_detection(frame)) # type: ignore[no-untyped-call] ) @staticmethod def with_optical_flow( frame_processor: "FrameProcessor", - ) -> Callable[[Observable], Observable]: + ) -> Callable[[Observable], Observable]: # type: ignore[type-arg] return lambda source: source.pipe( ops.scan( - lambda acc, frame: frame_processor.compute_optical_flow( - acc, frame, compute_relevancy=False + lambda acc, frame: frame_processor.compute_optical_flow( # type: ignore[arg-type, return-value] + acc, # type: ignore[arg-type] + frame, # type: ignore[arg-type] + compute_relevancy=False, ), (None, None, None), ), - ops.map(lambda result: result[1]), # Extract flow component + ops.map(lambda result: result[1]), # type: ignore[index] # Extract flow component ops.filter(lambda flow: flow is not None), ops.map(frame_processor.visualize_flow), ) @staticmethod - def with_zmq_socket( - socket: zmq.Socket, scheduler: Any | None = None - ) -> Callable[[Observable], Observable]: - def send_frame(frame, socket) -> None: - _, img_encoded = cv2.imencode(".jpg", frame) - socket.send(img_encoded.tobytes()) - # print(f"Frame received: {frame.shape}") - - # Use a default scheduler if none is provided - if scheduler is None: - from reactivex.scheduler import ThreadPoolScheduler - - scheduler = ThreadPoolScheduler(1) # Single-threaded pool for isolation - - return lambda source: source.pipe( - ops.observe_on(scheduler), # Ensure this part runs on its own thread - ops.do_action(lambda frame: send_frame(frame, socket)), - ) - - @staticmethod - def encode_image() -> Callable[[Observable], Observable]: + def encode_image() -> Callable[[Observable], Observable]: # type: ignore[type-arg] """ Operator to encode an image to JPEG format and convert it to a Base64 string. @@ -243,8 +224,8 @@ def encode_image() -> Callable[[Observable], Observable]: of tuples containing the Base64 string of the encoded image and its dimensions. """ - def _operator(source: Observable) -> Observable: - def _encode_image(image: np.ndarray) -> tuple[str, tuple[int, int]]: + def _operator(source: Observable) -> Observable: # type: ignore[type-arg] + def _encode_image(image: np.ndarray) -> tuple[str, tuple[int, int]]: # type: ignore[type-arg] try: width, height = image.shape[:2] _, buffer = cv2.imencode(".jpg", image) @@ -268,15 +249,15 @@ def _encode_image(image: np.ndarray) -> tuple[str, tuple[int, int]]: class Operators: @staticmethod - def exhaust_lock(process_item): + def exhaust_lock(process_item): # type: ignore[no-untyped-def] """ For each incoming item, call `process_item(item)` to get an Observable. - If we're busy processing the previous one, skip new items. - Use a lock to ensure concurrency safety across threads. """ - def _exhaust_lock(source: Observable) -> Observable: - def _subscribe(observer, scheduler=None): + def _exhaust_lock(source: Observable) -> Observable: # type: ignore[type-arg] + def _subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] in_flight = False lock = Lock() upstream_done = False @@ -290,7 +271,7 @@ def dispose_all() -> None: if active_inner_disp: active_inner_disp.dispose() - def on_next(value) -> None: + def on_next(value) -> None: # type: ignore[no-untyped-def] nonlocal in_flight, active_inner_disp lock.acquire() try: @@ -310,10 +291,10 @@ def on_next(value) -> None: observer.on_error(ex) return - def inner_on_next(ivalue) -> None: + def inner_on_next(ivalue) -> None: # type: ignore[no-untyped-def] observer.on_next(ivalue) - def inner_on_error(err) -> None: + def inner_on_error(err) -> None: # type: ignore[no-untyped-def] nonlocal in_flight with lock: in_flight = False @@ -335,7 +316,7 @@ def inner_on_completed() -> None: scheduler=scheduler, ) - def on_error(err) -> None: + def on_error(err) -> None: # type: ignore[no-untyped-def] dispose_all() observer.on_error(err) @@ -357,15 +338,15 @@ def on_completed() -> None: return _exhaust_lock @staticmethod - def exhaust_lock_per_instance(process_item, lock: Lock): + def exhaust_lock_per_instance(process_item, lock: Lock): # type: ignore[no-untyped-def] """ - For each item from upstream, call process_item(item) -> Observable. - If a frame arrives while one is "in flight", discard it. - 'lock' ensures we safely check/modify the 'in_flight' state in a multithreaded environment. """ - def _exhaust_lock(source: Observable) -> Observable: - def _subscribe(observer, scheduler=None): + def _exhaust_lock(source: Observable) -> Observable: # type: ignore[type-arg] + def _subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] in_flight = False upstream_done = False @@ -378,7 +359,7 @@ def dispose_all() -> None: if active_inner_disp: active_inner_disp.dispose() - def on_next(value) -> None: + def on_next(value) -> None: # type: ignore[no-untyped-def] nonlocal in_flight, active_inner_disp with lock: # If not busy, claim the slot @@ -397,10 +378,10 @@ def on_next(value) -> None: observer.on_error(ex) return - def inner_on_next(ivalue) -> None: + def inner_on_next(ivalue) -> None: # type: ignore[no-untyped-def] observer.on_next(ivalue) - def inner_on_error(err) -> None: + def inner_on_error(err) -> None: # type: ignore[no-untyped-def] nonlocal in_flight with lock: in_flight = False @@ -424,7 +405,7 @@ def inner_on_completed() -> None: scheduler=scheduler, ) - def on_error(e) -> None: + def on_error(e) -> None: # type: ignore[no-untyped-def] dispose_all() observer.on_error(e) @@ -450,12 +431,12 @@ def on_completed() -> None: return _exhaust_lock @staticmethod - def exhaust_map(project): - def _exhaust_map(source: Observable): - def subscribe(observer, scheduler=None): + def exhaust_map(project): # type: ignore[no-untyped-def] + def _exhaust_map(source: Observable): # type: ignore[no-untyped-def, type-arg] + def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] is_processing = False - def on_next(item) -> None: + def on_next(item) -> None: # type: ignore[no-untyped-def] nonlocal is_processing if not is_processing: is_processing = True @@ -490,10 +471,10 @@ def set_not_processing() -> None: return _exhaust_map @staticmethod - def with_lock(lock: Lock): - def operator(source: Observable): - def subscribe(observer, scheduler=None): - def on_next(item) -> None: + def with_lock(lock: Lock): # type: ignore[no-untyped-def] + def operator(source: Observable): # type: ignore[no-untyped-def, type-arg] + def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] + def on_next(item) -> None: # type: ignore[no-untyped-def] if not lock.locked(): # Check if the lock is free if lock.acquire(blocking=False): # Non-blocking acquire try: @@ -506,7 +487,7 @@ def on_next(item) -> None: else: print("\033[34mLock busy, skipping item.\033[0m") - def on_error(error) -> None: + def on_error(error) -> None: # type: ignore[no-untyped-def] observer.on_error(error) def on_completed() -> None: @@ -524,10 +505,10 @@ def on_completed() -> None: return operator @staticmethod - def with_lock_check(lock: Lock): # Renamed for clarity - def operator(source: Observable): - def subscribe(observer, scheduler=None): - def on_next(item) -> None: + def with_lock_check(lock: Lock): # type: ignore[no-untyped-def] # Renamed for clarity + def operator(source: Observable): # type: ignore[no-untyped-def, type-arg] + def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] + def on_next(item) -> None: # type: ignore[no-untyped-def] if not lock.locked(): # Check if the lock is held WITHOUT acquiring print(f"\033[32mLock is free, processing item: {item}\033[0m") observer.on_next(item) @@ -535,7 +516,7 @@ def on_next(item) -> None: print(f"\033[34mLock is busy, skipping item: {item}\033[0m") # observer.on_completed() - def on_error(error) -> None: + def on_error(error) -> None: # type: ignore[no-untyped-def] observer.on_error(error) def on_completed() -> None: @@ -564,11 +545,11 @@ class PrintColor(Enum): RESET = "\033[0m" @staticmethod - def print_emission( + def print_emission( # type: ignore[no-untyped-def] id: str, dev_name: str = "NA", - counts: dict | None = None, - color: "Operators.PrintColor" = None, + counts: dict | None = None, # type: ignore[type-arg] + color: "Operators.PrintColor" = None, # type: ignore[assignment] enabled: bool = True, ): """ @@ -591,9 +572,9 @@ def print_emission( if color is None: color = Operators.PrintColor.RED - def _operator(source: Observable) -> Observable: - def _subscribe(observer: Observer, scheduler=None): - def on_next(value) -> None: + def _operator(source: Observable) -> Observable: # type: ignore[type-arg] + def _subscribe(observer: Observer, scheduler=None): # type: ignore[no-untyped-def, type-arg] + def on_next(value) -> None: # type: ignore[no-untyped-def] if counts is not None: # Initialize count if necessary if id not in counts: @@ -619,6 +600,6 @@ def on_next(value) -> None: scheduler=scheduler, ) - return create(_subscribe) + return create(_subscribe) # type: ignore[arg-type] return _operator diff --git a/dimos/stream/video_provider.py b/dimos/stream/video_provider.py index 0b7e815ae2..38406fd5a5 100644 --- a/dimos/stream/video_provider.py +++ b/dimos/stream/video_provider.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ def __init__( self.disposables = CompositeDisposable() @abstractmethod - def capture_video_as_observable(self, fps: int = 30) -> Observable: + def capture_video_as_observable(self, fps: int = 30) -> Observable: # type: ignore[type-arg] """Create an observable from video capture. Args: @@ -135,7 +135,7 @@ def _initialize_capture(self) -> None: logger.info("Released previous capture") # Attempt to open new capture - self.cap = cv2.VideoCapture(self.video_source) + self.cap = cv2.VideoCapture(self.video_source) # type: ignore[assignment] if self.cap is None or not self.cap.isOpened(): error_msg = f"Failed to open video source: {self.video_source}" logger.error(error_msg) @@ -143,7 +143,7 @@ def _initialize_capture(self) -> None: logger.info(f"Opened new capture: {self.video_source}") - def capture_video_as_observable(self, realtime: bool = True, fps: int = 30) -> Observable: + def capture_video_as_observable(self, realtime: bool = True, fps: int = 30) -> Observable: # type: ignore[override, type-arg] """Creates an observable from video capture. Creates an observable that emits frames at specified FPS or the video's @@ -162,14 +162,14 @@ def capture_video_as_observable(self, realtime: bool = True, fps: int = 30) -> O VideoFrameError: If frames cannot be read properly. """ - def emit_frames(observer, scheduler) -> None: + def emit_frames(observer, scheduler) -> None: # type: ignore[no-untyped-def] try: self._initialize_capture() # Determine the FPS to use based on configuration and availability local_fps: float = fps if realtime: - native_fps: float = self.cap.get(cv2.CAP_PROP_FPS) + native_fps: float = self.cap.get(cv2.CAP_PROP_FPS) # type: ignore[attr-defined] if native_fps > 0: local_fps = native_fps else: @@ -178,16 +178,16 @@ def emit_frames(observer, scheduler) -> None: frame_interval: float = 1.0 / local_fps frame_time: float = time.monotonic() - while self.cap.isOpened(): + while self.cap.isOpened(): # type: ignore[attr-defined] # Thread-safe access to video capture with self.lock: - ret, frame = self.cap.read() + ret, frame = self.cap.read() # type: ignore[attr-defined] if not ret: # Loop video when we reach the end logger.warning("End of video reached, restarting playback") with self.lock: - self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # type: ignore[attr-defined] continue # Control frame rate to match target FPS @@ -215,7 +215,7 @@ def emit_frames(observer, scheduler) -> None: logger.info("Capture released") observer.on_completed() - return rx.create(emit_frames).pipe( + return rx.create(emit_frames).pipe( # type: ignore[arg-type] ops.subscribe_on(self.pool_scheduler), ops.observe_on(self.pool_scheduler), ops.share(), # Share the stream among multiple subscribers diff --git a/dimos/stream/video_providers/unitree.py b/dimos/stream/video_providers/unitree.py deleted file mode 100644 index ba28cb1d6f..0000000000 --- a/dimos/stream/video_providers/unitree.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging -from queue import Queue -import threading -import time - -from aiortc import MediaStreamTrack -from go2_webrtc_driver.webrtc_driver import Go2WebRTCConnection, WebRTCConnectionMethod -from reactivex import Observable, create, operators as ops - -from dimos.stream.video_provider import AbstractVideoProvider - - -class UnitreeVideoProvider(AbstractVideoProvider): - def __init__( - self, - dev_name: str = "UnitreeGo2", - connection_method: WebRTCConnectionMethod = WebRTCConnectionMethod.LocalSTA, - serial_number: str | None = None, - ip: str | None = None, - ) -> None: - """Initialize the Unitree video stream with WebRTC connection. - - Args: - dev_name: Name of the device - connection_method: WebRTC connection method (LocalSTA, LocalAP, Remote) - serial_number: Serial number of the robot (required for LocalSTA with serial) - ip: IP address of the robot (required for LocalSTA with IP) - """ - super().__init__(dev_name) - self.frame_queue = Queue() - self.loop = None - self.asyncio_thread = None - - # Initialize WebRTC connection based on method - if connection_method == WebRTCConnectionMethod.LocalSTA: - if serial_number: - self.conn = Go2WebRTCConnection(connection_method, serialNumber=serial_number) - elif ip: - self.conn = Go2WebRTCConnection(connection_method, ip=ip) - else: - raise ValueError( - "Either serial_number or ip must be provided for LocalSTA connection" - ) - elif connection_method == WebRTCConnectionMethod.LocalAP: - self.conn = Go2WebRTCConnection(connection_method) - else: - raise ValueError("Unsupported connection method") - - async def _recv_camera_stream(self, track: MediaStreamTrack) -> None: - """Receive video frames from WebRTC and put them in the queue.""" - while True: - frame = await track.recv() - # Convert the frame to a NumPy array in BGR format - img = frame.to_ndarray(format="bgr24") - self.frame_queue.put(img) - - def _run_asyncio_loop(self, loop) -> None: - """Run the asyncio event loop in a separate thread.""" - asyncio.set_event_loop(loop) - - async def setup(): - try: - await self.conn.connect() - self.conn.video.switchVideoChannel(True) - self.conn.video.add_track_callback(self._recv_camera_stream) - - await self.conn.datachannel.switchToNormalMode() - # await self.conn.datachannel.sendDamp() - - # await asyncio.sleep(5) - - # await self.conn.datachannel.sendDamp() - # await asyncio.sleep(5) - # await self.conn.datachannel.sendStandUp() - # await asyncio.sleep(5) - - # Wiggle the robot - # await self.conn.datachannel.switchToNormalMode() - # await self.conn.datachannel.sendWiggle() - # await asyncio.sleep(3) - - # Stretch the robot - # await self.conn.datachannel.sendStretch() - # await asyncio.sleep(3) - - except Exception as e: - logging.error(f"Error in WebRTC connection: {e}") - raise - - loop.run_until_complete(setup()) - loop.run_forever() - - def capture_video_as_observable(self, fps: int = 30) -> Observable: - """Create an observable that emits video frames at the specified FPS. - - Args: - fps: Frames per second to emit (default: 30) - - Returns: - Observable emitting video frames - """ - frame_interval = 1.0 / fps - - def emit_frames(observer, scheduler) -> None: - try: - # Start asyncio loop if not already running - if not self.loop: - self.loop = asyncio.new_event_loop() - self.asyncio_thread = threading.Thread( - target=self._run_asyncio_loop, args=(self.loop,) - ) - self.asyncio_thread.start() - - frame_time = time.monotonic() - - while True: - if not self.frame_queue.empty(): - frame = self.frame_queue.get() - - # Control frame rate - now = time.monotonic() - next_frame_time = frame_time + frame_interval - sleep_time = next_frame_time - now - - if sleep_time > 0: - time.sleep(sleep_time) - - observer.on_next(frame) - frame_time = next_frame_time - else: - time.sleep(0.001) # Small sleep to prevent CPU overuse - - except Exception as e: - logging.error(f"Error during frame emission: {e}") - observer.on_error(e) - finally: - if self.loop: - self.loop.call_soon_threadsafe(self.loop.stop) - if self.asyncio_thread: - self.asyncio_thread.join() - observer.on_completed() - - return create(emit_frames).pipe( - ops.share() # Share the stream among multiple subscribers - ) - - def dispose_all(self) -> None: - """Clean up resources.""" - if self.loop: - self.loop.call_soon_threadsafe(self.loop.stop) - if self.asyncio_thread: - self.asyncio_thread.join() - super().dispose_all() diff --git a/dimos/stream/videostream.py b/dimos/stream/videostream.py deleted file mode 100644 index 9c99ddea3a..0000000000 --- a/dimos/stream/videostream.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Iterator - -import cv2 - - -class VideoStream: - def __init__(self, source: int = 0) -> None: - """ - Initialize the video stream from a camera source. - - Args: - source (int or str): Camera index or video file path. - """ - self.capture = cv2.VideoCapture(source) - if not self.capture.isOpened(): - raise ValueError(f"Unable to open video source {source}") - - def __iter__(self) -> Iterator: - return self - - def __next__(self): - ret, frame = self.capture.read() - if not ret: - self.capture.release() - raise StopIteration - return frame - - def release(self) -> None: - self.capture.release() diff --git a/dimos/types/constants.py b/dimos/types/constants.py index 91841e8bef..b02726cb0b 100644 --- a/dimos/types/constants.py +++ b/dimos/types/constants.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/types/label.py b/dimos/types/label.py deleted file mode 100644 index 83b91c8152..0000000000 --- a/dimos/types/label.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any - - -class LabelType: - def __init__(self, labels: dict[str, Any], metadata: Any = None) -> None: - """ - Initializes a standardized label type. - - Args: - labels (Dict[str, Any]): A dictionary of labels with descriptions. - metadata (Any, optional): Additional metadata related to the labels. - """ - self.labels = labels - self.metadata = metadata - - def get_label_descriptions(self): - """Return a list of label descriptions.""" - return [desc["description"] for desc in self.labels.values()] - - def save_to_json(self, filepath: str) -> None: - """Save the labels to a JSON file.""" - import json - - with open(filepath, "w") as f: - json.dump(self.labels, f, indent=4) diff --git a/dimos/types/manipulation.py b/dimos/types/manipulation.py index 0df62362a4..507b9e9b85 100644 --- a/dimos/types/manipulation.py +++ b/dimos/types/manipulation.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ from dimos.types.vector import Vector if TYPE_CHECKING: - import open3d as o3d + import open3d as o3d # type: ignore[import-untyped] class ConstraintType(Enum): @@ -47,7 +47,7 @@ class AbstractConstraint(ABC): class TranslationConstraint(AbstractConstraint): """Constraint parameters for translational movement along a single axis.""" - translation_axis: Literal["x", "y", "z"] = None # Axis to translate along + translation_axis: Literal["x", "y", "z"] = None # type: ignore[assignment] # Axis to translate along reference_point: Vector | None = None bounds_min: Vector | None = None # For bounded translation bounds_max: Vector | None = None # For bounded translation @@ -58,7 +58,7 @@ class TranslationConstraint(AbstractConstraint): class RotationConstraint(AbstractConstraint): """Constraint parameters for rotational movement around a single axis.""" - rotation_axis: Literal["roll", "pitch", "yaw"] = None # Axis to rotate around + rotation_axis: Literal["roll", "pitch", "yaw"] = None # type: ignore[assignment] # Axis to rotate around start_angle: Vector | None = None # Angle values applied to the specified rotation axis end_angle: Vector | None = None # Angle values applied to the specified rotation axis pivot_point: Vector | None = None # Point of rotation @@ -85,7 +85,7 @@ class ObjectData(TypedDict, total=False): class_id: int # Class ID from the detector label: str # Semantic label (e.g., 'cup', 'table') movement_tolerance: float # (0.0 = immovable, 1.0 = freely movable) - segmentation_mask: np.ndarray # Binary mask of the object's pixels + segmentation_mask: np.ndarray # type: ignore[type-arg] # Binary mask of the object's pixels # 3D pose and dimensions position: dict[str, float] | Vector # 3D position {x, y, z} or Vector @@ -94,8 +94,8 @@ class ObjectData(TypedDict, total=False): # Point cloud data point_cloud: "o3d.geometry.PointCloud" # Open3D point cloud object - point_cloud_numpy: np.ndarray # Nx6 array of XYZRGB points - color: np.ndarray # RGB color for visualization [R, G, B] + point_cloud_numpy: np.ndarray # type: ignore[type-arg] # Nx6 array of XYZRGB points + color: np.ndarray # type: ignore[type-arg] # RGB color for visualization [R, G, B] class ManipulationMetadata(TypedDict, total=False): @@ -130,7 +130,7 @@ class ManipulationTask: target_point: tuple[float, float] | None = ( None # (X,Y) point in pixel-space of the point to manipulate on target object ) - metadata: ManipulationMetadata = field(default_factory=dict) + metadata: ManipulationMetadata = field(default_factory=dict) # type: ignore[assignment] timestamp: float = field(default_factory=time.time) task_id: str = "" result: dict[str, Any] | None = None # Any result data from the task execution diff --git a/dimos/types/robot_capabilities.py b/dimos/types/robot_capabilities.py index 8c9a7fcd41..9a3f5da14e 100644 --- a/dimos/types/robot_capabilities.py +++ b/dimos/types/robot_capabilities.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/types/robot_location.py b/dimos/types/robot_location.py index 59a780daf5..78077092f8 100644 --- a/dimos/types/robot_location.py +++ b/dimos/types/robot_location.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -54,13 +54,13 @@ def __post_init__(self) -> None: if len(self.position) == 2: self.position = (self.position[0], self.position[1], 0.0) else: - self.position = tuple(float(x) for x in self.position) + self.position = tuple(float(x) for x in self.position) # type: ignore[assignment] # Ensure rotation is a tuple of 3 floats if len(self.rotation) == 1: self.rotation = (0.0, 0.0, self.rotation[0]) else: - self.rotation = tuple(float(x) for x in self.rotation) + self.rotation = tuple(float(x) for x in self.rotation) # type: ignore[assignment] def to_vector_metadata(self) -> dict[str, Any]: """ diff --git a/dimos/types/ros_polyfill.py b/dimos/types/ros_polyfill.py index c8919caec3..4bad99740d 100644 --- a/dimos/types/ros_polyfill.py +++ b/dimos/types/ros_polyfill.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,18 +13,28 @@ # limitations under the License. try: - from geometry_msgs.msg import Vector3 + from geometry_msgs.msg import Vector3 # type: ignore[attr-defined] except ImportError: - from dimos.msgs.geometry_msgs import Vector3 # type: ignore[import] + from dimos.msgs.geometry_msgs import Vector3 try: - from geometry_msgs.msg import Point, Pose, Quaternion, Twist - from nav_msgs.msg import OccupancyGrid, Odometry - from std_msgs.msg import Header + from geometry_msgs.msg import ( # type: ignore[attr-defined] + Point, + Pose, + Quaternion, + Twist, + ) + from nav_msgs.msg import OccupancyGrid, Odometry # type: ignore[attr-defined] + from std_msgs.msg import Header # type: ignore[attr-defined] except ImportError: - from dimos_lcm.geometry_msgs import Point, Pose, Quaternion, Twist - from dimos_lcm.nav_msgs import OccupancyGrid, Odometry - from dimos_lcm.std_msgs import Header + from dimos_lcm.geometry_msgs import ( # type: ignore[no-redef] + Point, + Pose, + Quaternion, + Twist, + ) + from dimos_lcm.nav_msgs import OccupancyGrid, Odometry # type: ignore[no-redef] + from dimos_lcm.std_msgs import Header # type: ignore[no-redef] __all__ = [ "Header", diff --git a/dimos/types/sample.py b/dimos/types/sample.py index 6d84942c55..16ca96b611 100644 --- a/dimos/types/sample.py +++ b/dimos/types/sample.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,15 +21,16 @@ from pathlib import Path from typing import Annotated, Any, Literal, Union, get_origin -from datasets import Dataset -from gymnasium import spaces -from jsonref import replace_refs -from mbodied.data.utils import to_features -from mbodied.utils.import_utils import smart_import +from datasets import Dataset # type: ignore[import-not-found] +from gymnasium import spaces # type: ignore[import-not-found] +from jsonref import replace_refs # type: ignore[import-not-found] +from mbodied.data.utils import to_features # type: ignore[import-not-found] +from mbodied.utils.import_utils import smart_import # type: ignore[import-not-found] import numpy as np from pydantic import BaseModel, ConfigDict, ValidationError from pydantic.fields import FieldInfo from pydantic_core import from_json +import torch Flattenable = Annotated[Literal["dict", "np", "pt", "list"], "Numpy, PyTorch, list, or dict"] @@ -73,7 +74,7 @@ class Sample(BaseModel): __doc__ = "A base model class for serializing, recording, and manipulating arbitray data." - model_config: ConfigDict = ConfigDict( + model_config: ConfigDict = ConfigDict( # type: ignore[misc] use_enum_values=False, from_attributes=True, validate_assignment=False, @@ -81,7 +82,7 @@ class Sample(BaseModel): arbitrary_types_allowed=True, ) - def __init__(self, datum=None, **data) -> None: + def __init__(self, datum=None, **data) -> None: # type: ignore[no-untyped-def] """Accepts an arbitrary datum as well as keyword arguments.""" if datum is not None: if isinstance(datum, Sample): @@ -100,7 +101,7 @@ def __str__(self) -> str: """Return a string representation of the Sample instance.""" return f"{self.__class__.__name__}({', '.join([f'{k}={v}' for k, v in self.dict().items() if v is not None])})" - def dict(self, exclude_none: bool = True, exclude: set[str] | None = None) -> dict[str, Any]: + def dict(self, exclude_none: bool = True, exclude: set[str] | None = None) -> dict[str, Any]: # type: ignore[override] """Return the Sample object as a dictionary with None values excluded. Args: @@ -113,7 +114,7 @@ def dict(self, exclude_none: bool = True, exclude: set[str] | None = None) -> di return self.model_dump(exclude_none=exclude_none, exclude=exclude) @classmethod - def unflatten(cls, one_d_array_or_dict, schema=None) -> "Sample": + def unflatten(cls, one_d_array_or_dict, schema=None) -> "Sample": # type: ignore[no-untyped-def] """Unflatten a one-dimensional array or dictionary into a Sample instance. If a dictionary is provided, its keys are ignored. @@ -142,7 +143,7 @@ def unflatten(cls, one_d_array_or_dict, schema=None) -> "Sample": else: flat_data = list(one_d_array_or_dict) - def unflatten_recursive(schema_part, index: int = 0): + def unflatten_recursive(schema_part, index: int = 0): # type: ignore[no-untyped-def] if schema_part["type"] == "object": result = {} for prop, prop_schema in schema_part["properties"].items(): @@ -165,10 +166,10 @@ def flatten( self, output_type: Flattenable = "dict", non_numerical: Literal["ignore", "forbid", "allow"] = "allow", - ) -> builtins.dict[str, Any] | np.ndarray | "torch.Tensor" | list: - accumulator = {} if output_type == "dict" else [] + ) -> builtins.dict[str, Any] | np.ndarray | torch.Tensor | list: # type: ignore[type-arg] + accumulator = {} if output_type == "dict" else [] # type: ignore[var-annotated] - def flatten_recursive(obj, path: str = "") -> None: + def flatten_recursive(obj, path: str = "") -> None: # type: ignore[no-untyped-def] if isinstance(obj, Sample): for k, v in obj.dict().items(): flatten_recursive(v, path + k + "/") @@ -182,20 +183,20 @@ def flatten_recursive(obj, path: str = "") -> None: flat_list = obj.flatten().tolist() if output_type == "dict": # Convert to list for dict storage - accumulator[path[:-1]] = flat_list + accumulator[path[:-1]] = flat_list # type: ignore[index] else: - accumulator.extend(flat_list) + accumulator.extend(flat_list) # type: ignore[attr-defined] else: if non_numerical == "ignore" and not isinstance(obj, int | float | bool): return final_key = path[:-1] # Remove trailing slash if output_type == "dict": - accumulator[final_key] = obj + accumulator[final_key] = obj # type: ignore[index] else: - accumulator.append(obj) + accumulator.append(obj) # type: ignore[attr-defined] flatten_recursive(self) - accumulator = accumulator.values() if output_type == "dict" else accumulator + accumulator = accumulator.values() if output_type == "dict" else accumulator # type: ignore[attr-defined] if non_numerical == "forbid" and any( not isinstance(v, int | float | bool) for v in accumulator ): @@ -204,11 +205,11 @@ def flatten_recursive(obj, path: str = "") -> None: return np.array(accumulator) if output_type == "pt": torch = smart_import("torch") - return torch.tensor(accumulator) - return accumulator + return torch.tensor(accumulator) # type: ignore[no-any-return] + return accumulator # type: ignore[return-value] @staticmethod - def obj_to_schema(value: Any) -> builtins.dict: + def obj_to_schema(value: Any) -> builtins.dict: # type: ignore[type-arg] """Generates a simplified JSON schema from a dictionary. Args: @@ -237,8 +238,10 @@ def obj_to_schema(value: Any) -> builtins.dict: return {} def schema( - self, resolve_refs: bool = True, include_descriptions: bool = False - ) -> builtins.dict: + self, + resolve_refs: bool = True, + include_descriptions: bool = False, # type: ignore[override] + ) -> builtins.dict: # type: ignore[type-arg] """Returns a simplified json schema. Removing additionalProperties, @@ -314,8 +317,8 @@ def to(self, container: Any) -> Any: Returns: Any: The converted container. """ - if isinstance(container, Sample) and not issubclass(container, Sample): - return container(**self.dict()) + if isinstance(container, Sample) and not issubclass(container, Sample): # type: ignore[arg-type] + return container(**self.dict()) # type: ignore[operator] if isinstance(container, type) and issubclass(container, Sample): return container.unflatten(self.flatten()) @@ -353,7 +356,7 @@ def space_for( cls, value: Any, max_text_length: int = 1000, - info: Annotated = None, + info: Annotated = None, # type: ignore[valid-type] ) -> spaces.Space: """Default Gym space generation for a given value. @@ -390,7 +393,7 @@ def space_for( dtype, ) try: - value = np.asfarray(value) + value = np.asfarray(value) # type: ignore[attr-defined] shape = shape or value.shape dtype = dtype or value.dtype le = le or -np.inf @@ -411,7 +414,7 @@ def space_for( def init_from(cls, d: Any, pack: bool = False) -> "Sample": if isinstance(d, spaces.Space): return cls.from_space(d) - if isinstance(d, Union[Sequence, np.ndarray]): + if isinstance(d, Union[Sequence, np.ndarray]): # type: ignore[arg-type] if pack: return cls.pack_from(d) return cls.unflatten(d) @@ -430,7 +433,9 @@ def init_from(cls, d: Any, pack: bool = False) -> "Sample": @classmethod def from_flat_dict( - cls, flat_dict: builtins.dict[str, Any], schema: builtins.dict | None = None + cls, + flat_dict: builtins.dict[str, Any], + schema: builtins.dict | None = None, # type: ignore[type-arg] ) -> "Sample": """Initialize a Sample instance from a flattened dictionary.""" """ @@ -444,7 +449,7 @@ def from_flat_dict( dict: The reconstructed JSON object. """ schema = schema or replace_refs(cls.model_json_schema()) - reconstructed = {} + reconstructed = {} # type: ignore[var-annotated] for flat_key, value in flat_dict.items(): keys = flat_key.split(".") @@ -455,7 +460,7 @@ def from_flat_dict( current = current[key] current[keys[-1]] = value - return reconstructed + return reconstructed # type: ignore[return-value] @classmethod def from_space(cls, space: spaces.Space) -> "Sample": @@ -466,11 +471,11 @@ def from_space(cls, space: spaces.Space) -> "Sample": if hasattr(sampled, "__len__") and not isinstance(sampled, str): sampled = np.asarray(sampled) if len(sampled.shape) > 0 and isinstance(sampled[0], dict | Sample): - return cls.pack_from(sampled) + return cls.pack_from(sampled) # type: ignore[arg-type] return cls(sampled) @classmethod - def pack_from(cls, samples: list[Union["Sample", builtins.dict]]) -> "Sample": + def pack_from(cls, samples: list[Union["Sample", builtins.dict]]) -> "Sample": # type: ignore[type-arg] """Pack a list of samples into a single sample with lists for attributes. Args: @@ -490,7 +495,7 @@ def pack_from(cls, samples: list[Union["Sample", builtins.dict]]) -> "Sample": else: attributes = ["item" + str(i) for i in range(len(samples))] - aggregated = {attr: [] for attr in attributes} + aggregated = {attr: [] for attr in attributes} # type: ignore[var-annotated] for sample in samples: for attr in attributes: # Handle both Sample instances and dictionaries @@ -500,9 +505,9 @@ def pack_from(cls, samples: list[Union["Sample", builtins.dict]]) -> "Sample": aggregated[attr].append(getattr(sample, attr, None)) return cls(**aggregated) - def unpack(self, to_dicts: bool = False) -> list[Union["Sample", builtins.dict]]: + def unpack(self, to_dicts: bool = False) -> list[Union["Sample", builtins.dict]]: # type: ignore[type-arg] """Unpack the packed Sample object into a list of Sample objects or dictionaries.""" - attributes = list(self.model_extra.keys()) + list(self.model_fields.keys()) + attributes = list(self.model_extra.keys()) + list(self.model_fields.keys()) # type: ignore[union-attr] attributes = [attr for attr in attributes if getattr(self, attr) is not None] if not attributes or getattr(self, attributes[0]) is None: return [] @@ -543,13 +548,13 @@ def default_sample( def model_field_info(self, key: str) -> FieldInfo: """Get the FieldInfo for a given attribute key.""" if self.model_extra and self.model_extra.get(key) is not None: - info = FieldInfo(metadata=self.model_extra[key]) + info = FieldInfo(metadata=self.model_extra[key]) # type: ignore[call-arg] if self.model_fields.get(key) is not None: - info = FieldInfo(metadata=self.model_fields[key]) + info = FieldInfo(metadata=self.model_fields[key]) # type: ignore[call-arg] if info and hasattr(info, "annotation"): - return info.annotation - return None + return info.annotation # type: ignore[return-value] + return None # type: ignore[return-value] def space(self) -> spaces.Dict: """Return the corresponding Gym space for the Sample instance based on its instance attributes. Omits None values. diff --git a/dimos/types/segmentation.py b/dimos/types/segmentation.py deleted file mode 100644 index 1f3c2a0773..0000000000 --- a/dimos/types/segmentation.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any - -import numpy as np - - -class SegmentationType: - def __init__(self, masks: list[np.ndarray], metadata: Any = None) -> None: - """ - Initializes a standardized segmentation type. - - Args: - masks (List[np.ndarray]): A list of binary masks for segmentation. - metadata (Any, optional): Additional metadata related to the segmentations. - """ - self.masks = masks - self.metadata = metadata - - def combine_masks(self): - """Combine all masks into a single mask.""" - combined_mask = np.zeros_like(self.masks[0]) - for mask in self.masks: - combined_mask = np.logical_or(combined_mask, mask) - return combined_mask - - def save_masks(self, directory: str) -> None: - """Save each mask to a separate file.""" - import os - - os.makedirs(directory, exist_ok=True) - for i, mask in enumerate(self.masks): - np.save(os.path.join(directory, f"mask_{i}.npy"), mask) diff --git a/dimos/types/test_timestamped.py b/dimos/types/test_timestamped.py index 7eae7a8ad3..88a8d65102 100644 --- a/dimos/types/test_timestamped.py +++ b/dimos/types/test_timestamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/types/test_vector.py b/dimos/types/test_vector.py index 5462fda9a4..285d021bea 100644 --- a/dimos/types/test_vector.py +++ b/dimos/types/test_vector.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/types/test_weaklist.py b/dimos/types/test_weaklist.py index a37d893de9..990cc0d164 100644 --- a/dimos/types/test_weaklist.py +++ b/dimos/types/test_weaklist.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/types/timestamped.py b/dimos/types/timestamped.py index 0045c73ef4..765b1adbcb 100644 --- a/dimos/types/timestamped.py +++ b/dimos/types/timestamped.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,12 +22,12 @@ # from dimos_lcm.std_msgs import Time as ROSTime from reactivex.observable import Observable -from sortedcontainers import SortedKeyList +from sortedcontainers import SortedKeyList # type: ignore[import-untyped] from dimos.types.weaklist import WeakList from dimos.utils.logging_config import setup_logger -logger = setup_logger("dimos.timestampAlignment") +logger = setup_logger() # any class that carries a timestamp should inherit from this # this allows us to work with timeseries in consistent way, allign messages, replay etc @@ -49,14 +49,14 @@ def to_timestamp(ts: TimeLike) -> float: if isinstance(ts, int | float): return float(ts) if isinstance(ts, dict) and "sec" in ts and "nanosec" in ts: - return ts["sec"] + ts["nanosec"] / 1e9 + return ts["sec"] + ts["nanosec"] / 1e9 # type: ignore[no-any-return] # Check for ROS Time-like objects by attributes if hasattr(ts, "sec") and (hasattr(ts, "nanosec") or hasattr(ts, "nsec")): # Handle both std_msgs.Time (nsec) and builtin_interfaces.Time (nanosec) if hasattr(ts, "nanosec"): - return ts.sec + ts.nanosec / 1e9 + return ts.sec + ts.nanosec / 1e9 # type: ignore[no-any-return] else: # has nsec - return ts.sec + ts.nsec / 1e9 + return ts.sec + ts.nsec / 1e9 # type: ignore[no-any-return] raise TypeError("unsupported timestamp type") @@ -78,7 +78,7 @@ def to_human_readable(ts: float) -> str: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts)) -def to_datetime(ts: TimeLike, tz=None) -> datetime: +def to_datetime(ts: TimeLike, tz=None) -> datetime: # type: ignore[no-untyped-def] if isinstance(ts, datetime): if ts.tzinfo is None: # Assume UTC for naive datetime @@ -137,7 +137,7 @@ def find_closest(self, timestamp: float, tolerance: float | None = None) -> T | # Check exact match if idx < len(self._items) and self._items[idx].ts == timestamp: - return self._items[idx] + return self._items[idx] # type: ignore[no-any-return] # Find candidates: item before and after candidates = [] @@ -161,7 +161,7 @@ def find_closest(self, timestamp: float, tolerance: float | None = None) -> T | if tolerance is not None and closest_distance > tolerance: return None - return self._items[closest_idx] + return self._items[closest_idx] # type: ignore[no-any-return] def find_before(self, timestamp: float) -> T | None: """Find the last item before the given timestamp.""" @@ -183,7 +183,7 @@ def duration(self) -> float: """Get the duration of the collection in seconds.""" if len(self._items) < 2: return 0.0 - return self._items[-1].ts - self._items[0].ts + return self._items[-1].ts - self._items[0].ts # type: ignore[no-any-return] def time_range(self) -> tuple[float, float] | None: """Get the time range (start, end) of the collection.""" @@ -210,11 +210,11 @@ def end_ts(self) -> float | None: def __len__(self) -> int: return len(self._items) - def __iter__(self) -> Iterator: + def __iter__(self) -> Iterator: # type: ignore[type-arg] return iter(self._items) def __getitem__(self, idx: int) -> T: - return self._items[idx] + return self._items[idx] # type: ignore[no-any-return] PRIMARY = TypeVar("PRIMARY", bound=Timestamped) @@ -287,7 +287,7 @@ def is_complete(self) -> bool: def get_tuple(self) -> tuple[PRIMARY, ...]: """Get the result tuple for emission.""" - return (self.primary, *self.matches) + return (self.primary, *self.matches) # type: ignore[arg-type] def align_timestamped( @@ -311,14 +311,14 @@ def align_timestamped( secondary observable, or None if no match within tolerance. """ - def subscribe(observer, scheduler=None): + def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] # Create a timed buffer collection for each secondary observable secondary_collections: list[TimestampedBufferCollection[SECONDARY]] = [ TimestampedBufferCollection(buffer_size) for _ in secondary_observables ] # WeakLists to track subscribers to each secondary observable - secondary_stakeholders = defaultdict(WeakList) + secondary_stakeholders = defaultdict(WeakList) # type: ignore[var-annotated] # Buffer for unmatched MatchContainers - automatically expires old items primary_buffer: TimestampedBufferCollection[MatchContainer[PRIMARY, SECONDARY]] = ( @@ -332,7 +332,7 @@ def has_secondary_progressed_past(secondary_ts: float, primary_ts: float) -> boo """Check if secondary stream has progressed past the primary + tolerance.""" return secondary_ts > primary_ts + match_tolerance - def remove_stakeholder(stakeholder: MatchContainer) -> None: + def remove_stakeholder(stakeholder: MatchContainer) -> None: # type: ignore[type-arg] """Remove a stakeholder from all tracking structures.""" primary_buffer.remove(stakeholder) for weak_list in secondary_stakeholders.values(): @@ -365,7 +365,8 @@ def on_secondary(i: int, secondary_item: SECONDARY) -> None: for i, secondary_obs in enumerate(secondary_observables): secondary_subs.append( secondary_obs.subscribe( - lambda x, idx=i: on_secondary(idx, x), on_error=observer.on_error + lambda x, idx=i: on_secondary(idx, x), # type: ignore[misc] + on_error=observer.on_error, ) ) @@ -376,7 +377,7 @@ def on_primary(primary_item: PRIMARY) -> None: for i, collection in enumerate(secondary_collections): closest = collection.find_closest(primary_item.ts, tolerance=match_tolerance) if closest is not None: - matches[i] = closest + matches[i] = closest # type: ignore[call-overload] else: # Check if this secondary stream has already progressed past this primary if collection.end_ts is not None and has_secondary_progressed_past( @@ -392,8 +393,8 @@ def on_primary(primary_item: PRIMARY) -> None: observer.on_next(result) else: logger.debug(f"Deferred match attempt {primary_item.ts}") - match_container = MatchContainer(primary_item, matches) - primary_buffer.add(match_container) + match_container = MatchContainer(primary_item, matches) # type: ignore[type-var] + primary_buffer.add(match_container) # type: ignore[arg-type] for i, match in enumerate(matches): if match is None: diff --git a/dimos/types/vector.py b/dimos/types/vector.py index 161084fc2c..654dc1f378 100644 --- a/dimos/types/vector.py +++ b/dimos/types/vector.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ T = TypeVar("T", bound="Vector") # Vector-like types that can be converted to/from Vector -VectorLike = Union[Sequence[int | float], Vector3, "Vector", np.ndarray] +VectorLike = Union[Sequence[int | float], Vector3, "Vector", np.ndarray] # type: ignore[type-arg] class Vector: @@ -42,7 +42,7 @@ def __init__(self, *args: VectorLike) -> None: self._data = np.array(args[0], dtype=float) elif len(args) == 1: - self._data = np.array([args[0].x, args[0].y, args[0].z], dtype=float) + self._data = np.array([args[0].x, args[0].y, args[0].z], dtype=float) # type: ignore[union-attr] else: self._data = np.array(args, dtype=float) @@ -77,11 +77,11 @@ def dim(self) -> int: return len(self._data) @property - def data(self) -> np.ndarray: + def data(self) -> np.ndarray: # type: ignore[type-arg] """Get the underlying numpy array.""" return self._data - def __getitem__(self, idx: int): + def __getitem__(self, idx: int): # type: ignore[no-untyped-def] return self._data[idx] def __repr__(self) -> str: @@ -91,7 +91,7 @@ def __str__(self) -> str: if self.dim < 2: return self.__repr__() - def getArrow(): + def getArrow(): # type: ignore[no-untyped-def] repr = ["←", "↖", "↑", "↗", "→", "ā†˜", "↓", "↙"] if self.x == 0 and self.y == 0: @@ -104,13 +104,13 @@ def getArrow(): # Get directional arrow symbol return repr[dir_index] - return f"{getArrow()} Vector {self.__repr__()}" + return f"{getArrow()} Vector {self.__repr__()}" # type: ignore[no-untyped-call] - def serialize(self) -> builtins.tuple: + def serialize(self) -> builtins.tuple: # type: ignore[type-arg] """Serialize the vector to a tuple.""" - return {"type": "vector", "c": self._data.tolist()} + return {"type": "vector", "c": self._data.tolist()} # type: ignore[return-value] - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] """Check if two vectors are equal using numpy's allclose for floating point comparison.""" if not isinstance(other, Vector): return False @@ -226,12 +226,6 @@ def project(self: T, onto: VectorLike) -> T: scalar_projection = np.dot(self._data, onto._data) / onto_length_sq return self.__class__(scalar_projection * onto._data) - # this is here to test ros_observable_topic - # doesn't happen irl afaik that we want a vector from ros message - @classmethod - def from_msg(cls: type[T], msg) -> T: - return cls(*msg) - @classmethod def zeros(cls: type[T], dim: int) -> T: """Create a zero vector of given dimension.""" @@ -266,13 +260,13 @@ def unit_z(cls: type[T], dim: int = 3) -> T: def to_list(self) -> list[float]: """Convert the vector to a list.""" - return self._data.tolist() + return self._data.tolist() # type: ignore[no-any-return] def to_tuple(self) -> builtins.tuple[float, ...]: """Convert the vector to a tuple.""" return tuple(self._data) - def to_numpy(self) -> np.ndarray: + def to_numpy(self) -> np.ndarray: # type: ignore[type-arg] """Convert the vector to a numpy array.""" return self._data @@ -296,7 +290,7 @@ def __bool__(self) -> bool: return not self.is_zero() -def to_numpy(value: VectorLike) -> np.ndarray: +def to_numpy(value: VectorLike) -> np.ndarray: # type: ignore[type-arg] """Convert a vector-compatible value to a numpy array. Args: @@ -361,13 +355,13 @@ def to_list(value: VectorLike) -> list[float]: List of floats """ if isinstance(value, Vector): - return value.data.tolist() + return value.data.tolist() # type: ignore[no-any-return] elif isinstance(value, np.ndarray): - return value.tolist() + return value.tolist() # type: ignore[no-any-return] elif isinstance(value, list): return value else: - return list(value) + return list(value) # type: ignore[arg-type] # Helper functions to check dimensionality @@ -383,7 +377,7 @@ def is_2d(value: VectorLike) -> bool: if isinstance(value, Vector3): return False elif isinstance(value, Vector): - return len(value) == 2 + return len(value) == 2 # type: ignore[arg-type] elif isinstance(value, np.ndarray): return value.shape[-1] == 2 or value.size == 2 else: @@ -400,7 +394,7 @@ def is_3d(value: VectorLike) -> bool: True if the value is 3D """ if isinstance(value, Vector): - return len(value) == 3 + return len(value) == 3 # type: ignore[arg-type] elif isinstance(value, Vector3): return True elif isinstance(value, np.ndarray): @@ -422,7 +416,7 @@ def x(value: VectorLike) -> float: if isinstance(value, Vector): return value.x elif isinstance(value, Vector3): - return value.x + return value.x # type: ignore[no-any-return] else: return float(to_numpy(value)[0]) @@ -439,7 +433,7 @@ def y(value: VectorLike) -> float: if isinstance(value, Vector): return value.y elif isinstance(value, Vector3): - return value.y + return value.y # type: ignore[no-any-return] else: arr = to_numpy(value) return float(arr[1]) if len(arr) > 1 else 0.0 @@ -457,7 +451,7 @@ def z(value: VectorLike) -> float: if isinstance(value, Vector): return value.z elif isinstance(value, Vector3): - return value.z + return value.z # type: ignore[no-any-return] else: arr = to_numpy(value) return float(arr[2]) if len(arr) > 2 else 0.0 diff --git a/dimos/types/weaklist.py b/dimos/types/weaklist.py index e09b36157c..a720d54e2d 100644 --- a/dimos/types/weaklist.py +++ b/dimos/types/weaklist.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,12 +27,12 @@ class WeakList: """ def __init__(self) -> None: - self._refs = [] + self._refs = [] # type: ignore[var-annotated] def append(self, obj: Any) -> None: """Add an object to the list (stored as weak reference).""" - def _cleanup(ref) -> None: + def _cleanup(ref) -> None: # type: ignore[no-untyped-def] try: self._refs.remove(ref) except ValueError: diff --git a/dimos/utils/actor_registry.py b/dimos/utils/actor_registry.py index 9cd589bed2..6f6d219594 100644 --- a/dimos/utils/actor_registry.py +++ b/dimos/utils/actor_registry.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -67,15 +67,15 @@ def clear() -> None: pass @staticmethod - def _read_from_shm(shm) -> dict[str, str]: + def _read_from_shm(shm) -> dict[str, str]: # type: ignore[no-untyped-def] """Read JSON data from shared memory.""" raw = bytes(shm.buf[:]).rstrip(b"\x00") if not raw: return {} - return json.loads(raw.decode("utf-8")) + return json.loads(raw.decode("utf-8")) # type: ignore[no-any-return] @staticmethod - def _write_to_shm(shm, data: dict[str, str]): + def _write_to_shm(shm, data: dict[str, str]): # type: ignore[no-untyped-def] """Write JSON data to shared memory.""" json_bytes = json.dumps(data).encode("utf-8") if len(json_bytes) > ActorRegistry.SHM_SIZE: diff --git a/dimos/utils/cli/agentspy/agentspy.py b/dimos/utils/cli/agentspy/agentspy.py index 84f68c10af..52760cb2da 100644 --- a/dimos/utils/cli/agentspy/agentspy.py +++ b/dimos/utils/cli/agentspy/agentspy.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -58,7 +58,7 @@ def __init__(self, topic: str = "/agent", max_messages: int = 1000) -> None: self.messages: deque[MessageEntry] = deque(maxlen=max_messages) self.transport = PickleLCM() self.transport.start() - self.callbacks: list[callable] = [] + self.callbacks: list[callable] = [] # type: ignore[valid-type] pass def start(self) -> None: @@ -79,11 +79,11 @@ def _handle_message(self, msg: Any, topic: str) -> None: # Notify callbacks for callback in self.callbacks: - callback(entry) + callback(entry) # type: ignore[misc] else: pass - def subscribe(self, callback: callable) -> None: + def subscribe(self, callback: callable) -> None: # type: ignore[valid-type] """Subscribe to new messages.""" self.callbacks.append(callback) @@ -130,12 +130,12 @@ def format_message_content(msg: AnyMessage) -> str: return f"{content}\n[Tool Calls: {', '.join(tool_info)}]" elif tool_info: return f"[Tool Calls: {', '.join(tool_info)}]" - return content + return content # type: ignore[return-value] else: return str(msg.content) if hasattr(msg, "content") else str(msg) -class AgentSpyApp(App): +class AgentSpyApp(App): # type: ignore[type-arg] """TUI application for monitoring agent messages.""" CSS_PATH = theme.CSS_PATH @@ -165,7 +165,7 @@ class AgentSpyApp(App): Binding("ctrl+c", "quit", show=False), ] - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) self.monitor = AgentMessageMonitor() self.message_log: RichLog | None = None @@ -225,7 +225,7 @@ def main() -> None: if len(sys.argv) > 1 and sys.argv[1] == "web": import os - from textual_serve.server import Server + from textual_serve.server import Server # type: ignore[import-not-found] server = Server(f"python {os.path.abspath(__file__)}") server.serve() diff --git a/dimos/utils/cli/agentspy/demo_agentspy.py b/dimos/utils/cli/agentspy/demo_agentspy.py index 100f22522d..c747ab65f6 100755 --- a/dimos/utils/cli/agentspy/demo_agentspy.py +++ b/dimos/utils/cli/agentspy/demo_agentspy.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ ToolMessage, ) -from dimos.protocol.pubsub import lcm +from dimos.protocol.pubsub import lcm # type: ignore[attr-defined] from dimos.protocol.pubsub.lcmpubsub import PickleLCM diff --git a/dimos/utils/cli/boxglove/boxglove.py b/dimos/utils/cli/boxglove/boxglove.py index 1e0e09a277..3ace1c1aaa 100644 --- a/dimos/utils/cli/boxglove/boxglove.py +++ b/dimos/utils/cli/boxglove/boxglove.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ full = alphabet[0] # Full block -class OccupancyGridApp(App): +class OccupancyGridApp(App): # type: ignore[type-arg] """A Textual app for visualizing OccupancyGrid data in real-time.""" CSS = """ @@ -90,7 +90,7 @@ class OccupancyGridApp(App): ("ctrl+c", "quit", "Quit"), ] - def __init__(self, connection: Connection, *args, **kwargs) -> None: + def __init__(self, connection: Connection, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) self.connection = connection self.subscription: Disposable | None = None @@ -117,7 +117,7 @@ def on_grid(grid: OccupancyGrid) -> None: def on_error(error: Exception) -> None: self.notify(f"Error: {error}", severity="error") - self.subscription = self.connection().subscribe(on_next=on_grid, on_error=on_error) + self.subscription = self.connection().subscribe(on_next=on_grid, on_error=on_error) # type: ignore[assignment] async def on_unmount(self) -> None: """Clean up subscription when app closes.""" @@ -134,14 +134,14 @@ def watch_grid_data(self, grid: OccupancyGrid | None) -> None: # Render the grid as ASCII art grid_text = self.render_grid(grid) - self.grid_display.update(grid_text) + self.grid_display.update(grid_text) # type: ignore[union-attr] - def on_resize(self, event) -> None: + def on_resize(self, event) -> None: # type: ignore[no-untyped-def] """Handle terminal resize events.""" if self.cached_grid: # Re-render with new terminal dimensions grid_text = self.render_grid(self.cached_grid) - self.grid_display.update(grid_text) + self.grid_display.update(grid_text) # type: ignore[union-attr] def render_grid(self, grid: OccupancyGrid) -> Text: """Render the OccupancyGrid as colored ASCII art, scaled to fit terminal.""" @@ -181,7 +181,7 @@ def render_grid(self, grid: OccupancyGrid) -> Text: actual_scale_y = grid.height / render_height if render_height > 0 else 1 # Function to get value with fractional scaling - def get_cell_value(grid_data: np.ndarray, x: int, y: int) -> int: + def get_cell_value(grid_data: np.ndarray, x: int, y: int) -> int: # type: ignore[type-arg] # Use fractional coordinates for smoother scaling y_center = int((y + 0.5) * actual_scale_y) x_center = int((x + 0.5) * actual_scale_x) @@ -192,17 +192,17 @@ def get_cell_value(grid_data: np.ndarray, x: int, y: int) -> int: # For now, just sample the center point # Could do area averaging for smoother results - return grid_data[y_center, x_center] + return grid_data[y_center, x_center] # type: ignore[no-any-return] # Helper function to check if a cell is an obstacle - def is_obstacle(grid_data: np.ndarray, x: int, y: int) -> bool: + def is_obstacle(grid_data: np.ndarray, x: int, y: int) -> bool: # type: ignore[type-arg] if x < 0 or x >= render_width or y < 0 or y >= render_height: return False value = get_cell_value(grid_data, x, y) return value > 90 # Consider cells with >90% probability as obstacles # Character and color mapping with intelligent obstacle rendering - def get_cell_char_and_style(grid_data: np.ndarray, x: int, y: int) -> tuple[str, str]: + def get_cell_char_and_style(grid_data: np.ndarray, x: int, y: int) -> tuple[str, str]: # type: ignore[type-arg] value = get_cell_value(grid_data, x, y) norm_value = min(value, 100) / 100.0 @@ -255,9 +255,9 @@ def get_cell_char_and_style(grid_data: np.ndarray, x: int, y: int) -> tuple[str, # No neighbors - isolated obstacle symbol = full + full - return symbol, None + return symbol, None # type: ignore[return-value] else: - return " ", None + return " ", None # type: ignore[return-value] # Render the scaled grid row by row (flip Y axis for proper display) for y in range(render_height - 1, -1, -1): @@ -277,9 +277,9 @@ def main() -> None: # app = OccupancyGridApp(core.LCMTransport("/global_costmap", OccupancyGrid).observable) app = OccupancyGridApp( - lambda: core.LCMTransport("/lidar", LidarMessage) + lambda: core.LCMTransport("/lidar", LidarMessage) # type: ignore[no-untyped-call] .observable() - .pipe(ops.map(lambda msg: msg.costmap())) + .pipe(ops.map(lambda msg: msg.costmap())) # type: ignore[attr-defined] ) app.run() import time diff --git a/dimos/utils/cli/boxglove/connection.py b/dimos/utils/cli/boxglove/connection.py index 5d3b3f8806..1743684626 100644 --- a/dimos/utils/cli/boxglove/connection.py +++ b/dimos/utils/cli/boxglove/connection.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ from dimos.msgs.nav_msgs import OccupancyGrid from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.protocol.pubsub import lcm +from dimos.protocol.pubsub import lcm # type: ignore[attr-defined] from dimos.robot.unitree_webrtc.type.lidar import LidarMessage from dimos.robot.unitree_webrtc.type.map import Map from dimos.utils.data import get_data @@ -33,11 +33,11 @@ def live_connection() -> Observable[OccupancyGrid]: - def subscribe(observer, scheduler=None): + def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] lcm.autoconf() l = lcm.LCM() - def on_message(grid: OccupancyGrid, _) -> None: + def on_message(grid: OccupancyGrid, _) -> None: # type: ignore[no-untyped-def] observer.on_next(grid) l.subscribe(lcm.Topic("/global_costmap", OccupancyGrid), on_message) @@ -57,7 +57,7 @@ def recorded_connection() -> Observable[OccupancyGrid]: return backpressure( lidar_store.stream(speed=1).pipe( ops.map(mapper.add_frame), - ops.map(lambda _: mapper.costmap().inflate(0.1).gradient()), + ops.map(lambda _: mapper.costmap().inflate(0.1).gradient()), # type: ignore[attr-defined] ) ) @@ -68,4 +68,4 @@ def single_message() -> Observable[OccupancyGrid]: pointcloud = PointCloud2.lcm_decode(pickle.load(f)) mapper = Map() mapper.add_frame(pointcloud) - return rx.just(mapper.costmap()) + return rx.just(mapper.costmap()) # type: ignore[attr-defined] diff --git a/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py b/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py index 8244d16d39..949a500f21 100644 --- a/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py +++ b/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ def bridge_thread() -> None: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: - bridge_instance = FoxgloveBridge(host="0.0.0.0", port=8765, debug=True, num_threads=4) + bridge_instance = FoxgloveBridge(host="0.0.0.0", port=8765, debug=False, num_threads=4) loop.run_until_complete(bridge_instance.run()) except Exception as e: diff --git a/dimos/utils/cli/human/humancli.py b/dimos/utils/cli/human/humancli.py index 4c474b88d2..a0ce0afff4 100644 --- a/dimos/utils/cli/human/humancli.py +++ b/dimos/utils/cli/human/humancli.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ ) -class HumanCLIApp(App): +class HumanCLIApp(App): # type: ignore[type-arg] """IRC-like interface for interacting with DimOS agents.""" CSS_PATH = theme.CSS_PATH @@ -77,10 +77,10 @@ class HumanCLIApp(App): Binding("ctrl+l", "clear", "Clear chat"), ] - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) - self.human_transport = pLCMTransport("/human_input") - self.agent_transport = pLCMTransport("/agent") + self.human_transport = pLCMTransport("/human_input") # type: ignore[var-annotated] + self.agent_transport = pLCMTransport("/agent") # type: ignore[var-annotated] self.chat_log: RichLog | None = None self.input_widget: Input | None = None self._subscription_thread: threading.Thread | None = None @@ -103,25 +103,16 @@ def on_mount(self) -> None: self.console.push_theme(JSON_THEME) # Set custom highlighter for RichLog - self.chat_log.highlighter = JSONHighlighter() + self.chat_log.highlighter = JSONHighlighter() # type: ignore[union-attr] # Start subscription thread self._subscription_thread = threading.Thread(target=self._subscribe_to_agent, daemon=True) self._subscription_thread.start() # Focus on input - self.input_widget.focus() - - # Display ASCII art banner - ascii_art = """ - ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— - ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ - ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ - ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ - ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— - ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā• -""" - self.chat_log.write(f"[{theme.ACCENT}]{ascii_art}[/{theme.ACCENT}]") + self.input_widget.focus() # type: ignore[union-attr] + + self.chat_log.write(f"[{theme.ACCENT}]{theme.ascii_logo}[/{theme.ACCENT}]") # type: ignore[union-attr] # Welcome message self._add_system_message("Connected to DimOS Agent Interface") @@ -133,7 +124,7 @@ def on_unmount(self) -> None: def _subscribe_to_agent(self) -> None: """Subscribe to agent messages in a separate thread.""" - def receive_msg(msg) -> None: + def receive_msg(msg) -> None: # type: ignore[no-untyped-def] if not self._running: return @@ -184,8 +175,8 @@ def receive_msg(msg) -> None: def _format_tool_call(self, tool_call: ToolCall) -> str: """Format a tool call for display.""" f = tool_call.get("function", {}) - name = f.get("name", "unknown") - return f"ā–¶ {name}({f.get('arguments', '')})" + name = f.get("name", "unknown") # type: ignore[attr-defined] + return f"ā–¶ {name}({f.get('arguments', '')})" # type: ignore[attr-defined] def _add_message(self, timestamp: str, sender: str, content: str, color: str) -> None: """Add a message to the chat log.""" @@ -209,7 +200,7 @@ def _add_message(self, timestamp: str, sender: str, content: str, color: str) -> indent = " " * 19 # Spaces to align with the content after the separator # Get the width of the chat area (accounting for borders and padding) - width = self.chat_log.size.width - 4 if self.chat_log.size else 76 + width = self.chat_log.size.width - 4 if self.chat_log.size else 76 # type: ignore[union-attr] # Calculate the available width for text (subtract prefix length) text_width = max(width - 20, 40) # Minimum 40 chars for text @@ -225,12 +216,12 @@ def _add_message(self, timestamp: str, sender: str, content: str, color: str) -> line, width=text_width, initial_indent="", subsequent_indent="" ) if wrapped: - self.chat_log.write(prefix + f"[{color}]{wrapped[0]}[/{color}]") + self.chat_log.write(prefix + f"[{color}]{wrapped[0]}[/{color}]") # type: ignore[union-attr] for wrapped_line in wrapped[1:]: - self.chat_log.write(indent + f"│ [{color}]{wrapped_line}[/{color}]") + self.chat_log.write(indent + f"│ [{color}]{wrapped_line}[/{color}]") # type: ignore[union-attr] else: # Empty line - self.chat_log.write(prefix) + self.chat_log.write(prefix) # type: ignore[union-attr] else: # Subsequent lines from explicit newlines wrapped = textwrap.wrap( @@ -238,10 +229,10 @@ def _add_message(self, timestamp: str, sender: str, content: str, color: str) -> ) if wrapped: for wrapped_line in wrapped: - self.chat_log.write(indent + f"│ [{color}]{wrapped_line}[/{color}]") + self.chat_log.write(indent + f"│ [{color}]{wrapped_line}[/{color}]") # type: ignore[union-attr] else: # Empty line - self.chat_log.write(indent + "│") + self.chat_log.write(indent + "│") # type: ignore[union-attr] def _add_system_message(self, content: str) -> None: """Add a system message to the chat.""" @@ -261,7 +252,7 @@ def on_input_submitted(self, event: Input.Submitted) -> None: return # Clear input - self.input_widget.value = "" + self.input_widget.value = "" # type: ignore[union-attr] # Check for commands if message.lower() in ["/exit", "/quit"]: @@ -286,9 +277,9 @@ def on_input_submitted(self, event: Input.Submitted) -> None: def action_clear(self) -> None: """Clear the chat log.""" - self.chat_log.clear() + self.chat_log.clear() # type: ignore[union-attr] - def action_quit(self) -> None: + def action_quit(self) -> None: # type: ignore[override] """Quit the application.""" self._running = False self.exit() @@ -302,7 +293,7 @@ def main() -> None: # Support for textual-serve web mode import os - from textual_serve.server import Server + from textual_serve.server import Server # type: ignore[import-not-found] server = Server(f"python {os.path.abspath(__file__)}") server.serve() diff --git a/dimos/utils/cli/human/humanclianim.py b/dimos/utils/cli/human/humanclianim.py new file mode 100644 index 0000000000..cdd3bf3b00 --- /dev/null +++ b/dimos/utils/cli/human/humanclianim.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import random +import sys +import threading +import time + +from terminaltexteffects import Color # type: ignore[attr-defined, import-not-found] + +from dimos.utils.cli import theme + +# Global to store the imported main function +_humancli_main = None +_import_complete = threading.Event() + +print(theme.ACCENT) + + +def import_cli_in_background() -> None: + """Import the heavy CLI modules in the background""" + global _humancli_main + try: + from dimos.utils.cli.human.humancli import main as humancli_main + + _humancli_main = humancli_main + except Exception as e: + print(f"Failed to import CLI: {e}") + finally: + _import_complete.set() + + +def get_effect_config(effect_name: str): # type: ignore[no-untyped-def] + """Get hardcoded configuration for a specific effect""" + # Hardcoded configs for each effect + global_config = { + "final_gradient_stops": [Color(theme.ACCENT)], + } + + configs = { + "randomsequence": { + "speed": 0.075, + }, + "slide": {"direction": "left", "movement_speed": 1.5}, + "sweep": {"direction": "left"}, + "print": { + "print_speed": 10, + "print_head_return_speed": 10, + "final_gradient_stops": [Color(theme.ACCENT)], + }, + "pour": {"pour_speed": 9}, + "matrix": {"rain_symbols": "01", "rain_fall_speed_range": (4, 7)}, + "decrypt": {"typing_speed": 5, "decryption_speed": 3}, + "burn": {"fire_chars": "ā–ˆ", "flame_color": "ffffff"}, + "expand": {"expand_direction": "center"}, + "scattered": {"movement_speed": 0.5}, + "beams": {"movement_speed": 0.5, "beam_delay": 0}, + "middleout": {"center_movement_speed": 3, "full_movement_speed": 0.5}, + "rain": { + "rain_symbols": "ā–‘ā–’ā–“ā–ˆ", + "rain_fall_speed_range": (5, 10), + }, + "highlight": {"highlight_brightness": 3}, + } + + return {**configs.get(effect_name, {}), **global_config} # type: ignore[dict-item] + + +def run_banner_animation() -> None: + """Run the ASCII banner animation before launching Textual""" + + # Check if we should animate + random_anim = ["scattered", "print", "expand", "slide", "rain"] + animation_style = os.environ.get("DIMOS_BANNER_ANIMATION", random.choice(random_anim)).lower() + + if animation_style == "none": + return # Skip animation + from terminaltexteffects.effects.effect_beams import Beams # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_burn import Burn # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_decrypt import Decrypt # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_expand import Expand # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_highlight import ( # type: ignore[import-not-found] + Highlight, + ) + from terminaltexteffects.effects.effect_matrix import Matrix # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_middleout import ( # type: ignore[import-not-found] + MiddleOut, + ) + from terminaltexteffects.effects.effect_overflow import ( # type: ignore[import-not-found] + Overflow, + ) + from terminaltexteffects.effects.effect_pour import Pour # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_print import Print # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_rain import Rain # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_random_sequence import ( # type: ignore[import-not-found] + RandomSequence, + ) + from terminaltexteffects.effects.effect_scattered import ( # type: ignore[import-not-found] + Scattered, + ) + from terminaltexteffects.effects.effect_slide import Slide # type: ignore[import-not-found] + from terminaltexteffects.effects.effect_sweep import Sweep # type: ignore[import-not-found] + + # The DIMENSIONAL ASCII art + ascii_art = "\n" + theme.ascii_logo.replace("\n", "\n ") + # Choose effect based on style + effect_map = { + "slide": Slide, + "sweep": Sweep, + "print": Print, + "pour": Pour, + "burn": Burn, + "matrix": Matrix, + "rain": Rain, + "scattered": Scattered, + "expand": Expand, + "decrypt": Decrypt, + "overflow": Overflow, + "randomsequence": RandomSequence, + "beams": Beams, + "middleout": MiddleOut, + "highlight": Highlight, + } + + EffectClass = effect_map.get(animation_style, Slide) + + # Clear screen before starting animation + print("\033[2J\033[H", end="", flush=True) + + # Get effect configuration + effect_config = get_effect_config(animation_style) + + # Create and run the effect with config + effect = EffectClass(ascii_art) + for key, value in effect_config.items(): + setattr(effect.effect_config, key, value) # type: ignore[attr-defined] + + # Run the animation - terminal.print() handles all screen management + with effect.terminal_output() as terminal: # type: ignore[attr-defined] + for frame in effect: # type: ignore[attr-defined] + terminal.print(frame) + + # Brief pause to see the final frame + time.sleep(0.5) + + # Clear screen for Textual to take over + print("\033[2J\033[H", end="") + + +def main() -> None: + """Main entry point - run animation then launch the real CLI""" + + # Start importing CLI in background (this is slow) + import_thread = threading.Thread(target=import_cli_in_background, daemon=True) + import_thread.start() + + # Run the animation while imports happen (if not in web mode) + if not (len(sys.argv) > 1 and sys.argv[1] == "web"): + run_banner_animation() + + # Wait for import to complete + _import_complete.wait(timeout=10) # Max 10 seconds wait + + # Launch the real CLI + if _humancli_main: + _humancli_main() + else: + # Fallback if threaded import failed + from dimos.utils.cli.human.humancli import main as humancli_main + + humancli_main() + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/cli/lcmspy/lcmspy.py b/dimos/utils/cli/lcmspy/lcmspy.py index 42f811ffbc..5493e53024 100755 --- a/dimos/utils/cli/lcmspy/lcmspy.py +++ b/dimos/utils/cli/lcmspy/lcmspy.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ import threading import time -import lcm - from dimos.protocol.service.lcmservice import LCMConfig, LCMService @@ -48,7 +46,7 @@ class Topic: def __init__(self, name: str, history_window: float = 60.0) -> None: self.name = name # Store (timestamp, data_size) tuples for statistics - self.message_history = deque() + self.message_history = deque() # type: ignore[var-annotated] self.history_window = history_window # Total traffic accumulator (doesn't get cleaned up) self.total_traffic_bytes = 0 @@ -68,7 +66,7 @@ def _cleanup_old_messages(self, max_age: float | None = None) -> None: ): self.message_history.popleft() - def _get_messages_in_window(self, time_window: float): + def _get_messages_in_window(self, time_window: float): # type: ignore[no-untyped-def] """Get messages within the specified time window""" current_time = time.time() cutoff_time = current_time - time_window @@ -88,7 +86,7 @@ def kbps(self, time_window: float) -> float: return 0.0 total_bytes = sum(size for _, size in messages) total_kbytes = total_bytes / 1000 # Convert bytes to kB - return total_kbytes / time_window + return total_kbytes / time_window # type: ignore[no-any-return] def kbps_hr(self, time_window: float, round_to: int = 2) -> tuple[float, BandwidthUnit]: """Return human-readable bandwidth with appropriate units""" @@ -103,7 +101,7 @@ def size(self, time_window: float) -> float: if not messages: return 0.0 total_size = sum(size for _, size in messages) - return total_size / len(messages) + return total_size / len(messages) # type: ignore[no-any-return] def total_traffic(self) -> int: """Return total traffic passed in bytes since the beginning""" @@ -129,36 +127,36 @@ class LCMSpy(LCMService, Topic): graph_log_window: float = 1.0 topic_class: type[Topic] = Topic - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) - Topic.__init__(self, name="total", history_window=self.config.topic_history_window) - self.topic = {} - self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() + Topic.__init__(self, name="total", history_window=self.config.topic_history_window) # type: ignore[attr-defined] + self.topic = {} # type: ignore[assignment] def start(self) -> None: super().start() - self.l.subscribe(".*", self.msg) + self.l.subscribe(".*", self.msg) # type: ignore[union-attr] def stop(self) -> None: """Stop the LCM spy and clean up resources""" super().stop() - def msg(self, topic, data) -> None: + def msg(self, topic, data) -> None: # type: ignore[no-untyped-def, override] Topic.msg(self, data) - if topic not in self.topic: + if topic not in self.topic: # type: ignore[operator] print(self.config) - self.topic[topic] = self.topic_class( - topic, history_window=self.config.topic_history_window + self.topic[topic] = self.topic_class( # type: ignore[assignment, call-arg] + topic, + history_window=self.config.topic_history_window, # type: ignore[attr-defined] ) - self.topic[topic].msg(data) + self.topic[topic].msg(data) # type: ignore[attr-defined, type-arg] class GraphTopic(Topic): - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) - self.freq_history = deque(maxlen=20) - self.bandwidth_history = deque(maxlen=20) + self.freq_history = deque(maxlen=20) # type: ignore[var-annotated] + self.bandwidth_history = deque(maxlen=20) # type: ignore[var-annotated] def update_graphs(self, step_window: float = 1.0) -> None: """Update historical data for graphing""" @@ -180,9 +178,9 @@ class GraphLCMSpy(LCMSpy, GraphTopic): graph_log_stop_event: threading.Event = threading.Event() topic_class: type[Topic] = GraphTopic - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(**kwargs) - GraphTopic.__init__(self, name="total", history_window=self.config.topic_history_window) + GraphTopic.__init__(self, name="total", history_window=self.config.topic_history_window) # type: ignore[attr-defined] def start(self) -> None: super().start() @@ -191,10 +189,11 @@ def start(self) -> None: def graph_log(self) -> None: while not self.graph_log_stop_event.is_set(): - self.update_graphs(self.config.graph_log_window) # Update global history - for topic in self.topic.values(): - topic.update_graphs(self.config.graph_log_window) - time.sleep(self.config.graph_log_window) + self.update_graphs(self.config.graph_log_window) # type: ignore[attr-defined] # Update global history + # Copy to list to avoid RuntimeError: dictionary changed size during iteration + for topic in list(self.topic.values()): # type: ignore[call-arg] + topic.update_graphs(self.config.graph_log_window) # type: ignore[attr-defined] + time.sleep(self.config.graph_log_window) # type: ignore[attr-defined] def stop(self) -> None: """Stop the graph logging and LCM spy""" diff --git a/dimos/utils/cli/lcmspy/run_lcmspy.py b/dimos/utils/cli/lcmspy/run_lcmspy.py index 2e96156852..f3d31b48ba 100644 --- a/dimos/utils/cli/lcmspy/run_lcmspy.py +++ b/dimos/utils/cli/lcmspy/run_lcmspy.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ def topic_text(topic_name: str) -> Text: return Text(topic_name, style=theme.BRIGHT_WHITE) -class LCMSpyApp(App): +class LCMSpyApp(App): # type: ignore[type-arg] """A real-time CLI dashboard for LCM traffic statistics using Textual.""" CSS_PATH = "../dimos.tcss" @@ -78,13 +78,13 @@ class LCMSpyApp(App): ("ctrl+c", "quit"), ] - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) self.spy = GraphLCMSpy(autoconf=True, graph_log_window=0.5) - self.table: DataTable | None = None + self.table: DataTable | None = None # type: ignore[type-arg] def compose(self) -> ComposeResult: - self.table = DataTable(zebra_stripes=False, cursor_type=None) + self.table = DataTable(zebra_stripes=False, cursor_type=None) # type: ignore[arg-type] self.table.add_column("Topic") self.table.add_column("Freq (Hz)") self.table.add_column("Bandwidth") @@ -99,9 +99,9 @@ async def on_unmount(self) -> None: self.spy.stop() def refresh_table(self) -> None: - topics: list[SpyTopic] = list(self.spy.topic.values()) + topics: list[SpyTopic] = list(self.spy.topic.values()) # type: ignore[arg-type, call-arg] topics.sort(key=lambda t: t.total_traffic(), reverse=True) - self.table.clear(columns=False) + self.table.clear(columns=False) # type: ignore[union-attr] for t in topics: freq = t.freq(5.0) @@ -109,7 +109,7 @@ def refresh_table(self) -> None: bw_val, bw_unit = t.kbps_hr(5.0) total_val, total_unit = t.total_traffic_hr() - self.table.add_row( + self.table.add_row( # type: ignore[union-attr] topic_text(t.name), Text(f"{freq:.1f}", style=gradient(10, freq)), Text(f"{bw_val} {bw_unit.value}/s", style=gradient(1024 * 3, kbps)), @@ -123,7 +123,7 @@ def main() -> None: if len(sys.argv) > 1 and sys.argv[1] == "web": import os - from textual_serve.server import Server + from textual_serve.server import Server # type: ignore[import-not-found] server = Server(f"python {os.path.abspath(__file__)}") server.serve() diff --git a/dimos/utils/cli/lcmspy/test_lcmspy.py b/dimos/utils/cli/lcmspy/test_lcmspy.py index 56e8e72c3b..3016a723fe 100644 --- a/dimos/utils/cli/lcmspy/test_lcmspy.py +++ b/dimos/utils/cli/lcmspy/test_lcmspy.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/utils/cli/plot.py b/dimos/utils/cli/plot.py new file mode 100644 index 0000000000..336aeca6d8 --- /dev/null +++ b/dimos/utils/cli/plot.py @@ -0,0 +1,281 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Terminal plotting utilities using plotext.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +import plotext as plt + +if TYPE_CHECKING: + from collections.abc import Sequence + + +def _default_size() -> tuple[int, int]: + """Return default plot size (terminal width, half terminal height).""" + tw, th = plt.terminal_size() + return tw, th // 2 + + +@dataclass +class Series: + """A data series for plotting.""" + + y: Sequence[float] + x: Sequence[float] | None = None + label: str | None = None + color: tuple[int, int, int] | None = None + marker: str = "braille" # braille, dot, hd, fhd, sd + + +@dataclass +class Plot: + """Terminal plot.""" + + title: str | None = None + xlabel: str | None = None + ylabel: str | None = None + width: int | None = None + height: int | None = None + series: list[Series] = field(default_factory=list) + + def add( + self, + y: Sequence[float], + x: Sequence[float] | None = None, + label: str | None = None, + color: tuple[int, int, int] | None = None, + marker: str = "braille", + ) -> Plot: + """Add a data series to the plot. + + Args: + y: Y values + x: X values (optional, defaults to 0, 1, 2, ...) + label: Series label for legend + color: RGB tuple (optional, auto-assigned from theme) + marker: Marker style (braille, dot, hd, fhd, sd) + + Returns: + Self for chaining + """ + self.series.append(Series(y=y, x=x, label=label, color=color, marker=marker)) + return self + + def build(self) -> str: + """Build the plot and return as string.""" + plt.clf() + plt.theme("dark") + + # Set size (default to terminal width, half terminal height) + dw, dh = _default_size() + plt.plotsize(self.width or dw, self.height or dh) + + # Plot each series + for _i, s in enumerate(self.series): + x = list(s.x) if s.x is not None else list(range(len(s.y))) + y = list(s.y) + if s.color: + plt.plot(x, y, label=s.label, marker=s.marker, color=s.color) + else: + plt.plot(x, y, label=s.label, marker=s.marker) + + # Set labels and title + if self.title: + plt.title(self.title) + if self.xlabel: + plt.xlabel(self.xlabel) + if self.ylabel: + plt.ylabel(self.ylabel) + + result: str = plt.build() + return result + + def show(self) -> None: + """Print the plot to stdout.""" + print(self.build()) + + +def plot( + y: Sequence[float], + x: Sequence[float] | None = None, + title: str | None = None, + xlabel: str | None = None, + ylabel: str | None = None, + label: str | None = None, + width: int | None = None, + height: int | None = None, +) -> None: + """Quick single-series plot. + + Args: + y: Y values + x: X values (optional) + title: Plot title + xlabel: X-axis label + ylabel: Y-axis label + label: Series label + width: Plot width in characters + height: Plot height in characters + """ + p = Plot(title=title, xlabel=xlabel, ylabel=ylabel, width=width, height=height) + p.add(y, x, label=label) + p.show() + + +def bar( + labels: Sequence[str], + values: Sequence[float], + title: str | None = None, + xlabel: str | None = None, + ylabel: str | None = None, + width: int | None = None, + height: int | None = None, + horizontal: bool = False, +) -> None: + """Quick bar chart. + + Args: + labels: Category labels + values: Values for each category + title: Plot title + xlabel: X-axis label + ylabel: Y-axis label + width: Plot width in characters + height: Plot height in characters + horizontal: If True, draw horizontal bars + """ + plt.clf() + plt.theme("dark") + dw, dh = _default_size() + plt.plotsize(width or dw, height or dh) + + if horizontal: + plt.bar(list(labels), list(values), orientation="h") + else: + plt.bar(list(labels), list(values)) + + if title: + plt.title(title) + if xlabel: + plt.xlabel(xlabel) + if ylabel: + plt.ylabel(ylabel) + + print(plt.build()) + + +def scatter( + x: Sequence[float], + y: Sequence[float], + title: str | None = None, + xlabel: str | None = None, + ylabel: str | None = None, + width: int | None = None, + height: int | None = None, +) -> None: + """Quick scatter plot. + + Args: + x: X values + y: Y values + title: Plot title + xlabel: X-axis label + ylabel: Y-axis label + width: Plot width in characters + height: Plot height in characters + """ + plt.clf() + plt.theme("dark") + dw, dh = _default_size() + plt.plotsize(width or dw, height or dh) + + plt.scatter(list(x), list(y), marker="dot") + + if title: + plt.title(title) + if xlabel: + plt.xlabel(xlabel) + if ylabel: + plt.ylabel(ylabel) + + print(plt.build()) + + +def compare_bars( + labels: Sequence[str], + data: dict[str, Sequence[float]], + title: str | None = None, + xlabel: str | None = None, + ylabel: str | None = None, + width: int | None = None, + height: int | None = None, +) -> None: + """Compare multiple series as grouped bars. + + Args: + labels: Category labels (x-axis) + data: Dict mapping series name to values + title: Plot title + xlabel: X-axis label + ylabel: Y-axis label + width: Plot width in characters + height: Plot height in characters + + Example: + compare_bars( + ["moondream-full", "moondream-512", "moondream-256"], + {"query_time": [2.1, 1.5, 0.8], "accuracy": [95, 92, 85]}, + title="Model Performance" + ) + """ + plt.clf() + plt.theme("dark") + dw, dh = _default_size() + plt.plotsize(width or dw, height or dh) + + for name, values in data.items(): + plt.bar(list(labels), list(values), label=name) + + if title: + plt.title(title) + if xlabel: + plt.xlabel(xlabel) + if ylabel: + plt.ylabel(ylabel) + + print(plt.build()) + + +if __name__ == "__main__": + # Demo + print("Line plot:") + plot([1, 4, 9, 16, 25], title="Squares", xlabel="n", ylabel="n²") + + print("\nBar chart:") + bar( + ["moondream-full", "moondream-512", "moondream-256"], + [2.1, 1.5, 0.8], + title="Query Time (s)", + ylabel="seconds", + ) + + print("\nMulti-series plot:") + p = Plot(title="Model Performance", xlabel="resize", ylabel="time (s)") + p.add([2.1, 1.5, 0.8], label="moondream") + p.add([1.8, 1.2, 0.6], label="qwen") + p.show() diff --git a/dimos/utils/cli/skillspy/demo_skillspy.py b/dimos/utils/cli/skillspy/demo_skillspy.py index f7d4875e01..602381020a 100644 --- a/dimos/utils/cli/skillspy/demo_skillspy.py +++ b/dimos/utils/cli/skillspy/demo_skillspy.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/utils/cli/skillspy/skillspy.py b/dimos/utils/cli/skillspy/skillspy.py index 769478b00e..beb2421eec 100644 --- a/dimos/utils/cli/skillspy/skillspy.py +++ b/dimos/utils/cli/skillspy/skillspy.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ if TYPE_CHECKING: from collections.abc import Callable - from dimos.protocol.skill.comms import SkillMsg + from dimos.protocol.skill.comms import SkillMsg # type: ignore[attr-defined] class AgentSpy: @@ -58,7 +58,7 @@ def stop(self) -> None: time.sleep(0.2) self.agent_interface.stop() - def _handle_message(self, msg: SkillMsg) -> None: + def _handle_message(self, msg: SkillMsg) -> None: # type: ignore[type-arg] """Handle incoming skill messages.""" if not self._running: return @@ -111,7 +111,7 @@ def format_duration(duration: float) -> str: return f"{duration / 3600:.1f}h" -class AgentSpyApp(App): +class AgentSpyApp(App): # type: ignore[type-arg] """A real-time CLI dashboard for agent skill monitoring using Textual.""" CSS_PATH = theme.CSS_PATH @@ -140,14 +140,14 @@ class AgentSpyApp(App): Binding("ctrl+c", "quit", "Quit", show=False), ] - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) self.spy = AgentSpy() - self.table: DataTable | None = None + self.table: DataTable | None = None # type: ignore[type-arg] self.skill_history: list[tuple[str, SkillState, float]] = [] # (call_id, state, start_time) def compose(self) -> ComposeResult: - self.table = DataTable(zebra_stripes=False, cursor_type=None) + self.table = DataTable(zebra_stripes=False, cursor_type=None) # type: ignore[arg-type] self.table.add_column("Call ID") self.table.add_column("Skill Name") self.table.add_column("State") @@ -268,7 +268,7 @@ def main() -> None: if len(sys.argv) > 1 and sys.argv[1] == "web": import os - from textual_serve.server import Server + from textual_serve.server import Server # type: ignore[import-not-found] server = Server(f"python {os.path.abspath(__file__)}") server.serve() diff --git a/dimos/utils/cli/theme.py b/dimos/utils/cli/theme.py index e3d98b07de..b6b6b9ccae 100644 --- a/dimos/utils/cli/theme.py +++ b/dimos/utils/cli/theme.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -97,3 +97,12 @@ def get(name: str, default: str = "#ffffff") -> str: ERROR = COLORS.get("error", "#ff0000") WARNING = COLORS.get("warning", "#ffcc00") INFO = COLORS.get("info", "#00eeee") + +ascii_logo = """ + ▇▇▇▇▇▇╗ ▇▇╗▇▇▇╗ ▇▇▇╗▇▇▇▇▇▇▇╗▇▇▇╗ ▇▇╗▇▇▇▇▇▇▇╗▇▇╗ ▇▇▇▇▇▇╗ ▇▇▇╗ ▇▇╗ ▇▇▇▇▇╗ ▇▇╗ + ▇▇╔══▇▇╗▇▇║▇▇▇▇╗ ā–‡ā–‡ā–‡ā–‡ā•‘ā–‡ā–‡ā•”ā•ā•ā•ā•ā•ā–‡ā–‡ā–‡ā–‡ā•— ā–‡ā–‡ā•‘ā–‡ā–‡ā•”ā•ā•ā•ā•ā•ā–‡ā–‡ā•‘ā–‡ā–‡ā•”ā•ā•ā•ā–‡ā–‡ā•—ā–‡ā–‡ā–‡ā–‡ā•— ▇▇║▇▇╔══▇▇╗▇▇║ + ▇▇║ ▇▇║▇▇║▇▇╔▇▇▇▇╔▇▇║▇▇▇▇▇╗ ▇▇╔▇▇╗ ▇▇║▇▇▇▇▇▇▇╗▇▇║▇▇║ ▇▇║▇▇╔▇▇╗ ▇▇║▇▇▇▇▇▇▇║▇▇║ + ▇▇║ ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ā•šā–‡ā–‡ā•”ā•ā–‡ā–‡ā•‘ā–‡ā–‡ā•”ā•ā•ā• ā–‡ā–‡ā•‘ā•šā–‡ā–‡ā•—ā–‡ā–‡ā•‘ā•šā•ā•ā•ā•ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ā•šā–‡ā–‡ā•—ā–‡ā–‡ā•‘ā–‡ā–‡ā•”ā•ā•ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ + ā–‡ā–‡ā–‡ā–‡ā–‡ā–‡ā•”ā•ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ ā•šā•ā• ▇▇║▇▇▇▇▇▇▇╗▇▇║ ā•šā–‡ā–‡ā–‡ā–‡ā•‘ā–‡ā–‡ā–‡ā–‡ā–‡ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ā•šā–‡ā–‡ā–‡ā–‡ā–‡ā–‡ā•”ā•ā–‡ā–‡ā•‘ ā•šā–‡ā–‡ā–‡ā–‡ā•‘ā–‡ā–‡ā•‘ ▇▇║▇▇▇▇▇▇▇╗ + ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā• +""" diff --git a/dimos/utils/data.py b/dimos/utils/data.py index 8b70c2ad27..4ba9c73b0c 100644 --- a/dimos/utils/data.py +++ b/dimos/utils/data.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,20 +13,88 @@ # limitations under the License. from functools import cache +import os from pathlib import Path +import platform import subprocess import tarfile +import tempfile + +from dimos.constants import DIMOS_PROJECT_ROOT +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +def _get_user_data_dir() -> Path: + """Get platform-specific user data directory.""" + system = platform.system() + + if system == "Linux": + # Use XDG_DATA_HOME if set, otherwise default to ~/.local/share + xdg_data_home = os.environ.get("XDG_DATA_HOME") + if xdg_data_home: + return Path(xdg_data_home) / "dimos" + return Path.home() / ".local" / "share" / "dimos" + elif system == "Darwin": # macOS + return Path.home() / "Library" / "Application Support" / "dimos" + else: + # Fallback for other systems + return Path.home() / ".dimos" @cache def _get_repo_root() -> Path: + # Check if running from git repo + if (DIMOS_PROJECT_ROOT / ".git").exists(): + return DIMOS_PROJECT_ROOT + + # Running as installed package - clone repo to data dir try: - result = subprocess.run( - ["git", "rev-parse", "--show-toplevel"], capture_output=True, check=True, text=True - ) - return Path(result.stdout.strip()) - except subprocess.CalledProcessError: - raise RuntimeError("Not in a Git repository") + data_dir = _get_user_data_dir() + data_dir.mkdir(parents=True, exist_ok=True) + # Test if writable + test_file = data_dir / ".write_test" + test_file.touch() + test_file.unlink() + logger.info(f"Using local user data directory at '{data_dir}'") + except (OSError, PermissionError): + # Fall back to temp dir if data dir not writable + data_dir = Path(tempfile.gettempdir()) / "dimos" + data_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"Using tmp data directory at '{data_dir}'") + + repo_dir = data_dir / "repo" + + # Clone if not already cloned + if not (repo_dir / ".git").exists(): + try: + env = os.environ.copy() + env["GIT_LFS_SKIP_SMUDGE"] = "1" + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "--branch", + # TODO: Use "main", + "dev", + "git@github.com:dimensionalOS/dimos.git", + str(repo_dir), + ], + check=True, + capture_output=True, + text=True, + env=env, + ) + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Failed to clone dimos repository: {e.stderr}\n" + f"Make sure you have access to git@github.com:dimensionalOS/dimos.git" + ) + + return repo_dir @cache @@ -42,13 +110,26 @@ def _get_lfs_dir() -> Path: def _check_git_lfs_available() -> bool: + missing = [] + + # Check if git is available try: - subprocess.run(["git", "lfs", "version"], capture_output=True, check=True, text=True) + subprocess.run(["git", "--version"], capture_output=True, check=True, text=True) except (subprocess.CalledProcessError, FileNotFoundError): + missing.append("git") + + # Check if git-lfs is available + try: + subprocess.run(["git-lfs", "version"], capture_output=True, check=True, text=True) + except (subprocess.CalledProcessError, FileNotFoundError): + missing.append("git-lfs") + + if missing: raise RuntimeError( - "Git LFS is not installed. Please install git-lfs to use test data utilities.\n" - "Installation instructions: https://git-lfs.github.io/" + f"Missing required tools: {', '.join(missing)}.\n\n" + "Git LFS installation instructions: https://git-lfs.github.io/" ) + return True @@ -70,11 +151,14 @@ def _lfs_pull(file_path: Path, repo_root: Path) -> None: try: relative_path = file_path.relative_to(repo_root) + env = os.environ.copy() + env["GIT_LFS_FORCE_PROGRESS"] = "1" + subprocess.run( ["git", "lfs", "pull", "--include", str(relative_path)], cwd=repo_root, check=True, - capture_output=True, + env=env, ) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to pull LFS file {file_path}: {e}") diff --git a/dimos/utils/decorators/__init__.py b/dimos/utils/decorators/__init__.py index ee17260c20..79623922a0 100644 --- a/dimos/utils/decorators/__init__.py +++ b/dimos/utils/decorators/__init__.py @@ -1,12 +1,14 @@ """Decorators and accumulators for rate limiting and other utilities.""" from .accumulators import Accumulator, LatestAccumulator, RollingAverageAccumulator -from .decorators import limit, retry +from .decorators import CachedMethod, limit, retry, simple_mcache __all__ = [ "Accumulator", + "CachedMethod", "LatestAccumulator", "RollingAverageAccumulator", "limit", "retry", + "simple_mcache", ] diff --git a/dimos/utils/decorators/accumulators.py b/dimos/utils/decorators/accumulators.py index 7672ff7033..75cb25661d 100644 --- a/dimos/utils/decorators/accumulators.py +++ b/dimos/utils/decorators/accumulators.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,12 +23,12 @@ class Accumulator(ABC, Generic[T]): """Base class for accumulating messages between rate-limited calls.""" @abstractmethod - def add(self, *args, **kwargs) -> None: + def add(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] """Add args and kwargs to the accumulator.""" pass @abstractmethod - def get(self) -> tuple[tuple, dict] | None: + def get(self) -> tuple[tuple, dict] | None: # type: ignore[type-arg] """Get the accumulated args and kwargs and reset the accumulator.""" pass @@ -42,14 +42,14 @@ class LatestAccumulator(Accumulator[T]): """Simple accumulator that remembers only the latest args and kwargs.""" def __init__(self) -> None: - self._latest: tuple[tuple, dict] | None = None + self._latest: tuple[tuple, dict] | None = None # type: ignore[type-arg] self._lock = threading.Lock() - def add(self, *args, **kwargs) -> None: + def add(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] with self._lock: self._latest = (args, kwargs) - def get(self) -> tuple[tuple, dict] | None: + def get(self) -> tuple[tuple, dict] | None: # type: ignore[type-arg] with self._lock: result = self._latest self._latest = None @@ -70,10 +70,10 @@ class RollingAverageAccumulator(Accumulator[T]): def __init__(self) -> None: self._sum: float = 0.0 self._count: int = 0 - self._latest_kwargs: dict = {} + self._latest_kwargs: dict = {} # type: ignore[type-arg] self._lock = threading.Lock() - def add(self, *args, **kwargs) -> None: + def add(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] if not args: raise ValueError("RollingAverageAccumulator requires at least one argument") @@ -86,7 +86,7 @@ def add(self, *args, **kwargs) -> None: except (TypeError, ValueError): raise TypeError(f"First argument must be numeric, got {type(args[0])}") - def get(self) -> tuple[tuple, dict] | None: + def get(self) -> tuple[tuple, dict] | None: # type: ignore[type-arg] with self._lock: if self._count == 0: return None diff --git a/dimos/utils/decorators/decorators.py b/dimos/utils/decorators/decorators.py index 4511aea309..01e9f8b553 100644 --- a/dimos/utils/decorators/decorators.py +++ b/dimos/utils/decorators/decorators.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,11 +16,22 @@ from functools import wraps import threading import time +from typing import Any, Protocol, TypeVar from .accumulators import Accumulator, LatestAccumulator +_CacheResult_co = TypeVar("_CacheResult_co", covariant=True) +_CacheReturn = TypeVar("_CacheReturn") -def limit(max_freq: float, accumulator: Accumulator | None = None): + +class CachedMethod(Protocol[_CacheResult_co]): + """Protocol for methods decorated with simple_mcache.""" + + def __call__(self) -> _CacheResult_co: ... + def invalidate_cache(self, instance: Any) -> None: ... + + +def limit(max_freq: float, accumulator: Accumulator | None = None): # type: ignore[no-untyped-def, type-arg] """ Decorator that limits function call frequency. @@ -43,7 +54,7 @@ def limit(max_freq: float, accumulator: Accumulator | None = None): if accumulator is None: accumulator = LatestAccumulator() - def decorator(func: Callable) -> Callable: + def decorator(func: Callable) -> Callable: # type: ignore[type-arg] last_call_time = 0.0 lock = threading.Lock() timer: threading.Timer | None = None @@ -52,13 +63,13 @@ def execute_accumulated() -> None: nonlocal last_call_time, timer with lock: if len(accumulator): - acc_args, acc_kwargs = accumulator.get() + acc_args, acc_kwargs = accumulator.get() # type: ignore[misc] last_call_time = time.time() timer = None func(*acc_args, **acc_kwargs) @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] nonlocal last_call_time, timer current_time = time.time() @@ -77,7 +88,7 @@ def wrapper(*args, **kwargs): # if we have accumulated data, we get a compound value if len(accumulator): accumulator.add(*args, **kwargs) - acc_args, acc_kwargs = accumulator.get() # accumulator resets here + acc_args, acc_kwargs = accumulator.get() # type: ignore[misc] # accumulator resets here return func(*acc_args, **acc_kwargs) # No accumulated data, normal call @@ -102,7 +113,7 @@ def wrapper(*args, **kwargs): return decorator -def simple_mcache(method: Callable) -> Callable: +def simple_mcache(method: Callable) -> Callable: # type: ignore[type-arg] """ Decorator to cache the result of a method call on the instance. @@ -124,11 +135,9 @@ def simple_mcache(method: Callable) -> Callable: lock_name = f"_lock_{method.__name__}" @wraps(method) - def getter(self): + def getter(self): # type: ignore[no-untyped-def] # Get or create the lock for this instance if not hasattr(self, lock_name): - # This is a one-time operation, race condition here is acceptable - # as worst case we create multiple locks but only one gets stored setattr(self, lock_name, threading.Lock()) lock = getattr(self, lock_name) @@ -142,10 +151,22 @@ def getter(self): setattr(self, attr_name, method(self)) return getattr(self, attr_name) + def invalidate_cache(instance: Any) -> None: + """Clear the cached value for the given instance.""" + if not hasattr(instance, lock_name): + return + + lock = getattr(instance, lock_name) + with lock: + if hasattr(instance, attr_name): + delattr(instance, attr_name) + + getter.invalidate_cache = invalidate_cache # type: ignore[attr-defined] + return getter -def retry(max_retries: int = 3, on_exception: type[Exception] = Exception, delay: float = 0.0): +def retry(max_retries: int = 3, on_exception: type[Exception] = Exception, delay: float = 0.0): # type: ignore[no-untyped-def] """ Decorator that retries a function call if it raises an exception. @@ -173,9 +194,9 @@ def risky_operation(): if delay < 0: raise ValueError("delay must be non-negative") - def decorator(func: Callable) -> Callable: + def decorator(func: Callable) -> Callable: # type: ignore[type-arg] @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] last_exception = None for attempt in range(max_retries + 1): diff --git a/dimos/utils/decorators/test_decorators.py b/dimos/utils/decorators/test_decorators.py index fdad670454..a40a806a80 100644 --- a/dimos/utils/decorators/test_decorators.py +++ b/dimos/utils/decorators/test_decorators.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import pytest -from dimos.utils.decorators import RollingAverageAccumulator, limit, retry +from dimos.utils.decorators import RollingAverageAccumulator, limit, retry, simple_mcache def test_limit() -> None: @@ -260,3 +260,59 @@ def static_method(attempts_list, fail_times: int = 1) -> str: result = obj2.instance_method() assert result == "instance success with value 100" assert len(obj2.instance_attempts) == 3 + + +def test_simple_mcache() -> None: + """Test simple_mcache decorator caches and can be invalidated.""" + call_count = 0 + + class Counter: + @simple_mcache + def expensive(self) -> int: + nonlocal call_count + call_count += 1 + return call_count + + obj = Counter() + + # First call computes + assert obj.expensive() == 1 + assert call_count == 1 + + # Second call returns cached + assert obj.expensive() == 1 + assert call_count == 1 + + # Invalidate and call again + obj.expensive.invalidate_cache(obj) + assert obj.expensive() == 2 + assert call_count == 2 + + # Cached again + assert obj.expensive() == 2 + assert call_count == 2 + + +def test_simple_mcache_separate_instances() -> None: + """Test that simple_mcache caches per instance.""" + call_count = 0 + + class Counter: + @simple_mcache + def expensive(self) -> int: + nonlocal call_count + call_count += 1 + return call_count + + obj1 = Counter() + obj2 = Counter() + + assert obj1.expensive() == 1 + assert obj2.expensive() == 2 # separate cache + assert obj1.expensive() == 1 # still cached + assert call_count == 2 + + # Invalidating one doesn't affect the other + obj1.expensive.invalidate_cache(obj1) + assert obj1.expensive() == 3 + assert obj2.expensive() == 2 # still cached diff --git a/dimos/utils/demo_image_encoding.py b/dimos/utils/demo_image_encoding.py index a98924260c..42374029f2 100644 --- a/dimos/utils/demo_image_encoding.py +++ b/dimos/utils/demo_image_encoding.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -40,26 +40,26 @@ class EmitterModule(Module): - image: Out[Image] = None + image: Out[Image] _thread: threading.Thread | None = None _stop_event: threading.Event | None = None - def start(self): + def start(self) -> None: super().start() self._stop_event = threading.Event() self._thread = threading.Thread(target=self._publish_image, daemon=True) self._thread.start() - def stop(self): + def stop(self) -> None: if self._thread: - self._stop_event.set() + self._stop_event.set() # type: ignore[union-attr] self._thread.join(timeout=2) super().stop() - def _publish_image(self): + def _publish_image(self) -> None: open_file = open("/tmp/emitter-times", "w") - while not self._stop_event.is_set(): + while not self._stop_event.is_set(): # type: ignore[union-attr] start = time.time() data = random_image(1280, 720) total = time.time() - start @@ -70,25 +70,25 @@ def _publish_image(self): class ReceiverModule(Module): - image: In[Image] = None + image: In[Image] _open_file = None - def start(self): + def start(self) -> None: super().start() self._disposables.add(Disposable(self.image.subscribe(self._on_image))) self._open_file = open("/tmp/receiver-times", "w") - def stop(self): - self._open_file.close() + def stop(self) -> None: + self._open_file.close() # type: ignore[union-attr] super().stop() - def _on_image(self, image: Image): - self._open_file.write(str(time.time()) + "\n") + def _on_image(self, image: Image) -> None: + self._open_file.write(str(time.time()) + "\n") # type: ignore[union-attr] print("image") -def main(): +def main() -> None: parser = argparse.ArgumentParser(description="Demo image encoding with transport options") parser.add_argument( "--use-jpeg", @@ -120,7 +120,7 @@ def main(): pass finally: foxglove_bridge.stop() - dimos.close() + dimos.close() # type: ignore[attr-defined] if __name__ == "__main__": diff --git a/dimos/utils/deprecation.py b/dimos/utils/deprecation.py deleted file mode 100644 index 3c4dd5929e..0000000000 --- a/dimos/utils/deprecation.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import functools -import warnings - - -def deprecated(reason: str): - """ - This function itself is deprecated as we can use `from warnings import deprecated` in Python 3.13+. - """ - - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - warnings.warn( - f"{func.__name__} is deprecated: {reason}", - category=DeprecationWarning, - stacklevel=2, - ) - return func(*args, **kwargs) - - return wrapper - - return decorator diff --git a/dimos/utils/docs/doclinks.md b/dimos/utils/docs/doclinks.md new file mode 100644 index 0000000000..dce2e67fec --- /dev/null +++ b/dimos/utils/docs/doclinks.md @@ -0,0 +1,96 @@ +# doclinks + +A Markdown link resolver that automatically fills in correct file paths for code references in documentation. + +## What it does + +When writing docs, you can use placeholder links like: + + +```markdown +See [`service/spec.py`]() for the implementation. +``` + + +Running `doclinks` resolves these to actual paths: + + +```markdown +See [`service/spec.py`](/dimos/protocol/service/spec.py) for the implementation. +``` + + +## Features + + +- **Code file links**: `[`filename.py`]()` resolves to the file's path +- **Symbol line linking**: If another backticked term appears on the same line, it finds that symbol in the file and adds `#L`: + ```markdown + See `Configurable` in [`config.py`]() + → [`config.py`](/path/config.py#L42) + ``` +- **Doc-to-doc links**: `[Modules](.md)` resolves to `modules.md` or `modules/index.md` + +- **Multiple link modes**: absolute, relative, or GitHub URLs +- **Watch mode**: Automatically re-process on file changes +- **Ignore regions**: Skip sections with `` comments + +## Usage + +```bash +# Process a single file +doclinks docs/guide.md + +# Process a directory recursively +doclinks docs/ + +# Relative links (from doc location) +doclinks --link-mode relative docs/ + +# GitHub links +doclinks --link-mode github \ + --github-url https://github.com/org/repo docs/ + +# Dry run (preview changes) +doclinks --dry-run docs/ + +# CI check (exit 1 if changes needed) +doclinks --check docs/ + +# Watch mode (auto-update on changes) +doclinks --watch docs/ +``` + +## Options + +| Option | Description | +|--------------------|-------------------------------------------------| +| `--root PATH` | Repository root (default: auto-detect git root) | +| `--link-mode MODE` | `absolute` (default), `relative`, or `github` | +| `--github-url URL` | Base GitHub URL (required for github mode) | +| `--github-ref REF` | Branch/ref for GitHub links (default: `main`) | +| `--dry-run` | Show changes without modifying files | +| `--check` | Exit with error if changes needed (for CI) | +| `--watch` | Watch for changes and re-process | + +## Link patterns + + +| Pattern | Description | +|----------------------|------------------------------------------------| +| `[`file.py`]()` | Code file reference (empty or any link) | +| `[`path/file.py`]()` | Code file with partial path for disambiguation | +| `[`file.py`](#L42)` | Preserves existing line fragments | +| `[Doc Name](.md)` | Doc-to-doc link (resolves by name) | + + +## How resolution works + +The tool builds an index of all files in the repo. For `/dimos/protocol/service/spec.py`, it creates lookup entries for: + +- `spec.py` +- `service/spec.py` +- `protocol/service/spec.py` +- `dimos/protocol/service/spec.py` + +Use longer paths when multiple files share the same name. diff --git a/dimos/utils/docs/doclinks.py b/dimos/utils/docs/doclinks.py new file mode 100644 index 0000000000..3f0af10a7b --- /dev/null +++ b/dimos/utils/docs/doclinks.py @@ -0,0 +1,589 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Markdown reference lookup tool. + +Finds markdown links like [`service/spec.py`](...) and fills in the correct +file path from the codebase. + +Usage: + python reference_lookup.py --root /repo/root [options] markdownfile.md +""" + +import argparse +from collections import defaultdict +import os +from pathlib import Path +import re +import subprocess +import sys +from typing import Any + + +def find_git_root() -> Path | None: + """Find the git repository root from current directory.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ) + return Path(result.stdout.strip()) + except (subprocess.CalledProcessError, FileNotFoundError): + return None + + +def get_git_tracked_files(root: Path) -> list[Path]: + """ + Get list of tracked files from git ls-files. + + Returns list of Path objects relative to root. + Only includes files tracked by git, respecting .gitignore. + + Args: + root: Repository root directory + + Returns: + List of Path objects relative to root, sorted. + Returns empty list if not in git repo or on error. + """ + try: + result = subprocess.run( + ["git", "ls-files", "--full-name", "--cached", "--others", "--exclude-standard"], + capture_output=True, + text=True, + check=True, + cwd=root, + ) + if not result.stdout.strip(): + return [] + + paths = [Path(line) for line in result.stdout.strip().split("\n") if line] + return sorted(paths) + except (subprocess.CalledProcessError, FileNotFoundError): + return [] + + +def build_file_index(root: Path) -> dict[str, list[Path]]: + """ + Build an index mapping filename suffixes to full paths. + + For /dimos/protocol/service/spec.py, creates entries for: + - spec.py + - service/spec.py + - protocol/service/spec.py + - dimos/protocol/service/spec.py + """ + index: dict[str, list[Path]] = defaultdict(list) + tracked_files = get_git_tracked_files(root) + + for rel_path in tracked_files: + parts = rel_path.parts + + # Add all suffix combinations + for i in range(len(parts)): + suffix = "/".join(parts[i:]) + index[suffix].append(rel_path) + + return index + + +def build_doc_index(root: Path) -> dict[str, list[Path]]: + """ + Build an index mapping lowercase doc names to .md file paths. + + For docs/concepts/modules.md, creates entry: + - "modules" -> [Path("docs/concepts/modules.md")] + + Also indexes directory index files: + - "modules" -> [Path("docs/modules/index.md")] (if modules/index.md exists) + """ + index: dict[str, list[Path]] = defaultdict(list) + tracked_files = get_git_tracked_files(root) + + for rel_path in tracked_files: + if rel_path.suffix != ".md": + continue + + stem = rel_path.stem.lower() + + # For index.md files, also index by parent directory name + if stem == "index": + parent_name = rel_path.parent.name.lower() + if parent_name: + index[parent_name].append(rel_path) + else: + index[stem].append(rel_path) + + return index + + +def find_symbol_line(file_path: Path, symbol: str) -> int | None: + """Find the first line number where symbol appears.""" + try: + with open(file_path, encoding="utf-8", errors="replace") as f: + for line_num, line in enumerate(f, start=1): + if symbol in line: + return line_num + except OSError: + pass + return None + + +def extract_other_backticks(line: str, file_ref: str) -> list[str]: + """Extract other backticked terms from a line, excluding the file reference.""" + pattern = r"`([^`]+)`" + matches = re.findall(pattern, line) + return [m for m in matches if m != file_ref and not m.endswith(".py") and "/" not in m] + + +def generate_link( + rel_path: Path, + root: Path, + doc_path: Path, + link_mode: str, + github_url: str | None, + github_ref: str, + line_fragment: str = "", +) -> str: + """Generate the appropriate link format.""" + if link_mode == "absolute": + return f"/{rel_path}{line_fragment}" + elif link_mode == "relative": + doc_dir = ( + doc_path.parent.relative_to(root) if doc_path.is_relative_to(root) else doc_path.parent + ) + target = root / rel_path + try: + rel_link = os.path.relpath(target, root / doc_dir) + except ValueError: + rel_link = str(rel_path) + return f"{rel_link}{line_fragment}" + elif link_mode == "github": + if not github_url: + raise ValueError("--github-url required when using --link-mode=github") + return f"{github_url.rstrip('/')}/blob/{github_ref}/{rel_path}{line_fragment}" + else: + raise ValueError(f"Unknown link mode: {link_mode}") + + +def split_by_ignore_regions(content: str) -> list[tuple[str, bool]]: + """ + Split content into regions, marking which should be processed. + + Returns list of (text, should_process) tuples. + Regions between and are skipped. + """ + ignore_start = re.compile(r"", re.IGNORECASE) + ignore_end = re.compile(r"", re.IGNORECASE) + + regions = [] + pos = 0 + in_ignore = False + + while pos < len(content): + if not in_ignore: + # Look for start of ignore region + match = ignore_start.search(content, pos) + if match: + # Add content before ignore marker (to be processed) + if match.start() > pos: + regions.append((content[pos : match.start()], True)) + # Add the marker itself (not processed) + regions.append((content[match.start() : match.end()], False)) + pos = match.end() + in_ignore = True + else: + # No more ignore regions, add rest of content + regions.append((content[pos:], True)) + break + else: + # Look for end of ignore region + match = ignore_end.search(content, pos) + if match: + # Add ignored content including end marker + regions.append((content[pos : match.end()], False)) + pos = match.end() + in_ignore = False + else: + # Unclosed ignore region, add rest as ignored + regions.append((content[pos:], False)) + break + + return regions + + +def process_markdown( + content: str, + root: Path, + doc_path: Path, + file_index: dict[str, list[Path]], + link_mode: str, + github_url: str | None, + github_ref: str, + doc_index: dict[str, list[Path]] | None = None, +) -> tuple[str, list[str], list[str]]: + """ + Process markdown content, replacing file and doc links. + + Regions between and + are skipped. + + Returns (new_content, changes, errors). + """ + changes = [] + errors = [] + + # Pattern 1: [`filename`](link) - code file links + code_pattern = r"\[`([^`]+)`\]\(([^)]*)\)" + + # Pattern 2: [Text](.md) - doc file links + doc_pattern = r"\[([^\]]+)\]\(\.md\)" + + def replace_code_match(match: re.Match[str]) -> str: + file_ref = match.group(1) + current_link = match.group(2) + full_match = match.group(0) + + # Skip anchor-only links (e.g., [`Symbol`](#section)) + if current_link.startswith("#"): + return full_match + + # Skip if the reference doesn't look like a file path (no extension or path separator) + if "." not in file_ref and "/" not in file_ref: + return full_match + + # Look up in index + candidates = file_index.get(file_ref, []) + + if len(candidates) == 0: + errors.append(f"No file matching '{file_ref}' found in codebase") + return full_match + elif len(candidates) > 1: + errors.append(f"'{file_ref}' matches multiple files: {[str(c) for c in candidates]}") + return full_match + + resolved_path = candidates[0] + + # Determine line fragment + line_fragment = "" + + # Check if current link has a line fragment to preserve + if "#" in current_link: + line_fragment = "#" + current_link.split("#", 1)[1] + else: + # Look for other backticked symbols on the same line + line_start = content.rfind("\n", 0, match.start()) + 1 + line_end = content.find("\n", match.end()) + if line_end == -1: + line_end = len(content) + line = content[line_start:line_end] + + symbols = extract_other_backticks(line, file_ref) + if symbols: + # Try to find the first symbol in the target file + full_file_path = root / resolved_path + for symbol in symbols: + line_num = find_symbol_line(full_file_path, symbol) + if line_num is not None: + line_fragment = f"#L{line_num}" + break + + new_link = generate_link( + resolved_path, root, doc_path, link_mode, github_url, github_ref, line_fragment + ) + new_match = f"[`{file_ref}`]({new_link})" + + if new_match != full_match: + changes.append(f" {file_ref}: {current_link} -> {new_link}") + + return new_match + + def replace_doc_match(match: re.Match[str]) -> str: + """Replace [Text](.md) with resolved doc path.""" + if doc_index is None: + return match.group(0) + + link_text = match.group(1) + full_match = match.group(0) + lookup_key = link_text.lower() + + # Look up in doc index + candidates = doc_index.get(lookup_key, []) + + if len(candidates) == 0: + errors.append(f"No doc matching '{link_text}' found") + return full_match + elif len(candidates) > 1: + errors.append(f"'{link_text}' matches multiple docs: {[str(c) for c in candidates]}") + return full_match + + resolved_path = candidates[0] + new_link = generate_link(resolved_path, root, doc_path, link_mode, github_url, github_ref) + new_match = f"[{link_text}]({new_link})" + + if new_match != full_match: + changes.append(f" {link_text}: .md -> {new_link}") + + return new_match + + # Split by ignore regions and only process non-ignored parts + regions = split_by_ignore_regions(content) + result_parts = [] + + for region_content, should_process in regions: + if should_process: + # Process code links first, then doc links + processed = re.sub(code_pattern, replace_code_match, region_content) + processed = re.sub(doc_pattern, replace_doc_match, processed) + result_parts.append(processed) + else: + result_parts.append(region_content) + + new_content = "".join(result_parts) + return new_content, changes, errors + + +def collect_markdown_files(paths: list[str]) -> list[Path]: + """Collect markdown files from paths, expanding directories recursively.""" + result: list[Path] = [] + for p in paths: + path = Path(p) + if path.is_dir(): + result.extend(path.rglob("*.md")) + elif path.exists(): + result.append(path) + return sorted(set(result)) + + +USAGE = """\ +doclinks - Update markdown file links to correct codebase paths + +Finds [`filename.py`](...) patterns and resolves them to actual file paths. +Also auto-links symbols: `Configurable` on same line adds #L fragment. + +Supports doc-to-doc linking: [Modules](.md) resolves to modules.md or modules/index.md. + +Usage: + doclinks [options] + +Examples: + # Single file (auto-detects git root) + doclinks docs/guide.md + + # Recursive directory + doclinks docs/ + + # GitHub links + doclinks --root . --link-mode github \\ + --github-url https://github.com/org/repo docs/ + + # Relative links (from doc location) + doclinks --root . --link-mode relative docs/ + + # CI check (exit 1 if changes needed) + doclinks --root . --check docs/ + + # Dry run (show changes without writing) + doclinks --root . --dry-run docs/ + +Options: + --root PATH Repository root (default: git root) + --link-mode MODE absolute (default), relative, or github + --github-url URL Base GitHub URL (for github mode) + --github-ref REF Branch/ref for GitHub links (default: main) + --dry-run Show changes without modifying files + --check Exit with error if changes needed + --watch Watch for changes and re-process (requires watchdog) + -h, --help Show this help +""" + + +def main() -> None: + if len(sys.argv) == 1: + print(USAGE) + sys.exit(0) + + parser = argparse.ArgumentParser( + description="Update markdown file links to correct codebase paths", + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, + ) + parser.add_argument("paths", nargs="*", help="Markdown files or directories to process") + parser.add_argument("--root", type=Path, help="Repository root path") + parser.add_argument("-h", "--help", action="store_true", help="Show help") + parser.add_argument( + "--link-mode", + choices=["absolute", "relative", "github"], + default="absolute", + help="Link format (default: absolute)", + ) + parser.add_argument("--github-url", help="Base GitHub URL (required for github mode)") + parser.add_argument("--github-ref", default="main", help="GitHub branch/ref (default: main)") + parser.add_argument( + "--dry-run", action="store_true", help="Show changes without modifying files" + ) + parser.add_argument( + "--check", action="store_true", help="Exit with error if changes needed (CI mode)" + ) + parser.add_argument("--watch", action="store_true", help="Watch for changes and re-process") + + args = parser.parse_args() + + if args.help: + print(USAGE) + sys.exit(0) + + # Auto-detect git root if --root not provided + if args.root: + root = args.root.resolve() + else: + root = find_git_root() + if root is None: + print("Error: --root not provided and not in a git repository\n", file=sys.stderr) + sys.exit(1) + + if not args.paths: + print("Error: at least one path is required\n", file=sys.stderr) + print(USAGE) + sys.exit(1) + + if args.link_mode == "github" and not args.github_url: + print("Error: --github-url is required when using --link-mode=github\n", file=sys.stderr) + sys.exit(1) + + if not root.is_dir(): + print(f"Error: {root} is not a directory", file=sys.stderr) + sys.exit(1) + + print(f"Building file index from {root}...") + file_index = build_file_index(root) + doc_index = build_doc_index(root) + print( + f"Indexed {sum(len(v) for v in file_index.values())} file paths, {len(doc_index)} doc names" + ) + + def process_file(md_path: Path, quiet: bool = False) -> tuple[bool, list[str]]: + """Process a single markdown file. Returns (changed, errors).""" + md_path = md_path.resolve() + if not quiet: + rel = md_path.relative_to(root) if md_path.is_relative_to(root) else md_path + print(f"\nProcessing {rel}...") + + content = md_path.read_text() + new_content, changes, errors = process_markdown( + content, + root, + md_path, + file_index, + args.link_mode, + args.github_url, + args.github_ref, + doc_index=doc_index, + ) + + if errors: + for err in errors: + print(f" Error: {err}", file=sys.stderr) + + if changes: + if not quiet: + print(" Changes:") + for change in changes: + print(change) + if not args.dry_run and not args.check: + md_path.write_text(new_content) + if not quiet: + print(" Updated") + return True, errors + else: + if not quiet: + print(" No changes needed") + return False, errors + + # Watch mode + if args.watch: + try: + from watchdog.events import FileSystemEventHandler + from watchdog.observers import Observer + except ImportError: + print( + "Error: --watch requires watchdog. Install with: pip install watchdog", + file=sys.stderr, + ) + sys.exit(1) + + watch_paths = args.paths if args.paths else [str(root / "docs")] + + class MarkdownHandler(FileSystemEventHandler): + def on_modified(self, event: Any) -> None: + if not event.is_directory and event.src_path.endswith(".md"): + process_file(Path(event.src_path)) + + def on_created(self, event: Any) -> None: + if not event.is_directory and event.src_path.endswith(".md"): + process_file(Path(event.src_path)) + + observer = Observer() + handler = MarkdownHandler() + + for watch_path in watch_paths: + p = Path(watch_path) + if p.is_file(): + p = p.parent + print(f"Watching {p} for changes...") + observer.schedule(handler, str(p), recursive=True) + + observer.start() + try: + while True: + import time + + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() + return + + # Normal mode + markdown_files = collect_markdown_files(args.paths) + if not markdown_files: + print("No markdown files found", file=sys.stderr) + sys.exit(1) + + print(f"Found {len(markdown_files)} markdown file(s)") + + all_errors = [] + any_changes = False + + for md_path in markdown_files: + changed, errors = process_file(md_path) + if changed: + any_changes = True + all_errors.extend(errors) + + if all_errors: + print(f"\n{len(all_errors)} error(s) encountered", file=sys.stderr) + sys.exit(1) + + if args.check and any_changes: + print("\nChanges needed (--check mode)", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/docs/test_doclinks.py b/dimos/utils/docs/test_doclinks.py new file mode 100644 index 0000000000..7313ec3676 --- /dev/null +++ b/dimos/utils/docs/test_doclinks.py @@ -0,0 +1,524 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for doclinks - using virtual markdown content against actual repo.""" + +from pathlib import Path + +from doclinks import ( + build_doc_index, + build_file_index, + extract_other_backticks, + find_symbol_line, + process_markdown, + split_by_ignore_regions, +) +import pytest + +# Use the actual repo root +REPO_ROOT = Path(__file__).parent.parent.parent.parent + + +@pytest.fixture(scope="module") +def file_index(): + """Build file index once for all tests.""" + return build_file_index(REPO_ROOT) + + +@pytest.fixture(scope="module") +def doc_index(): + """Build doc index once for all tests.""" + return build_doc_index(REPO_ROOT) + + +class TestFileIndex: + def test_finds_spec_files(self, file_index): + """Should find spec.py files with various path suffixes.""" + # Exact match with path + assert "protocol/service/spec.py" in file_index + candidates = file_index["protocol/service/spec.py"] + assert len(candidates) == 1 + assert candidates[0] == Path("dimos/protocol/service/spec.py") + + def test_service_spec_unique(self, file_index): + """service/spec.py should uniquely match one file.""" + candidates = file_index.get("service/spec.py", []) + assert len(candidates) == 1 + assert "protocol/service/spec.py" in str(candidates[0]) + + def test_spec_ambiguous(self, file_index): + """spec.py alone should match multiple files.""" + candidates = file_index.get("spec.py", []) + assert len(candidates) > 1 # Multiple spec.py files exist + + def test_excludes_venv(self, file_index): + """Should not include files from .venv directory.""" + for paths in file_index.values(): + for p in paths: + # Check for .venv as a path component, not just substring + assert ".venv" not in p.parts + + +class TestSymbolLookup: + def test_find_configurable_in_spec(self): + """Should find Configurable class in service/spec.py.""" + spec_path = REPO_ROOT / "dimos/protocol/service/spec.py" + line = find_symbol_line(spec_path, "Configurable") + assert line is not None + assert line > 0 + + # Verify it's the class definition line + with open(spec_path) as f: + lines = f.readlines() + assert "class Configurable" in lines[line - 1] + + def test_find_nonexistent_symbol(self): + """Should return None for symbols that don't exist.""" + spec_path = REPO_ROOT / "dimos/protocol/service/spec.py" + line = find_symbol_line(spec_path, "NonExistentSymbol12345") + assert line is None + + +class TestExtractBackticks: + def test_extracts_symbols(self): + """Should extract backticked terms excluding file refs.""" + line = "See [`service/spec.py`]() for `Configurable` and `Service`" + symbols = extract_other_backticks(line, "service/spec.py") + assert "Configurable" in symbols + assert "Service" in symbols + assert "service/spec.py" not in symbols + + def test_excludes_file_paths(self): + """Should exclude things that look like file paths.""" + line = "See [`foo.py`]() and `bar.py` and `Symbol`" + symbols = extract_other_backticks(line, "foo.py") + assert "Symbol" in symbols + assert "bar.py" not in symbols # Has .py extension + assert "foo.py" not in symbols + + +class TestProcessMarkdown: + def test_resolves_service_spec(self, file_index): + """Should resolve service/spec.py to full path.""" + content = "See [`service/spec.py`]() for details" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + ) + + assert len(errors) == 0 + assert len(changes) == 1 + assert "/dimos/protocol/service/spec.py" in new_content + + def test_auto_links_symbol(self, file_index): + """Should auto-add line number for symbol on same line.""" + content = "The `Configurable` class is in [`service/spec.py`]()" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, _changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + ) + + assert len(errors) == 0 + assert "#L" in new_content # Should have line number + + def test_preserves_existing_line_fragment(self, file_index): + """Should preserve existing #L fragments.""" + content = "See [`service/spec.py`](#L99)" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, _changes, _errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + ) + + assert "#L99" in new_content + + def test_skips_anchor_links(self, file_index): + """Should skip anchor-only links like [`Symbol`](#section).""" + content = "See [`SomeClass`](#some-section) for details" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + ) + + assert len(errors) == 0 + assert len(changes) == 0 + assert new_content == content # Unchanged + + def test_skips_non_file_refs(self, file_index): + """Should skip refs that don't look like files.""" + content = "The `MyClass` is documented at [`MyClass`]()" + doc_path = REPO_ROOT / "docs/test.md" + + _new_content, changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + ) + + assert len(errors) == 0 + assert len(changes) == 0 + + def test_errors_on_ambiguous(self, file_index): + """Should error when file reference is ambiguous.""" + content = "See [`spec.py`]() for details" # Multiple spec.py files + doc_path = REPO_ROOT / "docs/test.md" + + _new_content, _changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + ) + + assert len(errors) == 1 + assert "matches multiple files" in errors[0] + + def test_errors_on_not_found(self, file_index): + """Should error when file doesn't exist.""" + content = "See [`nonexistent/file.py`]() for details" + doc_path = REPO_ROOT / "docs/test.md" + + _new_content, _changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + ) + + assert len(errors) == 1 + assert "No file matching" in errors[0] + + def test_github_mode(self, file_index): + """Should generate GitHub URLs in github mode.""" + content = "See [`service/spec.py`]()" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, _changes, _errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="github", + github_url="https://github.com/org/repo", + github_ref="main", + ) + + assert "https://github.com/org/repo/blob/main/dimos/protocol/service/spec.py" in new_content + + def test_relative_mode(self, file_index): + """Should generate relative paths in relative mode.""" + content = "See [`service/spec.py`]()" + doc_path = REPO_ROOT / "docs/concepts/test.md" + + new_content, _changes, _errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="relative", + github_url=None, + github_ref="main", + ) + + assert new_content.startswith("See [`service/spec.py`](../../") + assert "dimos/protocol/service/spec.py" in new_content + + +class TestDocIndex: + def test_indexes_by_stem(self, doc_index): + """Should index docs by lowercase stem.""" + assert "configuration" in doc_index + assert "modules" in doc_index + assert "development" in doc_index + + def test_case_insensitive(self, doc_index): + """Should use lowercase keys.""" + # All keys should be lowercase + for key in doc_index: + assert key == key.lower() + + +class TestDocLinking: + def test_resolves_doc_link(self, file_index, doc_index): + """Should resolve [Text](.md) to doc path.""" + content = "See [Configuration](.md) for details" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + doc_index=doc_index, + ) + + assert len(errors) == 0 + assert len(changes) == 1 + assert "[Configuration](/docs/" in new_content + assert ".md)" in new_content + + def test_case_insensitive_lookup(self, file_index, doc_index): + """Should match case-insensitively.""" + content = "See [CONFIGURATION](.md) for details" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, _changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + doc_index=doc_index, + ) + + assert len(errors) == 0 + assert "[CONFIGURATION](" in new_content # Preserves original text + assert ".md)" in new_content + + def test_doc_link_github_mode(self, file_index, doc_index): + """Should generate GitHub URLs for doc links.""" + content = "See [Configuration](.md)" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, _changes, _errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="github", + github_url="https://github.com/org/repo", + github_ref="main", + doc_index=doc_index, + ) + + assert "https://github.com/org/repo/blob/main/docs/" in new_content + assert ".md)" in new_content + + def test_doc_link_relative_mode(self, file_index, doc_index): + """Should generate relative paths for doc links.""" + content = "See [Development](.md)" + doc_path = REPO_ROOT / "docs/concepts/test.md" + + new_content, _changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="relative", + github_url=None, + github_ref="main", + doc_index=doc_index, + ) + + assert len(errors) == 0 + # Should be relative path from docs/concepts/ to docs/ + assert "../" in new_content + + def test_doc_not_found_error(self, file_index, doc_index): + """Should error when doc doesn't exist.""" + content = "See [NonexistentDoc](.md)" + doc_path = REPO_ROOT / "docs/test.md" + + _new_content, _changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + doc_index=doc_index, + ) + + assert len(errors) == 1 + assert "No doc matching" in errors[0] + + def test_skips_regular_links(self, file_index, doc_index): + """Should not affect regular markdown links.""" + content = "See [regular link](https://example.com) here" + doc_path = REPO_ROOT / "docs/test.md" + + new_content, _changes, _errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + doc_index=doc_index, + ) + + assert new_content == content # Unchanged + + +class TestIgnoreRegions: + def test_split_no_ignore(self): + """Content without ignore markers should be fully processed.""" + content = "Hello world" + regions = split_by_ignore_regions(content) + assert len(regions) == 1 + assert regions[0] == ("Hello world", True) + + def test_split_single_ignore(self): + """Should correctly split around a single ignore region.""" + content = "beforeignoredafter" + regions = split_by_ignore_regions(content) + + # Should have: before (process), marker (no), ignored+end (no), after (process) + assert len(regions) == 4 + assert regions[0] == ("before", True) + assert regions[1][1] is False # Start marker + assert regions[2][1] is False # Ignored content + end marker + assert regions[3] == ("after", True) + + def test_split_multiple_ignores(self): + """Should handle multiple ignore regions.""" + content = ( + "ax" + "byc" + ) + regions = split_by_ignore_regions(content) + + # Check that processable regions are correctly identified + processable = [r[0] for r in regions if r[1]] + assert "a" in processable + assert "b" in processable + assert "c" in processable + + def test_split_case_insensitive(self): + """Should handle different case in markers.""" + content = "beforeignoredafter" + regions = split_by_ignore_regions(content) + + processable = [r[0] for r in regions if r[1]] + assert "before" in processable + assert "after" in processable + assert "ignored" not in processable + + def test_split_unclosed_ignore(self): + """Unclosed ignore region should ignore rest of content.""" + content = "beforerest of file" + regions = split_by_ignore_regions(content) + + processable = [r[0] for r in regions if r[1]] + assert "before" in processable + assert "rest of file" not in processable + + def test_ignores_links_in_region(self, file_index): + """Links inside ignore region should not be processed.""" + content = ( + "Process [`service/spec.py`]() here\n" + "\n" + "Skip [`service/spec.py`]() here\n" + "\n" + "Process [`service/spec.py`]() again" + ) + doc_path = REPO_ROOT / "docs/test.md" + + new_content, changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + ) + + assert len(errors) == 0 + # Should have 2 changes (before and after ignore region) + assert len(changes) == 2 + + # Verify the ignored region is untouched + assert "Skip [`service/spec.py`]() here" in new_content + + # Verify the processed regions have resolved links + lines = new_content.split("\n") + assert "/dimos/protocol/service/spec.py" in lines[0] + assert "/dimos/protocol/service/spec.py" in lines[-1] + + def test_ignores_doc_links_in_region(self, file_index, doc_index): + """Doc links inside ignore region should not be processed.""" + content = ( + "[Configuration](.md)\n" + "\n" + "[Configuration](.md) example\n" + "\n" + "[Configuration](.md)" + ) + doc_path = REPO_ROOT / "docs/test.md" + + new_content, changes, errors = process_markdown( + content, + REPO_ROOT, + doc_path, + file_index, + link_mode="absolute", + github_url=None, + github_ref="main", + doc_index=doc_index, + ) + + assert len(errors) == 0 + assert len(changes) == 2 # Only 2 links processed + + # Verify the ignored region still has .md placeholder + assert "[Configuration](.md) example" in new_content + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/dimos/utils/extract_frames.py b/dimos/utils/extract_frames.py index d57b0641cd..1719c77620 100644 --- a/dimos/utils/extract_frames.py +++ b/dimos/utils/extract_frames.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import cv2 -def extract_frames(video_path, output_dir, frame_rate) -> None: +def extract_frames(video_path, output_dir, frame_rate) -> None: # type: ignore[no-untyped-def] """ Extract frames from a video file at a specified frame rate. diff --git a/dimos/utils/fast_image_generator.py b/dimos/utils/fast_image_generator.py index f8e02cb71b..66c4fcf951 100644 --- a/dimos/utils/fast_image_generator.py +++ b/dimos/utils/fast_image_generator.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,38 @@ """Fast stateful image generator with visual features for encoding tests.""" +from typing import Literal, TypedDict, Union + import numpy as np +from numpy.typing import NDArray + + +class CircleObject(TypedDict): + """Type definition for circle objects.""" + + type: Literal["circle"] + x: float + y: float + vx: float + vy: float + radius: int + color: NDArray[np.float32] + + +class RectObject(TypedDict): + """Type definition for rectangle objects.""" + + type: Literal["rect"] + x: float + y: float + vx: float + vy: float + width: int + height: int + color: NDArray[np.float32] + + +Object = Union[CircleObject, RectObject] class FastImageGenerator: @@ -31,11 +62,12 @@ class FastImageGenerator: - High contrast boundaries (tests blocking artifacts) """ - def __init__(self, width: int = 1280, height: int = 720): + def __init__(self, width: int = 1280, height: int = 720) -> None: """Initialize the generator with pre-computed elements.""" self.width = width self.height = height self.frame_count = 0 + self.objects: list[Object] = [] # Pre-allocate the main canvas self.canvas = np.zeros((height, width, 3), dtype=np.float32) @@ -57,7 +89,7 @@ def __init__(self, width: int = 1280, height: int = 720): # Pre-allocate shape masks for reuse self._init_shape_masks() - def _init_gradients(self): + def _init_gradients(self) -> None: """Pre-compute gradient patterns.""" # Diagonal gradient self.diag_gradient = (self.x_grid + self.y_grid) * 0.5 @@ -71,7 +103,7 @@ def _init_gradients(self): self.h_gradient = self.x_grid self.v_gradient = self.y_grid - def _init_moving_objects(self): + def _init_moving_objects(self) -> None: """Initialize properties of moving objects.""" self.objects = [ { @@ -104,7 +136,7 @@ def _init_moving_objects(self): }, ] - def _init_texture(self): + def _init_texture(self) -> None: """Pre-compute a texture pattern.""" # Create a simple checkerboard pattern at lower resolution checker_size = 20 @@ -118,7 +150,7 @@ def _init_texture(self): self.texture = np.repeat(np.repeat(checker, checker_size, axis=0), checker_size, axis=1) self.texture = self.texture[: self.height, : self.width].astype(np.float32) * 30 - def _init_shape_masks(self): + def _init_shape_masks(self) -> None: """Pre-allocate reusable masks for shapes.""" # Pre-allocate a mask array self.temp_mask = np.zeros((self.height, self.width), dtype=np.float32) @@ -126,7 +158,7 @@ def _init_shape_masks(self): # Pre-compute indices for the entire image self.y_indices, self.x_indices = np.indices((self.height, self.width)) - def _draw_circle_fast(self, cx: int, cy: int, radius: int, color: np.ndarray): + def _draw_circle_fast(self, cx: int, cy: int, radius: int, color: NDArray[np.float32]) -> None: """Draw a circle using vectorized operations - optimized version without anti-aliasing.""" # Compute bounding box to minimize calculations y1 = max(0, cy - radius - 1) @@ -141,7 +173,7 @@ def _draw_circle_fast(self, cx: int, cy: int, radius: int, color: np.ndarray): mask = dist_sq <= radius**2 self.canvas[y1:y2, x1:x2][mask] = color - def _draw_rect_fast(self, x: int, y: int, w: int, h: int, color: np.ndarray): + def _draw_rect_fast(self, x: int, y: int, w: int, h: int, color: NDArray[np.float32]) -> None: """Draw a rectangle using slicing.""" # Clip to canvas boundaries x1 = max(0, x) @@ -152,7 +184,7 @@ def _draw_rect_fast(self, x: int, y: int, w: int, h: int, color: np.ndarray): if x1 < x2 and y1 < y2: self.canvas[y1:y2, x1:x2] = color - def _update_objects(self): + def _update_objects(self) -> None: """Update positions of moving objects.""" for obj in self.objects: # Update position @@ -182,7 +214,7 @@ def _update_objects(self): obj["vy"] *= -1 obj["y"] = np.clip(obj["y"], 0, 1 - h) - def generate_frame(self) -> np.ndarray: + def generate_frame(self) -> NDArray[np.uint8]: """ Generate a single frame with visual features - optimized for 30+ FPS. @@ -242,17 +274,17 @@ def generate_frame(self) -> np.ndarray: # Direct conversion to uint8 (already in valid range) return self.canvas.astype(np.uint8) - def reset(self): + def reset(self) -> None: """Reset the generator to initial state.""" self.frame_count = 0 self._init_moving_objects() # Convenience function for backward compatibility -_generator = None +_generator: FastImageGenerator | None = None -def random_image(width: int, height: int) -> np.ndarray: +def random_image(width: int, height: int) -> NDArray[np.uint8]: """ Generate an image with visual features suitable for encoding tests. Maintains state for efficient stream generation. diff --git a/dimos/utils/generic.py b/dimos/utils/generic.py index adbb18988f..84168ce057 100644 --- a/dimos/utils/generic.py +++ b/dimos/utils/generic.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,13 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections.abc import Callable import hashlib import json import os import string -from typing import Any +from typing import Any, Generic, TypeVar, overload import uuid +_T = TypeVar("_T") + def truncate_display_string(arg: Any, max: int | None = None) -> str: """ @@ -73,6 +76,13 @@ def short_id(from_string: str | None = None) -> str: return "".join(reversed(chars))[:min_chars] -class classproperty(property): - def __get__(self, obj, cls): +class classproperty(Generic[_T]): + def __init__(self, fget: Callable[..., _T]) -> None: + self.fget = fget + + @overload + def __get__(self, obj: None, cls: type) -> _T: ... + @overload + def __get__(self, obj: object, cls: type) -> _T: ... + def __get__(self, obj: object | None, cls: type) -> _T: return self.fget(cls) diff --git a/dimos/utils/generic_subscriber.py b/dimos/utils/generic_subscriber.py deleted file mode 100644 index 5f687c494a..0000000000 --- a/dimos/utils/generic_subscriber.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import threading -from typing import TYPE_CHECKING, Any - -from reactivex import Observable - -if TYPE_CHECKING: - from reactivex.disposable import Disposable - -logger = logging.getLogger(__name__) - - -class GenericSubscriber: - """Subscribes to an RxPy Observable stream and stores the latest message.""" - - def __init__(self, stream: Observable) -> None: - """Initialize the subscriber and subscribe to the stream. - - Args: - stream: The RxPy Observable stream to subscribe to. - """ - self.latest_message: Any | None = None - self._lock = threading.Lock() - self._subscription: Disposable | None = None - self._stream_completed = threading.Event() - self._stream_error: Exception | None = None - - if stream is not None: - try: - self._subscription = stream.subscribe( - on_next=self._on_next, on_error=self._on_error, on_completed=self._on_completed - ) - logger.debug(f"Subscribed to stream {stream}") - except Exception as e: - logger.error(f"Error subscribing to stream {stream}: {e}") - self._stream_error = e # Store error if subscription fails immediately - else: - logger.warning("Initialized GenericSubscriber with a None stream.") - - def _on_next(self, message: Any) -> None: - """Callback for receiving a new message.""" - with self._lock: - self.latest_message = message - # logger.debug("Received new message") # Can be noisy - - def _on_error(self, error: Exception) -> None: - """Callback for stream error.""" - logger.error(f"Stream error: {error}") - with self._lock: - self._stream_error = error - self._stream_completed.set() # Signal completion/error - - def _on_completed(self) -> None: - """Callback for stream completion.""" - logger.info("Stream completed.") - self._stream_completed.set() - - def get_data(self) -> Any | None: - """Get the latest message received from the stream. - - Returns: - The latest message, or None if no message has been received yet. - """ - with self._lock: - # Optionally check for errors if needed by the caller - # if self._stream_error: - # logger.warning("Attempting to get message after stream error.") - return self.latest_message - - def has_error(self) -> bool: - """Check if the stream encountered an error.""" - with self._lock: - return self._stream_error is not None - - def is_completed(self) -> bool: - """Check if the stream has completed or encountered an error.""" - return self._stream_completed.is_set() - - def dispose(self) -> None: - """Dispose of the subscription to stop receiving messages.""" - if self._subscription is not None: - try: - self._subscription.dispose() - logger.debug("Subscription disposed.") - self._subscription = None - except Exception as e: - logger.error(f"Error disposing subscription: {e}") - self._stream_completed.set() # Ensure completed flag is set on manual dispose - - def __del__(self) -> None: - """Ensure cleanup on object deletion.""" - self.dispose() diff --git a/dimos/utils/gpu_utils.py b/dimos/utils/gpu_utils.py index e0a1a23734..c1ec67b417 100644 --- a/dimos/utils/gpu_utils.py +++ b/dimos/utils/gpu_utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ # limitations under the License. -def is_cuda_available(): +def is_cuda_available(): # type: ignore[no-untyped-def] try: import pycuda.driver as cuda diff --git a/dimos/utils/llm_utils.py b/dimos/utils/llm_utils.py index 124169e794..47d848807c 100644 --- a/dimos/utils/llm_utils.py +++ b/dimos/utils/llm_utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import re -def extract_json(response: str) -> dict | list: +def extract_json(response: str) -> dict | list: # type: ignore[type-arg] """Extract JSON from potentially messy LLM response. Tries multiple strategies: @@ -35,7 +35,7 @@ def extract_json(response: str) -> dict | list: """ # First try to parse the whole response as JSON try: - return json.loads(response) + return json.loads(response) # type: ignore[no-any-return] except json.JSONDecodeError: pass @@ -64,7 +64,7 @@ def extract_json(response: str) -> dict | list: matches = re.findall(object_pattern, response, re.DOTALL) for match in matches: try: - return json.loads(match) + return json.loads(match) # type: ignore[no-any-return] except json.JSONDecodeError: continue diff --git a/dimos/utils/logging_config.py b/dimos/utils/logging_config.py index d0a347f2cd..ce1494025c 100644 --- a/dimos/utils/logging_config.py +++ b/dimos/utils/logging_config.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,79 +12,223 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Logging configuration module with color support. - -This module sets up a logger with color output for different log levels. -""" - +from collections.abc import Mapping +from datetime import datetime +import inspect import logging +import logging.handlers import os +from pathlib import Path +import sys +import tempfile +import traceback +from types import TracebackType +from typing import Any + +import structlog +from structlog.processors import CallsiteParameter, CallsiteParameterAdder + +from dimos.constants import DIMOS_LOG_DIR, DIMOS_PROJECT_ROOT + +# Suppress noisy loggers +logging.getLogger("aiortc.codecs.h264").setLevel(logging.ERROR) +logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) +logging.getLogger("websockets.server").setLevel(logging.ERROR) +logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) +logging.getLogger("asyncio").setLevel(logging.ERROR) + +_LOG_FILE_PATH = None + + +def _get_log_directory() -> Path: + # Check if running from a git repository + if (DIMOS_PROJECT_ROOT / ".git").exists(): + log_dir = DIMOS_LOG_DIR + else: + # Running from an installed package - use XDG_STATE_HOME + xdg_state_home = os.getenv("XDG_STATE_HOME") + if xdg_state_home: + log_dir = Path(xdg_state_home) / "dimos" / "logs" + else: + log_dir = Path.home() / ".local" / "state" / "dimos" / "logs" -import colorlog - -logging.basicConfig(format="%(name)s - %(levelname)s - %(message)s") - - -def setup_logger( - name: str, level: int | None = None, log_format: str | None = None -) -> logging.Logger: - """Set up a logger with color output. + try: + log_dir.mkdir(parents=True, exist_ok=True) + except (PermissionError, OSError): + log_dir = Path(tempfile.gettempdir()) / "dimos" / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + + return log_dir + + +def _get_log_file_path() -> Path: + log_dir = _get_log_directory() + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + pid = os.getpid() + return log_dir / f"dimos_{timestamp}_{pid}.jsonl" + + +def _configure_structlog() -> Path: + global _LOG_FILE_PATH + + if _LOG_FILE_PATH: + return _LOG_FILE_PATH + + _LOG_FILE_PATH = _get_log_file_path() + + shared_processors: list[Any] = [ + structlog.stdlib.add_log_level, + structlog.stdlib.add_logger_name, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.UnicodeDecoder(), + CallsiteParameterAdder( + parameters=[ + CallsiteParameter.FUNC_NAME, + CallsiteParameter.LINENO, + ] + ), + structlog.processors.format_exc_info, # Add this to format exception info + ] + + structlog.configure( + processors=[ + structlog.stdlib.filter_by_level, + *shared_processors, + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + cache_logger_on_first_use=True, + ) + + return _LOG_FILE_PATH + + +def setup_logger(*, level: int | None = None) -> Any: + """Set up a structured logger using structlog. Args: - name: The name of the logger. - level: The logging level (e.g., logging.INFO, logging.DEBUG). - If None, will use DIMOS_LOG_LEVEL env var or default to INFO. - log_format: Optional custom log format. + level: The logging level. Returns: - A configured logger instance. + A configured structlog logger instance. """ + + caller_frame = inspect.stack()[1] + name = caller_frame.filename + + # Convert absolute path to relative path + try: + name = str(Path(name).relative_to(DIMOS_PROJECT_ROOT)) + except (ValueError, TypeError): + pass + + log_file_path = _configure_structlog() + if level is None: - # Get level from environment variable or default to INFO level_name = os.getenv("DIMOS_LOG_LEVEL", "INFO") level = getattr(logging, level_name) - if log_format is None: - log_format = "%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s" - - try: - # Get or create logger - logger = logging.getLogger(name) - - # Remove any existing handlers to avoid duplicates - if logger.hasHandlers(): - logger.handlers.clear() - - # Set logger level first - logger.setLevel(level) - - # Ensure we're not blocked by parent loggers - logger.propagate = False - - # Create and configure handler - handler = colorlog.StreamHandler() - handler.setLevel(level) # Explicitly set handler level - formatter = colorlog.ColoredFormatter( - log_format, - log_colors={ - "DEBUG": "cyan", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "bold_red", - }, + stdlib_logger = logging.getLogger(name) + + # Remove any existing handlers. + if stdlib_logger.hasHandlers(): + stdlib_logger.handlers.clear() + + stdlib_logger.setLevel(level) + stdlib_logger.propagate = False + + # Create console handler with pretty formatting. + # We use exception_formatter=None because we handle exceptions + # separately with Rich in the global exception handler + + console_renderer = structlog.dev.ConsoleRenderer( + colors=True, + pad_event=60, + force_colors=False, + sort_keys=True, + # Don't format exceptions in console logs + exception_formatter=None, # type: ignore[arg-type] + ) + + # Wrapper to remove callsite info and exception details before rendering to console. + def console_processor_without_callsite( + logger: Any, method_name: str, event_dict: Mapping[str, Any] + ) -> str: + event_dict = dict(event_dict) + # Remove callsite info + event_dict.pop("func_name", None) + event_dict.pop("lineno", None) + # Remove exception fields since we handle them with Rich + event_dict.pop("exception", None) + event_dict.pop("exc_info", None) + event_dict.pop("exception_type", None) + event_dict.pop("exception_message", None) + event_dict.pop("traceback_lines", None) + return console_renderer(logger, method_name, event_dict) + + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(level) + console_formatter = structlog.stdlib.ProcessorFormatter( + processor=console_processor_without_callsite, + ) + console_handler.setFormatter(console_formatter) + stdlib_logger.addHandler(console_handler) + + # Create rotating file handler with JSON formatting. + file_handler = logging.handlers.RotatingFileHandler( + log_file_path, + mode="a", + maxBytes=10 * 1024 * 1024, # 10MiB + backupCount=20, + encoding="utf-8", + ) + file_handler.setLevel(level) + file_formatter = structlog.stdlib.ProcessorFormatter( + processor=structlog.processors.JSONRenderer(), + ) + file_handler.setFormatter(file_formatter) + stdlib_logger.addHandler(file_handler) + + return structlog.get_logger(name) + + +def setup_exception_handler() -> None: + def handle_exception( + exc_type: type[BaseException], + exc_value: BaseException, + exc_traceback: TracebackType | None, + ) -> None: + # Don't log KeyboardInterrupt + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + # Get a logger for uncaught exceptions + logger = setup_logger() + + # Log the exception with full traceback to JSON + logger.error( + "Uncaught exception occurred", + exc_info=(exc_type, exc_value, exc_traceback), + exception_type=exc_type.__name__, + exception_message=str(exc_value), + traceback_lines=traceback.format_exception(exc_type, exc_value, exc_traceback), ) - handler.setFormatter(formatter) - logger.addHandler(handler) - - return logger - except Exception as e: - logging.error(f"Failed to set up logger: {e}") - raise + # Still display the exception nicely on console using Rich if available + try: + from rich.console import Console + from rich.traceback import Traceback -# Initialize the logger for this module using environment variable -logger = setup_logger(__name__) + console = Console() + tb = Traceback.from_exception(exc_type, exc_value, exc_traceback) + console.print(tb) + except ImportError: + # Fall back to standard exception display if Rich is not available + sys.__excepthook__(exc_type, exc_value, exc_traceback) -# Example usage: -# logger.debug("This is a debug message") + # Set our custom exception handler + sys.excepthook = handle_exception diff --git a/dimos/utils/metrics.py b/dimos/utils/metrics.py new file mode 100644 index 0000000000..bf7bf45cdc --- /dev/null +++ b/dimos/utils/metrics.py @@ -0,0 +1,90 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +import functools +import time +from typing import Any, TypeVar, cast + +from dimos_lcm.std_msgs import Float32 +import rerun as rr + +from dimos.core import LCMTransport, Transport + +F = TypeVar("F", bound=Callable[..., Any]) + + +def timed( + transport: Callable[[F], Transport[Float32]] | Transport[Float32] | None = None, +) -> Callable[[F], F]: + def timed_decorator(func: F) -> F: + t: Transport[Float32] + if transport is None: + t = LCMTransport(f"/metrics/{func.__name__}", Float32) + elif callable(transport): + t = transport(func) + else: + t = transport + + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + start = time.perf_counter() + result = func(*args, **kwargs) + elapsed = time.perf_counter() - start + + msg = Float32() + msg.data = elapsed * 1000 # ms + t.publish(msg) + return result + + return cast("F", wrapper) + + return timed_decorator + + +def log_timing_to_rerun(entity_path: str) -> Callable[[F], F]: + """Decorator to log function execution time to Rerun. + + Automatically measures the execution time of the decorated function + and logs it as a scalar value to the specified Rerun entity path. + + Args: + entity_path: Rerun entity path for timing metrics + (e.g., "metrics/costmap/calc_ms") + + Returns: + Decorator function + + Example: + @log_timing_to_rerun("metrics/costmap/calc_ms") + def _calculate_costmap(self, msg): + # ... expensive computation + return result + + # Timing automatically logged to Rerun as a time series! + """ + + def decorator(func: F) -> F: + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + start = time.perf_counter() + result = func(*args, **kwargs) + elapsed_ms = (time.perf_counter() - start) * 1000 + + rr.log(entity_path, rr.Scalars(elapsed_ms)) + return result + + return cast("F", wrapper) + + return decorator diff --git a/dimos/utils/monitoring.py b/dimos/utils/monitoring.py index 17415781b5..ca3e03c55e 100644 --- a/dimos/utils/monitoring.py +++ b/dimos/utils/monitoring.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ from dimos.utils.actor_registry import ActorRegistry from dimos.utils.logging_config import setup_logger -logger = setup_logger(__file__) +logger = setup_logger() -def print_data_table(data) -> None: +def print_data_table(data) -> None: # type: ignore[no-untyped-def] headers = [ "cpu_percent", "active_percent", @@ -85,9 +85,9 @@ def print_data_table(data) -> None: class UtilizationThread(threading.Thread): _module: "UtilizationModule" _stop_event: threading.Event - _monitors: dict + _monitors: dict # type: ignore[type-arg] - def __init__(self, module) -> None: + def __init__(self, module) -> None: # type: ignore[no-untyped-def] super().__init__(daemon=True) self._module = module self._stop_event = threading.Event() @@ -95,8 +95,8 @@ def __init__(self, module) -> None: def run(self) -> None: while not self._stop_event.is_set(): - workers = self._module.client.scheduler_info()["workers"] - pids = {pid: None for pid in get_worker_pids()} + workers = self._module.client.scheduler_info()["workers"] # type: ignore[union-attr] + pids = {pid: None for pid in get_worker_pids()} # type: ignore[no-untyped-call] for worker, info in workers.items(): pid = get_pid_by_port(worker.rsplit(":", 1)[-1]) if pid is None: @@ -129,7 +129,7 @@ def stop(self) -> None: monitor.stop() monitor.join(timeout=2) - def _fix_missing_ids(self, data) -> None: + def _fix_missing_ids(self, data) -> None: # type: ignore[no-untyped-def] """ Some worker IDs are None. But if we order the workers by PID and all non-None ids are in order, then we can deduce that the None ones are the @@ -153,7 +153,7 @@ def __init__(self) -> None: logger.info("Set `MEASURE_GIL_UTILIZATION=true` to print GIL utilization.") return - if not _can_use_py_spy(): + if not _can_use_py_spy(): # type: ignore[no-untyped-call] logger.warning( "Cannot start UtilizationModule because in order to run py-spy without " "being root you need to enable this:\n" @@ -190,7 +190,7 @@ def stop(self) -> None: __all__ = ["UtilizationModule", "utilization"] -def _can_use_py_spy(): +def _can_use_py_spy(): # type: ignore[no-untyped-def] try: with open("/proc/sys/kernel/yama/ptrace_scope") as f: value = f.read().strip() @@ -212,7 +212,7 @@ def get_pid_by_port(port: int) -> int | None: return None -def get_worker_pids(): +def get_worker_pids(): # type: ignore[no-untyped-def] pids = [] for pid in os.listdir("/proc"): if not pid.isdigit(): @@ -240,7 +240,7 @@ def __init__(self, pid: int) -> None: self._stop_event = threading.Event() self._lock = threading.Lock() - def run(self): + def run(self): # type: ignore[no-untyped-def] command = ["py-spy", "top", "--pid", str(self.pid), "--rate", "100"] process = None try: @@ -252,7 +252,7 @@ def run(self): bufsize=1, # Line-buffered output ) - for line in iter(process.stdout.readline, ""): + for line in iter(process.stdout.readline, ""): # type: ignore[union-attr] if self._stop_event.is_set(): break @@ -289,7 +289,7 @@ def run(self): process.wait(timeout=1) self._stop_event.set() - def get_values(self): + def get_values(self): # type: ignore[no-untyped-def] with self._lock: return self._latest_values diff --git a/dimos/utils/path_utils.py b/dimos/utils/path_utils.py index d60014d068..794d36e34d 100644 --- a/dimos/utils/path_utils.py +++ b/dimos/utils/path_utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/utils/reactive.py b/dimos/utils/reactive.py index f7885d3129..bfc9cd0465 100644 --- a/dimos/utils/reactive.py +++ b/dimos/utils/reactive.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections.abc import Callable +from collections.abc import Callable, Generator +from queue import Queue import threading from typing import Any, Generic, TypeVar @@ -21,8 +22,8 @@ from reactivex.disposable import Disposable from reactivex.observable import Observable from reactivex.scheduler import ThreadPoolScheduler -from rxpy_backpressure import BackPressure +from dimos.rxpy_backpressure import BackPressure from dimos.utils.threadpool import get_scheduler T = TypeVar("T") @@ -46,7 +47,7 @@ def backpressure( ) # per-subscriber factory - def per_sub(): + def per_sub(): # type: ignore[no-untyped-def] # Move processing to thread pool base = core.pipe(ops.observe_on(scheduler)) @@ -54,19 +55,19 @@ def per_sub(): if not drop_unprocessed: return base - def _subscribe(observer, sch=None): + def _subscribe(observer, sch=None): # type: ignore[no-untyped-def] return base.subscribe(BackPressure.LATEST(observer), scheduler=sch) return rx.create(_subscribe) # each `.subscribe()` call gets its own async backpressure chain - return rx.defer(lambda *_: per_sub()) + return rx.defer(lambda *_: per_sub()) # type: ignore[no-untyped-call] class LatestReader(Generic[T]): """A callable object that returns the latest value from an observable.""" - def __init__(self, initial_value: T, subscription, connection=None) -> None: + def __init__(self, initial_value: T, subscription, connection=None) -> None: # type: ignore[no-untyped-def] self._value = initial_value self._subscription = subscription self._connection = connection @@ -83,16 +84,16 @@ def dispose(self) -> None: def getter_ondemand(observable: Observable[T], timeout: float | None = 30.0) -> T: - def getter(): + def getter(): # type: ignore[no-untyped-def] result = [] error = [] event = threading.Event() - def on_next(value) -> None: + def on_next(value) -> None: # type: ignore[no-untyped-def] result.append(value) event.set() - def on_error(e) -> None: + def on_error(e) -> None: # type: ignore[no-untyped-def] error.append(e) event.set() @@ -121,10 +122,14 @@ def on_completed() -> None: finally: subscription.dispose() - return getter + return getter # type: ignore[return-value] -T = TypeVar("T") +def getter_cold(source: Observable[T], timeout: float | None = 30.0) -> T: + return getter_ondemand(source, timeout) + + +T = TypeVar("T") # type: ignore[misc] def getter_streaming( @@ -171,10 +176,16 @@ def _dispose() -> None: sub.dispose() reader.dispose = _dispose # type: ignore[attr-defined] - return reader + return reader # type: ignore[return-value] -T = TypeVar("T") +def getter_hot( + source: Observable[T], timeout: float | None = 30.0, *, nonblocking: bool = False +) -> LatestReader[T]: + return getter_streaming(source, timeout, nonblocking=nonblocking) + + +T = TypeVar("T") # type: ignore[misc] CB = Callable[[T], Any] @@ -182,7 +193,7 @@ def callback_to_observable( start: Callable[[CB[T]], Any], stop: Callable[[CB[T]], Any], ) -> Observable[T]: - def _subscribe(observer, _scheduler=None): + def _subscribe(observer, _scheduler=None): # type: ignore[no-untyped-def] def _on_msg(value: T) -> None: observer.on_next(value) @@ -192,15 +203,15 @@ def _on_msg(value: T) -> None: return rx.create(_subscribe) -def spy(name: str): - def spyfun(x): +def spy(name: str): # type: ignore[no-untyped-def] + def spyfun(x): # type: ignore[no-untyped-def] print(f"SPY {name}:", x) return x return ops.map(spyfun) -def quality_barrier(quality_func: Callable[[T], float], target_frequency: float): +def quality_barrier(quality_func: Callable[[T], float], target_frequency: float): # type: ignore[no-untyped-def] """ RxPY pipe operator that selects the highest quality item within each time window. @@ -219,12 +230,44 @@ def _quality_barrier(source: Observable[T]) -> Observable[T]: ops.window_with_time(window_duration, window_duration), # For each window, find the highest quality item ops.flat_map( - lambda window: window.pipe( + lambda window: window.pipe( # type: ignore[attr-defined] ops.to_list(), - ops.map(lambda items: max(items, key=quality_func) if items else None), - ops.filter(lambda x: x is not None), + ops.map(lambda items: max(items, key=quality_func) if items else None), # type: ignore[call-overload] + ops.filter(lambda x: x is not None), # type: ignore[arg-type] ) ), ) return _quality_barrier + + +def iter_observable(observable: Observable[T]) -> Generator[T, None, None]: + """Convert an Observable to a blocking iterator. + + Yields items as they arrive from the observable. Properly disposes + the subscription when the generator is closed. + """ + q: Queue[T | None] = Queue() + done = threading.Event() + + def on_next(value: T) -> None: + q.put(value) + + def on_complete() -> None: + done.set() + q.put(None) + + def on_error(e: Exception) -> None: + done.set() + q.put(None) + + sub = observable.subscribe(on_next=on_next, on_completed=on_complete, on_error=on_error) + + try: + while not done.is_set() or not q.empty(): + item = q.get() + if item is None and done.is_set(): + break + yield item # type: ignore[misc] + finally: + sub.dispose() diff --git a/dimos/utils/s3_utils.py b/dimos/utils/s3_utils.py deleted file mode 100644 index f4c3227a71..0000000000 --- a/dimos/utils/s3_utils.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import boto3 - -try: - import open3d as o3d -except Exception as e: - print(f"Open3D not importing, assuming to be running outside of docker. {e}") - - -class S3Utils: - def __init__(self, bucket_name: str) -> None: - self.s3 = boto3.client("s3") - self.bucket_name = bucket_name - - def download_file(self, s3_key, local_path) -> None: - try: - self.s3.download_file(self.bucket_name, s3_key, local_path) - print(f"Downloaded {s3_key} to {local_path}") - except Exception as e: - print(f"Error downloading {s3_key}: {e}") - - def upload_file(self, local_path, s3_key) -> None: - try: - self.s3.upload_file(local_path, self.bucket_name, s3_key) - print(f"Uploaded {local_path} to {s3_key}") - except Exception as e: - print(f"Error uploading {local_path}: {e}") - - def save_pointcloud_to_s3(self, inlier_cloud, s3_key) -> None: - try: - temp_pcd_file = "/tmp/temp_pointcloud.pcd" - o3d.io.write_point_cloud(temp_pcd_file, inlier_cloud) - with open(temp_pcd_file, "rb") as pcd_file: - self.s3.put_object(Bucket=self.bucket_name, Key=s3_key, Body=pcd_file.read()) - os.remove(temp_pcd_file) - print(f"Saved pointcloud to {s3_key}") - except Exception as e: - print(f"error downloading {s3_key}: {e}") - - def restore_pointcloud_from_s3(self, pointcloud_paths): - restored_pointclouds = [] - - for path in pointcloud_paths: - # Download the point cloud file from S3 to memory - pcd_obj = self.s3.get_object(Bucket=self.bucket_name, Key=path) - pcd_data = pcd_obj["Body"].read() - - # Save the point cloud data to a temporary file - temp_pcd_file = "/tmp/temp_pointcloud.pcd" - with open(temp_pcd_file, "wb") as f: - f.write(pcd_data) - - # Read the point cloud from the temporary file - pcd = o3d.io.read_point_cloud(temp_pcd_file) - restored_pointclouds.append(pcd) - - # Remove the temporary file - os.remove(temp_pcd_file) - - return restored_pointclouds - - @staticmethod - def upload_text_file(bucket_name: str, local_path, s3_key) -> None: - s3 = boto3.client("s3") - try: - with open(local_path) as file: - content = file.read() - - # Ensure the s3_key includes the file name - if not s3_key.endswith("/"): - s3_key = s3_key + "/" - - # Extract the file name from the local_path - file_name = local_path.split("/")[-1] - full_s3_key = s3_key + file_name - - s3.put_object(Bucket=bucket_name, Key=full_s3_key, Body=content) - print(f"Uploaded text file {local_path} to {full_s3_key}") - except Exception as e: - print(f"Error uploading text file {local_path}: {e}") diff --git a/dimos/utils/simple_controller.py b/dimos/utils/simple_controller.py index dd92ae0c55..f95350552c 100644 --- a/dimos/utils/simple_controller.py +++ b/dimos/utils/simple_controller.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ import math -def normalize_angle(angle: float): +def normalize_angle(angle: float): # type: ignore[no-untyped-def] """Normalize angle to the range [-pi, pi].""" return math.atan2(math.sin(angle), math.cos(angle)) @@ -24,7 +24,7 @@ def normalize_angle(angle: float): # PID Controller Class # ---------------------------- class PIDController: - def __init__( + def __init__( # type: ignore[no-untyped-def] self, kp, ki: float = 0.0, @@ -59,7 +59,7 @@ def __init__( self.prev_error = 0.0 self.inverse_output = inverse_output - def update(self, error, dt): + def update(self, error, dt): # type: ignore[no-untyped-def] """Compute the PID output with anti-windup, output deadband compensation and output saturation.""" # Update integral term with windup protection. self.integral += error * dt @@ -78,7 +78,7 @@ def update(self, error, dt): output = self.kp * error + self.ki * self.integral + self.kd * derivative # Apply deadband compensation to the output - output = self._apply_output_deadband_compensation(output) + output = self._apply_output_deadband_compensation(output) # type: ignore[no-untyped-call] # Apply output limits if specified. if self.max_output is not None: @@ -91,7 +91,7 @@ def update(self, error, dt): return -output return output - def _apply_output_deadband_compensation(self, output): + def _apply_output_deadband_compensation(self, output): # type: ignore[no-untyped-def] """ Apply deadband compensation to the output. @@ -110,7 +110,7 @@ def _apply_output_deadband_compensation(self, output): else: return output - def _apply_deadband_compensation(self, error): + def _apply_deadband_compensation(self, error): # type: ignore[no-untyped-def] """ Apply deadband compensation to the error. @@ -124,7 +124,7 @@ def _apply_deadband_compensation(self, error): # Visual Servoing Controller Class # ---------------------------- class VisualServoingController: - def __init__(self, distance_pid_params, angle_pid_params) -> None: + def __init__(self, distance_pid_params, angle_pid_params) -> None: # type: ignore[no-untyped-def] """ Initialize the visual servoing controller using enhanced PID controllers. @@ -136,7 +136,7 @@ def __init__(self, distance_pid_params, angle_pid_params) -> None: self.angle_pid = PIDController(*angle_pid_params) self.prev_measured_angle = 0.0 # Used for angular feed-forward damping - def compute_control( + def compute_control( # type: ignore[no-untyped-def] self, measured_distance, measured_angle, desired_distance, desired_angle, dt ): """ @@ -157,8 +157,8 @@ def compute_control( error_angle = normalize_angle(measured_angle - desired_angle) # Get raw PID outputs. - forward_command_raw = self.distance_pid.update(error_distance, dt) - angular_command_raw = self.angle_pid.update(error_angle, dt) + forward_command_raw = self.distance_pid.update(error_distance, dt) # type: ignore[no-untyped-call] + angular_command_raw = self.angle_pid.update(error_angle, dt) # type: ignore[no-untyped-call] # print("forward: {} angular: {}".format(forward_command_raw, angular_command_raw)) diff --git a/dimos/utils/test_data.py b/dimos/utils/test_data.py index b6df8e1a12..01f145f60c 100644 --- a/dimos/utils/test_data.py +++ b/dimos/utils/test_data.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/utils/test_foxglove_bridge.py b/dimos/utils/test_foxglove_bridge.py index ad597c8720..c45dcde660 100644 --- a/dimos/utils/test_foxglove_bridge.py +++ b/dimos/utils/test_foxglove_bridge.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/utils/test_generic.py b/dimos/utils/test_generic.py index 51e7a2007a..0f691bc23c 100644 --- a/dimos/utils/test_generic.py +++ b/dimos/utils/test_generic.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/utils/test_llm_utils.py b/dimos/utils/test_llm_utils.py index 2eb2da9867..0a3812aeaf 100644 --- a/dimos/utils/test_llm_utils.py +++ b/dimos/utils/test_llm_utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/utils/test_reactive.py b/dimos/utils/test_reactive.py index 8fae6de0db..a0f3fe42ef 100644 --- a/dimos/utils/test_reactive.py +++ b/dimos/utils/test_reactive.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ callback_to_observable, getter_ondemand, getter_streaming, + iter_observable, ) @@ -283,3 +284,12 @@ def stop_fn(cb) -> None: # Dispose subscription and check that stop was called subscription.dispose() assert stop_called, "Stop function should be called on dispose" + + +def test_iter_observable() -> None: + source = dispose_spy(rx.of(1, 2, 3, 4, 5)) + + result = list(iter_observable(source)) + + assert result == [1, 2, 3, 4, 5] + assert source.is_disposed(), "Observable should be disposed after iteration" diff --git a/dimos/utils/test_testing.py b/dimos/utils/test_testing.py deleted file mode 100644 index 3684031170..0000000000 --- a/dimos/utils/test_testing.py +++ /dev/null @@ -1,279 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from reactivex import operators as ops - -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.odometry import Odometry -from dimos.utils import testing -from dimos.utils.data import get_data - - -def test_sensor_replay() -> None: - counter = 0 - for message in testing.SensorReplay(name="office_lidar").iterate(): - counter += 1 - assert isinstance(message, dict) - assert counter == 500 - - -def test_sensor_replay_cast() -> None: - counter = 0 - for message in testing.SensorReplay( - name="office_lidar", autocast=LidarMessage.from_msg - ).iterate(): - counter += 1 - assert isinstance(message, LidarMessage) - assert counter == 500 - - -def test_timed_sensor_replay() -> None: - get_data("unitree_office_walk") - odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - itermsgs = [] - for msg in odom_store.iterate(): - itermsgs.append(msg) - if len(itermsgs) > 9: - break - - assert len(itermsgs) == 10 - - print("\n") - - timed_msgs = [] - - for msg in odom_store.stream().pipe(ops.take(10), ops.to_list()).run(): - timed_msgs.append(msg) - - assert len(timed_msgs) == 10 - - for i in range(10): - print(itermsgs[i], timed_msgs[i]) - assert itermsgs[i] == timed_msgs[i] - - -def test_iterate_ts_no_seek() -> None: - """Test iterate_ts without seek (start_timestamp=None)""" - odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - # Test without seek - ts_msgs = [] - for ts, msg in odom_store.iterate_ts(): - ts_msgs.append((ts, msg)) - if len(ts_msgs) >= 5: - break - - assert len(ts_msgs) == 5 - # Check that we get tuples of (timestamp, data) - for ts, msg in ts_msgs: - assert isinstance(ts, float) - assert isinstance(msg, Odometry) - - -def test_iterate_ts_with_from_timestamp() -> None: - """Test iterate_ts with from_timestamp (absolute timestamp)""" - odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - # First get all messages to find a good seek point - all_msgs = [] - for ts, msg in odom_store.iterate_ts(): - all_msgs.append((ts, msg)) - if len(all_msgs) >= 10: - break - - # Seek to timestamp of 5th message - seek_timestamp = all_msgs[4][0] - - # Test with from_timestamp - seeked_msgs = [] - for ts, msg in odom_store.iterate_ts(from_timestamp=seek_timestamp): - seeked_msgs.append((ts, msg)) - if len(seeked_msgs) >= 5: - break - - assert len(seeked_msgs) == 5 - # First message should be at or after seek timestamp - assert seeked_msgs[0][0] >= seek_timestamp - # Should match the data from position 5 onward - assert seeked_msgs[0][1] == all_msgs[4][1] - - -def test_iterate_ts_with_relative_seek() -> None: - """Test iterate_ts with seek (relative seconds after first timestamp)""" - odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - # Get first few messages to understand timing - all_msgs = [] - for ts, msg in odom_store.iterate_ts(): - all_msgs.append((ts, msg)) - if len(all_msgs) >= 10: - break - - # Calculate relative seek time (e.g., 0.5 seconds after start) - first_ts = all_msgs[0][0] - seek_seconds = 0.5 - expected_start_ts = first_ts + seek_seconds - - # Test with relative seek - seeked_msgs = [] - for ts, msg in odom_store.iterate_ts(seek=seek_seconds): - seeked_msgs.append((ts, msg)) - if len(seeked_msgs) >= 5: - break - - # First message should be at or after expected timestamp - assert seeked_msgs[0][0] >= expected_start_ts - # Make sure we're actually skipping some messages - assert seeked_msgs[0][0] > first_ts - - -def test_stream_with_seek() -> None: - """Test stream method with seek parameters""" - odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - # Test stream with relative seek - msgs_with_seek = [] - for msg in odom_store.stream(seek=0.2).pipe(ops.take(5), ops.to_list()).run(): - msgs_with_seek.append(msg) - - assert len(msgs_with_seek) == 5 - - # Test stream with from_timestamp - # First get a reference timestamp - first_msgs = [] - for msg in odom_store.stream().pipe(ops.take(3), ops.to_list()).run(): - first_msgs.append(msg) - - # Now test from_timestamp (would need actual timestamps from iterate_ts to properly test) - # This is a basic test to ensure the parameter is accepted - msgs_with_timestamp = [] - for msg in ( - odom_store.stream(from_timestamp=1000000000.0).pipe(ops.take(3), ops.to_list()).run() - ): - msgs_with_timestamp.append(msg) - - -def test_duration_with_loop() -> None: - """Test duration parameter with looping in TimedSensorReplay""" - odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - # Collect timestamps from a small duration window - collected_ts = [] - duration = 0.3 # 300ms window - - # First pass: collect timestamps in the duration window - for ts, _msg in odom_store.iterate_ts(duration=duration): - collected_ts.append(ts) - if len(collected_ts) >= 100: # Safety limit - break - - # Should have some messages but not too many - assert len(collected_ts) > 0 - assert len(collected_ts) < 20 # Assuming ~30Hz data - - # Test looping with duration - should repeat the same window - loop_count = 0 - prev_ts = None - - for ts, _msg in odom_store.iterate_ts(duration=duration, loop=True): - if prev_ts is not None and ts < prev_ts: - # We've looped back to the beginning - loop_count += 1 - if loop_count >= 2: # Stop after 2 full loops - break - prev_ts = ts - - assert loop_count >= 2 # Verify we actually looped - - -def test_first_methods() -> None: - """Test first() and first_timestamp() methods""" - - # Test SensorReplay.first() - lidar_replay = testing.SensorReplay("office_lidar", autocast=LidarMessage.from_msg) - - print("first file", lidar_replay.files[0]) - # Verify the first file ends with 000.pickle using regex - assert re.search(r"000\.pickle$", str(lidar_replay.files[0])), ( - f"Expected first file to end with 000.pickle, got {lidar_replay.files[0]}" - ) - - first_msg = lidar_replay.first() - assert first_msg is not None - assert isinstance(first_msg, LidarMessage) - - # Verify it's the same type as first item from iterate() - first_from_iterate = next(lidar_replay.iterate()) - print("DONE") - assert type(first_msg) is type(first_from_iterate) - # Since LidarMessage.from_msg uses time.time(), timestamps will be slightly different - assert abs(first_msg.ts - first_from_iterate.ts) < 1.0 # Within 1 second tolerance - - # Test TimedSensorReplay.first_timestamp() - odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - first_ts = odom_store.first_timestamp() - assert first_ts is not None - assert isinstance(first_ts, float) - - # Verify it matches the timestamp from iterate_ts - ts_from_iterate, _ = next(odom_store.iterate_ts()) - assert first_ts == ts_from_iterate - - # Test that first() returns just the data - first_data = odom_store.first() - assert first_data is not None - assert isinstance(first_data, Odometry) - - -def test_find_closest() -> None: - """Test find_closest method in TimedSensorReplay""" - odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - # Get some reference timestamps - timestamps = [] - for ts, _msg in odom_store.iterate_ts(): - timestamps.append(ts) - if len(timestamps) >= 10: - break - - # Test exact match - target_ts = timestamps[5] - result = odom_store.find_closest(target_ts) - assert result is not None - assert isinstance(result, Odometry) - - # Test between timestamps - mid_ts = (timestamps[3] + timestamps[4]) / 2 - result = odom_store.find_closest(mid_ts) - assert result is not None - - # Test with tolerance - far_future = timestamps[-1] + 100.0 - result = odom_store.find_closest(far_future, tolerance=1.0) - assert result is None # Too far away - - result = odom_store.find_closest(timestamps[0] - 0.001, tolerance=0.01) - assert result is not None # Within tolerance - - # Test find_closest_seek - result = odom_store.find_closest_seek(0.5) # 0.5 seconds from start - assert result is not None - assert isinstance(result, Odometry) - - # Test with negative seek (before start) - result = odom_store.find_closest_seek(-1.0) - assert result is not None # Should still return closest (first frame) diff --git a/dimos/utils/test_transform_utils.py b/dimos/utils/test_transform_utils.py index 8054971d3f..b404579598 100644 --- a/dimos/utils/test_transform_utils.py +++ b/dimos/utils/test_transform_utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/utils/test_trigonometry.py b/dimos/utils/test_trigonometry.py new file mode 100644 index 0000000000..199061a629 --- /dev/null +++ b/dimos/utils/test_trigonometry.py @@ -0,0 +1,36 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +import pytest + +from dimos.utils.trigonometry import angle_diff + + +def from_rad(x): + return x / (math.pi / 180) + + +def to_rad(x): + return x * (math.pi / 180) + + +def test_angle_diff(): + a = to_rad(1) + b = to_rad(359) + + assert from_rad(angle_diff(a, b)) == pytest.approx(2, abs=0.00000000001) + + assert from_rad(angle_diff(b, a)) == pytest.approx(-2, abs=0.00000000001) diff --git a/dimos/utils/testing.py b/dimos/utils/testing.py deleted file mode 100644 index 5e3725bc81..0000000000 --- a/dimos/utils/testing.py +++ /dev/null @@ -1,375 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections.abc import Callable, Iterator -import functools -import glob -import os -from pathlib import Path -import pickle -import re -import time -from typing import Any, Generic, TypeVar - -from reactivex import ( - from_iterable, - interval, - operators as ops, -) -from reactivex.observable import Observable -from reactivex.scheduler import TimeoutScheduler - -from dimos.utils.data import _get_data_dir, get_data - -T = TypeVar("T") - - -class SensorReplay(Generic[T]): - """Generic sensor data replay utility. - - Args: - name: The name of the test dataset - autocast: Optional function that takes unpickled data and returns a processed result. - For example: lambda data: LidarMessage.from_msg(data) - """ - - def __init__(self, name: str, autocast: Callable[[Any], T] | None = None) -> None: - self.root_dir = get_data(name) - self.autocast = autocast - - def load(self, *names: int | str) -> T | Any | list[T] | list[Any]: - if len(names) == 1: - return self.load_one(names[0]) - return list(map(lambda name: self.load_one(name), names)) - - def load_one(self, name: int | str | Path) -> T | Any: - if isinstance(name, int): - full_path = self.root_dir / f"/{name:03d}.pickle" - elif isinstance(name, Path): - full_path = name - else: - full_path = self.root_dir / Path(f"{name}.pickle") - - with open(full_path, "rb") as f: - data = pickle.load(f) - if self.autocast: - return self.autocast(data) - return data - - def first(self) -> T | Any | None: - try: - return next(self.iterate()) - except StopIteration: - return None - - @functools.cached_property - def files(self) -> list[Path]: - def extract_number(filepath): - """Extract last digits before .pickle extension""" - basename = os.path.basename(filepath) - match = re.search(r"(\d+)\.pickle$", basename) - return int(match.group(1)) if match else 0 - - return sorted( - glob.glob(os.path.join(self.root_dir, "*")), - key=extract_number, - ) - - def iterate(self, loop: bool = False) -> Iterator[T | Any]: - while True: - for file_path in self.files: - yield self.load_one(Path(file_path)) - if not loop: - break - - def stream(self, rate_hz: float | None = None, loop: bool = False) -> Observable[T | Any]: - if rate_hz is None: - return from_iterable(self.iterate(loop=loop)) - - sleep_time = 1.0 / rate_hz - - return from_iterable(self.iterate(loop=loop)).pipe( - ops.zip(interval(sleep_time)), - ops.map(lambda x: x[0] if isinstance(x, tuple) else x), - ) - - -class SensorStorage(Generic[T]): - """Generic sensor data storage utility - . - Creates a directory in the test data directory and stores pickled sensor data. - - Args: - name: The name of the storage directory - autocast: Optional function that takes data and returns a processed result before storage. - """ - - def __init__(self, name: str, autocast: Callable[[T], Any] | None = None) -> None: - self.name = name - self.autocast = autocast - self.cnt = 0 - - # Create storage directory in the data dir - self.root_dir = _get_data_dir() / name - - # Check if directory exists and is not empty - if self.root_dir.exists(): - existing_files = list(self.root_dir.glob("*.pickle")) - if existing_files: - raise RuntimeError( - f"Storage directory '{name}' already exists and contains {len(existing_files)} files. " - f"Please use a different name or clean the directory first." - ) - else: - # Create the directory - self.root_dir.mkdir(parents=True, exist_ok=True) - - def consume_stream(self, observable: Observable[T | Any]) -> None: - """Consume an observable stream of sensor data without saving.""" - return observable.subscribe(self.save_one) - - def save_stream(self, observable: Observable[T | Any]) -> Observable[int]: - """Save an observable stream of sensor data to pickle files.""" - return observable.pipe(ops.map(lambda frame: self.save_one(frame))) - - def save(self, *frames) -> int: - """Save one or more frames to pickle files.""" - for frame in frames: - self.save_one(frame) - return self.cnt - - def save_one(self, frame) -> int: - """Save a single frame to a pickle file.""" - file_name = f"{self.cnt:03d}.pickle" - full_path = self.root_dir / file_name - - if full_path.exists(): - raise RuntimeError(f"File {full_path} already exists") - - # Apply autocast if provided - data_to_save = frame - if self.autocast: - data_to_save = self.autocast(frame) - # Convert to raw message if frame has a raw_msg attribute - elif hasattr(frame, "raw_msg"): - data_to_save = frame.raw_msg - - with open(full_path, "wb") as f: - pickle.dump(data_to_save, f) - - self.cnt += 1 - return self.cnt - - -class TimedSensorStorage(SensorStorage[T]): - def save_one(self, frame: T) -> int: - return super().save_one((time.time(), frame)) - - -class TimedSensorReplay(SensorReplay[T]): - def load_one(self, name: int | str | Path) -> T | Any: - if isinstance(name, int): - full_path = self.root_dir / f"/{name:03d}.pickle" - elif isinstance(name, Path): - full_path = name - else: - full_path = self.root_dir / Path(f"{name}.pickle") - - with open(full_path, "rb") as f: - data = pickle.load(f) - if self.autocast: - return (data[0], self.autocast(data[1])) - return data - - def find_closest(self, timestamp: float, tolerance: float | None = None) -> T | Any | None: - """Find the frame closest to the given timestamp. - - Args: - timestamp: The target timestamp to search for - tolerance: Optional maximum time difference allowed - - Returns: - The data frame closest to the timestamp, or None if no match within tolerance - """ - closest_data = None - closest_diff = float("inf") - - # Check frames before and after the timestamp - for ts, data in self.iterate_ts(): - diff = abs(ts - timestamp) - - if diff < closest_diff: - closest_diff = diff - closest_data = data - elif diff > closest_diff: - # We're moving away from the target, can stop - break - - if tolerance is not None and closest_diff > tolerance: - return None - - return closest_data - - def find_closest_seek( - self, relative_seconds: float, tolerance: float | None = None - ) -> T | Any | None: - """Find the frame closest to a time relative to the start. - - Args: - relative_seconds: Seconds from the start of the dataset - tolerance: Optional maximum time difference allowed - - Returns: - The data frame closest to the relative timestamp, or None if no match within tolerance - """ - # Get the first timestamp - first_ts = self.first_timestamp() - if first_ts is None: - return None - - # Calculate absolute timestamp and use find_closest - target_timestamp = first_ts + relative_seconds - return self.find_closest(target_timestamp, tolerance) - - def first_timestamp(self) -> float | None: - """Get the timestamp of the first item in the dataset. - - Returns: - The first timestamp, or None if dataset is empty - """ - try: - ts, _ = next(self.iterate_ts()) - return ts - except StopIteration: - return None - - def iterate(self, loop: bool = False) -> Iterator[T | Any]: - return (x[1] for x in super().iterate(loop=loop)) - - def iterate_ts( - self, - seek: float | None = None, - duration: float | None = None, - from_timestamp: float | None = None, - loop: bool = False, - ) -> Iterator[tuple[float, T] | Any]: - first_ts = None - if (seek is not None) or (duration is not None): - first_ts = self.first_timestamp() - if first_ts is None: - return - - if seek is not None: - from_timestamp = first_ts + seek - - end_timestamp = None - if duration is not None: - end_timestamp = (from_timestamp if from_timestamp else first_ts) + duration - - while True: - for ts, data in super().iterate(): - if from_timestamp is None or ts >= from_timestamp: - if end_timestamp is not None and ts >= end_timestamp: - break - yield (ts, data) - if not loop: - break - - def stream( - self, - speed: float = 1.0, - seek: float | None = None, - duration: float | None = None, - from_timestamp: float | None = None, - loop: bool = False, - ) -> Observable[T | Any]: - def _subscribe(observer, scheduler=None): - from reactivex.disposable import CompositeDisposable, Disposable - - scheduler = scheduler or TimeoutScheduler() - disp = CompositeDisposable() - is_disposed = False - - iterator = self.iterate_ts( - seek=seek, duration=duration, from_timestamp=from_timestamp, loop=loop - ) - - # Get first message - try: - first_ts, first_data = next(iterator) - except StopIteration: - observer.on_completed() - return Disposable() - - # Establish timing reference - start_local_time = time.time() - start_replay_time = first_ts - - # Emit first sample immediately - observer.on_next(first_data) - - # Pre-load next message - try: - next_message = next(iterator) - except StopIteration: - observer.on_completed() - return disp - - def schedule_emission(message) -> None: - nonlocal next_message, is_disposed - - if is_disposed: - return - - ts, data = message - - # Pre-load the following message while we have time - try: - next_message = next(iterator) - except StopIteration: - next_message = None - - # Calculate absolute emission time - target_time = start_local_time + (ts - start_replay_time) / speed - delay = max(0.0, target_time - time.time()) - - def emit() -> None: - if is_disposed: - return - observer.on_next(data) - if next_message is not None: - schedule_emission(next_message) - else: - observer.on_completed() - # Dispose of the scheduler to clean up threads - if hasattr(scheduler, "dispose"): - scheduler.dispose() - - disp.add(scheduler.schedule_relative(delay, lambda sc, _: emit())) - - schedule_emission(next_message) - - # Create a custom disposable that properly cleans up - def dispose() -> None: - nonlocal is_disposed - is_disposed = True - disp.dispose() - # Ensure scheduler is disposed to clean up any threads - if hasattr(scheduler, "dispose"): - scheduler.dispose() - - return Disposable(dispose) - - from reactivex import create - - return create(_subscribe) diff --git a/dimos/utils/testing/__init__.py b/dimos/utils/testing/__init__.py new file mode 100644 index 0000000000..ffb640de39 --- /dev/null +++ b/dimos/utils/testing/__init__.py @@ -0,0 +1,11 @@ +from dimos.utils.testing.moment import Moment, OutputMoment, SensorMoment +from dimos.utils.testing.replay import SensorReplay, TimedSensorReplay, TimedSensorStorage + +__all__ = [ + "Moment", + "OutputMoment", + "SensorMoment", + "SensorReplay", + "TimedSensorReplay", + "TimedSensorStorage", +] diff --git a/dimos/utils/testing/moment.py b/dimos/utils/testing/moment.py new file mode 100644 index 0000000000..436240a48b --- /dev/null +++ b/dimos/utils/testing/moment.py @@ -0,0 +1,99 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from dimos.core.resource import Resource +from dimos.utils.testing.replay import TimedSensorReplay + +if TYPE_CHECKING: + from dimos.core import Transport + +T = TypeVar("T") + + +class SensorMoment(Generic[T], Resource): + value: T | None = None + + def __init__(self, name: str, transport: Transport[T]) -> None: + self.replay: TimedSensorReplay[T] = TimedSensorReplay(name) + self.transport = transport + + def seek(self, timestamp: float) -> None: + self.value = self.replay.find_closest_seek(timestamp) + + def publish(self) -> None: + if self.value is not None: + self.transport.publish(self.value) + + def start(self) -> None: + pass + + def stop(self) -> None: + self.transport.stop() + + +class OutputMoment(Generic[T], Resource): + value: T | None = None + transport: Transport[T] + + def __init__(self, transport: Transport[T]): + self.transport = transport + + def set(self, value: T) -> None: + self.value = value + + def publish(self) -> None: + if self.value is not None: + self.transport.publish(self.value) + + def start(self) -> None: + pass + + def stop(self) -> None: + self.transport.stop() + + +class Moment(Resource): + def moments( + self, *classes: type[SensorMoment[Any]] | type[OutputMoment[Any]] + ) -> list[SensorMoment[Any] | OutputMoment[Any]]: + moments: list[SensorMoment[Any] | OutputMoment[Any]] = [] + for attr_name in dir(self): + attr_value = getattr(self, attr_name) + if isinstance(attr_value, classes): + moments.append(attr_value) + return moments + + def seekable_moments(self) -> list[SensorMoment[Any]]: + return [m for m in self.moments(SensorMoment) if isinstance(m, SensorMoment)] + + def publishable_moments(self) -> list[SensorMoment[Any] | OutputMoment[Any]]: + return self.moments(OutputMoment, SensorMoment) + + def seek(self, timestamp: float) -> None: + for moment in self.seekable_moments(): + moment.seek(timestamp) + + def publish(self) -> None: + for moment in self.publishable_moments(): + moment.publish() + + def start(self) -> None: ... + + def stop(self) -> None: + for moment in self.publishable_moments(): + moment.stop() diff --git a/dimos/utils/testing/replay.py b/dimos/utils/testing/replay.py new file mode 100644 index 0000000000..e9b69b6ecd --- /dev/null +++ b/dimos/utils/testing/replay.py @@ -0,0 +1,409 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections.abc import Callable, Iterator +import functools +import glob +import os +from pathlib import Path +import pickle +import re +import time +from typing import Any, Generic, TypeVar + +from reactivex import ( + from_iterable, + interval, + operators as ops, +) +from reactivex.observable import Observable +from reactivex.scheduler import TimeoutScheduler + +from dimos.utils.data import _get_data_dir, get_data + +T = TypeVar("T") + + +class SensorReplay(Generic[T]): + """Generic sensor data replay utility. + + Args: + name: The name of the test dataset + autocast: Optional function that takes unpickled data and returns a processed result. + For example: lambda data: LidarMessage.from_msg(data) + """ + + def __init__(self, name: str, autocast: Callable[[Any], T] | None = None) -> None: + self.root_dir = get_data(name) + self.autocast = autocast + + def load(self, *names: int | str) -> T | Any | list[T] | list[Any]: + if len(names) == 1: + return self.load_one(names[0]) + return list(map(lambda name: self.load_one(name), names)) + + def load_one(self, name: int | str | Path) -> T | Any: + if isinstance(name, int): + full_path = self.root_dir / f"/{name:03d}.pickle" + elif isinstance(name, Path): + full_path = name + else: + full_path = self.root_dir / Path(f"{name}.pickle") + + with open(full_path, "rb") as f: + data = pickle.load(f) + if self.autocast: + return self.autocast(data) + return data + + def first(self) -> T | Any | None: + try: + return next(self.iterate()) + except StopIteration: + return None + + @functools.cached_property + def files(self) -> list[Path]: + def extract_number(filepath): # type: ignore[no-untyped-def] + """Extract last digits before .pickle extension""" + basename = os.path.basename(filepath) + match = re.search(r"(\d+)\.pickle$", basename) + return int(match.group(1)) if match else 0 + + return sorted( + glob.glob(os.path.join(self.root_dir, "*")), # type: ignore[arg-type] + key=extract_number, + ) + + def iterate(self, loop: bool = False) -> Iterator[T | Any]: + while True: + for file_path in self.files: + yield self.load_one(Path(file_path)) + if not loop: + break + + def stream(self, rate_hz: float | None = None, loop: bool = False) -> Observable[T | Any]: + if rate_hz is None: + return from_iterable(self.iterate(loop=loop)) + + sleep_time = 1.0 / rate_hz + + return from_iterable(self.iterate(loop=loop)).pipe( + ops.zip(interval(sleep_time)), + ops.map(lambda x: x[0] if isinstance(x, tuple) else x), + ) + + +class SensorStorage(Generic[T]): + """Generic sensor data storage utility + . + Creates a directory in the test data directory and stores pickled sensor data. + + Args: + name: The name of the storage directory + autocast: Optional function that takes data and returns a processed result before storage. + """ + + def __init__(self, name: str, autocast: Callable[[T], Any] | None = None) -> None: + self.name = name + self.autocast = autocast + self.cnt = 0 + + # Create storage directory in the data dir + self.root_dir = _get_data_dir() / name + + # Check if directory exists and is not empty + if self.root_dir.exists(): + existing_files = list(self.root_dir.glob("*.pickle")) + if existing_files: + raise RuntimeError( + f"Storage directory '{name}' already exists and contains {len(existing_files)} files. " + f"Please use a different name or clean the directory first." + ) + else: + # Create the directory + self.root_dir.mkdir(parents=True, exist_ok=True) + + def consume_stream(self, observable: Observable[T | Any]) -> None: + """Consume an observable stream of sensor data without saving.""" + return observable.subscribe(self.save_one) # type: ignore[arg-type, return-value] + + def save_stream(self, observable: Observable[T | Any]) -> Observable[int]: + """Save an observable stream of sensor data to pickle files.""" + return observable.pipe(ops.map(lambda frame: self.save_one(frame))) + + def save(self, *frames) -> int: # type: ignore[no-untyped-def] + """Save one or more frames to pickle files.""" + for frame in frames: + self.save_one(frame) + return self.cnt + + def save_one(self, frame) -> int: # type: ignore[no-untyped-def] + """Save a single frame to a pickle file.""" + file_name = f"{self.cnt:03d}.pickle" + full_path = self.root_dir / file_name + + if full_path.exists(): + raise RuntimeError(f"File {full_path} already exists") + + # Apply autocast if provided + data_to_save = frame + if self.autocast: + data_to_save = self.autocast(frame) + # Convert to raw message if frame has a raw_msg attribute + elif hasattr(frame, "raw_msg"): + data_to_save = frame.raw_msg + + with open(full_path, "wb") as f: + pickle.dump(data_to_save, f) + + self.cnt += 1 + return self.cnt + + +class TimedSensorStorage(SensorStorage[T]): + def save_one(self, frame: T) -> int: + return super().save_one((time.time(), frame)) + + +class TimedSensorReplay(SensorReplay[T]): + def load_one(self, name: int | str | Path) -> T | Any: + if isinstance(name, int): + full_path = self.root_dir / f"/{name:03d}.pickle" + elif isinstance(name, Path): + full_path = name + else: + full_path = self.root_dir / Path(f"{name}.pickle") + + with open(full_path, "rb") as f: + data = pickle.load(f) + if self.autocast: + return (data[0], self.autocast(data[1])) + return data + + def find_closest(self, timestamp: float, tolerance: float | None = None) -> T | Any | None: + """Find the frame closest to the given timestamp. + + Args: + timestamp: The target timestamp to search for + tolerance: Optional maximum time difference allowed + + Returns: + The data frame closest to the timestamp, or None if no match within tolerance + """ + closest_data = None + closest_diff = float("inf") + + # Check frames before and after the timestamp + for ts, data in self.iterate_ts(): + diff = abs(ts - timestamp) + + if diff < closest_diff: + closest_diff = diff + closest_data = data + elif diff > closest_diff: + # We're moving away from the target, can stop + break + + if tolerance is not None and closest_diff > tolerance: + return None + + return closest_data + + def find_closest_seek( + self, relative_seconds: float, tolerance: float | None = None + ) -> T | Any | None: + """Find the frame closest to a time relative to the start. + + Args: + relative_seconds: Seconds from the start of the dataset + tolerance: Optional maximum time difference allowed + + Returns: + The data frame closest to the relative timestamp, or None if no match within tolerance + """ + # Get the first timestamp + first_ts = self.first_timestamp() + if first_ts is None: + return None + + # Calculate absolute timestamp and use find_closest + target_timestamp = first_ts + relative_seconds + return self.find_closest(target_timestamp, tolerance) + + def first_timestamp(self) -> float | None: + """Get the timestamp of the first item in the dataset. + + Returns: + The first timestamp, or None if dataset is empty + """ + try: + ts, _ = next(self.iterate_ts()) + return ts + except StopIteration: + return None + + def iterate(self, loop: bool = False) -> Iterator[T | Any]: + return (x[1] for x in super().iterate(loop=loop)) # type: ignore[index] + + def iterate_duration(self, **kwargs: Any) -> Iterator[tuple[float, T] | Any]: + """Iterate with timestamps relative to the start of the dataset.""" + first_ts = self.first_timestamp() + if first_ts is None: + return + for ts, data in self.iterate_ts(**kwargs): + yield (ts - first_ts, data) + + def iterate_realtime(self, speed: float = 1.0, **kwargs: Any) -> Iterator[T | Any]: + """Iterate data, sleeping to match original timing. + + Args: + speed: Playback speed multiplier (1.0 = realtime, 2.0 = 2x speed) + **kwargs: Passed to iterate_ts (seek, duration, from_timestamp, loop) + """ + iterator = self.iterate_ts(**kwargs) + + try: + first_ts, first_data = next(iterator) + except StopIteration: + return + + start_time = time.time() + start_ts = first_ts + yield first_data + + for ts, data in iterator: + target_time = start_time + (ts - start_ts) / speed + sleep_duration = target_time - time.time() + if sleep_duration > 0: + time.sleep(sleep_duration) + yield data + + def iterate_ts( + self, + seek: float | None = None, + duration: float | None = None, + from_timestamp: float | None = None, + loop: bool = False, + ) -> Iterator[tuple[float, T] | Any]: + """Iterate with absolute timestamps, with optional seek and duration.""" + first_ts = None + if (seek is not None) or (duration is not None): + first_ts = self.first_timestamp() + if first_ts is None: + return + + if seek is not None: + from_timestamp = first_ts + seek # type: ignore[operator] + + end_timestamp = None + if duration is not None: + end_timestamp = (from_timestamp if from_timestamp else first_ts) + duration # type: ignore[operator] + + while True: + for ts, data in super().iterate(): # type: ignore[misc] + if from_timestamp is None or ts >= from_timestamp: + if end_timestamp is not None and ts >= end_timestamp: + break + yield (ts, data) + if not loop: + break + + def stream( # type: ignore[override] + self, + speed: float = 1.0, + seek: float | None = None, + duration: float | None = None, + from_timestamp: float | None = None, + loop: bool = False, + ) -> Observable[T | Any]: + def _subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] + from reactivex.disposable import CompositeDisposable, Disposable + + scheduler = scheduler or TimeoutScheduler() + disp = CompositeDisposable() + is_disposed = False + + iterator = self.iterate_ts( + seek=seek, duration=duration, from_timestamp=from_timestamp, loop=loop + ) + + # Get first message + try: + first_ts, first_data = next(iterator) + except StopIteration: + observer.on_completed() + return Disposable() + + # Establish timing reference + start_local_time = time.time() + start_replay_time = first_ts + + # Emit first sample immediately + observer.on_next(first_data) + + # Pre-load next message + try: + next_message = next(iterator) + except StopIteration: + observer.on_completed() + return disp + + def schedule_emission(message) -> None: # type: ignore[no-untyped-def] + nonlocal next_message, is_disposed + + if is_disposed: + return + + ts, data = message + + # Pre-load the following message while we have time + try: + next_message = next(iterator) + except StopIteration: + next_message = None + + # Calculate absolute emission time + target_time = start_local_time + (ts - start_replay_time) / speed + delay = max(0.0, target_time - time.time()) + + def emit() -> None: + if is_disposed: + return + observer.on_next(data) + if next_message is not None: + schedule_emission(next_message) + else: + observer.on_completed() + # Dispose of the scheduler to clean up threads + if hasattr(scheduler, "dispose"): + scheduler.dispose() + + scheduler.schedule_relative(delay, lambda sc, _: emit()) + + schedule_emission(next_message) + + # Create a custom disposable that properly cleans up + def dispose() -> None: + nonlocal is_disposed + is_disposed = True + disp.dispose() + # Ensure scheduler is disposed to clean up any threads + if hasattr(scheduler, "dispose"): + scheduler.dispose() + + return Disposable(dispose) + + from reactivex import create + + return create(_subscribe) diff --git a/dimos/utils/testing/test_moment.py b/dimos/utils/testing/test_moment.py new file mode 100644 index 0000000000..92b71e59ac --- /dev/null +++ b/dimos/utils/testing/test_moment.py @@ -0,0 +1,75 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import time + +from dimos.core import LCMTransport +from dimos.msgs.geometry_msgs import PoseStamped, Transform +from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 +from dimos.protocol.tf import TF +from dimos.robot.unitree.connection import go2 +from dimos.utils.data import get_data +from dimos.utils.testing.moment import Moment, SensorMoment + +data_dir = get_data("unitree_go2_office_walk2") + + +class Go2Moment(Moment): + lidar: SensorMoment[PointCloud2] + video: SensorMoment[Image] + odom: SensorMoment[PoseStamped] + + def __init__(self) -> None: + self.lidar = SensorMoment(f"{data_dir}/lidar", LCMTransport("/lidar", PointCloud2)) + self.video = SensorMoment(f"{data_dir}/video", LCMTransport("/color_image", Image)) + self.odom = SensorMoment(f"{data_dir}/odom", LCMTransport("/odom", PoseStamped)) + + @property + def transforms(self) -> list[Transform]: + if self.odom.value is None: + return [] + + # we just make sure to change timestamps so that we can jump + # back and forth through time and foxglove doesn't get confused + odom = self.odom.value + odom.ts = time.time() + return go2.GO2Connection._odom_to_tf(odom) + + def publish(self) -> None: + t = TF() + t.publish(*self.transforms) + t.stop() + + camera_info = go2._camera_info_static() + camera_info.ts = time.time() + camera_info_transport: LCMTransport[CameraInfo] = LCMTransport("/camera_info", CameraInfo) + camera_info_transport.publish(camera_info) + camera_info_transport.stop() + + super().publish() + + +def test_moment_seek_and_publish() -> None: + moment = Go2Moment() + + # Seek to 5 seconds + moment.seek(5.0) + + # Check that frames were loaded + assert moment.lidar.value is not None + assert moment.video.value is not None + assert moment.odom.value is not None + + # Publish all frames + moment.publish() + moment.stop() diff --git a/dimos/utils/testing/test_replay.py b/dimos/utils/testing/test_replay.py new file mode 100644 index 0000000000..44b6a232c8 --- /dev/null +++ b/dimos/utils/testing/test_replay.py @@ -0,0 +1,279 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from reactivex import operators as ops + +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.utils.data import get_data +from dimos.utils.testing import replay + + +def test_sensor_replay() -> None: + counter = 0 + for message in replay.SensorReplay(name="office_lidar").iterate(): + counter += 1 + assert isinstance(message, dict) + assert counter == 500 + + +def test_sensor_replay_cast() -> None: + counter = 0 + for message in replay.SensorReplay( + name="office_lidar", autocast=LidarMessage.from_msg + ).iterate(): + counter += 1 + assert isinstance(message, LidarMessage) + assert counter == 500 + + +def test_timed_sensor_replay() -> None: + get_data("unitree_office_walk") + odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + itermsgs = [] + for msg in odom_store.iterate(): + itermsgs.append(msg) + if len(itermsgs) > 9: + break + + assert len(itermsgs) == 10 + + print("\n") + + timed_msgs = [] + + for msg in odom_store.stream().pipe(ops.take(10), ops.to_list()).run(): + timed_msgs.append(msg) + + assert len(timed_msgs) == 10 + + for i in range(10): + print(itermsgs[i], timed_msgs[i]) + assert itermsgs[i] == timed_msgs[i] + + +def test_iterate_ts_no_seek() -> None: + """Test iterate_ts without seek (start_timestamp=None)""" + odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Test without seek + ts_msgs = [] + for ts, msg in odom_store.iterate_ts(): + ts_msgs.append((ts, msg)) + if len(ts_msgs) >= 5: + break + + assert len(ts_msgs) == 5 + # Check that we get tuples of (timestamp, data) + for ts, msg in ts_msgs: + assert isinstance(ts, float) + assert isinstance(msg, Odometry) + + +def test_iterate_ts_with_from_timestamp() -> None: + """Test iterate_ts with from_timestamp (absolute timestamp)""" + odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # First get all messages to find a good seek point + all_msgs = [] + for ts, msg in odom_store.iterate_ts(): + all_msgs.append((ts, msg)) + if len(all_msgs) >= 10: + break + + # Seek to timestamp of 5th message + seek_timestamp = all_msgs[4][0] + + # Test with from_timestamp + seeked_msgs = [] + for ts, msg in odom_store.iterate_ts(from_timestamp=seek_timestamp): + seeked_msgs.append((ts, msg)) + if len(seeked_msgs) >= 5: + break + + assert len(seeked_msgs) == 5 + # First message should be at or after seek timestamp + assert seeked_msgs[0][0] >= seek_timestamp + # Should match the data from position 5 onward + assert seeked_msgs[0][1] == all_msgs[4][1] + + +def test_iterate_ts_with_relative_seek() -> None: + """Test iterate_ts with seek (relative seconds after first timestamp)""" + odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Get first few messages to understand timing + all_msgs = [] + for ts, msg in odom_store.iterate_ts(): + all_msgs.append((ts, msg)) + if len(all_msgs) >= 10: + break + + # Calculate relative seek time (e.g., 0.5 seconds after start) + first_ts = all_msgs[0][0] + seek_seconds = 0.5 + expected_start_ts = first_ts + seek_seconds + + # Test with relative seek + seeked_msgs = [] + for ts, msg in odom_store.iterate_ts(seek=seek_seconds): + seeked_msgs.append((ts, msg)) + if len(seeked_msgs) >= 5: + break + + # First message should be at or after expected timestamp + assert seeked_msgs[0][0] >= expected_start_ts + # Make sure we're actually skipping some messages + assert seeked_msgs[0][0] > first_ts + + +def test_stream_with_seek() -> None: + """Test stream method with seek parameters""" + odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Test stream with relative seek + msgs_with_seek = [] + for msg in odom_store.stream(seek=0.2).pipe(ops.take(5), ops.to_list()).run(): + msgs_with_seek.append(msg) + + assert len(msgs_with_seek) == 5 + + # Test stream with from_timestamp + # First get a reference timestamp + first_msgs = [] + for msg in odom_store.stream().pipe(ops.take(3), ops.to_list()).run(): + first_msgs.append(msg) + + # Now test from_timestamp (would need actual timestamps from iterate_ts to properly test) + # This is a basic test to ensure the parameter is accepted + msgs_with_timestamp = [] + for msg in ( + odom_store.stream(from_timestamp=1000000000.0).pipe(ops.take(3), ops.to_list()).run() + ): + msgs_with_timestamp.append(msg) + + +def test_duration_with_loop() -> None: + """Test duration parameter with looping in TimedSensorReplay""" + odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Collect timestamps from a small duration window + collected_ts = [] + duration = 0.3 # 300ms window + + # First pass: collect timestamps in the duration window + for ts, _msg in odom_store.iterate_ts(duration=duration): + collected_ts.append(ts) + if len(collected_ts) >= 100: # Safety limit + break + + # Should have some messages but not too many + assert len(collected_ts) > 0 + assert len(collected_ts) < 20 # Assuming ~30Hz data + + # Test looping with duration - should repeat the same window + loop_count = 0 + prev_ts = None + + for ts, _msg in odom_store.iterate_ts(duration=duration, loop=True): + if prev_ts is not None and ts < prev_ts: + # We've looped back to the beginning + loop_count += 1 + if loop_count >= 2: # Stop after 2 full loops + break + prev_ts = ts + + assert loop_count >= 2 # Verify we actually looped + + +def test_first_methods() -> None: + """Test first() and first_timestamp() methods""" + + # Test SensorReplay.first() + lidar_replay = replay.SensorReplay("office_lidar", autocast=LidarMessage.from_msg) + + print("first file", lidar_replay.files[0]) + # Verify the first file ends with 000.pickle using regex + assert re.search(r"000\.pickle$", str(lidar_replay.files[0])), ( + f"Expected first file to end with 000.pickle, got {lidar_replay.files[0]}" + ) + + first_msg = lidar_replay.first() + assert first_msg is not None + assert isinstance(first_msg, LidarMessage) + + # Verify it's the same type as first item from iterate() + first_from_iterate = next(lidar_replay.iterate()) + print("DONE") + assert type(first_msg) is type(first_from_iterate) + # Since LidarMessage.from_msg uses time.time(), timestamps will be slightly different + assert abs(first_msg.ts - first_from_iterate.ts) < 1.0 # Within 1 second tolerance + + # Test TimedSensorReplay.first_timestamp() + odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + first_ts = odom_store.first_timestamp() + assert first_ts is not None + assert isinstance(first_ts, float) + + # Verify it matches the timestamp from iterate_ts + ts_from_iterate, _ = next(odom_store.iterate_ts()) + assert first_ts == ts_from_iterate + + # Test that first() returns just the data + first_data = odom_store.first() + assert first_data is not None + assert isinstance(first_data, Odometry) + + +def test_find_closest() -> None: + """Test find_closest method in TimedSensorReplay""" + odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Get some reference timestamps + timestamps = [] + for ts, _msg in odom_store.iterate_ts(): + timestamps.append(ts) + if len(timestamps) >= 10: + break + + # Test exact match + target_ts = timestamps[5] + result = odom_store.find_closest(target_ts) + assert result is not None + assert isinstance(result, Odometry) + + # Test between timestamps + mid_ts = (timestamps[3] + timestamps[4]) / 2 + result = odom_store.find_closest(mid_ts) + assert result is not None + + # Test with tolerance + far_future = timestamps[-1] + 100.0 + result = odom_store.find_closest(far_future, tolerance=1.0) + assert result is None # Too far away + + result = odom_store.find_closest(timestamps[0] - 0.001, tolerance=0.01) + assert result is not None # Within tolerance + + # Test find_closest_seek + result = odom_store.find_closest_seek(0.5) # 0.5 seconds from start + assert result is not None + assert isinstance(result, Odometry) + + # Test with negative seek (before start) + result = odom_store.find_closest_seek(-1.0) + assert result is not None # Should still return closest (first frame) diff --git a/dimos/utils/threadpool.py b/dimos/utils/threadpool.py index 45625e9980..a2adc90725 100644 --- a/dimos/utils/threadpool.py +++ b/dimos/utils/threadpool.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,9 @@ from reactivex.scheduler import ThreadPoolScheduler -from .logging_config import logger +from .logging_config import setup_logger + +logger = setup_logger() def get_max_workers() -> int: diff --git a/dimos/utils/transform_utils.py b/dimos/utils/transform_utils.py index 21421b4390..ed82f6116f 100644 --- a/dimos/utils/transform_utils.py +++ b/dimos/utils/transform_utils.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,17 +14,17 @@ import numpy as np -from scipy.spatial.transform import Rotation as R +from scipy.spatial.transform import Rotation as R # type: ignore[import-untyped] from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 def normalize_angle(angle: float) -> float: """Normalize angle to [-pi, pi] range""" - return np.arctan2(np.sin(angle), np.cos(angle)) + return np.arctan2(np.sin(angle), np.cos(angle)) # type: ignore[no-any-return] -def pose_to_matrix(pose: Pose) -> np.ndarray: +def pose_to_matrix(pose: Pose) -> np.ndarray: # type: ignore[type-arg] """ Convert pose to 4x4 homogeneous transform matrix. @@ -57,7 +57,7 @@ def pose_to_matrix(pose: Pose) -> np.ndarray: return T -def matrix_to_pose(T: np.ndarray) -> Pose: +def matrix_to_pose(T: np.ndarray) -> Pose: # type: ignore[type-arg] """ Convert 4x4 transformation matrix to Pose object. @@ -80,7 +80,7 @@ def matrix_to_pose(T: np.ndarray) -> Pose: return Pose(pos, orientation) -def apply_transform(pose: Pose, transform: np.ndarray | Transform) -> Pose: +def apply_transform(pose: Pose, transform: np.ndarray | Transform) -> Pose: # type: ignore[type-arg] """ Apply a transformation matrix to a pose. @@ -202,7 +202,7 @@ def robot_to_optical_frame(pose: Pose) -> Pose: ) -def yaw_towards_point(position: Vector3, target_point: Vector3 = None) -> float: +def yaw_towards_point(position: Vector3, target_point: Vector3 = None) -> float: # type: ignore[assignment] """ Calculate yaw angle from target point to position (away from target). This is commonly used for object orientation in grasping applications. @@ -219,10 +219,10 @@ def yaw_towards_point(position: Vector3, target_point: Vector3 = None) -> float: target_point = Vector3(0.0, 0.0, 0.0) direction_x = position.x - target_point.x direction_y = position.y - target_point.y - return np.arctan2(direction_y, direction_x) + return np.arctan2(direction_y, direction_x) # type: ignore[no-any-return] -def create_transform_from_6dof(translation: Vector3, euler_angles: Vector3) -> np.ndarray: +def create_transform_from_6dof(translation: Vector3, euler_angles: Vector3) -> np.ndarray: # type: ignore[type-arg] """ Create a 4x4 transformation matrix from 6DOF parameters. @@ -247,7 +247,7 @@ def create_transform_from_6dof(translation: Vector3, euler_angles: Vector3) -> n return T -def invert_transform(T: np.ndarray) -> np.ndarray: +def invert_transform(T: np.ndarray) -> np.ndarray: # type: ignore[type-arg] """ Invert a 4x4 transformation matrix efficiently. @@ -271,7 +271,7 @@ def invert_transform(T: np.ndarray) -> np.ndarray: return T_inv -def compose_transforms(*transforms: np.ndarray) -> np.ndarray: +def compose_transforms(*transforms: np.ndarray) -> np.ndarray: # type: ignore[type-arg] """ Compose multiple transformation matrices. @@ -345,7 +345,7 @@ def get_distance(pose1: Pose | Vector3, pose2: Pose | Vector3) -> float: dy = pose1.y - pose2.y dz = pose1.z - pose2.z - return np.linalg.norm(np.array([dx, dy, dz])) + return np.linalg.norm(np.array([dx, dy, dz])) # type: ignore[return-value] def offset_distance( diff --git a/dimos/utils/trigonometry.py b/dimos/utils/trigonometry.py new file mode 100644 index 0000000000..528192050c --- /dev/null +++ b/dimos/utils/trigonometry.py @@ -0,0 +1,19 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + + +def angle_diff(a: float, b: float) -> float: + return (a - b + math.pi) % (2 * math.pi) - math.pi diff --git a/dimos/utils/urdf.py b/dimos/utils/urdf.py new file mode 100644 index 0000000000..474658df1a --- /dev/null +++ b/dimos/utils/urdf.py @@ -0,0 +1,69 @@ +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""URDF generation utilities.""" + +from __future__ import annotations + + +def box_urdf( + width: float, + height: float, + depth: float, + name: str = "box_robot", + mass: float = 1.0, + rgba: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.5), +) -> str: + """Generate a simple URDF with a box as the base_link. + + Args: + width: Box size in X direction (meters) + height: Box size in Y direction (meters) + depth: Box size in Z direction (meters) + name: Robot name + mass: Mass of the box (kg) + rgba: Color as (red, green, blue, alpha), default red with 0.5 transparency + + Returns: + URDF XML string + """ + # Simple box inertia (solid cuboid) + ixx = (mass / 12.0) * (height**2 + depth**2) + iyy = (mass / 12.0) * (width**2 + depth**2) + izz = (mass / 12.0) * (width**2 + height**2) + + r, g, b, a = rgba + return f""" + + + + + + + + + + + + + + + + + + + + + +""" diff --git a/dimos/web/README.md b/dimos/web/README.md index 943d7551f9..28f418bb55 100644 --- a/dimos/web/README.md +++ b/dimos/web/README.md @@ -85,7 +85,7 @@ The frontend will be available at http://localhost:3000 ### Unitree Go2 Example ```python -from dimos.agents.agent import OpenAIAgent +from dimos.agents_deprecated.agent import OpenAIAgent from dimos.robot.unitree.unitree_go2 import UnitreeGo2 from dimos.robot.unitree.unitree_skills import MyUnitreeSkills from dimos.web.robot_web_interface import RobotWebInterface @@ -93,7 +93,7 @@ from dimos.web.robot_web_interface import RobotWebInterface robot_ip = os.getenv("ROBOT_IP") # Initialize robot -logger.info("Initializing Unitree Robot") +logger.info("Initializing Unitree Robot") robot = UnitreeGo2(ip=robot_ip, connection_method=connection_method, output_dir=output_dir) diff --git a/dimos/web/command-center-extension/.gitignore b/dimos/web/command-center-extension/.gitignore index 3f7224ed26..1cb79e0e3c 100644 --- a/dimos/web/command-center-extension/.gitignore +++ b/dimos/web/command-center-extension/.gitignore @@ -1,5 +1,6 @@ *.foxe /dist +/dist-standalone /node_modules !/package.json !/package-lock.json diff --git a/dimos/web/command-center-extension/index.html b/dimos/web/command-center-extension/index.html new file mode 100644 index 0000000000..e1e9ce85ad --- /dev/null +++ b/dimos/web/command-center-extension/index.html @@ -0,0 +1,18 @@ + + + + + + Command Center + + + +
+ + + diff --git a/dimos/web/command-center-extension/package-lock.json b/dimos/web/command-center-extension/package-lock.json index 771bae9aaa..09f9be88b4 100644 --- a/dimos/web/command-center-extension/package-lock.json +++ b/dimos/web/command-center-extension/package-lock.json @@ -1,16 +1,17 @@ { "name": "command-center-extension", - "version": "0.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "command-center-extension", - "version": "0.0.0", + "version": "0.0.1", "license": "UNLICENSED", "dependencies": { "@types/pako": "^2.0.4", "d3": "^7.9.0", + "leaflet": "^1.9.4", "pako": "^2.1.0", "react-leaflet": "^4.2.1", "socket.io-client": "^4.8.1" @@ -19,15 +20,735 @@ "@foxglove/eslint-plugin": "2.1.0", "@foxglove/extension": "2.34.0", "@types/d3": "^7.4.3", - "@types/leaflet": "^1.9.20", + "@types/leaflet": "^1.9.21", "@types/react": "18.3.24", "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "^4.3.4", "create-foxglove-extension": "1.0.6", "eslint": "9.34.0", "prettier": "3.6.2", "react": "18.3.1", "react-dom": "^18.3.1", - "typescript": "5.9.2" + "typescript": "5.9.2", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -374,6 +1095,16 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -475,6 +1206,298 @@ "react-dom": "^18.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -488,6 +1511,47 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -834,11 +1898,10 @@ "license": "MIT" }, "node_modules/@types/leaflet": { - "version": "1.9.20", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz", - "integrity": "sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==", + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", "dev": true, - "license": "MIT", "dependencies": { "@types/geojson": "*" } @@ -1139,6 +2202,26 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -1882,6 +2965,12 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2791,6 +3880,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3576,6 +4706,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3617,6 +4761,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4552,6 +5705,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4642,9 +5807,7 @@ "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause", - "peer": true + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" }, "node_modules/levn": { "version": "0.4.1", @@ -4875,6 +6038,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5295,6 +6476,34 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5430,7 +6639,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", - "license": "Hippocratic-2.1", "dependencies": { "@react-leaflet/core": "^2.1.0" }, @@ -5440,6 +6648,15 @@ "react-dom": "^18.0.0" } }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -5631,6 +6848,47 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6044,6 +7302,15 @@ "node": ">= 12" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -6488,6 +7755,51 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6798,6 +8110,109 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/watchpack": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", @@ -7164,6 +8579,12 @@ "node": ">=0.4.0" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/dimos/web/command-center-extension/package.json b/dimos/web/command-center-extension/package.json index 36eb7854c4..f3cd836205 100644 --- a/dimos/web/command-center-extension/package.json +++ b/dimos/web/command-center-extension/package.json @@ -1,15 +1,18 @@ { "name": "command-center-extension", "displayName": "command-center-extension", - "description": "", + "description": "2D costmap visualization with robot and path overlay", "publisher": "dimensional", "homepage": "", - "version": "0.0.0", + "version": "0.0.1", "license": "UNLICENSED", "main": "./dist/extension.js", "keywords": [], "scripts": { "build": "foxglove-extension build", + "build:standalone": "vite build", + "dev": "vite", + "preview": "vite preview", "foxglove:prepublish": "foxglove-extension build --mode production", "lint": "eslint .", "lint:ci": "eslint .", @@ -22,19 +25,22 @@ "@foxglove/eslint-plugin": "2.1.0", "@foxglove/extension": "2.34.0", "@types/d3": "^7.4.3", - "@types/leaflet": "^1.9.20", + "@types/leaflet": "^1.9.21", "@types/react": "18.3.24", "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "^4.3.4", "create-foxglove-extension": "1.0.6", "eslint": "9.34.0", "prettier": "3.6.2", "react": "18.3.1", "react-dom": "^18.3.1", - "typescript": "5.9.2" + "typescript": "5.9.2", + "vite": "^6.0.0" }, "dependencies": { "@types/pako": "^2.0.4", "d3": "^7.9.0", + "leaflet": "^1.9.4", "pako": "^2.1.0", "react-leaflet": "^4.2.1", "socket.io-client": "^4.8.1" diff --git a/dimos/web/command-center-extension/src/App.tsx b/dimos/web/command-center-extension/src/App.tsx index 838f15df59..dc0c90e7ea 100644 --- a/dimos/web/command-center-extension/src/App.tsx +++ b/dimos/web/command-center-extension/src/App.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import Connection from "./Connection"; import ExplorePanel from "./ExplorePanel"; import GpsButton from "./GpsButton"; +import Button from "./Button"; import KeyboardControlPanel from "./KeyboardControlPanel"; import VisualizerWrapper from "./components/VisualizerWrapper"; import LeafletMap from "./components/LeafletMap"; @@ -77,6 +78,16 @@ export default function App(): React.ReactElement { connectionRef.current?.stopMoveCommand(); }, []); + const handleReturnHome = React.useCallback(() => { + connectionRef.current?.worldClick(0, 0); + }, []); + + const handleStop = React.useCallback(() => { + if (state.robotPose) { + connectionRef.current?.worldClick(state.robotPose.coords[0]!, state.robotPose.coords[1]!); + } + }, [state.robotPose]); + return (
{isGpsMode ? ( @@ -105,6 +116,8 @@ export default function App(): React.ReactElement { onUseCostmap={() => setIsGpsMode(false)} > + + + + + ); +} else { + console.error("Root element not found"); +} diff --git a/dimos/web/command-center-extension/vite.config.ts b/dimos/web/command-center-extension/vite.config.ts new file mode 100644 index 0000000000..064f2bc7c5 --- /dev/null +++ b/dimos/web/command-center-extension/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { resolve } from "path"; + +export default defineConfig({ + plugins: [react()], + root: ".", + build: { + outDir: "dist-standalone", + emptyDirBeforeWrite: true, + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + }, + }, + }, + server: { + port: 3000, + open: false, + }, +}); diff --git a/dimos/web/dimos_interface/api/README.md b/dimos/web/dimos_interface/api/README.md index 38fd275e8a..a2c15015e8 100644 --- a/dimos/web/dimos_interface/api/README.md +++ b/dimos/web/dimos_interface/api/README.md @@ -31,10 +31,10 @@ The server will start on `http://0.0.0.0:5555`. ## Integration with DIMOS Agents -See DimOS Documentation for more info. +See DimOS Documentation for more info. ```python -from dimos.agents.agent import OpenAIAgent +from dimos.agents_deprecated.agent import OpenAIAgent from dimos.robot.unitree.unitree_go2 import UnitreeGo2 from dimos.robot.unitree.unitree_skills import MyUnitreeSkills from dimos.web.robot_web_interface import RobotWebInterface @@ -42,7 +42,7 @@ from dimos.web.robot_web_interface import RobotWebInterface robot_ip = os.getenv("ROBOT_IP") # Initialize robot -logger.info("Initializing Unitree Robot") +logger.info("Initializing Unitree Robot") robot = UnitreeGo2(ip=robot_ip, connection_method=connection_method, output_dir=output_dir) @@ -83,4 +83,4 @@ The frontend and backend are separate applications: 3. Vite's development server proxies requests from `/unitree/*` to the FastAPI server 4. The `unitree` command in the terminal interface sends requests to these endpoints -This architecture allows the frontend and backend to be developed and run independently. \ No newline at end of file +This architecture allows the frontend and backend to be developed and run independently. diff --git a/dimos/web/dimos_interface/api/requirements.txt b/dimos/web/dimos_interface/api/requirements.txt index a906146c35..a1ab33e428 100644 --- a/dimos/web/dimos_interface/api/requirements.txt +++ b/dimos/web/dimos_interface/api/requirements.txt @@ -4,4 +4,4 @@ reactivex==4.0.4 numpy<2.0.0 # Specify older NumPy version for cv2 compatibility opencv-python==4.8.1.78 python-multipart==0.0.6 -jinja2==3.1.2 \ No newline at end of file +jinja2==3.1.2 diff --git a/dimos/web/dimos_interface/api/server.py b/dimos/web/dimos_interface/api/server.py index 4f9979c085..6692e90f46 100644 --- a/dimos/web/dimos_interface/api/server.py +++ b/dimos/web/dimos_interface/api/server.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,12 +39,12 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from fastapi.templating import Jinja2Templates -import ffmpeg +import ffmpeg # type: ignore[import-untyped] import numpy as np import reactivex as rx from reactivex import operators as ops from reactivex.disposable import SingleAssignmentDisposable -import soundfile as sf +import soundfile as sf # type: ignore[import-untyped] from sse_starlette.sse import EventSourceResponse import uvicorn @@ -55,7 +55,7 @@ class FastAPIServer(EdgeIO): - def __init__( + def __init__( # type: ignore[no-untyped-def] self, dev_name: str = "FastAPI Server", edge_type: str = "Bidirectional", @@ -68,6 +68,7 @@ def __init__( print("Starting FastAPIServer initialization...") # Debug print super().__init__(dev_name, edge_type) self.app = FastAPI() + self._server: uvicorn.Server | None = None # Add CORS middleware with more permissive settings for development self.app.add_middleware( @@ -86,17 +87,17 @@ def __init__( self.streams = streams self.active_streams = {} self.stream_locks = {key: Lock() for key in self.streams} - self.stream_queues = {} - self.stream_disposables = {} + self.stream_queues = {} # type: ignore[var-annotated] + self.stream_disposables = {} # type: ignore[var-annotated] # Initialize text streams self.text_streams = text_streams or {} - self.text_queues = {} + self.text_queues = {} # type: ignore[var-annotated] self.text_disposables = {} - self.text_clients = set() + self.text_clients = set() # type: ignore[var-annotated] # Create a Subject for text queries - self.query_subject = rx.subject.Subject() + self.query_subject = rx.subject.Subject() # type: ignore[var-annotated] self.query_stream = self.query_subject.pipe(ops.share()) self.audio_subject = audio_subject @@ -122,15 +123,15 @@ def __init__( self.setup_routes() print("FastAPIServer initialization complete") # Debug print - def process_frame_fastapi(self, frame): + def process_frame_fastapi(self, frame): # type: ignore[no-untyped-def] """Convert frame to JPEG format for streaming.""" _, buffer = cv2.imencode(".jpg", frame) return buffer.tobytes() - def stream_generator(self, key): + def stream_generator(self, key): # type: ignore[no-untyped-def] """Generate frames for a given video stream.""" - def generate(): + def generate(): # type: ignore[no-untyped-def] if key not in self.stream_queues: self.stream_queues[key] = Queue(maxsize=10) @@ -175,17 +176,18 @@ def generate(): return generate - def create_video_feed_route(self, key): + def create_video_feed_route(self, key): # type: ignore[no-untyped-def] """Create a video feed route for a specific stream.""" - async def video_feed(): + async def video_feed(): # type: ignore[no-untyped-def] return StreamingResponse( - self.stream_generator(key)(), media_type="multipart/x-mixed-replace; boundary=frame" + self.stream_generator(key)(), # type: ignore[no-untyped-call] + media_type="multipart/x-mixed-replace; boundary=frame", ) return video_feed - async def text_stream_generator(self, key): + async def text_stream_generator(self, key): # type: ignore[no-untyped-def] """Generate SSE events for text stream.""" client_id = id(object()) self.text_clients.add(client_id) @@ -210,7 +212,7 @@ async def text_stream_generator(self, key): self.text_clients.remove(client_id) @staticmethod - def _decode_audio(raw: bytes) -> tuple[np.ndarray, int]: + def _decode_audio(raw: bytes) -> tuple[np.ndarray, int]: # type: ignore[type-arg] """Convert the webm/opus blob sent by the browser into mono 16-kHz PCM.""" try: # Use ffmpeg to convert to 16-kHz mono 16-bit PCM WAV in memory @@ -234,23 +236,23 @@ def _decode_audio(raw: bytes) -> tuple[np.ndarray, int]: return np.array(audio), sr except Exception as exc: print(f"ffmpeg decoding failed: {exc}") - return None, None + return None, None # type: ignore[return-value] def setup_routes(self) -> None: """Set up FastAPI routes.""" @self.app.get("/streams") - async def get_streams(): + async def get_streams(): # type: ignore[no-untyped-def] """Get list of available video streams""" return {"streams": list(self.streams.keys())} @self.app.get("/text_streams") - async def get_text_streams(): + async def get_text_streams(): # type: ignore[no-untyped-def] """Get list of available text streams""" return {"streams": list(self.text_streams.keys())} @self.app.get("/", response_class=HTMLResponse) - async def index(request: Request): + async def index(request: Request): # type: ignore[no-untyped-def] stream_keys = list(self.streams.keys()) text_stream_keys = list(self.text_streams.keys()) return self.templates.TemplateResponse( @@ -264,7 +266,7 @@ async def index(request: Request): ) @self.app.post("/submit_query") - async def submit_query(query: str = Form(...)): + async def submit_query(query: str = Form(...)): # type: ignore[no-untyped-def] # Using Form directly as a dependency ensures proper form handling try: if query: @@ -280,7 +282,7 @@ async def submit_query(query: str = Form(...)): ) @self.app.post("/upload_audio") - async def upload_audio(file: UploadFile = File(...)): + async def upload_audio(file: UploadFile = File(...)): # type: ignore[no-untyped-def] """Handle audio upload from the browser.""" if self.audio_subject is None: return JSONResponse( @@ -306,7 +308,7 @@ async def upload_audio(file: UploadFile = File(...)): # Push to reactive stream self.audio_subject.on_next(event) - print(f"Received audio – {event.data.shape[0] / sr:.2f} s, {sr} Hz") + print(f"Received audio - {event.data.shape[0] / sr:.2f} s, {sr} Hz") return {"success": True} except Exception as e: print(f"Failed to process uploaded audio: {e}") @@ -314,12 +316,12 @@ async def upload_audio(file: UploadFile = File(...)): # Unitree API endpoints @self.app.get("/unitree/status") - async def unitree_status(): + async def unitree_status(): # type: ignore[no-untyped-def] """Check the status of the Unitree API server""" return JSONResponse({"status": "online", "service": "unitree"}) @self.app.post("/unitree/command") - async def unitree_command(request: Request): + async def unitree_command(request: Request): # type: ignore[no-untyped-def] """Process commands sent from the terminal frontend""" try: data = await request.json() @@ -343,19 +345,27 @@ async def unitree_command(request: Request): ) @self.app.get("/text_stream/{key}") - async def text_stream(key: str): + async def text_stream(key: str): # type: ignore[no-untyped-def] if key not in self.text_streams: raise HTTPException(status_code=404, detail=f"Text stream '{key}' not found") - return EventSourceResponse(self.text_stream_generator(key)) + return EventSourceResponse(self.text_stream_generator(key)) # type: ignore[no-untyped-call] for key in self.streams: - self.app.get(f"/video_feed/{key}")(self.create_video_feed_route(key)) + self.app.get(f"/video_feed/{key}")(self.create_video_feed_route(key)) # type: ignore[no-untyped-call] def run(self) -> None: - """Run the FastAPI server.""" - uvicorn.run( - self.app, host=self.host, port=self.port - ) # TODO: Translate structure to enable in-built workers' + config = uvicorn.Config( + self.app, + host=self.host, + port=self.port, + log_level="error", # Reduce verbosity + ) + self._server = uvicorn.Server(config) + self._server.run() + + def shutdown(self) -> None: + if self._server is not None: + self._server.should_exit = True if __name__ == "__main__": diff --git a/dimos/web/dimos_interface/api/templates/index_fastapi.html b/dimos/web/dimos_interface/api/templates/index_fastapi.html index 406557c04a..4cfe943fc7 100644 --- a/dimos/web/dimos_interface/api/templates/index_fastapi.html +++ b/dimos/web/dimos_interface/api/templates/index_fastapi.html @@ -126,13 +126,13 @@ border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } - + .query-form { display: flex; gap: 10px; align-items: center; } - + .query-input { flex-grow: 1; padding: 10px; @@ -140,7 +140,7 @@ border-radius: 5px; font-size: 16px; } - + .query-button { padding: 10px 20px; background-color: #28a745; @@ -151,11 +151,11 @@ font-size: 16px; transition: background-color 0.3s; } - + .query-button:hover { background-color: #218838; } - + /* Voice button styles */ .voice-button { width: 50px; @@ -173,17 +173,17 @@ box-shadow: 0 2px 5px rgba(0,0,0,0.2); position: relative; } - + .voice-button:hover { transform: scale(1.1); box-shadow: 0 4px 8px rgba(0,0,0,0.3); } - + .voice-button.recording { background-color: #ff0000; animation: pulse 1.5s infinite; } - + .voice-button.recording::after { content: ''; position: absolute; @@ -195,13 +195,13 @@ border-radius: 50%; animation: ripple 1.5s infinite; } - + @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } - + @keyframes ripple { 0% { transform: scale(1); @@ -212,7 +212,7 @@ opacity: 0; } } - + .voice-status { position: absolute; top: -25px; @@ -226,34 +226,34 @@ white-space: nowrap; display: none; } - + .voice-button.recording .voice-status { display: block; } - + .query-response { margin-top: 15px; padding: 10px; border-radius: 5px; display: none; } - + .success { background-color: #d4edda; color: #155724; } - + .error { background-color: #f8d7da; color: #721c24; } - + .text-streams-container { max-width: 800px; margin: 30px auto; } - + .text-stream-container { background: white; padding: 15px; @@ -261,12 +261,12 @@ border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } - + .text-stream-container h3 { margin-top: 0; color: #444; } - + .text-messages { height: 200px; overflow-y: auto; @@ -276,7 +276,7 @@ margin-bottom: 10px; background-color: #f9f9f9; } - + .text-message { padding: 8px; margin-bottom: 8px; @@ -288,7 +288,7 @@

Live Video Streams

- +

Ask a Question

@@ -303,7 +303,7 @@

Ask a Question

- + {% if text_stream_keys %}
@@ -377,28 +377,28 @@

{{ key.replace('_', ' ').title() }}

if (!mediaRecorder) { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorder = new MediaRecorder(stream); - + mediaRecorder.ondataavailable = e => chunks.push(e.data); - + mediaRecorder.onstop = async () => { const blob = new Blob(chunks, { type: 'audio/webm' }); chunks = []; - + // Show uploading status queryResponse.textContent = 'Processing voice command...'; queryResponse.className = 'query-response'; queryResponse.style.display = 'block'; - + const formData = new FormData(); formData.append('file', blob, 'recording.webm'); - + try { - const res = await fetch('/upload_audio', { - method: 'POST', - body: formData + const res = await fetch('/upload_audio', { + method: 'POST', + body: formData }); const json = await res.json(); - + if (json.success) { queryResponse.textContent = 'Voice command received!'; queryResponse.className = 'query-response success'; @@ -415,7 +415,7 @@

{{ key.replace('_', ' ').title() }}

} }; } - + mediaRecorder.start(); voiceBtn.classList.add('recording'); } catch (err) { @@ -428,24 +428,24 @@

{{ key.replace('_', ' ').title() }}

// Handle query form submission document.getElementById('queryForm').addEventListener('submit', async function(e) { e.preventDefault(); - + const queryInput = document.getElementById('queryInput'); const queryResponse = document.getElementById('queryResponse'); const query = queryInput.value.trim(); - + if (!query) return; - + try { const formData = new FormData(); formData.append('query', query); - + const response = await fetch('/submit_query', { method: 'POST', body: formData }); - + let result; - + // Better error handling for non-200 responses if (!response.ok) { try { @@ -463,14 +463,14 @@

{{ key.replace('_', ' ').title() }}

} else { result = await response.json(); } - + queryResponse.textContent = result.message; queryResponse.className = 'query-response ' + (result.success ? 'success' : 'error'); queryResponse.style.display = 'block'; - + if (result.success) { queryInput.value = ''; - + // Hide the success message after 3 seconds setTimeout(() => { queryResponse.style.display = 'none'; @@ -483,20 +483,20 @@

{{ key.replace('_', ' ').title() }}

} }); - + // Text stream event sources const textEventSources = {}; - + function connectTextStream(key) { // Close if already open if (textEventSources[key]) { textEventSources[key].close(); } - + // Connect to the server-sent events endpoint const eventSource = new EventSource(`/text_stream/${key}`); textEventSources[key] = eventSource; - + // Handle incoming messages eventSource.addEventListener('message', function(event) { const messagesContainer = document.getElementById(`text_messages_${key}`); @@ -504,11 +504,11 @@

{{ key.replace('_', ' ').title() }}

messageElement.className = 'text-message'; messageElement.textContent = event.data; messagesContainer.appendChild(messageElement); - + // Scroll to the bottom to show latest message messagesContainer.scrollTop = messagesContainer.scrollHeight; }); - + // Handle connection errors eventSource.onerror = function() { console.error(`Error in text stream ${key}`); @@ -516,26 +516,26 @@

{{ key.replace('_', ' ').title() }}

delete textEventSources[key]; }; } - + function disconnectTextStream(key) { if (textEventSources[key]) { textEventSources[key].close(); delete textEventSources[key]; } } - + function clearTextStream(key) { const messagesContainer = document.getElementById(`text_messages_${key}`); messagesContainer.innerHTML = ''; } - + // Auto-connect text streams on page load window.addEventListener('load', function() { {% for key in text_stream_keys %} connectTextStream('{{ key }}'); {% endfor %} }); - + - \ No newline at end of file + diff --git a/dimos/web/dimos_interface/public/fonts/CascadiaCode.ttf b/dimos/web/dimos_interface/public/fonts/CascadiaCode.ttf deleted file mode 100644 index 22785c2431..0000000000 Binary files a/dimos/web/dimos_interface/public/fonts/CascadiaCode.ttf and /dev/null differ diff --git a/dimos/web/dimos_interface/public/icon.png b/dimos/web/dimos_interface/public/icon.png index 2ade10a7c5..4b0b2f153a 100644 Binary files a/dimos/web/dimos_interface/public/icon.png and b/dimos/web/dimos_interface/public/icon.png differ diff --git a/dimos/web/dimos_interface/src/App.svelte b/dimos/web/dimos_interface/src/App.svelte index c249f3e3ea..8ca51f866d 100644 --- a/dimos/web/dimos_interface/src/App.svelte +++ b/dimos/web/dimos_interface/src/App.svelte @@ -10,17 +10,17 @@ const handleVoiceCommand = async (event: CustomEvent) => { if (event.detail.success) { // Show voice processing message - history.update(h => [...h, { - command: '[voice command]', - outputs: ['Processing voice command...'] + history.update(h => [...h, { + command: '[voice command]', + outputs: ['Processing voice command...'] }]); - + // The actual command will be processed by the agent through the audio pipeline // and will appear in the text stream } else { - history.update(h => [...h, { - command: '[voice command]', - outputs: [`Error: ${event.detail.error}`] + history.update(h => [...h, { + command: '[voice command]', + outputs: [`Error: ${event.detail.error}`] }]); } }; diff --git a/dimos/web/dimos_interface/src/app.css b/dimos/web/dimos_interface/src/app.css index d564a656ea..0a6e38b76b 100644 --- a/dimos/web/dimos_interface/src/app.css +++ b/dimos/web/dimos_interface/src/app.css @@ -18,13 +18,8 @@ @tailwind components; @tailwind utilities; -@font-face { - font-family: 'Cascadia Code'; - src: url('/fonts/CascadiaCode.ttf') -} - * { - font-family: 'Cascadia Code', monospace; + font-family: monospace; } * { @@ -47,4 +42,4 @@ ::-webkit-scrollbar-thumb:hover { background: #555; -} \ No newline at end of file +} diff --git a/dimos/web/dimos_interface/src/components/StreamViewer.svelte b/dimos/web/dimos_interface/src/components/StreamViewer.svelte index 08cf937299..43fe4739dd 100644 --- a/dimos/web/dimos_interface/src/components/StreamViewer.svelte +++ b/dimos/web/dimos_interface/src/components/StreamViewer.svelte @@ -27,15 +27,15 @@ function retryConnection(streamKey: string) { if (!retryCount[streamKey]) retryCount[streamKey] = 0; - + if (retryCount[streamKey] < MAX_RETRIES) { retryCount[streamKey]++; const timeLeft = TOTAL_TIMEOUT - (retryCount[streamKey] * RETRY_INTERVAL); errorMessages[streamKey] = `Connection attempt ${retryCount[streamKey]}/${MAX_RETRIES}... (${Math.ceil(timeLeft / 1000)}s remaining)`; - + // Update timestamp to force a new connection attempt timestamps[streamKey] = Date.now(); - + clearRetryTimer(streamKey); retryTimers[streamKey] = setTimeout(() => retryConnection(streamKey), RETRY_INTERVAL); } else { @@ -193,4 +193,4 @@ align-items: center; justify-content: center; } - \ No newline at end of file + diff --git a/dimos/web/dimos_interface/src/components/VoiceButton.svelte b/dimos/web/dimos_interface/src/components/VoiceButton.svelte index 0f9682519a..a316836d2e 100644 --- a/dimos/web/dimos_interface/src/components/VoiceButton.svelte +++ b/dimos/web/dimos_interface/src/components/VoiceButton.svelte @@ -44,53 +44,53 @@ if (!mediaRecorder) { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorder = new MediaRecorder(stream); - + mediaRecorder.ondataavailable = (e) => chunks.push(e.data); - + mediaRecorder.onstop = async () => { isProcessing = true; const blob = new Blob(chunks, { type: 'audio/webm' }); chunks = []; - + // Upload to backend const formData = new FormData(); formData.append('file', blob, 'recording.webm'); - + try { const res = await fetch(`${getServerUrl()}/upload_audio`, { method: 'POST', body: formData }); - + const json = await res.json(); - + if (json.success) { // Connect to agent_responses stream to see the output connectTextStream('agent_responses'); dispatch('voiceCommand', { success: true }); } else { - dispatch('voiceCommand', { - success: false, - error: json.message + dispatch('voiceCommand', { + success: false, + error: json.message }); } } catch (err) { - dispatch('voiceCommand', { - success: false, - error: err instanceof Error ? err.message : 'Upload failed' + dispatch('voiceCommand', { + success: false, + error: err instanceof Error ? err.message : 'Upload failed' }); } finally { isProcessing = false; } }; } - + mediaRecorder.start(); isRecording = true; } catch (err) { - dispatch('voiceCommand', { - success: false, - error: 'Microphone access denied' + dispatch('voiceCommand', { + success: false, + error: 'Microphone access denied' }); } } @@ -197,15 +197,15 @@ } @keyframes pulse { - 0% { + 0% { transform: scale(1); box-shadow: 0 4px 12px rgba(255, 0, 0, 0.4); } - 50% { + 50% { transform: scale(1.05); box-shadow: 0 4px 20px rgba(255, 0, 0, 0.6); } - 100% { + 100% { transform: scale(1); box-shadow: 0 4px 12px rgba(255, 0, 0, 0.4); } @@ -259,4 +259,4 @@ font-size: 44px; /* Increased from 22px to 2x */ } } - \ No newline at end of file + diff --git a/dimos/web/dimos_interface/src/stores/stream.ts b/dimos/web/dimos_interface/src/stores/stream.ts index eee46f84bf..649fd515ce 100644 --- a/dimos/web/dimos_interface/src/stores/stream.ts +++ b/dimos/web/dimos_interface/src/stores/stream.ts @@ -95,7 +95,7 @@ fetchAvailableStreams().then(streams => { export const showStream = async (streamKey?: string) => { streamStore.update(state => ({ ...state, isLoading: true, error: null })); - + try { const streams = await fetchAvailableStreams(); if (streams.length === 0) { @@ -178,4 +178,3 @@ export const disconnectTextStream = (key: string): void => { delete textEventSources[key]; } }; - diff --git a/dimos/web/dimos_interface/src/utils/commands.ts b/dimos/web/dimos_interface/src/utils/commands.ts index 455a0092e0..53755630ac 100644 --- a/dimos/web/dimos_interface/src/utils/commands.ts +++ b/dimos/web/dimos_interface/src/utils/commands.ts @@ -182,14 +182,14 @@ export const commands: Record Promise }, banner: () => ` -ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— -ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ -ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ -ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ +ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— +ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ +ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ +ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā•v${packageJson.version} -Powering generalist robotics +Powering generalist robotics Type 'help' to see list of available commands. `, @@ -272,7 +272,7 @@ Type 'help' to see list of available commands. } const jointPositions = args.join(' '); - + try { const jointPositionsArray = jointPositions.split(',').map(x => parseFloat(x.trim())); const response = await fetch(`${state.connection.url}/control?t=${Date.now()}`, { @@ -285,7 +285,7 @@ Type 'help' to see list of available commands. }); const data = await response.json(); - + if (response.ok) { return `${data.message} āœ“`; } else { @@ -302,7 +302,7 @@ Type 'help' to see list of available commands. } const subcommand = args[0].toLowerCase(); - + if (subcommand === 'status') { try { const response = await fetch('/unitree/status'); @@ -331,14 +331,14 @@ Type 'help' to see list of available commands. hideStream(); return 'Stopped Unitree video stream.'; } - + if (subcommand === 'command') { if (args.length < 2) { return 'Usage: unitree command - Send a command to the Unitree API'; } - + const commandText = args.slice(1).join(' '); - + try { // Ensure we have the text stream keys if (textStreamKeys.length === 0) { @@ -352,7 +352,7 @@ Type 'help' to see list of available commands. }, body: JSON.stringify({ command: commandText }) }); - + if (!response.ok) { throw new Error(`Server returned ${response.status}`); } @@ -362,13 +362,13 @@ Type 'help' to see list of available commands. streamKey: textStreamKeys[0], // Using the first available text stream initialMessage: `Command sent: ${commandText}\nPlanningAgent output...` }; - + } catch (error) { const message = error instanceof Error ? error.message : 'Server unreachable'; return `Failed to send command: ${message}. Make sure the API server is running.`; } } - + return 'Invalid subcommand. Available subcommands: status, start_stream, stop_stream, command'; }, }; diff --git a/dimos/web/dimos_interface/src/utils/simulation.ts b/dimos/web/dimos_interface/src/utils/simulation.ts index 5373bdb8b8..6e71dda358 100644 --- a/dimos/web/dimos_interface/src/utils/simulation.ts +++ b/dimos/web/dimos_interface/src/utils/simulation.ts @@ -115,7 +115,7 @@ export class SimulationManager { async requestSimulation(): Promise { simulationStore.update(state => ({ ...state, isConnecting: true, error: null })); - + try { // Request instance allocation const response = await this.fetchWithRetry(this.apiEndpoint, { @@ -129,7 +129,7 @@ export class SimulationManager { }); const instanceInfo = await response.json(); - + if (import.meta.env.DEV) { console.log('API Response:', instanceInfo); } @@ -175,11 +175,11 @@ export class SimulationManager { isConnecting: false, error: errorMessage })); - + if (import.meta.env.DEV) { console.error('Simulation request failed:', error); } - + throw error; } } @@ -211,4 +211,4 @@ export class SimulationManager { } } -export const simulationManager = new SimulationManager(); \ No newline at end of file +export const simulationManager = new SimulationManager(); diff --git a/dimos/web/edge_io.py b/dimos/web/edge_io.py index ad15614623..28ccae8733 100644 --- a/dimos/web/edge_io.py +++ b/dimos/web/edge_io.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimos/web/fastapi_server.py b/dimos/web/fastapi_server.py index 6c8a85344a..606e081fb3 100644 --- a/dimos/web/fastapi_server.py +++ b/dimos/web/fastapi_server.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ class FastAPIServer(EdgeIO): - def __init__( + def __init__( # type: ignore[no-untyped-def] self, dev_name: str = "FastAPI Server", edge_type: str = "Bidirectional", @@ -62,17 +62,17 @@ def __init__( self.streams = streams self.active_streams = {} self.stream_locks = {key: Lock() for key in self.streams} - self.stream_queues = {} - self.stream_disposables = {} + self.stream_queues = {} # type: ignore[var-annotated] + self.stream_disposables = {} # type: ignore[var-annotated] # Initialize text streams self.text_streams = text_streams or {} - self.text_queues = {} + self.text_queues = {} # type: ignore[var-annotated] self.text_disposables = {} - self.text_clients = set() + self.text_clients = set() # type: ignore[var-annotated] # Create a Subject for text queries - self.query_subject = rx.subject.Subject() + self.query_subject = rx.subject.Subject() # type: ignore[var-annotated] self.query_stream = self.query_subject.pipe(ops.share()) for key in self.streams: @@ -95,15 +95,15 @@ def __init__( self.setup_routes() - def process_frame_fastapi(self, frame): + def process_frame_fastapi(self, frame): # type: ignore[no-untyped-def] """Convert frame to JPEG format for streaming.""" _, buffer = cv2.imencode(".jpg", frame) return buffer.tobytes() - def stream_generator(self, key): + def stream_generator(self, key): # type: ignore[no-untyped-def] """Generate frames for a given video stream.""" - def generate(): + def generate(): # type: ignore[no-untyped-def] if key not in self.stream_queues: self.stream_queues[key] = Queue(maxsize=10) @@ -148,17 +148,18 @@ def generate(): return generate - def create_video_feed_route(self, key): + def create_video_feed_route(self, key): # type: ignore[no-untyped-def] """Create a video feed route for a specific stream.""" - async def video_feed(): + async def video_feed(): # type: ignore[no-untyped-def] return StreamingResponse( - self.stream_generator(key)(), media_type="multipart/x-mixed-replace; boundary=frame" + self.stream_generator(key)(), # type: ignore[no-untyped-call] + media_type="multipart/x-mixed-replace; boundary=frame", ) return video_feed - async def text_stream_generator(self, key): + async def text_stream_generator(self, key): # type: ignore[no-untyped-def] """Generate SSE events for text stream.""" client_id = id(object()) self.text_clients.add(client_id) @@ -181,7 +182,7 @@ def setup_routes(self) -> None: """Set up FastAPI routes.""" @self.app.get("/", response_class=HTMLResponse) - async def index(request: Request): + async def index(request: Request): # type: ignore[no-untyped-def] stream_keys = list(self.streams.keys()) text_stream_keys = list(self.text_streams.keys()) return self.templates.TemplateResponse( @@ -194,7 +195,7 @@ async def index(request: Request): ) @self.app.post("/submit_query") - async def submit_query(query: str = Form(...)): + async def submit_query(query: str = Form(...)): # type: ignore[no-untyped-def] # Using Form directly as a dependency ensures proper form handling try: if query: @@ -210,13 +211,13 @@ async def submit_query(query: str = Form(...)): ) @self.app.get("/text_stream/{key}") - async def text_stream(key: str): + async def text_stream(key: str): # type: ignore[no-untyped-def] if key not in self.text_streams: raise HTTPException(status_code=404, detail=f"Text stream '{key}' not found") - return EventSourceResponse(self.text_stream_generator(key)) + return EventSourceResponse(self.text_stream_generator(key)) # type: ignore[no-untyped-call] for key in self.streams: - self.app.get(f"/video_feed/{key}")(self.create_video_feed_route(key)) + self.app.get(f"/video_feed/{key}")(self.create_video_feed_route(key)) # type: ignore[no-untyped-call] def run(self) -> None: """Run the FastAPI server.""" diff --git a/dimos/web/flask_server.py b/dimos/web/flask_server.py index b0cf6fc143..4cd6d0a5e0 100644 --- a/dimos/web/flask_server.py +++ b/dimos/web/flask_server.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ class FlaskServer(EdgeIO): - def __init__( + def __init__( # type: ignore[no-untyped-def] self, dev_name: str = "Flask Server", edge_type: str = "Bidirectional", @@ -46,21 +46,21 @@ def __init__( self.setup_routes() - def process_frame_flask(self, frame): + def process_frame_flask(self, frame): # type: ignore[no-untyped-def] """Convert frame to JPEG format for streaming.""" _, buffer = cv2.imencode(".jpg", frame) return buffer.tobytes() def setup_routes(self) -> None: @self.app.route("/") - def index(): + def index(): # type: ignore[no-untyped-def] stream_keys = list(self.streams.keys()) # Get the keys from the streams dictionary return render_template("index_flask.html", stream_keys=stream_keys) # Function to create a streaming response - def stream_generator(key): - def generate(): - frame_queue = Queue() + def stream_generator(key): # type: ignore[no-untyped-def] + def generate(): # type: ignore[no-untyped-def] + frame_queue = Queue() # type: ignore[var-annotated] disposable = SingleAssignmentDisposable() # Subscribe to the shared, ref-counted stream @@ -82,10 +82,11 @@ def generate(): return generate - def make_response_generator(key): - def response_generator(): + def make_response_generator(key): # type: ignore[no-untyped-def] + def response_generator(): # type: ignore[no-untyped-def] return Response( - stream_generator(key)(), mimetype="multipart/x-mixed-replace; boundary=frame" + stream_generator(key)(), # type: ignore[no-untyped-call] + mimetype="multipart/x-mixed-replace; boundary=frame", ) return response_generator @@ -94,7 +95,9 @@ def response_generator(): for key in self.streams: endpoint = f"video_feed_{key}" self.app.add_url_rule( - f"/video_feed/{key}", endpoint, view_func=make_response_generator(key) + f"/video_feed/{key}", + endpoint, + view_func=make_response_generator(key), # type: ignore[no-untyped-call] ) def run(self, host: str = "0.0.0.0", port: int = 5555, threaded: bool = True) -> None: diff --git a/dimos/web/robot_web_interface.py b/dimos/web/robot_web_interface.py index 0dc7636ac9..f45319f1d2 100644 --- a/dimos/web/robot_web_interface.py +++ b/dimos/web/robot_web_interface.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ class RobotWebInterface(FastAPIServer): """Wrapper class for the dimos-interface FastAPI server.""" - def __init__(self, port: int = 5555, text_streams=None, audio_subject=None, **streams) -> None: + def __init__(self, port: int = 5555, text_streams=None, audio_subject=None, **streams) -> None: # type: ignore[no-untyped-def] super().__init__( dev_name="Robot Web Interface", edge_type="Bidirectional", diff --git a/dimos/web/templates/index_fastapi.html b/dimos/web/templates/index_fastapi.html index 9ab54dc170..75b0c1c179 100644 --- a/dimos/web/templates/index_fastapi.html +++ b/dimos/web/templates/index_fastapi.html @@ -125,12 +125,12 @@ border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } - + .query-form { display: flex; gap: 10px; } - + .query-input { flex-grow: 1; padding: 10px; @@ -138,7 +138,7 @@ border-radius: 5px; font-size: 16px; } - + .query-button { padding: 10px 20px; background-color: #28a745; @@ -149,33 +149,33 @@ font-size: 16px; transition: background-color 0.3s; } - + .query-button:hover { background-color: #218838; } - + .query-response { margin-top: 15px; padding: 10px; border-radius: 5px; display: none; } - + .success { background-color: #d4edda; color: #155724; } - + .error { background-color: #f8d7da; color: #721c24; } - + .text-streams-container { max-width: 800px; margin: 30px auto; } - + .text-stream-container { background: white; padding: 15px; @@ -183,12 +183,12 @@ border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } - + .text-stream-container h3 { margin-top: 0; color: #444; } - + .text-messages { height: 200px; overflow-y: auto; @@ -198,7 +198,7 @@ margin-bottom: 10px; background-color: #f9f9f9; } - + .text-message { padding: 8px; margin-bottom: 8px; @@ -210,7 +210,7 @@

Live Video Streams

- +

Ask a Question

@@ -219,7 +219,7 @@

Ask a Question

- + {% if text_stream_keys %}
@@ -237,7 +237,7 @@

{{ key.replace('_', ' ').title() }}

{% endfor %}
{% endif %} - +
{% for key in stream_keys %}
@@ -278,24 +278,24 @@

{{ key.replace('_', ' ').title() }}

// Handle query form submission document.getElementById('queryForm').addEventListener('submit', async function(e) { e.preventDefault(); - + const queryInput = document.getElementById('queryInput'); const queryResponse = document.getElementById('queryResponse'); const query = queryInput.value.trim(); - + if (!query) return; - + try { const formData = new FormData(); formData.append('query', query); - + const response = await fetch('/submit_query', { method: 'POST', body: formData }); - + let result; - + // Better error handling for non-200 responses if (!response.ok) { try { @@ -313,14 +313,14 @@

{{ key.replace('_', ' ').title() }}

} else { result = await response.json(); } - + queryResponse.textContent = result.message; queryResponse.className = 'query-response ' + (result.success ? 'success' : 'error'); queryResponse.style.display = 'block'; - + if (result.success) { queryInput.value = ''; - + // Hide the success message after 3 seconds setTimeout(() => { queryResponse.style.display = 'none'; @@ -335,17 +335,17 @@

{{ key.replace('_', ' ').title() }}

// Text stream event sources const textEventSources = {}; - + function connectTextStream(key) { // Close if already open if (textEventSources[key]) { textEventSources[key].close(); } - + // Connect to the server-sent events endpoint const eventSource = new EventSource(`/text_stream/${key}`); textEventSources[key] = eventSource; - + // Handle incoming messages eventSource.addEventListener('message', function(event) { const messagesContainer = document.getElementById(`text_messages_${key}`); @@ -353,11 +353,11 @@

{{ key.replace('_', ' ').title() }}

messageElement.className = 'text-message'; messageElement.textContent = event.data; messagesContainer.appendChild(messageElement); - + // Scroll to the bottom to show latest message messagesContainer.scrollTop = messagesContainer.scrollHeight; }); - + // Handle connection errors eventSource.onerror = function() { console.error(`Error in text stream ${key}`); @@ -365,19 +365,19 @@

{{ key.replace('_', ' ').title() }}

delete textEventSources[key]; }; } - + function disconnectTextStream(key) { if (textEventSources[key]) { textEventSources[key].close(); delete textEventSources[key]; } } - + function clearTextStream(key) { const messagesContainer = document.getElementById(`text_messages_${key}`); messagesContainer.innerHTML = ''; } - + // Auto-connect text streams on page load window.addEventListener('load', function() { {% for key in text_stream_keys %} @@ -386,4 +386,4 @@

{{ key.replace('_', ' ').title() }}

}); - \ No newline at end of file + diff --git a/dimos/web/templates/index_flask.html b/dimos/web/templates/index_flask.html index 4717553d95..e41665e588 100644 --- a/dimos/web/templates/index_flask.html +++ b/dimos/web/templates/index_flask.html @@ -98,7 +98,7 @@

Live Video Streams

- +
{% for key in stream_keys %}
@@ -115,4 +115,4 @@

{{ key.replace('_', ' ').title() }}

} - \ No newline at end of file + diff --git a/dimos/web/templates/rerun_dashboard.html b/dimos/web/templates/rerun_dashboard.html new file mode 100644 index 0000000000..9917d9d2af --- /dev/null +++ b/dimos/web/templates/rerun_dashboard.html @@ -0,0 +1,20 @@ + + + + Dimos Dashboard + + + +
+ + +
+ + diff --git a/dimos/web/websocket_vis/costmap_viz.py b/dimos/web/websocket_vis/costmap_viz.py index ec2088b3b8..21309c94bc 100644 --- a/dimos/web/websocket_vis/costmap_viz.py +++ b/dimos/web/websocket_vis/costmap_viz.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ def __init__(self, occupancy_grid: OccupancyGrid | None = None) -> None: self.occupancy_grid = occupancy_grid @property - def data(self) -> np.ndarray | None: + def data(self) -> np.ndarray | None: # type: ignore[type-arg] """Get the costmap data as a numpy array.""" if self.occupancy_grid: return self.occupancy_grid.grid @@ -58,7 +58,7 @@ def resolution(self) -> float: return 1.0 @property - def origin(self): + def origin(self): # type: ignore[no-untyped-def] """Get the origin pose of the costmap.""" if self.occupancy_grid: return self.occupancy_grid.origin diff --git a/dimos/web/websocket_vis/optimized_costmap.py b/dimos/web/websocket_vis/optimized_costmap.py index 03307ff2c0..34502744c4 100644 --- a/dimos/web/websocket_vis/optimized_costmap.py +++ b/dimos/web/websocket_vis/optimized_costmap.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. import base64 import hashlib @@ -30,12 +30,12 @@ class OptimizedCostmapEncoder: def __init__(self, chunk_size: int = 64) -> None: self.chunk_size = chunk_size - self.last_full_grid: np.ndarray | None = None + self.last_full_grid: np.ndarray | None = None # type: ignore[type-arg] self.last_full_sent_time: float = 0 # Track when last full update was sent self.chunk_hashes: dict[tuple[int, int], str] = {} self.full_update_interval = 3.0 # Send full update every 3 seconds - def encode_costmap(self, grid: np.ndarray, force_full: bool = False) -> dict[str, Any]: + def encode_costmap(self, grid: np.ndarray, force_full: bool = False) -> dict[str, Any]: # type: ignore[type-arg] """Encode a costmap grid with optimizations. Args: @@ -60,7 +60,7 @@ def encode_costmap(self, grid: np.ndarray, force_full: bool = False) -> dict[str else: return self._encode_delta(grid, current_time) - def _encode_full(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: + def _encode_full(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: # type: ignore[type-arg] height, width = grid.shape # Convert to uint8 for better compression (costmap values are -1 to 100) @@ -89,7 +89,7 @@ def _encode_full(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: "data": encoded, } - def _encode_delta(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: + def _encode_delta(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: # type: ignore[type-arg] height, width = grid.shape changed_chunks = [] @@ -146,7 +146,7 @@ def _encode_delta(self, grid: np.ndarray, current_time: float) -> dict[str, Any] "chunks": changed_chunks, } - def _update_chunk_hashes(self, grid: np.ndarray) -> None: + def _update_chunk_hashes(self, grid: np.ndarray) -> None: # type: ignore[type-arg] """Update all chunk hashes for the grid.""" self.chunk_hashes.clear() height, width = grid.shape diff --git a/dimos/web/websocket_vis/path_history.py b/dimos/web/websocket_vis/path_history.py index f60031bc51..39b6be08a3 100644 --- a/dimos/web/websocket_vis/path_history.py +++ b/dimos/web/websocket_vis/path_history.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ class PathHistory: """A simple container for storing a history of positions for visualization.""" - def __init__(self, points: list[Vector3 | tuple | list] | None = None) -> None: + def __init__(self, points: list[Vector3 | tuple | list] | None = None) -> None: # type: ignore[type-arg] """Initialize with optional list of points.""" self.points: list[Vector3] = [] if points: @@ -33,7 +33,7 @@ def __init__(self, points: list[Vector3 | tuple | list] | None = None) -> None: else: self.points.append(Vector3(*p)) - def ipush(self, point: Vector3 | tuple | list) -> "PathHistory": + def ipush(self, point: Vector3 | tuple | list) -> "PathHistory": # type: ignore[type-arg] """Add a point to the history (in-place) and return self.""" if isinstance(point, Vector3): self.points.append(point) diff --git a/dimos/web/websocket_vis/websocket_vis_module.py b/dimos/web/websocket_vis/websocket_vis_module.py index c679cca463..31aa0d3956 100644 --- a/dimos/web/websocket_vis/websocket_vis_module.py +++ b/dimos/web/websocket_vis/websocket_vis_module.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,22 +16,37 @@ """ WebSocket Visualization Module for Dimos navigation and mapping. + +This module provides a WebSocket data server for real-time visualization. +The frontend is served from a separate HTML file. """ import asyncio +from pathlib import Path as FilePath import threading import time from typing import Any -from dimos_lcm.std_msgs import Bool +from dimos_lcm.std_msgs import Bool # type: ignore[import-untyped] from reactivex.disposable import Disposable -import socketio +import socketio # type: ignore[import-untyped] from starlette.applications import Starlette -from starlette.responses import HTMLResponse -from starlette.routing import Route +from starlette.responses import FileResponse, RedirectResponse, Response +from starlette.routing import Mount, Route +from starlette.staticfiles import StaticFiles import uvicorn +# Path to the frontend HTML templates and command-center build +_TEMPLATES_DIR = FilePath(__file__).parent.parent / "templates" +_DASHBOARD_HTML = _TEMPLATES_DIR / "rerun_dashboard.html" +_COMMAND_CENTER_DIR = ( + FilePath(__file__).parent.parent / "command-center-extension" / "dist-standalone" +) + from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig +from dimos.mapping.occupancy.gradient import gradient +from dimos.mapping.occupancy.inflation import simple_inflate from dimos.mapping.types import LatLon from dimos.msgs.geometry_msgs import PoseStamped, Twist, TwistStamped, Vector3 from dimos.msgs.nav_msgs import OccupancyGrid, Path @@ -39,7 +54,7 @@ from .optimized_costmap import OptimizedCostmapEncoder -logger = setup_logger("dimos.web.websocket_vis") +logger = setup_logger() class WebsocketVisModule(Module): @@ -62,26 +77,33 @@ class WebsocketVisModule(Module): """ # LCM inputs - odom: In[PoseStamped] = None - gps_location: In[LatLon] = None - path: In[Path] = None - global_costmap: In[OccupancyGrid] = None + odom: In[PoseStamped] + gps_location: In[LatLon] + path: In[Path] + global_costmap: In[OccupancyGrid] # LCM outputs - goal_request: Out[PoseStamped] = None - gps_goal: Out[LatLon] = None - explore_cmd: Out[Bool] = None - stop_explore_cmd: Out[Bool] = None - cmd_vel: Out[Twist] = None - movecmd_stamped: Out[TwistStamped] = None - - def __init__(self, port: int = 7779, **kwargs) -> None: + goal_request: Out[PoseStamped] + gps_goal: Out[LatLon] + explore_cmd: Out[Bool] + stop_explore_cmd: Out[Bool] + cmd_vel: Out[Twist] + movecmd_stamped: Out[TwistStamped] + + def __init__( + self, + port: int = 7779, + global_config: GlobalConfig | None = None, + **kwargs: Any, + ) -> None: """Initialize the WebSocket visualization module. Args: port: Port to run the web server on + global_config: Optional global config for viewer backend settings """ super().__init__(**kwargs) + self._global_config = global_config or GlobalConfig() self.port = port self._uvicorn_server_thread: threading.Thread | None = None @@ -91,26 +113,29 @@ def __init__(self, port: int = 7779, **kwargs) -> None: self._broadcast_thread = None self._uvicorn_server: uvicorn.Server | None = None - self.vis_state = {} + self.vis_state = {} # type: ignore[var-annotated] self.state_lock = threading.Lock() - self.costmap_encoder = OptimizedCostmapEncoder(chunk_size=64) - logger.info(f"WebSocket visualization module initialized on port {port}") + # Track GPS goal points for visualization + self.gps_goal_points: list[dict[str, float]] = [] + logger.info( + f"WebSocket visualization module initialized on port {port}, GPS goal tracking enabled" + ) def _start_broadcast_loop(self) -> None: def websocket_vis_loop() -> None: - self._broadcast_loop = asyncio.new_event_loop() + self._broadcast_loop = asyncio.new_event_loop() # type: ignore[assignment] asyncio.set_event_loop(self._broadcast_loop) try: - self._broadcast_loop.run_forever() + self._broadcast_loop.run_forever() # type: ignore[attr-defined] except Exception as e: logger.error(f"Broadcast loop error: {e}") finally: - self._broadcast_loop.close() + self._broadcast_loop.close() # type: ignore[attr-defined] - self._broadcast_thread = threading.Thread(target=websocket_vis_loop, daemon=True) - self._broadcast_thread.start() + self._broadcast_thread = threading.Thread(target=websocket_vis_loop, daemon=True) # type: ignore[assignment] + self._broadcast_thread.start() # type: ignore[attr-defined] @rpc def start(self) -> None: @@ -123,17 +148,32 @@ def start(self) -> None: self._uvicorn_server_thread = threading.Thread(target=self._run_uvicorn_server, daemon=True) self._uvicorn_server_thread.start() - unsub = self.odom.subscribe(self._on_robot_pose) - self._disposables.add(Disposable(unsub)) - - unsub = self.gps_location.subscribe(self._on_gps_location) - self._disposables.add(Disposable(unsub)) - - unsub = self.path.subscribe(self._on_path) - self._disposables.add(Disposable(unsub)) - - unsub = self.global_costmap.subscribe(self._on_global_costmap) - self._disposables.add(Disposable(unsub)) + # Show control center link in terminal + logger.info(f"Command Center: http://localhost:{self.port}/command-center") + + try: + unsub = self.odom.subscribe(self._on_robot_pose) + self._disposables.add(Disposable(unsub)) + except Exception: + ... + + try: + unsub = self.gps_location.subscribe(self._on_gps_location) + self._disposables.add(Disposable(unsub)) + except Exception: + ... + + try: + unsub = self.path.subscribe(self._on_path) + self._disposables.add(Disposable(unsub)) + except Exception: + ... + + try: + unsub = self.global_costmap.subscribe(self._on_global_costmap) + self._disposables.add(Disposable(unsub)) + except Exception: + ... @rpc def stop(self) -> None: @@ -168,52 +208,112 @@ def _create_server(self) -> None: # Create SocketIO server self.sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") - async def serve_index(request): - return HTMLResponse("Use the extension.") + async def serve_index(request): # type: ignore[no-untyped-def] + """Serve appropriate HTML based on viewer mode.""" + # If running native Rerun, redirect to standalone command center + if self._global_config.viewer_backend == "rerun-native": + return RedirectResponse(url="/command-center") + # Otherwise serve full dashboard with Rerun iframe + return FileResponse(_DASHBOARD_HTML, media_type="text/html") + + async def serve_command_center(request): # type: ignore[no-untyped-def] + """Serve the command center 2D visualization (built React app).""" + index_file = _COMMAND_CENTER_DIR / "index.html" + if index_file.exists(): + return FileResponse(index_file, media_type="text/html") + else: + return Response( + content="Command center not built. Run: cd dimos/web/command-center-extension && npm install && npm run build:standalone", + status_code=503, + media_type="text/plain", + ) - routes = [Route("/", serve_index)] + routes = [ + Route("/", serve_index), + Route("/command-center", serve_command_center), + ] + + # Add static file serving for command-center assets if build exists + if _COMMAND_CENTER_DIR.exists(): + routes.append( + Mount( # type: ignore[arg-type] + "/assets", + app=StaticFiles(directory=_COMMAND_CENTER_DIR / "assets"), + name="assets", + ) + ) starlette_app = Starlette(routes=routes) self.app = socketio.ASGIApp(self.sio, starlette_app) # Register SocketIO event handlers - @self.sio.event - async def connect(sid, environ) -> None: + @self.sio.event # type: ignore[untyped-decorator] + async def connect(sid, environ) -> None: # type: ignore[no-untyped-def] with self.state_lock: current_state = dict(self.vis_state) + # Include GPS goal points in the initial state + if self.gps_goal_points: + current_state["gps_travel_goal_points"] = self.gps_goal_points + # Force full costmap update on new connection self.costmap_encoder.last_full_grid = None - await self.sio.emit("full_state", current_state, room=sid) + await self.sio.emit("full_state", current_state, room=sid) # type: ignore[union-attr] + logger.info( + f"Client {sid} connected, sent state with {len(self.gps_goal_points)} GPS goal points" + ) - @self.sio.event - async def click(sid, position) -> None: + @self.sio.event # type: ignore[untyped-decorator] + async def click(sid, position) -> None: # type: ignore[no-untyped-def] goal = PoseStamped( position=(position[0], position[1], 0), orientation=(0, 0, 0, 1), # Default orientation frame_id="world", ) self.goal_request.publish(goal) - logger.info(f"Click goal published: ({goal.position.x:.2f}, {goal.position.y:.2f})") + logger.info( + "Click goal published", x=round(goal.position.x, 3), y=round(goal.position.y, 3) + ) + + @self.sio.event # type: ignore[untyped-decorator] + async def gps_goal(sid: str, goal: dict[str, float]) -> None: + logger.info(f"Received GPS goal: {goal}") - @self.sio.event - async def gps_goal(sid, goal) -> None: - logger.info(f"Set GPS goal: {goal}") + # Publish the goal to LCM self.gps_goal.publish(LatLon(lat=goal["lat"], lon=goal["lon"])) - @self.sio.event - async def start_explore(sid) -> None: + # Add to goal points list for visualization + self.gps_goal_points.append(goal) + logger.info(f"Added GPS goal to list. Total goals: {len(self.gps_goal_points)}") + + # Emit updated goal points back to all connected clients + if self.sio is not None: + await self.sio.emit("gps_travel_goal_points", self.gps_goal_points) + logger.debug( + f"Emitted gps_travel_goal_points with {len(self.gps_goal_points)} points: {self.gps_goal_points}" + ) + + @self.sio.event # type: ignore[untyped-decorator] + async def start_explore(sid: str) -> None: logger.info("Starting exploration") self.explore_cmd.publish(Bool(data=True)) - @self.sio.event - async def stop_explore(sid) -> None: + @self.sio.event # type: ignore[untyped-decorator] + async def stop_explore(sid) -> None: # type: ignore[no-untyped-def] logger.info("Stopping exploration") self.stop_explore_cmd.publish(Bool(data=True)) - @self.sio.event - async def move_command(sid, data) -> None: + @self.sio.event # type: ignore[untyped-decorator] + async def clear_gps_goals(sid: str) -> None: + logger.info("Clearing all GPS goal points") + self.gps_goal_points.clear() + if self.sio is not None: + await self.sio.emit("gps_travel_goal_points", self.gps_goal_points) + logger.info("GPS goal points cleared and updated clients") + + @self.sio.event # type: ignore[untyped-decorator] + async def move_command(sid: str, data: dict[str, Any]) -> None: # Publish Twist if transport is configured if self.cmd_vel and self.cmd_vel.transport: twist = Twist( @@ -238,7 +338,7 @@ async def move_command(sid, data) -> None: def _run_uvicorn_server(self) -> None: config = uvicorn.Config( - self.app, + self.app, # type: ignore[arg-type] host="0.0.0.0", port=self.port, log_level="error", # Reduce verbosity @@ -269,7 +369,7 @@ def _on_global_costmap(self, msg: OccupancyGrid) -> None: def _process_costmap(self, costmap: OccupancyGrid) -> dict[str, Any]: """Convert OccupancyGrid to visualization format.""" - costmap = costmap.inflate(0.1).gradient(max_distance=1.0) + costmap = gradient(simple_inflate(costmap, 0.1), max_distance=1.0) grid_data = self.costmap_encoder.encode_costmap(costmap.grid) return { diff --git a/docker/deprecated/agent/Dockerfile b/docker/deprecated/agent/Dockerfile deleted file mode 100644 index a760bc3a6a..0000000000 --- a/docker/deprecated/agent/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -FROM python:3 - -# General -# RUN apt-get update && apt-get install -y \ -# libgl1-mesa-glx - -# Unitree Specific -RUN apt-get update && apt-get install -y \ - libgl1-mesa-glx \ - build-essential \ - libavformat-dev \ - libavcodec-dev \ - libavdevice-dev \ - libavutil-dev \ - libswscale-dev \ - libpostproc-dev \ - gcc \ - make \ - portaudio19-dev \ - python3-pyaudio \ - python3-all-dev - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -COPY requirements.txt ./ - -RUN pip install --no-cache-dir -r requirements.txt - -COPY ./dimos ./dimos - -COPY ./tests ./tests - -COPY ./dimos/__init__.py ./ - -# CMD [ "python", "-m", "tests.test_environment" ] - -# CMD [ "python", "-m", "tests.test_openai_agent_v3" ] - -CMD [ "python", "-m", "tests.test_agent" ] diff --git a/docker/deprecated/agent/docker-compose.yml b/docker/deprecated/agent/docker-compose.yml deleted file mode 100644 index 37b24f6abf..0000000000 --- a/docker/deprecated/agent/docker-compose.yml +++ /dev/null @@ -1,85 +0,0 @@ ---- -services: - dimos: - image: dimos:latest - build: - context: ../../ - dockerfile: docker/agent/Dockerfile - env_file: - - ../../.env - mem_limit: 8048m - volumes: - - ../../assets:/app/assets - ports: - - "5555:5555" - environment: - - PYTHONUNBUFFERED=1 - # command: [ "python", "-m", "tests.test_agent" ] - # ^^ Working Sanity Test Cases - Expand to Agent Class - # - # command: [ "python", "-m", "tests.stream.video_operators" ] - # ^^ Working Skeleton - Needs Impl. - # - # command: [ "python", "-m", "tests.stream.video_provider" ] - # ^^ Working Instance - Needs Tests. - # - # command: [ "python", "-m", "tests.web.edge_io" ] - # ^^ Working Instance - Needs Tests. - # - # command: [ "python", "-m", "tests.agent_manip_flow_flask_test" ] - # ^^ Working Instance - - # command: [ "python", "-m", "tests.agent_manip_flow_fastapi_test" ] - # ^^ Working Instance - Needs threading / start / stop functionality bugfix. - - # command: [ "python", "-m", "tests.test_standalone_project_out" ] - # ^^ WIP - Output Function Headers + Descriptions - - # command: [ "python", "-m", "tests.agent_memory_test" ] - # ^^ WIP - Agent Memory Testing - - # command: [ "python", "-m", "tests.test_standalone_fastapi" ] - # ^^ Working, FastAPI Multithreader Standalone - - # command: [ "python", "-m", "tests.test_standalone_rxpy_01" ] - # ^^ Working Instance - - # command: [ "python", "-m", "tests.test_standalone_openai_json" ] - # ^^ Working Instance - - # command: [ "python", "-m", "tests.test_standalone_openai_json_struct" ] - # ^^ Working Instance - - # command: [ "python", "-m", "tests.test_standalone_openai_json_struct_func" ] - # ^^ WIP - - # command: [ "python", "-m", "tests.test_standalone_openai_json_struct_func_playground" ] - # ^^ WIP - - # command: [ "python", "-m", "tests.test_skill_library" ] - # ^^ Working Instance - - # command: [ "python", "-m", "tests.test_video_rtsp" ] - # ^^ WIP - - command: [ "python", "-m", "tests.test_video_agent_threading" ] - # ^^ WIP - - # command: ["tail", "-f", "/dev/null"] - stdin_open: true - tty: true - -# ---- -# TO RUN: -# docker build -f ./Dockerfile -t dimos ../../ && docker compose up -# GO TO: -# 127.0.0.1:5555 (when flask server fixed) -# ---- - -# video-service: -# build: ./video-service -# image: video-service:latest -# volumes: -# - ./../../assets:/app/dimos-env/assets -# ports: -# - "23001:23001" diff --git a/docker/deprecated/interface/Dockerfile b/docker/deprecated/interface/Dockerfile deleted file mode 100644 index 9064f882e9..0000000000 --- a/docker/deprecated/interface/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM node:18-alpine - -WORKDIR /app - -# Start development server with host 0.0.0.0 to allow external connections -CMD ["sh", "-c", "yarn install && yarn dev --host 0.0.0.0"] \ No newline at end of file diff --git a/docker/deprecated/interface/docker-compose.yml b/docker/deprecated/interface/docker-compose.yml deleted file mode 100644 index 6571e92e16..0000000000 --- a/docker/deprecated/interface/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -services: - dimos-web-interface: - build: - context: ../../ # Root of the project - dockerfile: docker/interface/Dockerfile - image: dimos-web-interface:latest - container_name: dimos-web-interface - network_mode: "host" - ports: - - "3000:3000" - volumes: - - ../../dimos/web/dimos_interface:/app - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"] - interval: 30s - timeout: 10s - retries: 3 diff --git a/docker/deprecated/jetson/README.md b/docker/deprecated/jetson/README.md deleted file mode 100644 index 23ec6c250f..0000000000 --- a/docker/deprecated/jetson/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Jetson Setup Guide - -This guide explains how to set up and run local dimOS LLM Agents on NVIDIA Jetson devices. - -## Prerequisites - -> **Note**: This setup has been tested on: -> - Jetson Orin Nano (8GB) -> - JetPack 6.2 (L4T 36.4.3) -> - CUDA 12.6.68 - -### Requirements -- NVIDIA Jetson device (Orin/Xavier) -- Docker installed (with GPU support) -- Git installed -- CUDA installed - -## Basic Python Setup (Virtual Environment) - -### 1. Create a virtual environment: -```bash -python3 -m venv ~/jetson_env -source ~/jetson_env/bin/activate -``` - -### 2. Install cuSPARSELt: - -For PyTorch versions 24.06+ (see [Compatibility Matrix](https://docs.nvidia.com/deeplearning/frameworks/install-pytorch-jetson-platform-release-notes/pytorch-jetson-rel.html#pytorch-jetson-rel)), cuSPARSELt is required. Install it with the [instructions](https://developer.nvidia.com/cusparselt-downloads) by selecting Linux OS, aarch64-jetson architecture, and Ubuntu distribution - -For Jetpack 6.2, Pytorch 2.5, and CUDA 12.6: -```bash -wget https://developer.download.nvidia.com/compute/cusparselt/0.7.0/local_installers/cusparselt-local-tegra-repo-ubuntu2204-0.7.0_1.0-1_arm64.deb -sudo dpkg -i cusparselt-local-tegra-repo-ubuntu2204-0.7.0_1.0-1_arm64.deb -sudo cp /var/cusparselt-local-tegra-repo-ubuntu2204-0.7.0/cusparselt-*-keyring.gpg /usr/share/keyrings/ -sudo apt-get update -sudo apt-get -y install libcusparselt0 libcusparselt-dev -``` - -### 3. Install the Jetson-specific requirements: -```bash -cd /path/to/dimos -pip install -r docker/jetson/jetson_requirements.txt -``` - -### 4. Run testfile: -```bash -export PYTHONPATH=$PYTHONPATH:$(pwd) -python3 tests/test_agent_huggingface_local_jetson.py -``` - -## Docker Setup -for JetPack 6.2 (L4T 36.4.3), CUDA 12.6.68 - -### 1. Build and Run using Docker Compose - -From the DIMOS project root directory: -```bash -# Build and run the container -sudo docker compose -f docker/jetson/huggingface_local/docker-compose.yml up --build -``` - -This will: -- Build the Docker image with all necessary dependencies -- Start the container with GPU support -- Run the HuggingFace local agent test script - -## Troubleshooting - -### Libopenblas or other library errors - -Run the Jetson fix script: - -```bash -# From the DIMOS project root -chmod +x ./docker/jetson/fix_jetson.sh -./docker/jetson/fix_jetson.sh -``` - -This script will: -- Install cuSPARSELt library for tensor operations -- Fix libopenblas.so.0 dependencies -- Configure system libraries - -1. If you encounter CUDA/GPU issues: - - Ensure JetPack is properly installed - - Check nvidia-smi output - - Verify Docker has access to the GPU - -2. For memory issues: - - Consider using smaller / quantized models - - Adjust batch sizes and model parameters - - Run the jetson in non-GUI mode to maximize ram availability - -## Notes - -- The setup uses PyTorch built specifically for Jetson -- Models are downloaded and cached locally -- GPU acceleration is enabled by default diff --git a/docker/deprecated/jetson/fix_jetson.sh b/docker/deprecated/jetson/fix_jetson.sh deleted file mode 100644 index ade938a2c9..0000000000 --- a/docker/deprecated/jetson/fix_jetson.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Install cuSPARSELt -# wget https://developer.download.nvidia.com/compute/cusparselt/0.7.0/local_installers/cusparselt-local-tegra-repo-ubuntu2204-0.7.0_1.0-1_arm64.deb -# sudo dpkg -i cusparselt-local-tegra-repo-ubuntu2204-0.7.0_1.0-1_arm64.deb -# sudo cp /var/cusparselt-local-tegra-repo-ubuntu2204-0.7.0/cusparselt-*-keyring.gpg /usr/share/keyrings/ -# sudo apt-get update -# sudo apt-get install libcusparselt0 libcusparselt-dev - -# Fixes libopenblas.so.0 import error -sudo rm -r /lib/aarch64-linux-gnu/libopenblas.so.0 -sudo apt-get update -sudo apt-get remove --purge libopenblas-dev libopenblas0 libopenblas0-dev -sudo apt-get install libopenblas-dev -sudo apt-get update -sudo apt-get remove --purge libopenblas0-openmp -sudo apt-get install libopenblas0-openmp - -# Verify libopenblas.so.0 location and access -ls -l /lib/aarch64-linux-gnu/libopenblas.so.0 - diff --git a/docker/deprecated/jetson/huggingface_local/Dockerfile b/docker/deprecated/jetson/huggingface_local/Dockerfile deleted file mode 100644 index dcb1738b90..0000000000 --- a/docker/deprecated/jetson/huggingface_local/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -FROM python:3.10.12 - -# Unitree Specific -RUN apt-get update && apt-get install -y \ - libgl1-mesa-glx \ - build-essential \ - libavformat-dev \ - libavcodec-dev \ - libavdevice-dev \ - libavutil-dev \ - libswscale-dev \ - libpostproc-dev \ - gcc \ - make \ - portaudio19-dev \ - python3-pyaudio \ - python3-all-dev \ - libopenblas0-openmp - -# Jetson Orin Nano specific setup -RUN wget https://developer.download.nvidia.com/compute/cusparselt/0.7.0/local_installers/cusparselt-local-tegra-repo-ubuntu2204-0.7.0_1.0-1_arm64.deb && \ - dpkg -i cusparselt-local-tegra-repo-ubuntu2204-0.7.0_1.0-1_arm64.deb && \ - cp /var/cusparselt-local-tegra-repo-ubuntu2204-0.7.0/cusparselt-*-keyring.gpg /usr/share/keyrings/ && \ - apt-get update && \ - apt-get install -y libcusparselt0 libcusparselt-dev - - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -COPY docker/jetson/jetson_requirements.txt ./requirements.txt - -COPY ./dimos/perception/external ./dimos/perception/external - -RUN pip install --no-cache-dir -r requirements.txt - -COPY ./dimos ./dimos - -COPY ./tests ./tests - -COPY ./dimos/__init__.py ./ - -# Copy libopenblas.so.0 from host if it exists (Jetson path) -RUN ldconfig diff --git a/docker/deprecated/jetson/huggingface_local/docker-compose.yml b/docker/deprecated/jetson/huggingface_local/docker-compose.yml deleted file mode 100644 index 4d87ce30f7..0000000000 --- a/docker/deprecated/jetson/huggingface_local/docker-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ ---- -services: - dimos-model-huggingface-local: - image: dimos-jetson-huggingface-local:latest - build: - context: ../../../ - dockerfile: docker/jetson/huggingface_local/Dockerfile - env_file: - - ../../../.env - mem_limit: 8048m - volumes: - - ../../../assets:/app/assets - - ../../../assets/model-cache:/root/.cache/huggingface/hub - - /usr/local/cuda:/usr/local/cuda - - /usr/lib/aarch64-linux-gnu:/usr/lib/aarch64-linux-gnu - - ports: - - "5555:5555" - runtime: nvidia - environment: - - PYTHONUNBUFFERED=1 - - NVIDIA_VISIBLE_DEVICES=all - - NVIDIA_DRIVER_CAPABILITIES=all - # command: [ "python", "-m", "tests.test_agent_alibaba" ] - command: [ "python", "-m", "tests.test_agent_huggingface_local_jetson.py" ] - stdin_open: true - tty: true - -# IMPORTANT: This runs soley on the NVIDA GPU - -# ---- -# TO RUN: -# docker build -f ./Dockerfile -t dimos-models ../../ && docker compose up -# GO TO: -# 127.0.0.1:5555 (when flask server fixed) -# ---- diff --git a/docker/deprecated/jetson/jetson_requirements.txt b/docker/deprecated/jetson/jetson_requirements.txt deleted file mode 100644 index 6d42f2dc4c..0000000000 --- a/docker/deprecated/jetson/jetson_requirements.txt +++ /dev/null @@ -1,79 +0,0 @@ -opencv-python -python-dotenv -openai -anthropic>=0.19.0 -numpy -colorlog==6.9.0 -yapf==0.40.2 -typeguard -empy==3.3.4 -catkin_pkg -lark - -# pycolmap - -ffmpeg-python -pytest -python-dotenv -openai -tiktoken>=0.8.0 -Flask>=2.2 -python-multipart==0.0.20 -reactivex - -# Web Extensions -fastapi>=0.115.6 -sse-starlette>=2.2.1 -uvicorn>=0.34.0 - -# Agent Memory -langchain-chroma>=0.1.4 -langchain-openai>=0.2.14 - -# Class Extraction -pydantic - -# Developer Specific -ipykernel - -# Unitree webrtc streaming -aiortc==1.9.0 -pycryptodome -opencv-python -sounddevice -pyaudio -requests -wasmtime - -# Audio -openai-whisper -soundfile - -#Hugging Face -transformers[torch]==4.49.0 - -#Vector Embedding -sentence_transformers - -# CTransforms GGUF - GPU required -ctransformers[cuda]==0.2.27 - -# Perception Dependencies -ultralytics>=8.3.70 -filterpy>=1.4.5 -scipy>=1.15.1 - -# Pytorch wheel for JP6, cu12.6 -https://pypi.jetson-ai-lab.dev/jp6/cu126/+f/6cc/6ecfe8a5994fd/torch-2.6.0-cp310-cp310-linux_aarch64.whl - -# Torchvision wheel for JP6, cu12.6 -https://pypi.jetson-ai-lab.dev/jp6/cu126/+f/aa2/2da8dcf4c4c8d/torchvision-0.21.0-cp310-cp310-linux_aarch64.whl - -scikit-learn -Pillow -mmengine>=0.10.3 -mmcv==2.1.0 -timm==1.0.15 -lap==0.5.12 -# xformers==0.0.22 -# -e ./dimos/perception/external/vector_perception diff --git a/docker/deprecated/models/ctransformers_gguf/Dockerfile b/docker/deprecated/models/ctransformers_gguf/Dockerfile deleted file mode 100644 index a0e8a1edb0..0000000000 --- a/docker/deprecated/models/ctransformers_gguf/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04 - -# Set up Python environment -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y \ - python3.10 \ - python3-pip \ - python3.10-venv \ - python3-dev \ - libgl1-mesa-glx \ - build-essential \ - libavformat-dev \ - libavcodec-dev \ - libavdevice-dev \ - libavutil-dev \ - libswscale-dev \ - libpostproc-dev \ - gcc \ - make \ - portaudio19-dev \ - python3-pyaudio \ - python3-all-dev \ - git \ - wget \ - && rm -rf /var/lib/apt/lists/* - -# Create symlink for python -RUN ln -sf /usr/bin/python3.10 /usr/bin/python - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -COPY requirements.txt ./ - -RUN pip install --no-cache-dir -r requirements.txt - -COPY ./dimos ./dimos - -COPY ./tests ./tests - -COPY ./dimos/__init__.py ./ - -# Add CUDA libraries to the path -ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH - -CMD [ "python", "-m", "tests.test_agent_ctransformers_gguf" ] diff --git a/docker/deprecated/models/ctransformers_gguf/docker-compose.yml b/docker/deprecated/models/ctransformers_gguf/docker-compose.yml deleted file mode 100644 index 9cedfa4aa0..0000000000 --- a/docker/deprecated/models/ctransformers_gguf/docker-compose.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -services: - dimos-model-ctransformers-gguf: - image: dimos-model-ctransformers-gguf:latest - build: - context: ../../../ - dockerfile: docker/models/ctransformers_gguf/Dockerfile - env_file: - - ../../../.env - mem_limit: 8048m - volumes: - - ../../../assets:/app/assets - - ../../../assets/model-cache:/root/.cache/huggingface/hub - ports: - - "5555:5555" - runtime: nvidia - environment: - - PYTHONUNBUFFERED=1 - - NVIDIA_VISIBLE_DEVICES=all - - NVIDIA_DRIVER_CAPABILITIES=all - command: [ "python", "-m", "tests.test_agent_ctransformers_gguf" ] - stdin_open: true - tty: true - -# IMPORTANT: This runs soley on the NVIDA GPU - -# ---- -# TO RUN: -# docker build -f ./Dockerfile -t dimos-models ../../ && docker compose up -# GO TO: -# 127.0.0.1:5555 (when flask server fixed) -# ---- diff --git a/docker/deprecated/models/huggingface_local/Dockerfile b/docker/deprecated/models/huggingface_local/Dockerfile deleted file mode 100644 index 2c5435ae5f..0000000000 --- a/docker/deprecated/models/huggingface_local/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM python:3.10.12 - -# Unitree Specific -RUN apt-get update && apt-get install -y \ - libgl1-mesa-glx \ - build-essential \ - libavformat-dev \ - libavcodec-dev \ - libavdevice-dev \ - libavutil-dev \ - libswscale-dev \ - libpostproc-dev \ - gcc \ - make \ - portaudio19-dev \ - python3-pyaudio \ - python3-all-dev - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -COPY requirements.txt ./ - -RUN pip install --no-cache-dir -r requirements.txt - -COPY ./dimos ./dimos - -COPY ./tests ./tests - -COPY ./dimos/__init__.py ./ - -CMD [ "python", "-m", "tests.test_agent_alibaba" ] diff --git a/docker/deprecated/models/huggingface_local/docker-compose.yml b/docker/deprecated/models/huggingface_local/docker-compose.yml deleted file mode 100644 index e5739be2c2..0000000000 --- a/docker/deprecated/models/huggingface_local/docker-compose.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -services: - dimos-model-huggingface-local: - image: dimos-model-huggingface-local:latest - build: - context: ../../../ - dockerfile: docker/models/huggingface_local/Dockerfile - env_file: - - ../../../.env - mem_limit: 8048m - volumes: - - ../../../assets:/app/assets - - ../../../assets/model-cache:/root/.cache/huggingface/hub - ports: - - "5555:5555" - runtime: nvidia - environment: - - PYTHONUNBUFFERED=1 - - NVIDIA_VISIBLE_DEVICES=all - - NVIDIA_DRIVER_CAPABILITIES=all - # command: [ "python", "-m", "tests.test_agent_alibaba" ] - command: [ "python", "-m", "tests.test_agent_huggingface_local.py" ] - stdin_open: true - tty: true - -# IMPORTANT: This runs soley on the NVIDA GPU - -# ---- -# TO RUN: -# docker build -f ./Dockerfile -t dimos-models ../../ && docker compose up -# GO TO: -# 127.0.0.1:5555 (when flask server fixed) -# ---- diff --git a/docker/deprecated/models/huggingface_remote/Dockerfile b/docker/deprecated/models/huggingface_remote/Dockerfile deleted file mode 100644 index 2c5435ae5f..0000000000 --- a/docker/deprecated/models/huggingface_remote/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM python:3.10.12 - -# Unitree Specific -RUN apt-get update && apt-get install -y \ - libgl1-mesa-glx \ - build-essential \ - libavformat-dev \ - libavcodec-dev \ - libavdevice-dev \ - libavutil-dev \ - libswscale-dev \ - libpostproc-dev \ - gcc \ - make \ - portaudio19-dev \ - python3-pyaudio \ - python3-all-dev - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -COPY requirements.txt ./ - -RUN pip install --no-cache-dir -r requirements.txt - -COPY ./dimos ./dimos - -COPY ./tests ./tests - -COPY ./dimos/__init__.py ./ - -CMD [ "python", "-m", "tests.test_agent_alibaba" ] diff --git a/docker/deprecated/models/huggingface_remote/docker-compose.yml b/docker/deprecated/models/huggingface_remote/docker-compose.yml deleted file mode 100644 index e2337fcd37..0000000000 --- a/docker/deprecated/models/huggingface_remote/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -services: - dimos-model-huggingface-remote: - image: dimos-model-huggingface-remote:latest - build: - context: ../../../ - dockerfile: docker/models/huggingface_remote/Dockerfile - env_file: - - ../../../.env - mem_limit: 8048m - volumes: - - ../../../assets:/app/assets - # - ../../../assets/model-cache:/root/.cache/huggingface/hub - ports: - - "5555:5555" - environment: - - PYTHONUNBUFFERED=1 - command: [ "python", "-m", "tests.test_agent_huggingface_remote" ] - stdin_open: true - tty: true - -# ---- -# TO RUN: -# docker build -f ./Dockerfile -t dimos-models ../../ && docker compose up -# GO TO: -# 127.0.0.1:5555 (when flask server fixed) -# ---- diff --git a/docker/deprecated/simulation/entrypoint.sh b/docker/deprecated/simulation/entrypoint.sh deleted file mode 100644 index 373fa6f05c..0000000000 --- a/docker/deprecated/simulation/entrypoint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -export PYTHONPATH="${PYTHONPATH}:/app" -source /opt/ros/humble/setup.bash -#source /home/ros/dev_ws/install/setup.bash -exec "$@" \ No newline at end of file diff --git a/docker/deprecated/simulation/genesis/10_nvidia.json b/docker/deprecated/simulation/genesis/10_nvidia.json deleted file mode 100644 index 2bfcca059e..0000000000 --- a/docker/deprecated/simulation/genesis/10_nvidia.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "file_format_version" : "1.0.0", - "ICD" : { - "library_path" : "libEGL_nvidia.so.0" - } -} diff --git a/docker/deprecated/simulation/genesis/Dockerfile b/docker/deprecated/simulation/genesis/Dockerfile deleted file mode 100644 index d22473b7cd..0000000000 --- a/docker/deprecated/simulation/genesis/Dockerfile +++ /dev/null @@ -1,131 +0,0 @@ -# From https://github.com/Genesis-Embodied-AI/Genesis/blob/main/docker/Dockerfile -ARG CUDA_VERSION=12.1 - -# =============================================================== -# Stage 1: Build LuisaRender -# =============================================================== -FROM pytorch/pytorch:2.5.1-cuda${CUDA_VERSION}-cudnn9-devel AS builder - -ENV DEBIAN_FRONTEND=noninteractive -ARG PYTHON_VERSION=3.11 - -# Install necessary packages -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - manpages-dev \ - libvulkan-dev \ - zlib1g-dev \ - xorg-dev libglu1-mesa-dev \ - libsnappy-dev \ - software-properties-common \ - git \ - curl \ - wget -RUN add-apt-repository ppa:ubuntu-toolchain-r/test && \ - apt update && \ - apt install -y --no-install-recommends \ - gcc-11 \ - g++-11 \ - gcc-11 g++-11 patchelf && \ - rm -rf /var/lib/apt/lists/* - -# Set GCC-11 and G++-11 as the default -RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 && \ - update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 110 - -# Install Rust for build requirements -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - -RUN pip install "pybind11[global]" - -# Install CMake -RUN wget https://github.com/Kitware/CMake/releases/download/v3.31.0-rc2/cmake-3.31.0-rc2-linux-x86_64.sh && \ - chmod +x cmake-3.31.0-rc2-linux-x86_64.sh && \ - ./cmake-3.31.0-rc2-linux-x86_64.sh --skip-license --prefix=/usr/local && \ - rm cmake-3.31.0-rc2-linux-x86_64.sh - -# Build LuisaRender -WORKDIR /workspace -RUN git clone https://github.com/Genesis-Embodied-AI/Genesis.git && \ - cd Genesis && \ - git submodule update --init --recursive -COPY ./docker/simulation/genesis/build_luisa.sh /workspace/build_luisa.sh -RUN chmod +x ./build_luisa.sh && ./build_luisa.sh ${PYTHON_VERSION} - -# =============================================================== -# Stage 2: Runtime Environment -# =============================================================== -FROM pytorch/pytorch:2.5.1-cuda${CUDA_VERSION}-cudnn9-devel - -ARG PYTHON_VERSION=3.11 -ENV DEBIAN_FRONTEND=noninteractive -ENV NVIDIA_DRIVER_CAPABILITIES=all - -# Install runtime dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - tmux \ - git \ - curl \ - wget \ - bash-completion \ - libgl1 \ - libgl1-mesa-glx \ - libegl-dev \ - libegl1 \ - libxrender1 \ - libglib2.0-0 \ - ffmpeg \ - libgtk2.0-dev \ - pkg-config \ - libvulkan-dev \ - libgles2 \ - libglvnd0 \ - libglx0 \ - && apt clean \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /workspace - -# --------------------------- Genesis ---------------------------- -RUN pip install --no-cache-dir open3d -RUN git clone https://github.com/Genesis-Embodied-AI/Genesis.git && \ - cd Genesis && \ - pip install . && \ - pip install --no-cache-dir PyOpenGL==3.1.5 - -# ------------------------ Motion planning ----------------------- -RUN PYTHON_MAJOR_MINOR=$(echo ${PYTHON_VERSION} | tr -d '.') && \ - wget https://github.com/ompl/ompl/releases/download/prerelease/ompl-1.6.0-cp${PYTHON_MAJOR_MINOR}-cp${PYTHON_MAJOR_MINOR}-manylinux_2_28_x86_64.whl && \ - pip install ompl-1.6.0-cp${PYTHON_MAJOR_MINOR}-cp${PYTHON_MAJOR_MINOR}-manylinux_2_28_x86_64.whl && \ - rm ompl-1.6.0-cp${PYTHON_MAJOR_MINOR}-cp${PYTHON_MAJOR_MINOR}-manylinux_2_28_x86_64.whl - -# -------------------- Surface Reconstruction -------------------- -# Set the LD_LIBRARY_PATH directly in the environment -COPY --from=builder /workspace/Genesis/genesis/ext/ParticleMesher/ParticleMesherPy /opt/conda/lib/python3.1/site-packages/genesis/ext/ParticleMesher/ParticleMesherPy -ENV LD_LIBRARY_PATH=/opt/conda/lib/python3.1/site-packages/genesis/ext/ParticleMesher/ParticleMesherPy:$LD_LIBRARY_PATH - -# --------------------- Ray Tracing Renderer --------------------- -# Copy LuisaRender build artifacts from the builder stage -COPY --from=builder /workspace/Genesis/genesis/ext/LuisaRender/build/bin /opt/conda/lib/python3.1/site-packages/genesis/ext/LuisaRender/build/bin -# fix GLIBCXX_3.4.30 not found -RUN cd /opt/conda/lib && \ - mv libstdc++.so.6 libstdc++.so.6.old && \ - ln -s /usr/lib/x86_64-linux-gnu/libstdc++.so.6 libstdc++.so.6 - -COPY ./docker/simulation/genesis/10_nvidia.json /usr/share/glvnd/egl_vendor.d/10_nvidia.json -COPY ./docker/simulation/genesis/nvidia_icd.json /usr/share/vulkan/icd.d/nvidia_icd.json -COPY ./docker/simulation/genesis/nvidia_layers.json /etc/vulkan/implicit_layer.d/nvidia_layers.json - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -# Copy application code -COPY ./dimos ./dimos -COPY ./tests ./tests -COPY ./assets ./assets -COPY ./dimos/__init__.py ./ -COPY ./docker/simulation/entrypoint.sh / -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] -CMD [ "python3", "/app/tests/genesissim/stream_camera.py" ] diff --git a/docker/deprecated/simulation/genesis/build_luisa.sh b/docker/deprecated/simulation/genesis/build_luisa.sh deleted file mode 100644 index 95d861c57f..0000000000 --- a/docker/deprecated/simulation/genesis/build_luisa.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# Check if Python version is provided -if [ -z "$1" ]; then - echo "Usage: $0 " - exit 1 -fi - -PYTHON_VERSION=$1 - -cd Genesis/genesis/ext/LuisaRender && \ -git submodule update --init --recursive && \ -mkdir -p build && \ -cmake -S . -B build \ - -D CMAKE_BUILD_TYPE=Release \ - -D PYTHON_VERSIONS=$PYTHON_VERSION \ - -D LUISA_COMPUTE_DOWNLOAD_NVCOMP=ON \ - -D LUISA_COMPUTE_DOWNLOAD_OIDN=ON \ - -D LUISA_COMPUTE_ENABLE_GUI=OFF \ - -D LUISA_COMPUTE_ENABLE_CUDA=ON \ - -Dpybind11_DIR=$(python3 -c "import pybind11; print(pybind11.get_cmake_dir())") && \ -cmake --build build -j $(nproc) \ No newline at end of file diff --git a/docker/deprecated/simulation/genesis/docker-compose.yml b/docker/deprecated/simulation/genesis/docker-compose.yml deleted file mode 100644 index 2f1187a9c1..0000000000 --- a/docker/deprecated/simulation/genesis/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -services: - dimos_simulator: - image: dimos_simulator_genesis:latest - build: - context: ../../../ - dockerfile: docker/simulation/genesis/Dockerfile - env_file: - - ../../../.env - runtime: nvidia - environment: - - NVIDIA_VISIBLE_DEVICES=all - - NVIDIA_DRIVER_CAPABILITIES=all - - PYTHONUNBUFFERED=1 - - ACCEPT_EULA=Y - - PRIVACY_CONSENT=Y - volumes: - - ./../../../assets:/app/assets - networks: - - rtsp_net - depends_on: - - mediamtx - - mediamtx: - image: bluenviron/mediamtx:latest - networks: - - rtsp_net - ports: - - "8554:8554" - - "1935:1935" - - "8888:8888" - environment: - - MTX_PROTOCOLS=tcp - - MTX_LOG_LEVEL=info - -networks: - rtsp_net: - name: rtsp_net diff --git a/docker/deprecated/simulation/genesis/nvidia_icd.json b/docker/deprecated/simulation/genesis/nvidia_icd.json deleted file mode 100644 index 69600b17ae..0000000000 --- a/docker/deprecated/simulation/genesis/nvidia_icd.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "file_format_version" : "1.0.0", - "ICD": { - "library_path": "libGLX_nvidia.so.0", - "api_version" : "1.2.155" - } -} diff --git a/docker/deprecated/simulation/genesis/nvidia_layers.json b/docker/deprecated/simulation/genesis/nvidia_layers.json deleted file mode 100644 index a8e098eb9a..0000000000 --- a/docker/deprecated/simulation/genesis/nvidia_layers.json +++ /dev/null @@ -1,22 +0,0 @@ - -{ - "file_format_version" : "1.0.0", - "layer": { - "name": "VK_LAYER_NV_optimus", - "type": "INSTANCE", - "library_path": "libGLX_nvidia.so.0", - "api_version" : "1.2.155", - "implementation_version" : "1", - "description" : "NVIDIA Optimus layer", - "functions": { - "vkGetInstanceProcAddr": "vk_optimusGetInstanceProcAddr", - "vkGetDeviceProcAddr": "vk_optimusGetDeviceProcAddr" - }, - "enable_environment": { - "__NV_PRIME_RENDER_OFFLOAD": "1" - }, - "disable_environment": { - "DISABLE_LAYER_NV_OPTIMUS_1": "" - } - } -} diff --git a/docker/deprecated/simulation/isaac/Dockerfile b/docker/deprecated/simulation/isaac/Dockerfile deleted file mode 100644 index a908d5c6e0..0000000000 --- a/docker/deprecated/simulation/isaac/Dockerfile +++ /dev/null @@ -1,190 +0,0 @@ -FROM nvcr.io/nvidia/isaac-sim:4.2.0 - -# Set up locales -ENV LANG=en_US.UTF-8 -ENV LANGUAGE=en_US:en -ENV LC_ALL=en_US.UTF-8 - -RUN apt-get update && apt-get install -y locales && \ - locale-gen en_US en_US.UTF-8 && \ - update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 && \ - rm -rf /var/lib/apt/lists/* - -# Prevent interactive prompts during installation -ENV DEBIAN_FRONTEND=noninteractive - -# Install basic dependencies -RUN apt-get update && apt-get install -y \ - software-properties-common \ - curl \ - git \ - ffmpeg \ - && rm -rf /var/lib/apt/lists/* - -# Set timezone non-interactively -ENV TZ=America/Los_Angeles -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -# Setup ROS 2 -RUN add-apt-repository universe -y \ - && curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null \ - && apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - && apt-get upgrade -y \ - && apt-get install -y \ - ros-humble-desktop \ - ros-humble-ros-base \ - ros-dev-tools \ - python3-rosdep \ - python3-colcon-common-extensions \ - python3-pip \ - python3.10-venv \ - ament-cmake \ - ros-humble-ament-cmake \ - build-essential \ - cmake \ - build-essential \ - cmake \ - python3-colcon-common-extensions \ - python3-flake8 \ - python3-rosdep \ - python3-setuptools \ - python3-vcstool \ - python3-rosinstall \ - python3-rosinstall-generator \ - python3-wstool \ - nano \ - wget \ - curl \ - vim \ - git \ - x11-apps \ - tmux \ - ros-humble-foxglove-bridge \ - ros-humble-moveit \ - ros-humble-moveit-visual-tools \ - ros-humble-moveit-ros-visualization \ - ros-humble-moveit-servo \ - ros-humble-joint-state-publisher-gui \ - ros-humble-rosbridge-suite \ - ros-humble-xacro \ - ros-humble-robot-state-publisher \ - ros-humble-teleop-twist-keyboard \ - ros-humble-teleop-twist-joy \ - ros-humble-joy \ - ros-humble-controller-manager \ - ros-humble-ros2-control \ - ros-humble-ros2-controllers \ - ros-humble-robot-state-publisher \ - ros-humble-joint-state-publisher \ - ros-humble-joint-trajectory-controller \ - ros-humble-joint-state-broadcaster \ - ros-humble-vision-msgs \ - ros-humble-ackermann-msgs \ - ros-humble-navigation2 \ - ros-humble-nav2-bringup \ - ros-humble-nav2-msgs \ - ros-humble-nav2-common \ - ros-humble-nav2-behavior-tree \ - ros-humble-nav2-costmap-2d \ - ros-humble-nav2-core \ - ros-humble-nav2-bt-navigator \ - ros-humble-pointcloud-to-laserscan \ - iputils-ping \ - net-tools \ - htop \ - python3-pip \ - ros-humble-tf* \ - ros-humble-gazebo-ros-pkgs \ - dos2unix \ - python3-genmsg \ - gpg \ - pass \ - ros-humble-depthai-ros \ - zstd \ - && rm -rf /var/lib/apt/lists/* - -RUN apt-get upgrade -y - - -# Initialize rosdep -RUN rosdep init && rosdep update - -# Setup ROS environment -RUN echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc - -# Install Python packages directly -RUN pip install --no-cache-dir \ - rospkg \ - numpy==1.24.4 \ - jsonpickle \ - scipy \ - easydict \ - matplotlib==3.9.1 \ - opencv-python \ - pyyaml \ - pyquaternion \ - pybullet \ - requests \ - pillow \ - open3d \ - av==10.0.0 \ - transforms3d \ - torch \ - torchvision \ - torchaudio \ - transformers - - -ARG USERNAME=ros -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -# Create ros home directory -RUN mkdir -p /home/$USERNAME - -RUN cd /home/$USERNAME && git clone https://github.com/isaac-sim/IsaacSim-ros_workspaces.git -RUN rosdep update -RUN /bin/bash -c "cd /home/$USERNAME/IsaacSim-ros_workspaces/humble_ws && rosdep install -i --from-path src --rosdistro humble -y" - -RUN mkdir -p /home/$USERNAME/dev_ws/src -RUN cd /home/$USERNAME/dev_ws/src && git clone https://github.com/yashas-salankimatt/thesis_ros_ws.git - -# Install ZED SDK -RUN wget https://stereolabs.sfo2.cdn.digitaloceanspaces.com/zedsdk/4.2/ZED_SDK_Ubuntu22_cuda12.1_v4.2.1.zstd.run && chmod +x ZED_SDK_Ubuntu22_cuda12.1_v4.2.1.zstd.run -RUN /bin/bash -c "./ZED_SDK_Ubuntu22_cuda12.1_v4.2.1.zstd.run -- silent skip_cuda" - -ENV ZED_SDK_ROOT_DIR=/usr/local/zed -ENV CMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH}:${ZED_SDK_ROOT_DIR} - - -RUN mkdir -p /home/$USERNAME/deps -RUN cd /home/$USERNAME/deps && git clone https://github.com/facebookresearch/segment-anything-2.git -RUN cd /home/$USERNAME/deps/segment-anything-2 && pip install -e . -RUN cd /home/$USERNAME/dev_ws -RUN chown -R $USER_UID:$USER_GID /home/$USERNAME/ - -RUN /bin/bash -c "source /opt/ros/humble/setup.bash && cd /home/$USERNAME/IsaacSim-ros_workspaces/humble_ws && colcon build" -RUN rm -rf /var/lib/apt/lists/* - -ENV CUDA_HOME=/usr/local/lib/python3.10/dist-packages/nvidia/cuda_runtime -ENV CUDA_TOOLKIT_ROOT_DIR=${CUDA_HOME} -ENV PATH=${CUDA_HOME}/bin:${PATH} -ENV LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${LD_LIBRARY_PATH} - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -# Copy application code -COPY ./dimos ./dimos -COPY ./tests ./tests -COPY ./assets ./assets -COPY ./dimos/__init__.py ./ -COPY ./docker/simulation/entrypoint.sh / -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] -CMD [ "/isaac-sim/python.sh", "/app/tests/isaacsim/stream_camera.py" ] -# For testing -#CMD ["tail", "-f", "/dev/null"] \ No newline at end of file diff --git a/docker/deprecated/simulation/isaac/docker-compose.yml b/docker/deprecated/simulation/isaac/docker-compose.yml deleted file mode 100644 index a65040c4e2..0000000000 --- a/docker/deprecated/simulation/isaac/docker-compose.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -services: - dimos_simulator: - image: dimos_simulator_isaac:latest - build: - context: ../../../ - dockerfile: docker/simulation/isaac/Dockerfile - env_file: - - ../../../.env - runtime: nvidia - environment: - - NVIDIA_VISIBLE_DEVICES=all - - NVIDIA_DRIVER_CAPABILITIES=all - - PYTHONUNBUFFERED=1 - - ACCEPT_EULA=Y - - PRIVACY_CONSENT=Y - volumes: - - ./../../../assets:/app/assets - # Isaac Sim required volumes - - ~/docker/isaac-sim/cache/kit:/isaac-sim/kit/cache:rw - - ~/docker/isaac-sim/cache/ov:/root/.cache/ov:rw - - ~/docker/isaac-sim/cache/pip:/root/.cache/pip:rw - - ~/docker/isaac-sim/cache/glcache:/root/.cache/nvidia/GLCache:rw - - ~/docker/isaac-sim/cache/computecache:/root/.nv/ComputeCache:rw - - ~/docker/isaac-sim/logs:/root/.nvidia-omniverse/logs:rw - - ~/docker/isaac-sim/data:/root/.local/share/ov/data:rw - - ~/docker/isaac-sim/documents:/root/Documents:rw - networks: - - rtsp_net - depends_on: - - mediamtx - - mediamtx: - image: bluenviron/mediamtx:latest - networks: - - rtsp_net - ports: - - "8554:8554" - - "1935:1935" - - "8888:8888" - environment: - - MTX_PROTOCOLS=tcp - - MTX_LOG_LEVEL=info - -networks: - rtsp_net: - name: rtsp_net diff --git a/docker/deprecated/unitree/agents/Dockerfile b/docker/deprecated/unitree/agents/Dockerfile deleted file mode 100644 index c46fdd66e6..0000000000 --- a/docker/deprecated/unitree/agents/Dockerfile +++ /dev/null @@ -1,146 +0,0 @@ -FROM ubuntu:22.04 - -# Avoid prompts from apt -ENV DEBIAN_FRONTEND=noninteractive - -# Set locale -RUN apt-get update && apt-get install -y locales && \ - locale-gen en_US en_US.UTF-8 && \ - update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 -ENV LANG=en_US.UTF-8 - -# Set ROS distro -ENV ROS_DISTRO=humble - -# Install basic requirements -RUN apt-get update && apt-get install -y \ - curl \ - gnupg2 \ - lsb-release \ - python3-pip \ - clang \ - portaudio19-dev \ - git \ - mesa-utils \ - libgl1-mesa-glx \ - libgl1-mesa-dri \ - software-properties-common \ - libxcb1-dev \ - libxcb-keysyms1-dev \ - libxcb-util0-dev \ - libxcb-icccm4-dev \ - libxcb-image0-dev \ - libxcb-randr0-dev \ - libxcb-shape0-dev \ - libxcb-xinerama0-dev \ - libxcb-xkb-dev \ - libxkbcommon-x11-dev \ - qtbase5-dev \ - qtchooser \ - qt5-qmake \ - qtbase5-dev-tools \ - supervisor \ - && rm -rf /var/lib/apt/lists/* - -# Install specific numpy version first -RUN pip install 'numpy<2.0.0' - -# Add ROS2 apt repository -RUN curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null - -# Install ROS2 packages and dependencies -RUN apt-get update && apt-get install -y \ - ros-${ROS_DISTRO}-desktop \ - ros-${ROS_DISTRO}-ros-base \ - ros-${ROS_DISTRO}-image-tools \ - ros-${ROS_DISTRO}-compressed-image-transport \ - ros-${ROS_DISTRO}-vision-msgs \ - ros-${ROS_DISTRO}-rviz2 \ - ros-${ROS_DISTRO}-rqt \ - ros-${ROS_DISTRO}-rqt-common-plugins \ - ros-${ROS_DISTRO}-twist-mux \ - ros-${ROS_DISTRO}-joy \ - ros-${ROS_DISTRO}-teleop-twist-joy \ - ros-${ROS_DISTRO}-navigation2 \ - ros-${ROS_DISTRO}-nav2-bringup \ - ros-${ROS_DISTRO}-nav2-amcl \ - ros-${ROS_DISTRO}-nav2-map-server \ - ros-${ROS_DISTRO}-nav2-util \ - ros-${ROS_DISTRO}-pointcloud-to-laserscan \ - ros-${ROS_DISTRO}-slam-toolbox \ - ros-${ROS_DISTRO}-foxglove-bridge \ - python3-rosdep \ - python3-rosinstall \ - python3-rosinstall-generator \ - python3-wstool \ - python3-colcon-common-extensions \ - python3-vcstool \ - build-essential \ - screen \ - tmux \ - && rm -rf /var/lib/apt/lists/* - -# Initialize rosdep -RUN rosdep init && rosdep update - -# Create workspace -WORKDIR /ros2_ws - -# Clone the repository with submodules -RUN git clone --recurse-submodules https://github.com/dimensionalOS/go2_ros2_sdk src - -# Install Python requirements -RUN cd src && pip install -r requirements.txt - -# Create dimos directory structure -RUN mkdir -p /app/dimos /app/docker - -COPY requirements.txt /app/ - -WORKDIR /app - -# Install dimos requirements -RUN pip install --no-cache-dir -r requirements.txt - -# Set PYTHONPATH permanently -ENV PYTHONPATH=/app:${PYTHONPATH} - -# Install ROS dependencies -WORKDIR /ros2_ws -RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ - rosdep install --from-paths src --ignore-src -r -y - -# Build the workspace -RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ - colcon build - -# Source ROS2 and workspace in bashrc -RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> /root/.bashrc && \ - echo "source /ros2_ws/install/setup.bash" >> /root/.bashrc - -COPY docker /app/docker/ - -# Setup supervisor configuration -COPY docker/unitree/agents/supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Copy entrypoint script -COPY docker/unitree/agents/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Copy dimos and tests -COPY dimos /app/dimos/ -COPY tests /app/tests -COPY dimos/__init__.py /app/__init__.py - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -# Create output directories for supervisord and ROS -RUN mkdir -p /app/assets/output/ -RUN mkdir -p /app/assets/output/ros - -# TODO: Cleanup multiple working directories and seprate the dockerfiles for each service. - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/docker/deprecated/unitree/agents/docker-compose.yml b/docker/deprecated/unitree/agents/docker-compose.yml deleted file mode 100644 index 6cde23e98e..0000000000 --- a/docker/deprecated/unitree/agents/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -services: - dimos-unitree-agents: - image: dimos-unitree-agents:latest - build: - context: ../../../ - dockerfile: docker/unitree/agents/Dockerfile - env_file: - - ../../../.env - environment: - PYTHONUNBUFFERED: 1 - ROBOT_IP: ${ROBOT_IP} - CONN_TYPE: ${CONN_TYPE:-webrtc} - WEBRTC_SERVER_HOST: 0.0.0.0 # Listen on all interfaces - WEBRTC_SERVER_PORT: ${WEBRTC_SERVER_PORT:-9991} - DISPLAY: ${DISPLAY:-} # For GUI applications like rviz2 - ROS_OUTPUT_DIR: /app/assets/output/ros # Change output directory - # DIMOS_MAX_WORKERS: ${DIMOS_MAX_WORKERS} - # TODO: ipc: host - volumes: - - ../../../assets:/app/assets - ports: - - "5555:5555" - mem_limit: 8048m - stdin_open: true - tty: true - diff --git a/docker/deprecated/unitree/agents/entrypoint.sh b/docker/deprecated/unitree/agents/entrypoint.sh deleted file mode 100755 index 7a8ddcae6a..0000000000 --- a/docker/deprecated/unitree/agents/entrypoint.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e - -# Create supervisor log directory -mkdir -p /app/assets/output - -# Delete old logs -echo "Cleaning up old Supervisor logs..." -rm -f /app/assets/output/*.log - -# Source ROS2 environment -source /opt/ros/${ROS_DISTRO}/setup.bash -source /ros2_ws/install/setup.bash - -# Execute the command passed to docker run -exec "$@" -# python3 -m tests.test_unitree_agent diff --git a/docker/deprecated/unitree/agents/supervisord.conf b/docker/deprecated/unitree/agents/supervisord.conf deleted file mode 100644 index b66be13e30..0000000000 --- a/docker/deprecated/unitree/agents/supervisord.conf +++ /dev/null @@ -1,35 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:ros2] -command=/bin/bash -c "source /opt/ros/humble/setup.bash && source /ros2_ws/install/setup.bash && ros2 launch go2_robot_sdk robot.launch.py" -autostart=true -autorestart=true - -stderr_logfile=/app/assets/output/ros2.err.log -stdout_logfile=/app/assets/output/ros2.out.log -environment=PYTHONUNBUFFERED=1 - -[program:dimos] -command=/bin/bash -c "sleep 10 && source /opt/ros/humble/setup.bash && source /ros2_ws/install/setup.bash && python3 /app/tests/test_planning_agent_web_interface.py" -autostart=true -autorestart=true -startsecs=11 - -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -environment=PYTHONUNBUFFERED=1 - -[unix_http_server] -file=/var/run/supervisor.sock -chmod=0700 - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - -[supervisorctl] -serverurl=unix:///var/run/supervisor.sock \ No newline at end of file diff --git a/docker/deprecated/unitree/agents_interface/Dockerfile b/docker/deprecated/unitree/agents_interface/Dockerfile deleted file mode 100644 index 3bc00d2a16..0000000000 --- a/docker/deprecated/unitree/agents_interface/Dockerfile +++ /dev/null @@ -1,151 +0,0 @@ -FROM ubuntu:22.04 - -# Avoid prompts from apt -ENV DEBIAN_FRONTEND=noninteractive - -# Set locale -RUN apt-get update && apt-get install -y locales && \ - locale-gen en_US en_US.UTF-8 && \ - update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 -ENV LANG=en_US.UTF-8 - -# Set ROS distro -ENV ROS_DISTRO=humble - -# Install basic requirements -RUN apt-get update && apt-get install -y \ - curl \ - gnupg2 \ - lsb-release \ - python3-pip \ - clang \ - portaudio19-dev \ - git \ - mesa-utils \ - libgl1-mesa-glx \ - libgl1-mesa-dri \ - software-properties-common \ - libxcb1-dev \ - libxcb-keysyms1-dev \ - libxcb-util0-dev \ - libxcb-icccm4-dev \ - libxcb-image0-dev \ - libxcb-randr0-dev \ - libxcb-shape0-dev \ - libxcb-xinerama0-dev \ - libxcb-xkb-dev \ - libxkbcommon-x11-dev \ - qtbase5-dev \ - qtchooser \ - qt5-qmake \ - qtbase5-dev-tools \ - supervisor \ - && rm -rf /var/lib/apt/lists/* - -# Install specific numpy version first -RUN pip install 'numpy<2.0.0' - -# Add ROS2 apt repository -RUN curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null - -# Install ROS2 packages and dependencies -RUN apt-get update && apt-get install -y \ - ros-${ROS_DISTRO}-desktop \ - ros-${ROS_DISTRO}-ros-base \ - ros-${ROS_DISTRO}-image-tools \ - ros-${ROS_DISTRO}-compressed-image-transport \ - ros-${ROS_DISTRO}-vision-msgs \ - ros-${ROS_DISTRO}-rviz2 \ - ros-${ROS_DISTRO}-rqt \ - ros-${ROS_DISTRO}-rqt-common-plugins \ - ros-${ROS_DISTRO}-twist-mux \ - ros-${ROS_DISTRO}-joy \ - ros-${ROS_DISTRO}-teleop-twist-joy \ - ros-${ROS_DISTRO}-navigation2 \ - ros-${ROS_DISTRO}-nav2-bringup \ - ros-${ROS_DISTRO}-nav2-amcl \ - ros-${ROS_DISTRO}-nav2-map-server \ - ros-${ROS_DISTRO}-nav2-util \ - ros-${ROS_DISTRO}-pointcloud-to-laserscan \ - ros-${ROS_DISTRO}-slam-toolbox \ - ros-${ROS_DISTRO}-foxglove-bridge \ - python3-rosdep \ - python3-rosinstall \ - python3-rosinstall-generator \ - python3-wstool \ - python3-colcon-common-extensions \ - python3-vcstool \ - build-essential \ - screen \ - tmux \ - && rm -rf /var/lib/apt/lists/* - -# Initialize rosdep -RUN rosdep init && rosdep update - -# Create workspace -WORKDIR /ros2_ws - -# Clone the repository with submodules -RUN git clone --recurse-submodules https://github.com/dimensionalOS/go2_ros2_sdk src - -# Install Python requirements -RUN cd src && pip install -r requirements.txt - -# Create dimos directory structure -RUN mkdir -p /app/dimos /app/docker - -COPY requirements.txt /app/ - -COPY base-requirements.txt /app/ - -WORKDIR /app - -# Install torch and torchvision first due to builds in requirements.txt -RUN pip install --no-cache-dir -r base-requirements.txt - -# Install dimos requirements -RUN pip install --no-cache-dir -r requirements.txt - -# Set PYTHONPATH permanently -ENV PYTHONPATH=/app:${PYTHONPATH} - -# Install ROS dependencies -WORKDIR /ros2_ws -RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ - rosdep install --from-paths src --ignore-src -r -y - -# Build the workspace -RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ - colcon build - -# Source ROS2 and workspace in bashrc -RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> /root/.bashrc && \ - echo "source /ros2_ws/install/setup.bash" >> /root/.bashrc - -COPY docker /app/docker/ - -# Setup supervisor configuration -COPY docker/unitree/agents_interface/supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Copy entrypoint script -COPY docker/unitree/agents_interface/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Copy dimos and tests -COPY dimos /app/dimos/ -COPY tests /app/tests -COPY dimos/__init__.py /app/__init__.py - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -# Create output directories for supervisord and ROS -RUN mkdir -p /app/assets/output/ -RUN mkdir -p /app/assets/output/ros - -# TODO: Cleanup multiple working directories and seprate the dockerfiles for each service. - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/docker/deprecated/unitree/agents_interface/docker-compose.yml b/docker/deprecated/unitree/agents_interface/docker-compose.yml deleted file mode 100644 index 62b59d24ba..0000000000 --- a/docker/deprecated/unitree/agents_interface/docker-compose.yml +++ /dev/null @@ -1,43 +0,0 @@ ---- -services: - dimos-unitree-agents-interface: - image: dimos-unitree-agents-interface:latest - build: - context: ../../../ - dockerfile: docker/unitree/agents_interface/Dockerfile - env_file: - - ../../../.env - environment: - - PYTHONUNBUFFERED=1 - - ROS_OUTPUT_DIR=/app/assets/output/ros # Change output directory - - NVIDIA_VISIBLE_DEVICES=all - - DISPLAY=$DISPLAY - # DIMOS_MAX_WORKERS: ${DIMOS_MAX_WORKERS} - # TODO: ipc: host - volumes: - - ../../../assets:/app/assets - - /tmp/.X11-unix:/tmp/.X11-unix - - ~/.Xauthority:/root/.Xauthority:ro - # Persist model caches in host filesystem - - ../../../assets/model-cache/torch-hub:/root/.cache/torch/hub - - ../../../assets/model-cache/iopath-cache:/root/.torch/iopath_cache - - ../../../assets/model-cache/ultralytics:/root/.config/Ultralytics - network_mode: "host" - ports: - - "5555:5555" - mem_limit: 8048m - runtime: nvidia - stdin_open: true - tty: true - - dimos-web-interface: - build: - context: ../../../ - dockerfile: docker/interface/Dockerfile - image: dimos-web-interface:latest - container_name: dimos-web-interface - network_mode: "host" - volumes: - - ../../../dimos/web/dimos_interface:/app - depends_on: - - dimos-unitree-agents-interface \ No newline at end of file diff --git a/docker/deprecated/unitree/agents_interface/entrypoint.sh b/docker/deprecated/unitree/agents_interface/entrypoint.sh deleted file mode 100755 index 7a8ddcae6a..0000000000 --- a/docker/deprecated/unitree/agents_interface/entrypoint.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e - -# Create supervisor log directory -mkdir -p /app/assets/output - -# Delete old logs -echo "Cleaning up old Supervisor logs..." -rm -f /app/assets/output/*.log - -# Source ROS2 environment -source /opt/ros/${ROS_DISTRO}/setup.bash -source /ros2_ws/install/setup.bash - -# Execute the command passed to docker run -exec "$@" -# python3 -m tests.test_unitree_agent diff --git a/docker/deprecated/unitree/agents_interface/supervisord.conf b/docker/deprecated/unitree/agents_interface/supervisord.conf deleted file mode 100644 index b03b614fcd..0000000000 --- a/docker/deprecated/unitree/agents_interface/supervisord.conf +++ /dev/null @@ -1,35 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:ros2] -command=/bin/bash -c "source /opt/ros/humble/setup.bash && source /ros2_ws/install/setup.bash && ros2 launch go2_robot_sdk robot.launch.py" -autostart=true -autorestart=true - -stderr_logfile=/app/assets/output/ros2.err.log -stdout_logfile=/app/assets/output/ros2.out.log -environment=PYTHONUNBUFFERED=1 - -[program:dimos] -command=/bin/bash -c "sleep 10 && source /opt/ros/humble/setup.bash && source /ros2_ws/install/setup.bash && python3 /app/tests/run.py --new-memory" -autostart=true -autorestart=true -startsecs=11 - -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -environment=PYTHONUNBUFFERED=1 - -[unix_http_server] -file=/var/run/supervisor.sock -chmod=0700 - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - -[supervisorctl] -serverurl=unix:///var/run/supervisor.sock \ No newline at end of file diff --git a/docker/deprecated/unitree/ros/Dockerfile b/docker/deprecated/unitree/ros/Dockerfile deleted file mode 100644 index 6d495a5065..0000000000 --- a/docker/deprecated/unitree/ros/Dockerfile +++ /dev/null @@ -1,116 +0,0 @@ -FROM ubuntu:22.04 - -# Avoid prompts from apt -ENV DEBIAN_FRONTEND=noninteractive - -# Set locale -RUN apt-get update && apt-get install -y locales && \ - locale-gen en_US en_US.UTF-8 && \ - update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 -ENV LANG=en_US.UTF-8 - -# Set ROS distro -ENV ROS_DISTRO=humble - -# Install basic requirements -RUN apt-get update && apt-get install -y \ - curl \ - gnupg2 \ - lsb-release \ - python3-pip \ - clang \ - portaudio19-dev \ - git \ - mesa-utils \ - libgl1-mesa-glx \ - libgl1-mesa-dri \ - software-properties-common \ - libxcb1-dev \ - libxcb-keysyms1-dev \ - libxcb-util0-dev \ - libxcb-icccm4-dev \ - libxcb-image0-dev \ - libxcb-randr0-dev \ - libxcb-shape0-dev \ - libxcb-xinerama0-dev \ - libxcb-xkb-dev \ - libxkbcommon-x11-dev \ - qtbase5-dev \ - qtchooser \ - qt5-qmake \ - qtbase5-dev-tools \ - && rm -rf /var/lib/apt/lists/* - -# Install specific numpy version first -RUN pip install 'numpy<2.0.0' - -# Add ROS2 apt repository -RUN curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null - -# Install ROS2 packages and dependencies -RUN apt-get update && apt-get install -y \ - ros-${ROS_DISTRO}-desktop \ - ros-${ROS_DISTRO}-ros-base \ - ros-${ROS_DISTRO}-image-tools \ - ros-${ROS_DISTRO}-compressed-image-transport \ - ros-${ROS_DISTRO}-vision-msgs \ - ros-${ROS_DISTRO}-rviz2 \ - ros-${ROS_DISTRO}-rqt \ - ros-${ROS_DISTRO}-rqt-common-plugins \ - ros-${ROS_DISTRO}-twist-mux \ - ros-${ROS_DISTRO}-joy \ - ros-${ROS_DISTRO}-teleop-twist-joy \ - ros-${ROS_DISTRO}-navigation2 \ - ros-${ROS_DISTRO}-nav2-bringup \ - ros-${ROS_DISTRO}-nav2-amcl \ - ros-${ROS_DISTRO}-nav2-map-server \ - ros-${ROS_DISTRO}-nav2-util \ - ros-${ROS_DISTRO}-pointcloud-to-laserscan \ - ros-${ROS_DISTRO}-slam-toolbox \ - ros-${ROS_DISTRO}-foxglove-bridge \ - python3-rosdep \ - python3-rosinstall \ - python3-rosinstall-generator \ - python3-wstool \ - python3-colcon-common-extensions \ - python3-vcstool \ - build-essential \ - && rm -rf /var/lib/apt/lists/* - -# Initialize rosdep -RUN rosdep init && rosdep update - -# Create workspace -WORKDIR /ros2_ws - -# Clone the repository with submodules -RUN git clone --recurse-submodules https://github.com/dimensionalOS/go2_ros2_sdk src - -# Install Python requirements (with numpy constraint) -RUN cd src && pip install -r requirements.txt - -# Install ROS dependencies -RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ - rosdep install --from-paths src --ignore-src -r -y - -# Build the workspace -RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ - colcon build - -# Source ROS2 and workspace in bashrc -RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> /root/.bashrc && \ - echo "source /ros2_ws/install/setup.bash" >> /root/.bashrc - -# Set environment variables -ENV ROBOT_IP="" -ENV CONN_TYPE="webrtc" -ENV WEBRTC_SERVER_HOST="0.0.0.0" -ENV WEBRTC_SERVER_PORT="9991" - -# Copy entrypoint script -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["ros2", "launch", "go2_robot_sdk", "robot.launch.py"] diff --git a/docker/deprecated/unitree/ros/README.md b/docker/deprecated/unitree/ros/README.md deleted file mode 100644 index 3b6deff3ad..0000000000 --- a/docker/deprecated/unitree/ros/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Unitree Go2 ROS Docker Setup - -This README explains how to run the Unitree Go2 ROS nodes using Docker. - -## Prerequisites - -- Docker and Docker Compose installed -- A Unitree Go2 robot accessible on your network -- The robot's IP address - -## Configuration - -The connection can be configured through environment variables in two ways: - -1. Setting them before running docker-compose: - ```bash - export ROBOT_IP=192.168.9.140 - export CONN_TYPE=webrtc # or cyclonedds - ``` - -2. Hardcoding them directly in `docker/docker-compose.yaml` - -## Usage - -To run the ROS nodes: - -1. Navigate to the docker directory: - ```bash - cd docker/unitree/ros - ``` - -2. Run with environment variables: - ```bash - xhost +local:root # If running locally and desire RVIZ GUI - ROBOT_IP= CONN_TYPE= docker-compose up --build - ``` - - Where: - - `` is your Go2's IP address - - `` choose either: - - `webrtc`: For WebRTC video streaming connection - - `cyclonedds`: For DDS communication - -The containers will build and start, establishing connection with your Go2 robot and opening RVIZ. - - -## Known Issues - -1. If you encounter the error `unitree_ros-1 | exec /entrypoint.sh: no such file or directory`, this can be caused by: - - Incorrect file permissions - - Windows-style line endings (CRLF) in the entrypoint script - - To fix: - 1. Ensure the entrypoint script has execute permissions: - ```bash - chmod +x entrypoint.sh - ``` - - 2. If using Windows, convert line endings to Unix format (LF): - ```bash - # Using dos2unix - dos2unix entrypoint.sh - - # Or using sed - sed -i 's/\r$//' entrypoint.sh - ``` - - - diff --git a/docker/deprecated/unitree/ros/docker-compose.yml b/docker/deprecated/unitree/ros/docker-compose.yml deleted file mode 100644 index a16aaff4c9..0000000000 --- a/docker/deprecated/unitree/ros/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -services: - unitree_ros: - image: unitree_ros:latest - build: - context: ../../../ - dockerfile: docker/unitree/ros/Dockerfile - environment: - - PYTHONUNBUFFERED=1 - - ROBOT_IP=${ROBOT_IP} - - CONN_TYPE=${CONN_TYPE:-webrtc} - - WEBRTC_SERVER_HOST=0.0.0.0 # Listen on all interfaces - - WEBRTC_SERVER_PORT=${WEBRTC_SERVER_PORT:-9991} - - DISPLAY=${DISPLAY:-} # For GUI applications like rviz2 - volumes: - - /tmp/.X11-unix:/tmp/.X11-unix # X11 forwarding - - ${HOME}/.Xauthority:/root/.Xauthority:rw - network_mode: "host" # Required for ROS2 discovery and robot communication - privileged: true # Required for hardware access - devices: - - /dev/input:/dev/input # For joystick access - restart: unless-stopped diff --git a/docker/deprecated/unitree/ros/entrypoint.sh b/docker/deprecated/unitree/ros/entrypoint.sh deleted file mode 100755 index dcdc8660c4..0000000000 --- a/docker/deprecated/unitree/ros/entrypoint.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e -# Source ROS2 environment -source /opt/ros/${ROS_DISTRO}/setup.bash -source /ros2_ws/install/setup.bash -# Execute the command passed to docker run -exec "$@" diff --git a/docker/deprecated/unitree/ros_agents/docker-compose.yml b/docker/deprecated/unitree/ros_agents/docker-compose.yml deleted file mode 100644 index 6d93ea89ab..0000000000 --- a/docker/deprecated/unitree/ros_agents/docker-compose.yml +++ /dev/null @@ -1,67 +0,0 @@ ---- -services: - dimos-unitree-ros-agents: - image: dimos-unitree-ros-agents:latest - build: - context: ../../../ - dockerfile: docker/unitree/ros_agents/Dockerfile - env_file: - - ../../../.env - environment: - PYTHONUNBUFFERED: 1 - ROBOT_IP: ${ROBOT_IP} - CONN_TYPE: ${CONN_TYPE:-webrtc} - WEBRTC_SERVER_HOST: 0.0.0.0 # Listen on all interfaces - WEBRTC_SERVER_PORT: ${WEBRTC_SERVER_PORT:-9991} - DISPLAY: ${DISPLAY:-} # For GUI applications like rviz2 - ROS_OUTPUT_DIR: /app/assets/output/ros # Change output directory - # DIMOS_MAX_WORKERS: ${DIMOS_MAX_WORKERS} - # TODO: ipc: host - volumes: - - ../../../assets:/app/assets - network_mode: "host" - ports: - - "5555:5555" - mem_limit: 8048m - stdin_open: true - tty: true - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5555/unitree/status"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - - dimos-web-interface: - build: - context: ../../../ - dockerfile: docker/interface/Dockerfile - image: dimos-web-interface:latest - container_name: dimos-web-interface - network_mode: "host" - volumes: - - ../../../dimos/web/dimos_interface:/app - depends_on: - dimos-unitree-ros-agents: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"] - interval: 30s - timeout: 10s - retries: 3 - - -# ---- -# TO RUN: -# docker build -f ./Dockerfile -t dimos ../../ && docker compose up -# GO TO: -# 127.0.0.1:5555 (when flask server fixed) -# ---- - -# video-service: -# build: ./video-service -# image: video-service:latest -# volumes: -# - ./../../assets:/app/dimos-env/assets -# ports: -# - "23001:23001" diff --git a/docker/deprecated/unitree/ros_dimos/Dockerfile b/docker/deprecated/unitree/ros_dimos/Dockerfile deleted file mode 100644 index 3c712a3578..0000000000 --- a/docker/deprecated/unitree/ros_dimos/Dockerfile +++ /dev/null @@ -1,148 +0,0 @@ -FROM ubuntu:22.04 - -# Avoid prompts from apt -ENV DEBIAN_FRONTEND=noninteractive - -# Set locale -RUN apt-get update && apt-get install -y locales && \ - locale-gen en_US en_US.UTF-8 && \ - update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 -ENV LANG=en_US.UTF-8 - -# Set ROS distro -ENV ROS_DISTRO=humble - -# Install basic requirements -RUN apt-get update && apt-get install -y \ - curl \ - gnupg2 \ - lsb-release \ - python3-pip \ - clang \ - portaudio19-dev \ - git \ - mesa-utils \ - libgl1-mesa-glx \ - libgl1-mesa-dri \ - software-properties-common \ - libxcb1-dev \ - libxcb-keysyms1-dev \ - libxcb-util0-dev \ - libxcb-icccm4-dev \ - libxcb-image0-dev \ - libxcb-randr0-dev \ - libxcb-shape0-dev \ - libxcb-xinerama0-dev \ - libxcb-xkb-dev \ - libxkbcommon-x11-dev \ - qtbase5-dev \ - qtchooser \ - qt5-qmake \ - qtbase5-dev-tools \ - supervisor \ - && rm -rf /var/lib/apt/lists/* - -# Install specific numpy version first -RUN pip install 'numpy<2.0.0' - -# Add ROS2 apt repository -RUN curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null - -# Install ROS2 packages and dependencies -RUN apt-get update && apt-get install -y \ - ros-${ROS_DISTRO}-desktop \ - ros-${ROS_DISTRO}-ros-base \ - ros-${ROS_DISTRO}-image-tools \ - ros-${ROS_DISTRO}-compressed-image-transport \ - ros-${ROS_DISTRO}-vision-msgs \ - ros-${ROS_DISTRO}-rviz2 \ - ros-${ROS_DISTRO}-rqt \ - ros-${ROS_DISTRO}-rqt-common-plugins \ - ros-${ROS_DISTRO}-twist-mux \ - ros-${ROS_DISTRO}-joy \ - ros-${ROS_DISTRO}-teleop-twist-joy \ - ros-${ROS_DISTRO}-navigation2 \ - ros-${ROS_DISTRO}-nav2-bringup \ - ros-${ROS_DISTRO}-nav2-amcl \ - ros-${ROS_DISTRO}-nav2-map-server \ - ros-${ROS_DISTRO}-nav2-util \ - ros-${ROS_DISTRO}-pointcloud-to-laserscan \ - ros-${ROS_DISTRO}-slam-toolbox \ - ros-${ROS_DISTRO}-foxglove-bridge \ - python3-rosdep \ - python3-rosinstall \ - python3-rosinstall-generator \ - python3-wstool \ - python3-colcon-common-extensions \ - python3-vcstool \ - build-essential \ - && rm -rf /var/lib/apt/lists/* - -# Initialize rosdep -RUN rosdep init && rosdep update - -# Create workspace -WORKDIR /ros2_ws - -# Clone the repository with submodules -RUN git clone --recurse-submodules https://github.com/dimensionalOS/go2_ros2_sdk src - -# Install Python requirements -RUN cd src && pip install -r requirements.txt - -# Create dimos directory structure -RUN mkdir -p /app/dimos /app/docker - -COPY requirements.txt /app/ - -WORKDIR /app - -# Install dimos requirements -RUN pip install --no-cache-dir -r requirements.txt - -# Set PYTHONPATH permanently -ENV PYTHONPATH=/app:${PYTHONPATH} - -# Install ROS dependencies -WORKDIR /ros2_ws -RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ - rosdep install --from-paths src --ignore-src -r -y - -# Build the workspace -RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ - colcon build - -# Source ROS2 and workspace in bashrc -RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> /root/.bashrc && \ - echo "source /ros2_ws/install/setup.bash" >> /root/.bashrc - -# Set environment variables -# webrtc or cyclonedds -ENV CONN_TYPE="webrtc" -ENV WEBRTC_SERVER_HOST="0.0.0.0" -ENV WEBRTC_SERVER_PORT="9991" - -COPY docker /app/docker/ - -# Setup supervisor configuration -COPY docker/unitree/ros_dimos/supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Copy entrypoint script -COPY docker/unitree/ros_dimos/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -COPY dimos /app/dimos/ -COPY tests /app/tests/ - -# Change working directory to /app for proper relative pathing -WORKDIR /app - -# Create output directories for supervisord and ROS -RUN mkdir -p /app/assets/output/ -RUN mkdir -p /app/assets/output/ros - -# TODO: Cleanup multiple working directories and seprate the dockerfiles for each service. - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/docker/deprecated/unitree/ros_dimos/README.md b/docker/deprecated/unitree/ros_dimos/README.md deleted file mode 100644 index 4c63aaddb2..0000000000 --- a/docker/deprecated/unitree/ros_dimos/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# Unitree Go2 ROS + DIMOS Movement Agents Docker Setup - -This README explains how to run the Unitree Go2 ROS nodes with DIMOS integration using Docker. - -## Prerequisites - -- Docker and Docker Compose installed -- A Unitree Go2 robot accessible on your network -- The robot's IP address -- Python requirements installed (see root directory's requirements.txt) - -## Configuration - -1. Set environment variables in .env: - ```bash - ROBOT_IP= - CONN_TYPE=webrtc - WEBRTC_SERVER_HOST=0.0.0.0 - WEBRTC_SERVER_PORT=9991 - DISPLAY=:0 - ROS_OUTPUT_DIR=/app/assets/output/ros - ``` - -2. Or run with environment variables in command line docker-compose: - ```bash - ROBOT_IP=192.168.9.140 CONN_TYPE=webrtc docker compose -f docker/unitree/ros_dimos/docker-compose.yml up --build - ``` - -## Usage - -To run the ROS nodes with DIMOS: - -```bash -xhost +local:root # If running locally and desire RVIZ GUI -ROBOT_IP= CONN_TYPE= docker compose -f docker/unitree/ros_dimos/docker-compose.yml up --build -``` - -Where: -- `` is your Go2's IP address -- `` choose either: - - `webrtc`: For WebRTC video streaming connection - - `cyclonedds`: For DDS communication - -The containers will build and start, establishing connection with your Go2 robot and opening RVIZ. The DIMOS integration will start 10 seconds after ROS to ensure proper initialization. - -Note: You can run this command from any directory since the docker-compose.yml file handles all relative paths internally. - -## Process Management - -The setup uses supervisord to manage both ROS and DIMOS processes. To check process status or view logs when inside the container: - -```bash -# Get a shell in the container -docker compose -f docker/unitree/ros_dimos/docker-compose.yml exec unitree_ros_dimos bash - -# View process status -supervisorctl status - -# View logs -supervisorctl tail ros2 # ROS2 logs -supervisorctl tail dimos # DIMOS logs -supervisorctl tail -f ros2 # Follow ROS2 logs -``` - -## Known Issues - -1. ROS2 doesn't have time to initialize before DIMOS starts, so the DIMOS logs will show successful aioice.ice:Connection followed by aiortc.exceptions.InvalidStateError. - -This is currently solved by hardcoding a delay between ros2 and DIMOS start in supervisord.conf. - -```ini -[lifecycle_manager-18] [INFO] [1740128988.350926960] [lifecycle_manager_navigation]: Managed nodes are active -[lifecycle_manager-18] [INFO] [1740128988.350965828] [lifecycle_manager_navigation]: Creating bond timer... -[go2_driver_node-3] INFO:scripts.webrtc_driver:Connection state is connecting -[go2_driver_node-3] INFO:aioice.ice:Connection(1) Discovered peer reflexive candidate Candidate(3hokvTUH7e 1 udp 2130706431 192.168.9.140 37384 typ prflx) -[go2_driver_node-3] INFO:aioice.ice:Connection(1) Check CandidatePair(('192.168.9.155', 33483) -> ('192.168.9.140', 37384)) State.WAITING -> State.IN_PROGRESS -[go2_driver_node-3] [INFO] [1740128990.171453153] [go2_driver_node]: Move -[go2_driver_node-3] INFO:scripts.webrtc_driver:Receiving video -[go2_driver_node-3] ERROR:asyncio:Task exception was never retrieved -[go2_driver_node-3] future: exception=InvalidStateError()> -[go2_driver_node-3] Traceback (most recent call last): -[go2_driver_node-3] File "/ros2_ws/install/go2_robot_sdk/lib/python3.10/site-packages/go2_robot_sdk/go2_driver_node.py", line 634, in run -[go2_driver_node-3] self.joy_cmd(robot_num) -[go2_driver_node-3] File "/ros2_ws/install/go2_robot_sdk/lib/python3.10/site-packages/go2_robot_sdk/go2_driver_node.py", line 320, in joy_cmd -[go2_driver_node-3] self.conn[robot_num].data_channel.send( -[go2_driver_node-3] File "/usr/local/lib/python3.10/dist-packages/aiortc/rtcdatachannel.py", line 182, in send -[go2_driver_node-3] raise InvalidStateError -[go2_driver_node-3] aiortc.exceptions.InvalidStateError -[go2_driver_node-3] Exception in thread Thread-1 (_spin): -[go2_driver_node-3] Traceback (most recent call last): -[go2_driver_node-3] File "/usr/lib/python3.10/threading.py", line 1016, in _bootstrap_inner -[go2_driver_node-3] self.run() -[go2_driver_node-3] File "/usr/lib/python3.10/threading.py", line 953, in run -[go2_driver_node-3] self._target(*self._args, **self._kwargs) -[go2_driver_node-3] File "/ros2_ws/install/go2_robot_sdk/lib/python3.10/site-packages/go2_robot_sdk/go2_driver_node.py", line 646, in _spin -[go2_driver_node-3] rclpy.spin_once(node) -[go2_driver_node-3] File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/__init__.py", line 203, in spin_once -[go2_driver_node-3] executor = get_global_executor() if executor is None else executor -[go2_driver_node-3] File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/__init__.py", line 106, in get_global_executor -[go2_driver_node-3] __executor = SingleThreadedExecutor() -[go2_driver_node-3] File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 721, in __init__ -[go2_driver_node-3] super().__init__(context=context) -[go2_driver_node-3] File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 172, in __init__ -[go2_driver_node-3] self._guard = GuardCondition( -[go2_driver_node-3] File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/guard_condition.py", line 23, in __init__ -[go2_driver_node-3] with self._context.handle: -[go2_driver_node-3] AttributeError: __enter__ -[go2_driver_node-3] Exception ignored in: -[go2_driver_node-3] Traceback (most recent call last): -[go2_driver_node-3] File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 243, in __del__ -[go2_driver_node-3] if self._sigint_gc is not None: -[go2_driver_node-3] AttributeError: 'SingleThreadedExecutor' object has no attribute '_sigint_gc' -[go2_driver_node-3] ERROR:asyncio:Task was destroyed but it is pending! -[go2_driver_node-3] task: wait_for=._outer_done_callback() at /usr/lib/python3.10/asyncio/tasks.py:864, Task.task_wakeup()]>> -[go2_driver_node-3] ERROR:asyncio:Task was destroyed but it is pending! -[go2_driver_node-3] task: wait_for=> -[go2_driver_node-3] Exception ignored in: -[go2_driver_node-3] Traceback (most recent call last): -[go2_driver_node-3] File "/ros2_ws/install/go2_robot_sdk/lib/python3.10/site-packages/scripts/webrtc_driver.py", line 229, in on_track -[go2_driver_node-3] frame = await track.recv() -[go2_driver_node-3] File "/usr/local/lib/python3.10/dist-packages/aiortc/rtcrtpreceiver.py", line 203, in recv -[go2_driver_node-3] frame = await self._queue.get() -[go2_driver_node-3] File "/usr/lib/python3.10/asyncio/queues.py", line 161, in get -[go2_driver_node-3] getter.cancel() # Just in case getter is not done yet. -[go2_driver_node-3] File "/usr/lib/python3.10/asyncio/base_events.py", line 753, in call_soon -[go2_driver_node-3] self._check_closed() -[go2_driver_node-3] File "/usr/lib/python3.10/asyncio/base_events.py", line 515, in _check_closed -[go2_driver_node-3] raise RuntimeError('Event loop is closed') -[go2_driver_node-3] RuntimeError: Event loop is closed -[go2_driver_node-3] ERROR:asyncio:Task was destroyed but it is pending! -[go2_driver_node-3] task: wait_for= cb=[AsyncIOEventEmitter._emit_run..callback() at /usr/local/lib/python3.10/dist-packages/pyee/asyncio.py:95]> -[go2_driver_node-3] ERROR:asyncio:Task was destroyed but it is pending! -[go2_driver_node-3] task: wait_for=> -[go2_driver_node-3] ERROR:asyncio:Task was destroyed but it is pending! -[go2_driver_node-3] task: wait_for=> -[go2_driver_node-3] ERROR:asyncio:Task was destroyed but it is pending! -[go2_driver_node-3] task: wait_for=> -[INFO] [go2_driver_node-3]: process has finished cleanly [pid 120] -``` - - -2. If you encounter the error `unitree_ros_dimos-1 | exec /entrypoint.sh: no such file or directory`, this can be caused by: - - Incorrect file permissions - - Windows-style line endings (CRLF) in the entrypoint script - - To fix: - 1. Ensure the entrypoint script has execute permissions: - ```bash - chmod +x /path/to/dimos/docker/unitree/ros_dimos/entrypoint.sh - ``` - - 2. If using Windows, convert line endings to Unix format (LF): - ```bash - # Using dos2unix - dos2unix /path/to/dimos/docker/unitree/ros_dimos/entrypoint.sh - - # Or using sed - sed -i 's/\r$//' /path/to/dimos/docker/unitree/ros_dimos/entrypoint.sh - ``` - -2. If DIMOS fails to start, check: - - The ROS nodes are fully initialized (wait a few seconds) - - The environment variables are properly set - - The Python path includes the dimos directory - - The logs using supervisorctl for specific error messages \ No newline at end of file diff --git a/docker/deprecated/unitree/ros_dimos/docker-compose.yml b/docker/deprecated/unitree/ros_dimos/docker-compose.yml deleted file mode 100644 index 2d36b4d479..0000000000 --- a/docker/deprecated/unitree/ros_dimos/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -services: - unitree_ros_dimos: - image: unitree_ros_dimos:latest - build: - context: ../../../ - dockerfile: docker/unitree/ros_dimos/Dockerfile - env_file: - - ../../../.env - volumes: - - /tmp/.X11-unix:/tmp/.X11-unix # X11 forwarding - - ${HOME}/.Xauthority:/root/.Xauthority:rw - - ../../../assets/output/:/app/assets/output - network_mode: "host" # Required for ROS2 discovery and robot communication - privileged: true # Required for hardware access - devices: - - /dev/input:/dev/input # For joystick access - restart: unless-stopped diff --git a/docker/deprecated/unitree/ros_dimos/entrypoint.sh b/docker/deprecated/unitree/ros_dimos/entrypoint.sh deleted file mode 100755 index f7d753f1f7..0000000000 --- a/docker/deprecated/unitree/ros_dimos/entrypoint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -e - -# Create supervisor log directory - -mkdir -p /app/assets/output - -# Delete old logs -echo "Cleaning up old Supervisor logs..." -rm -f /app/assets/output/*.log - -# Source ROS2 environment -source /opt/ros/${ROS_DISTRO}/setup.bash -source /ros2_ws/install/setup.bash -# Execute the command passed to docker run -exec "$@" diff --git a/docker/deprecated/unitree/ros_dimos/supervisord.conf b/docker/deprecated/unitree/ros_dimos/supervisord.conf deleted file mode 100644 index 105742b844..0000000000 --- a/docker/deprecated/unitree/ros_dimos/supervisord.conf +++ /dev/null @@ -1,35 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:ros2] -command=/bin/bash -c "source /opt/ros/humble/setup.bash && source /ros2_ws/install/setup.bash && ros2 launch go2_robot_sdk robot.launch.py" -autostart=true -autorestart=true - -stderr_logfile=/app/assets/output/ros2.err.log -stdout_logfile=/app/assets/output/ros2.out.log -environment=PYTHONUNBUFFERED=1 - -[program:dimos] -command=/bin/bash -c "sleep 10 && source /opt/ros/humble/setup.bash && source /ros2_ws/install/setup.bash && python3 /app/tests/run_go2_ros.py" -autostart=true -autorestart=true -startsecs=11 - -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -environment=PYTHONUNBUFFERED=1 - -[unix_http_server] -file=/var/run/supervisor.sock -chmod=0700 - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - -[supervisorctl] -serverurl=unix:///var/run/supervisor.sock diff --git a/docker/deprecated/unitree/webrtc/Dockerfile b/docker/deprecated/unitree/webrtc/Dockerfile deleted file mode 100644 index c073fbbe08..0000000000 --- a/docker/deprecated/unitree/webrtc/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM python:3 - -RUN apt-get update && apt-get install -y \ - libgl1-mesa-glx \ - build-essential \ - libavformat-dev \ - libavcodec-dev \ - libavdevice-dev \ - libavutil-dev \ - libswscale-dev \ - libpostproc-dev \ - gcc \ - make \ - portaudio19-dev \ - python3-pyaudio \ - python3-all-dev - -WORKDIR /app - -COPY requirements.txt ./ - -RUN pip install --no-cache-dir -r requirements.txt - -COPY ./dimos ./dimos - -COPY ./tests ./tests - -COPY ./dimos/__init__.py ./ - -CMD [ "python", "-m", "dimos.robot.unitree.unitree_go2" ] diff --git a/docker/deprecated/unitree/webrtc/docker-compose.yml b/docker/deprecated/unitree/webrtc/docker-compose.yml deleted file mode 100644 index c8e9f234f6..0000000000 --- a/docker/deprecated/unitree/webrtc/docker-compose.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -services: - dimos-unitree-webrtc: - image: dimos-unitree-webrtc:latest - build: - context: ../../../ - dockerfile: docker/unitree/webrtc/Dockerfile - env_file: - - ../../../.env - mem_limit: 8048m - volumes: - - ../../../assets:/app/assets - - ../../../output:/app/output - ports: - - "5555:5555" - environment: - - PYTHONUNBUFFERED=1 - # Robot configuration - use shell variables with defaults - - ROBOT_IP=${ROBOT_IP} - - CONNECTION_METHOD=${CONNECTION_METHOD:-LocalSTA} - - SERIAL_NUMBER=${SERIAL_NUMBER:-} - - OUTPUT_DIR=${OUTPUT_DIR:-/app/assets} - stdin_open: true - tty: true - command: ["python", "-m", "dimos.robot.unitree.run_go2"] - # command: ["tail", "-f", "/dev/null"] - -# ---- -# TO RUN with default values: -# docker compose up -# -# TO RUN with custom parameters: -# ROBOT_IP=192.168.1.100 CONNECTION_METHOD=LocalAP SERIAL_NUMBER=ABC123 docker compose up -# -# Examples: -# - With IP: -# ROBOT_IP=192.168.1.100 docker compose up -# -# - With LocalAP: -# CONNECTION_METHOD=LocalAP docker compose up -# -# - With Serial Number: -# CONNECTION_METHOD=LocalSTA SERIAL_NUMBER=ABC123 docker compose up -# ---- diff --git a/docker/dev/bash.sh b/docker/dev/bash.sh index 878faa23c5..c5248841d9 100755 --- a/docker/dev/bash.sh +++ b/docker/dev/bash.sh @@ -60,7 +60,7 @@ function tmpUmask { oldUmask=$(umask) newUmask=$1 - + shift umask $newUmask echo umask $(umask -S) @@ -68,7 +68,7 @@ function tmpUmask eval $@ umask $oldUmask echo umask $(umask -S) - + } function newloginuser diff --git a/docker/dev/tmux.conf b/docker/dev/tmux.conf index aad055fe5a..ecf6b22ced 100644 --- a/docker/dev/tmux.conf +++ b/docker/dev/tmux.conf @@ -35,7 +35,7 @@ bind c new-window -c "#{pane_current_path}" #bind -n C-M-right swap-window -t +1 #set -g default-terminal "screen-256color" #set -g default-terminal "xterm" - + bind-key u capture-pane \; save-buffer /tmp/tmux-buffer \; run-shell "urxvtc --geometry 51x20 --title 'floatme' -e bash -c \"cat /tmp/tmux-buffer | urlview\" " bind-key r source-file ~/.tmux.conf @@ -79,6 +79,6 @@ setw -g window-status-separator "#[bg=colour235]" setw -g window-status-style "fg=colour253,bg=black,none" set -g status-left "" set -g status-right "#[bg=black]#[fg=colour244]#h#[fg=colour244]#[fg=colour3]/#[fg=colour244]#S" - + setw -g window-status-format " #[fg=colour3]#I#[fg=colour244] #W " setw -g window-status-current-format " #[fg=color3]#I#[fg=colour254] #W " diff --git a/docker/navigation/.env.hardware b/docker/navigation/.env.hardware new file mode 100644 index 0000000000..05e08bd375 --- /dev/null +++ b/docker/navigation/.env.hardware @@ -0,0 +1,64 @@ +# Hardware Configuration Environment Variables +# Copy this file to .env and customize for your hardware setup + +# ============================================ +# NVIDIA GPU Support +# ============================================ +# Set the Docker runtime to nvidia for GPU support (it's runc by default) +#DOCKER_RUNTIME=nvidia + +# ============================================ +# ROS Configuration +# ============================================ +# ROS domain ID for multi-robot setups +ROS_DOMAIN_ID=42 + +# Robot configuration ('mechanum_drive', 'unitree/unitree_g1', 'unitree/unitree_g1', etc) +ROBOT_CONFIG_PATH=mechanum_drive + +# Robot IP address on local network for connection over WebRTC +# For Unitree Go2, Unitree G1, if using WebRTCConnection +# This can be found in the unitree app under Device settings or via network scan +ROBOT_IP= + +# ============================================ +# Mid-360 Lidar Configuration +# ============================================ +# Network interface connected to the lidar (e.g., eth0, enp0s3) +# Find with: ip addr show +LIDAR_INTERFACE=eth0 + +# Processing computer IP address on the lidar subnet +# Must be on the same subnet as the lidar (e.g., 192.168.1.5) +# LIDAR_COMPUTER_IP=192.168.123.5 # FOR UNITREE G1 EDU +LIDAR_COMPUTER_IP=192.168.1.5 + +# Gateway IP address for the lidar subnet +# LIDAR_GATEWAY=192.168.123.1 # FOR UNITREE G1 EDU +LIDAR_GATEWAY=192.168.1.1 + +# Full IP address of your Mid-360 lidar +# This should match the IP configured on your lidar device +# Common patterns: 192.168.1.1XX or 192.168.123.1XX +# LIDAR_IP=192.168.123.120 # FOR UNITREE G1 EDU +LIDAR_IP=192.168.1.116 + +# ============================================ +# Motor Controller Configuration +# ============================================ +# Serial device for motor controller +# Check with: ls /dev/ttyACM* or ls /dev/ttyUSB* +MOTOR_SERIAL_DEVICE=/dev/ttyACM0 + +# ============================================ +# Network Communication (for base station) +# ============================================ +# Enable WiFi buffer optimization for data transmission +# Set to true if using wireless base station +ENABLE_WIFI_BUFFER=false + +# ============================================ +# Display Configuration +# ============================================ +# X11 display (usually auto-detected) +# DISPLAY=:0 diff --git a/docker/navigation/.gitignore b/docker/navigation/.gitignore new file mode 100644 index 0000000000..0eaccbc740 --- /dev/null +++ b/docker/navigation/.gitignore @@ -0,0 +1,20 @@ +# Cloned repository +ros-navigation-autonomy-stack/ + +# Unity models (large binary files) +unity_models/ + +# ROS bag files +bagfiles/ + +# Config files (may contain local settings) +config/ + +# Docker volumes +.docker/ + +# Temporary files +*.tmp +*.log +*.swp +*~ diff --git a/docker/navigation/Dockerfile b/docker/navigation/Dockerfile new file mode 100644 index 0000000000..69378ea7c7 --- /dev/null +++ b/docker/navigation/Dockerfile @@ -0,0 +1,228 @@ +# Base image with ROS Jazzy desktop full +FROM osrf/ros:jazzy-desktop-full + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive +ENV ROS_DISTRO=jazzy +ENV WORKSPACE=/ros2_ws +ENV DIMOS_PATH=/workspace/dimos + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + # ROS packages + ros-jazzy-pcl-ros \ + # Development tools + git \ + git-lfs \ + cmake \ + build-essential \ + python3-colcon-common-extensions \ + # PCL and system libraries + libpcl-dev \ + libgoogle-glog-dev \ + libgflags-dev \ + libatlas-base-dev \ + libeigen3-dev \ + libsuitesparse-dev \ + # X11 and GUI support for RVIZ + x11-apps \ + xorg \ + openbox \ + # Networking tools + iputils-ping \ + net-tools \ + iproute2 \ + ethtool \ + # USB and serial tools (for hardware support) + usbutils \ + udev \ + # Time synchronization (for multi-computer setup) + chrony \ + # Editor (optional but useful) + nano \ + vim \ + # Python tools + python3-pip \ + python3-setuptools \ + python3-venv \ + # Additional dependencies for dimos + ffmpeg \ + portaudio19-dev \ + libsndfile1 \ + # For OpenCV + libgl1 \ + libglib2.0-0 \ + # For Open3D + libgomp1 \ + # For TurboJPEG + libturbojpeg0-dev \ + # Clean up + && rm -rf /var/lib/apt/lists/* + +# Create workspace directory +RUN mkdir -p ${WORKSPACE}/src + +# Copy the autonomy stack repository (should be cloned by build.sh) +COPY docker/navigation/ros-navigation-autonomy-stack ${WORKSPACE}/src/ros-navigation-autonomy-stack + +# Set working directory +WORKDIR ${WORKSPACE} + +# Set up ROS environment +RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> ~/.bashrc + +# Build all hardware dependencies +RUN \ + # Build Livox-SDK2 for Mid-360 lidar + cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/Livox-SDK2 && \ + mkdir -p build && cd build && \ + cmake .. && make -j$(nproc) && make install && ldconfig && \ + # Install Sophus + cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/Sophus && \ + mkdir -p build && cd build && \ + cmake .. -DBUILD_TESTS=OFF && make -j$(nproc) && make install && \ + # Install Ceres Solver + cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/ceres-solver && \ + mkdir -p build && cd build && \ + cmake .. && make -j$(nproc) && make install && \ + # Install GTSAM + cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/gtsam && \ + mkdir -p build && cd build && \ + cmake .. -DGTSAM_USE_SYSTEM_EIGEN=ON -DGTSAM_BUILD_WITH_MARCH_NATIVE=OFF && \ + make -j$(nproc) && make install && ldconfig + +# Build the autonomy stack +RUN /bin/bash -c "source /opt/ros/${ROS_DISTRO}/setup.bash && \ + cd ${WORKSPACE} && \ + colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release" + +# Source the workspace setup +RUN echo "source ${WORKSPACE}/install/setup.bash" >> ~/.bashrc + +# Create directory for Unity environment models +RUN mkdir -p ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity + +# Copy the dimos repository +RUN mkdir -p ${DIMOS_PATH} +COPY . ${DIMOS_PATH}/ + +# Create a virtual environment in /opt (not in /workspace/dimos) +# This ensures the venv won't be overwritten when we mount the host dimos directory +# The container will always use its own dependencies, independent of the host +RUN python3 -m venv /opt/dimos-venv + +# Activate Python virtual environment in interactive shells +RUN echo "source /opt/dimos-venv/bin/activate" >> ~/.bashrc + +# Install Python dependencies for dimos +WORKDIR ${DIMOS_PATH} +RUN /bin/bash -c "source /opt/dimos-venv/bin/activate && \ + pip install --upgrade pip setuptools wheel && \ + pip install -e .[cpu,dev] 'mmengine>=0.10.3' 'mmcv>=2.1.0'" + +# Copy helper scripts +COPY docker/navigation/run_both.sh /usr/local/bin/run_both.sh +COPY docker/navigation/ros_launch_wrapper.py /usr/local/bin/ros_launch_wrapper.py +RUN chmod +x /usr/local/bin/run_both.sh /usr/local/bin/ros_launch_wrapper.py + +# Set up udev rules for USB devices (motor controller) +RUN echo 'SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", MODE="0666", GROUP="dialout"' > /etc/udev/rules.d/99-motor-controller.rules && \ + usermod -a -G dialout root || true + +# Set up entrypoint script +RUN echo '#!/bin/bash\n\ +set -e\n\ +\n\ +git config --global --add safe.directory /workspace/dimos\n\ +\n\ +# Source ROS setup\n\ +source /opt/ros/${ROS_DISTRO}/setup.bash\n\ +source ${WORKSPACE}/install/setup.bash\n\ +\n\ +# Activate Python virtual environment for dimos\n\ +source /opt/dimos-venv/bin/activate\n\ +\n\ +# Export ROBOT_CONFIG_PATH for autonomy stack\n\ +export ROBOT_CONFIG_PATH="${ROBOT_CONFIG_PATH:-mechanum_drive}"\n\ +\n\ +# Hardware-specific configurations\n\ +if [ "${HARDWARE_MODE}" = "true" ]; then\n\ + # Set network buffer sizes for WiFi data transmission (if needed)\n\ + if [ "${ENABLE_WIFI_BUFFER}" = "true" ]; then\n\ + sysctl -w net.core.rmem_max=67108864 net.core.rmem_default=67108864 2>/dev/null || true\n\ + sysctl -w net.core.wmem_max=67108864 net.core.wmem_default=67108864 2>/dev/null || true\n\ + fi\n\ + \n\ + # Configure network interface for Mid-360 lidar if specified\n\ + if [ -n "${LIDAR_INTERFACE}" ] && [ -n "${LIDAR_COMPUTER_IP}" ]; then\n\ + ip addr add ${LIDAR_COMPUTER_IP}/24 dev ${LIDAR_INTERFACE} 2>/dev/null || true\n\ + ip link set ${LIDAR_INTERFACE} up 2>/dev/null || true\n\ + if [ -n "${LIDAR_GATEWAY}" ]; then\n\ + ip route add default via ${LIDAR_GATEWAY} dev ${LIDAR_INTERFACE} 2>/dev/null || true\n\ + fi\n\ + fi\n\ + \n\ + # Generate MID360_config.json if LIDAR_COMPUTER_IP and LIDAR_IP are set\n\ + if [ -n "${LIDAR_COMPUTER_IP}" ] && [ -n "${LIDAR_IP}" ]; then\n\ + cat > ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/config/MID360_config.json < /ros_entrypoint.sh && \ + chmod +x /ros_entrypoint.sh + +# Set the entrypoint +ENTRYPOINT ["/ros_entrypoint.sh"] + +# Default command +CMD ["bash"] diff --git a/docker/navigation/README.md b/docker/navigation/README.md new file mode 100644 index 0000000000..1505786914 --- /dev/null +++ b/docker/navigation/README.md @@ -0,0 +1,124 @@ +# ROS Docker Integration for DimOS + +This directory contains Docker configuration files to run DimOS and the ROS autonomy stack in the same container, enabling communication between the two systems. + +## Prerequisites + +1. **Install Docker with `docker compose` support**. Follow the [official Docker installation guide](https://docs.docker.com/engine/install/). +2. **Install NVIDIA GPU drivers**. See [NVIDIA driver installation](https://www.nvidia.com/download/index.aspx). +3. **Install NVIDIA Container Toolkit**. Follow the [installation guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html). + +## Automated Quick Start + +This is an optimistic overview. Use the commands below for an in depth version. + +**Build the Docker image:** + +```bash +cd docker/navigation +./build.sh +``` + +This will: +- Clone the ros-navigation-autonomy-stack repository (jazzy branch) +- Build a Docker image with both ROS and DimOS dependencies +- Set up the environment for both systems + +Note that the build will take over 10 minutes and build an image over 30GiB. + +**Run the simulator to test it's working:** + +```bash +./start.sh --simulation +``` + +## Manual build + +Go to the docker dir and clone the ROS navigation stack. + +```bash +cd docker/navigation +git clone -b jazzy git@github.com:dimensionalOS/ros-navigation-autonomy-stack.git +``` + +Download a [Unity environment model for the Mecanum wheel platform](https://drive.google.com/drive/folders/1G1JYkccvoSlxyySuTlPfvmrWoJUO8oSs?usp=sharing) and unzip the files to `unity_models`. + +Alternativelly, extract `office_building_1` from LFS: + +```bash +tar -xf ../../data/.lfs/office_building_1.tar.gz +mv office_building_1 unity_models +``` + +Then, go back to the root and build the docker image: + +```bash +cd ../.. +docker compose -f docker/navigation/docker-compose.yml build +``` + +## On Real Hardware + +### Configure the WiFi + +[Read this](https://github.com/dimensionalOS/ros-navigation-autonomy-stack/tree/jazzy?tab=readme-ov-file#transmitting-data-over-wifi) to see how to configure the WiFi. + +### Configure the Livox Lidar + +The MID360_config.json file is automatically generated on container startup based on your environment variables (LIDAR_COMPUTER_IP and LIDAR_IP). + +### Copy Environment Template +```bash +cp .env.hardware .env +``` + +### Edit `.env` File + +Key configuration parameters: + +```bash +# Lidar Configuration +LIDAR_INTERFACE=eth0 # Your ethernet interface (find with: ip link show) +LIDAR_COMPUTER_IP=192.168.1.5 # Computer IP on the lidar subnet +LIDAR_GATEWAY=192.168.1.1 # Gateway IP address for the lidar subnet +LIDAR_IP=192.168.1.116 # Full IP address of your Mid-360 lidar +ROBOT_IP= # IP addres of robot on local network (if using WebRTC connection) + +# Motor Controller +MOTOR_SERIAL_DEVICE=/dev/ttyACM0 # Serial device (check with: ls /dev/ttyACM*) +``` + +### Start the Container + +Start the container and leave it open. + +```bash +./start.sh --hardware +``` + +It doesn't do anything by default. You have to run commands on it by `exec`-ing: + +```bash +docker exec -it dimos_hardware_container bash +``` + +### In the container + +In the container to run the full navigation stack you must run both the dimensional python runfile with connection module and the navigation stack. + +#### Dimensional Python + Connection Module + +For the Unitree G1 +```bash +dimos run unitree-g1 +ROBOT_IP=XX.X.X.XXX dimos run unitree-g1 # If ROBOT_IP env variable is not set in .env +``` + +#### Navigation Stack + +```bash +cd /ros2_ws/src/ros-navigation-autonomy-stack +./system_real_robot_with_route_planner.sh +``` + +Now you can place goal points/poses in RVIZ by clicking the "Goalpoint" button. The robot will navigate to the point, running both local and global planners for dynamic obstacle avoidance. diff --git a/docker/navigation/build.sh b/docker/navigation/build.sh new file mode 100755 index 0000000000..da0aa2de8c --- /dev/null +++ b/docker/navigation/build.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${GREEN}================================================${NC}" +echo -e "${GREEN}Building DimOS + ROS Autonomy Stack Docker Image${NC}" +echo -e "${GREEN}================================================${NC}" +echo "" + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +if [ ! -d "ros-navigation-autonomy-stack" ]; then + echo -e "${YELLOW}Cloning ros-navigation-autonomy-stack repository...${NC}" + git clone -b jazzy git@github.com:dimensionalOS/ros-navigation-autonomy-stack.git + echo -e "${GREEN}Repository cloned successfully!${NC}" +fi + +if [ ! -d "unity_models" ]; then + echo -e "${YELLOW}Using office_building_1 as the Unity environment...${NC}" + tar -xf ../../data/.lfs/office_building_1.tar.gz + mv office_building_1 unity_models +fi + +echo "" +echo -e "${YELLOW}Building Docker image with docker compose...${NC}" +echo "This will take a while as it needs to:" +echo " - Download base ROS Jazzy image" +echo " - Install ROS packages and dependencies" +echo " - Build the autonomy stack" +echo " - Build Livox-SDK2 for Mid-360 lidar" +echo " - Build SLAM dependencies (Sophus, Ceres, GTSAM)" +echo " - Install Python dependencies for DimOS" +echo "" + +cd ../.. + +docker compose -f docker/navigation/docker-compose.yml build + +echo "" +echo -e "${GREEN}================================${NC}" +echo -e "${GREEN}Docker image built successfully!${NC}" +echo -e "${GREEN}================================${NC}" +echo "" +echo "To run in SIMULATION mode:" +echo -e "${YELLOW} ./start.sh${NC}" +echo "" +echo "To run in HARDWARE mode:" +echo " 1. Configure your hardware settings in .env file" +echo " (copy from .env.hardware if needed)" +echo " 2. Run the hardware container:" +echo -e "${YELLOW} ./start.sh --hardware${NC}" +echo "" +echo "The script runs in foreground. Press Ctrl+C to stop." +echo "" diff --git a/docker/navigation/docker-compose.yml b/docker/navigation/docker-compose.yml new file mode 100644 index 0000000000..f26b7fbabd --- /dev/null +++ b/docker/navigation/docker-compose.yml @@ -0,0 +1,152 @@ +services: + # Simulation profile + dimos_simulation: + build: + context: ../.. + dockerfile: docker/navigation/Dockerfile + image: dimos_autonomy_stack:jazzy + container_name: dimos_simulation_container + profiles: ["", "simulation"] # Active by default (empty profile) AND with --profile simulation + + # Enable interactive terminal + stdin_open: true + tty: true + + # Network configuration - required for ROS communication + network_mode: host + + # Use nvidia runtime for GPU acceleration (falls back to runc if not available) + runtime: ${DOCKER_RUNTIME:-nvidia} + + # Environment variables for display and ROS + environment: + - DISPLAY=${DISPLAY} + - QT_X11_NO_MITSHM=1 + - NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:-all} + - NVIDIA_DRIVER_CAPABILITIES=${NVIDIA_DRIVER_CAPABILITIES:-all} + - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-42} + - ROBOT_CONFIG_PATH=${ROBOT_CONFIG_PATH:-mechanum_drive} + - ROBOT_IP=${ROBOT_IP:-} + - HARDWARE_MODE=false + + # Volume mounts + volumes: + # X11 socket for GUI + - /tmp/.X11-unix:/tmp/.X11-unix:rw + - ${HOME}/.Xauthority:/root/.Xauthority:rw + + # Mount Unity environment models (if available) + - ./unity_models:/ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity:rw + + # Mount the autonomy stack source for development + - ./ros-navigation-autonomy-stack:/ros2_ws/src/ros-navigation-autonomy-stack:rw + + # Mount entire dimos directory for live development + - ../..:/workspace/dimos:rw + + # Mount bagfiles directory + - ./bagfiles:/ros2_ws/bagfiles:rw + + # Mount config files for easy editing + - ./config:/ros2_ws/config:rw + + # Device access (for joystick controllers) + devices: + - /dev/input:/dev/input + - /dev/dri:/dev/dri + + # Working directory + working_dir: /workspace/dimos + + # Command to run both ROS and DimOS + command: /usr/local/bin/run_both.sh + + # Hardware profile - for real robot + dimos_hardware: + build: + context: ../.. + dockerfile: docker/navigation/Dockerfile + image: dimos_autonomy_stack:jazzy + container_name: dimos_hardware_container + profiles: ["hardware"] + + # Enable interactive terminal + stdin_open: true + tty: true + + # Network configuration - MUST be host for hardware access + network_mode: host + + # Privileged mode REQUIRED for hardware access + privileged: true + + # Override runtime for GPU support + runtime: ${DOCKER_RUNTIME:-runc} + + # Hardware environment variables + environment: + - DISPLAY=${DISPLAY} + - QT_X11_NO_MITSHM=1 + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=all + - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-42} + - ROBOT_CONFIG_PATH=${ROBOT_CONFIG_PATH:-mechanum_drive} + - ROBOT_IP=${ROBOT_IP:-} + - HARDWARE_MODE=true + # Mid-360 Lidar configuration + - LIDAR_INTERFACE=${LIDAR_INTERFACE:-} + - LIDAR_COMPUTER_IP=${LIDAR_COMPUTER_IP:-192.168.1.5} + - LIDAR_GATEWAY=${LIDAR_GATEWAY:-192.168.1.1} + - LIDAR_IP=${LIDAR_IP:-192.168.1.116} + # Motor controller + - MOTOR_SERIAL_DEVICE=${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0} + # Network optimization + - ENABLE_WIFI_BUFFER=true + + # Volume mounts + volumes: + # X11 socket for GUI + - /tmp/.X11-unix:/tmp/.X11-unix:rw + - ${HOME}/.Xauthority:/root/.Xauthority:rw + # Mount Unity environment models (optional for hardware) + - ./unity_models:/ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity:rw + # Mount the autonomy stack source + - ./ros-navigation-autonomy-stack:/ros2_ws/src/ros-navigation-autonomy-stack:rw + # Mount entire dimos directory + - ../..:/workspace/dimos:rw + # Mount bagfiles directory + - ./bagfiles:/ros2_ws/bagfiles:rw + # Mount config files for easy editing + - ./config:/ros2_ws/config:rw + # Hardware-specific volumes + - ./logs:/ros2_ws/logs:rw + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - /dev/bus/usb:/dev/bus/usb:rw + - /sys:/sys:ro + + # Device access for hardware + devices: + # Joystick controllers + - /dev/input:/dev/input + # GPU access + - /dev/dri:/dev/dri + # Motor controller serial ports + - ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}:${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0} + # Additional serial ports (can be enabled via environment) + # - /dev/ttyUSB0:/dev/ttyUSB0 + # - /dev/ttyUSB1:/dev/ttyUSB1 + # Cameras (can be enabled via environment) + # - /dev/video0:/dev/video0 + + # Working directory + working_dir: /workspace/dimos + + # Command - for hardware, we run bash as the user will launch specific scripts + command: bash + + # Capabilities for hardware operations + cap_add: + - NET_ADMIN # Network interface configuration + - SYS_ADMIN # System operations + - SYS_TIME # Time synchronization diff --git a/docker/navigation/ros_launch_wrapper.py b/docker/navigation/ros_launch_wrapper.py new file mode 100755 index 0000000000..dc28eabe72 --- /dev/null +++ b/docker/navigation/ros_launch_wrapper.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# Copyright 2025-2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Wrapper script to properly handle ROS2 launch file shutdown. +This script ensures clean shutdown of all ROS nodes when receiving SIGINT. +""" + +import os +import signal +import subprocess +import sys +import time + + +class ROSLaunchWrapper: + def __init__(self): + self.ros_process = None + self.dimos_process = None + self.shutdown_in_progress = False + + def signal_handler(self, _signum, _frame): + """Handle shutdown signals gracefully""" + if self.shutdown_in_progress: + return + + self.shutdown_in_progress = True + print("\n\nShutdown signal received. Stopping services gracefully...") + + # Stop DimOS first + if self.dimos_process and self.dimos_process.poll() is None: + print("Stopping DimOS...") + self.dimos_process.terminate() + try: + self.dimos_process.wait(timeout=5) + print("DimOS stopped cleanly.") + except subprocess.TimeoutExpired: + print("Force stopping DimOS...") + self.dimos_process.kill() + self.dimos_process.wait() + + # Stop ROS - send SIGINT first for graceful shutdown + if self.ros_process and self.ros_process.poll() is None: + print("Stopping ROS nodes (this may take a moment)...") + + # Send SIGINT to trigger graceful ROS shutdown + self.ros_process.send_signal(signal.SIGINT) + + # Wait for graceful shutdown with timeout + try: + self.ros_process.wait(timeout=15) + print("ROS stopped cleanly.") + except subprocess.TimeoutExpired: + print("ROS is taking too long to stop. Sending SIGTERM...") + self.ros_process.terminate() + try: + self.ros_process.wait(timeout=5) + except subprocess.TimeoutExpired: + print("Force stopping ROS...") + self.ros_process.kill() + self.ros_process.wait() + + # Clean up any remaining processes + print("Cleaning up any remaining processes...") + cleanup_commands = [ + "pkill -f 'ros2' || true", + "pkill -f 'localPlanner' || true", + "pkill -f 'pathFollower' || true", + "pkill -f 'terrainAnalysis' || true", + "pkill -f 'sensorScanGeneration' || true", + "pkill -f 'vehicleSimulator' || true", + "pkill -f 'visualizationTools' || true", + "pkill -f 'far_planner' || true", + "pkill -f 'graph_decoder' || true", + ] + + for cmd in cleanup_commands: + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("All services stopped.") + sys.exit(0) + + def run(self): + # Register signal handlers + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + + print("Starting ROS route planner and DimOS...") + + # Change to the ROS workspace directory + os.chdir("/ros2_ws/src/ros-navigation-autonomy-stack") + + # Start ROS route planner + print("Starting ROS route planner...") + self.ros_process = subprocess.Popen( + ["bash", "./system_simulation_with_route_planner.sh"], + preexec_fn=os.setsid, # Create new process group + ) + + print("Waiting for ROS to initialize...") + time.sleep(5) + + print("Starting DimOS navigation bot...") + + nav_bot_path = "/workspace/dimos/dimos/navigation/demo_ros_navigation.py" + venv_python = "/opt/dimos-venv/bin/python" + + if not os.path.exists(nav_bot_path): + print(f"ERROR: demo_ros_navigation.py not found at {nav_bot_path}") + nav_dir = "/workspace/dimos/dimos/navigation/" + if os.path.exists(nav_dir): + print(f"Contents of {nav_dir}:") + for item in os.listdir(nav_dir): + print(f" - {item}") + else: + print(f"Directory not found: {nav_dir}") + return + + if not os.path.exists(venv_python): + print(f"ERROR: venv Python not found at {venv_python}, using system Python") + return + + print(f"Using Python: {venv_python}") + print(f"Starting script: {nav_bot_path}") + + # Use the venv Python explicitly + try: + self.dimos_process = subprocess.Popen( + [venv_python, nav_bot_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + universal_newlines=True, + ) + + # Give it a moment to start and check if it's still running + time.sleep(2) + poll_result = self.dimos_process.poll() + if poll_result is not None: + # Process exited immediately + stdout, stderr = self.dimos_process.communicate(timeout=1) + print(f"ERROR: DimOS failed to start (exit code: {poll_result})") + if stdout: + print(f"STDOUT: {stdout}") + if stderr: + print(f"STDERR: {stderr}") + self.dimos_process = None + else: + print(f"DimOS started successfully (PID: {self.dimos_process.pid})") + + except Exception as e: + print(f"ERROR: Failed to start DimOS: {e}") + self.dimos_process = None + + if self.dimos_process: + print("Both systems are running. Press Ctrl+C to stop.") + else: + print("ROS is running (DimOS failed to start). Press Ctrl+C to stop.") + print("") + + # Wait for processes + try: + # Monitor both processes + while True: + # Check if either process has died + if self.ros_process.poll() is not None: + print("ROS process has stopped unexpectedly.") + self.signal_handler(signal.SIGTERM, None) + break + if self.dimos_process and self.dimos_process.poll() is not None: + print("DimOS process has stopped.") + # DimOS stopping is less critical, but we should still clean up ROS + self.signal_handler(signal.SIGTERM, None) + break + time.sleep(1) + except KeyboardInterrupt: + pass # Signal handler will take care of cleanup + + +if __name__ == "__main__": + wrapper = ROSLaunchWrapper() + wrapper.run() diff --git a/docker/navigation/run_both.sh b/docker/navigation/run_both.sh new file mode 100755 index 0000000000..24c480eaea --- /dev/null +++ b/docker/navigation/run_both.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# Script to run both ROS route planner and DimOS together + +echo "Starting ROS route planner and DimOS..." + +# Variables for process IDs +ROS_PID="" +DIMOS_PID="" +SHUTDOWN_IN_PROGRESS=false + +# Function to handle cleanup +cleanup() { + if [ "$SHUTDOWN_IN_PROGRESS" = true ]; then + return + fi + SHUTDOWN_IN_PROGRESS=true + + echo "" + echo "Shutdown initiated. Stopping services..." + + # First, try to gracefully stop DimOS + if [ -n "$DIMOS_PID" ] && kill -0 $DIMOS_PID 2>/dev/null; then + echo "Stopping DimOS..." + kill -TERM $DIMOS_PID 2>/dev/null || true + + # Wait up to 5 seconds for DimOS to stop + for i in {1..10}; do + if ! kill -0 $DIMOS_PID 2>/dev/null; then + echo "DimOS stopped cleanly." + break + fi + sleep 0.5 + done + + # Force kill if still running + if kill -0 $DIMOS_PID 2>/dev/null; then + echo "Force stopping DimOS..." + kill -9 $DIMOS_PID 2>/dev/null || true + fi + fi + + # Then handle ROS - send SIGINT to the launch process group + if [ -n "$ROS_PID" ] && kill -0 $ROS_PID 2>/dev/null; then + echo "Stopping ROS nodes (this may take a moment)..." + + # Send SIGINT to the process group to properly trigger ROS shutdown + kill -INT -$ROS_PID 2>/dev/null || kill -INT $ROS_PID 2>/dev/null || true + + # Wait up to 15 seconds for graceful shutdown + for i in {1..30}; do + if ! kill -0 $ROS_PID 2>/dev/null; then + echo "ROS stopped cleanly." + break + fi + sleep 0.5 + done + + # If still running, send SIGTERM + if kill -0 $ROS_PID 2>/dev/null; then + echo "Sending SIGTERM to ROS..." + kill -TERM -$ROS_PID 2>/dev/null || kill -TERM $ROS_PID 2>/dev/null || true + sleep 2 + fi + + # Final resort: SIGKILL + if kill -0 $ROS_PID 2>/dev/null; then + echo "Force stopping ROS..." + kill -9 -$ROS_PID 2>/dev/null || kill -9 $ROS_PID 2>/dev/null || true + fi + fi + + # Clean up any remaining ROS2 processes + echo "Cleaning up any remaining processes..." + pkill -f "ros2" 2>/dev/null || true + pkill -f "localPlanner" 2>/dev/null || true + pkill -f "pathFollower" 2>/dev/null || true + pkill -f "terrainAnalysis" 2>/dev/null || true + pkill -f "sensorScanGeneration" 2>/dev/null || true + pkill -f "vehicleSimulator" 2>/dev/null || true + pkill -f "visualizationTools" 2>/dev/null || true + pkill -f "far_planner" 2>/dev/null || true + pkill -f "graph_decoder" 2>/dev/null || true + + echo "All services stopped." +} + +# Set up trap to call cleanup on exit +trap cleanup EXIT INT TERM + +# Start ROS route planner in background (in new process group) +echo "Starting ROS route planner..." +cd /ros2_ws/src/ros-navigation-autonomy-stack +setsid bash -c './system_simulation_with_route_planner.sh' & +ROS_PID=$! + +# Wait a bit for ROS to initialize +echo "Waiting for ROS to initialize..." +sleep 5 + +# Start DimOS +echo "Starting DimOS navigation bot..." + +# Check if the script exists +if [ ! -f "/workspace/dimos/dimos/navigation/demo_ros_navigation.py" ]; then + echo "ERROR: demo_ros_navigation.py not found at /workspace/dimos/dimos/navigation/demo_ros_navigation.py" + echo "Available files in /workspace/dimos/dimos/navigation/:" + ls -la /workspace/dimos/dimos/navigation/ 2>/dev/null || echo "Directory not found" +else + echo "Found demo_ros_navigation.py, activating virtual environment..." + if [ -f "/opt/dimos-venv/bin/activate" ]; then + source /opt/dimos-venv/bin/activate + echo "Python path: $(which python)" + echo "Python version: $(python --version)" + else + echo "WARNING: Virtual environment not found at /opt/dimos-venv, using system Python" + fi + + echo "Starting demo_ros_navigation.py..." + # Capture any startup errors + python /workspace/dimos/dimos/navigation/demo_ros_navigation.py 2>&1 & + DIMOS_PID=$! + + # Give it a moment to start and check if it's still running + sleep 2 + if kill -0 $DIMOS_PID 2>/dev/null; then + echo "DimOS started successfully with PID: $DIMOS_PID" + else + echo "ERROR: DimOS failed to start (process exited immediately)" + echo "Check the logs above for error messages" + DIMOS_PID="" + fi +fi + +echo "" +if [ -n "$DIMOS_PID" ]; then + echo "Both systems are running. Press Ctrl+C to stop." +else + echo "ROS is running (DimOS failed to start). Press Ctrl+C to stop." +fi +echo "" + +# Wait for processes +if [ -n "$DIMOS_PID" ]; then + wait $ROS_PID $DIMOS_PID 2>/dev/null || true +else + wait $ROS_PID 2>/dev/null || true +fi diff --git a/docker/navigation/start.sh b/docker/navigation/start.sh new file mode 100755 index 0000000000..4347006957 --- /dev/null +++ b/docker/navigation/start.sh @@ -0,0 +1,234 @@ +#!/bin/bash + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# Parse command line arguments +MODE="simulation" +while [[ $# -gt 0 ]]; do + case $1 in + --hardware) + MODE="hardware" + shift + ;; + --simulation) + MODE="simulation" + shift + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --simulation Start simulation container (default)" + echo " --hardware Start hardware container for real robot" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 # Start simulation container" + echo " $0 --hardware # Start hardware container" + echo "" + echo "Press Ctrl+C to stop the container" + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + echo "Run '$0 --help' for usage information" + exit 1 + ;; + esac +done + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +echo -e "${GREEN}================================================${NC}" +echo -e "${GREEN}Starting DimOS Docker Container${NC}" +echo -e "${GREEN}Mode: ${MODE}${NC}" +echo -e "${GREEN}================================================${NC}" +echo "" + +# Hardware-specific checks +if [ "$MODE" = "hardware" ]; then + # Check if .env file exists + if [ ! -f ".env" ]; then + if [ -f ".env.hardware" ]; then + echo -e "${YELLOW}Creating .env from .env.hardware template...${NC}" + cp .env.hardware .env + echo -e "${RED}Please edit .env file with your hardware configuration:${NC}" + echo " - LIDAR_IP: Full IP address of your Mid-360 lidar" + echo " - LIDAR_COMPUTER_IP: IP address of this computer on the lidar subnet" + echo " - LIDAR_INTERFACE: Network interface connected to lidar" + echo " - MOTOR_SERIAL_DEVICE: Serial device for motor controller" + echo "" + echo "After editing, run this script again." + exit 1 + fi + fi + + # Source the environment file + if [ -f ".env" ]; then + set -a + source .env + set +a + + # Check for required environment variables + if [ -z "$LIDAR_IP" ] || [ "$LIDAR_IP" = "192.168.1.116" ]; then + echo -e "${YELLOW}Warning: LIDAR_IP still using default value in .env${NC}" + echo "Set LIDAR_IP to the actual IP address of your Mid-360 lidar" + fi + + if [ -z "$LIDAR_GATEWAY" ]; then + echo -e "${YELLOW}Warning: LIDAR_GATEWAY not configured in .env${NC}" + echo "Set LIDAR_GATEWAY to the gateway IP address for the lidar subnet" + fi + + # Check for robot IP configuration + if [ -n "$ROBOT_IP" ]; then + echo -e "${GREEN}Robot IP configured: $ROBOT_IP${NC}" + else + echo -e "${YELLOW}Note: ROBOT_IP not configured in .env${NC}" + echo "Set ROBOT_IP if using network connection to robot" + fi + + # Check for serial devices + echo -e "${GREEN}Checking for serial devices...${NC}" + if [ -e "${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}" ]; then + echo -e " Found device at: ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}" + else + echo -e "${YELLOW} Warning: Device not found at ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}${NC}" + echo -e "${YELLOW} Available serial devices:${NC}" + ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null || echo " None found" + fi + + # Check network interface for lidar + echo -e "${GREEN}Checking network interface for lidar...${NC}" + + # Get available ethernet interfaces + AVAILABLE_ETH="" + for i in /sys/class/net/*; do + if [ "$(cat $i/type 2>/dev/null)" = "1" ] && [ "$i" != "/sys/class/net/lo" ]; then + interface=$(basename $i) + if [ -z "$AVAILABLE_ETH" ]; then + AVAILABLE_ETH="$interface" + else + AVAILABLE_ETH="$AVAILABLE_ETH, $interface" + fi + fi + done + + if [ -z "$LIDAR_INTERFACE" ]; then + # No interface configured + echo -e "${RED}================================================================${NC}" + echo -e "${RED} ERROR: ETHERNET INTERFACE NOT CONFIGURED!${NC}" + echo -e "${RED}================================================================${NC}" + echo -e "${YELLOW} LIDAR_INTERFACE not set in .env file${NC}" + echo "" + echo -e "${YELLOW} Your ethernet interfaces: ${GREEN}${AVAILABLE_ETH}${NC}" + echo "" + echo -e "${YELLOW} ACTION REQUIRED:${NC}" + echo -e " 1. Edit the .env file and set:" + echo -e " ${GREEN}LIDAR_INTERFACE=${NC}" + echo -e " 2. Run this script again" + echo -e "${RED}================================================================${NC}" + exit 1 + elif ! ip link show "$LIDAR_INTERFACE" &>/dev/null; then + # Interface configured but doesn't exist + echo -e "${RED}================================================================${NC}" + echo -e "${RED} ERROR: ETHERNET INTERFACE '$LIDAR_INTERFACE' NOT FOUND!${NC}" + echo -e "${RED}================================================================${NC}" + echo -e "${YELLOW} You configured: LIDAR_INTERFACE=$LIDAR_INTERFACE${NC}" + echo -e "${YELLOW} But this interface doesn't exist on your system${NC}" + echo "" + echo -e "${YELLOW} Your ethernet interfaces: ${GREEN}${AVAILABLE_ETH}${NC}" + echo "" + echo -e "${YELLOW} ACTION REQUIRED:${NC}" + echo -e " 1. Edit the .env file and change to one of your interfaces:" + echo -e " ${GREEN}LIDAR_INTERFACE=${NC}" + echo -e " 2. Run this script again" + echo -e "${RED}================================================================${NC}" + exit 1 + else + # Interface exists and is configured correctly + echo -e " ${GREEN}āœ“${NC} Network interface $LIDAR_INTERFACE found" + echo -e " ${GREEN}āœ“${NC} Will configure static IP: ${LIDAR_COMPUTER_IP}/24" + echo -e " ${GREEN}āœ“${NC} Will set gateway: ${LIDAR_GATEWAY}" + echo "" + echo -e "${YELLOW} Network configuration mode: Static IP (Manual)${NC}" + echo -e " This will temporarily replace DHCP with static IP assignment" + echo -e " Configuration reverts when container stops" + fi + fi + +fi + +# Check if unified image exists +if ! docker images | grep -q "dimos_autonomy_stack.*jazzy"; then + echo -e "${YELLOW}Docker image not found. Building...${NC}" + ./build.sh +fi + +# Check for X11 display +if [ -z "$DISPLAY" ]; then + echo -e "${YELLOW}Warning: DISPLAY not set. GUI applications may not work.${NC}" + export DISPLAY=:0 +fi + +# Allow X11 connections from Docker +echo -e "${GREEN}Configuring X11 access...${NC}" +xhost +local:docker 2>/dev/null || true + +cleanup() { + xhost -local:docker 2>/dev/null || true +} + +trap cleanup EXIT + +# Check for NVIDIA runtime +if docker info 2>/dev/null | grep -q nvidia; then + echo -e "${GREEN}NVIDIA Docker runtime detected${NC}" + export DOCKER_RUNTIME=nvidia + if [ "$MODE" = "hardware" ]; then + export NVIDIA_VISIBLE_DEVICES=all + export NVIDIA_DRIVER_CAPABILITIES=all + fi +else + echo -e "${YELLOW}NVIDIA Docker runtime not found. GPU acceleration disabled.${NC}" + export DOCKER_RUNTIME=runc +fi + +# Set container name for reference +if [ "$MODE" = "hardware" ]; then + CONTAINER_NAME="dimos_hardware_container" +else + CONTAINER_NAME="dimos_simulation_container" +fi + +# Print helpful info before starting +echo "" +if [ "$MODE" = "hardware" ]; then + echo "Hardware mode - Interactive shell" + echo "" + echo -e "${GREEN}=================================================${NC}" + echo -e "${GREEN}The container is running. Exec in to run scripts:${NC}" + echo -e " ${YELLOW}docker exec -it ${CONTAINER_NAME} bash${NC}" + echo -e "${GREEN}=================================================${NC}" +else + echo "Simulation mode - Auto-starting ROS simulation and DimOS" + echo "" + echo "The container will automatically run:" + echo " - ROS navigation stack with route planner" + echo " - DimOS navigation demo" + echo "" + echo "To enter the container from another terminal:" + echo " docker exec -it ${CONTAINER_NAME} bash" +fi + +if [ "$MODE" = "hardware" ]; then + docker compose -f docker-compose.yml --profile hardware up +else + docker compose -f docker-compose.yml up +fi diff --git a/docker/python/Dockerfile b/docker/python/Dockerfile index 8acd7a52af..b85404f51a 100644 --- a/docker/python/Dockerfile +++ b/docker/python/Dockerfile @@ -49,4 +49,4 @@ COPY . /app/ # Install dependencies with UV (10-100x faster than pip) RUN uv pip install --upgrade 'pip>=24' 'setuptools>=70' 'wheel' 'packaging>=24' && \ - uv pip install '.[cpu]' \ No newline at end of file + uv pip install '.[misc,cpu,sim,drone,unitree,web,perception,visualization]' diff --git a/docker/ros/Dockerfile b/docker/ros/Dockerfile index 22bb3ed547..2dc2b5dbb7 100644 --- a/docker/ros/Dockerfile +++ b/docker/ros/Dockerfile @@ -83,7 +83,7 @@ RUN apt-get update && apt-get install -y \ # Initialize rosdep RUN rosdep init -RUN rosdep update +RUN rosdep update # Source ROS2 and workspace in bashrc RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> /root/.bashrc diff --git a/docs/VIEWER_BACKENDS.md b/docs/VIEWER_BACKENDS.md new file mode 100644 index 0000000000..5b069fea7c --- /dev/null +++ b/docs/VIEWER_BACKENDS.md @@ -0,0 +1,77 @@ +# Viewer Backends + +Dimos supports three visualization backends: Rerun (web or native) and Foxglove. + +## Quick Start + +Choose your viewer backend with the `VIEWER_BACKEND` environment variable: + +```bash +# Rerun native viewer (default) - Fast native window + control center +dimos run unitree-go2 +# or explicitly: +VIEWER_BACKEND=rerun-native dimos run unitree-go2 + +# Rerun web viewer - Full dashboard in browser +VIEWER_BACKEND=rerun-web dimos run unitree-go2 + +# Foxglove - Use Foxglove Studio instead of Rerun +VIEWER_BACKEND=foxglove dimos run unitree-go2 +``` + +## Viewer Modes Explained + +### Rerun Web (`rerun-web`) + +**What you get:** +- Full dashboard at http://localhost:7779 +- Rerun 3D viewer + command center sidebar in one page +- Works in browser, no display required (headless-friendly) + +--- + +### Rerun Native (`rerun-native`) + +**What you get:** +- Native Rerun application (separate window opens automatically) +- Command center at http://localhost:7779 +- Better performance with larger maps/higher resolution + +--- + +### Foxglove (`foxglove`) + +**What you get:** +- Foxglove bridge on ws://localhost:8765 +- No Rerun (saves resources) +- Better performance with larger maps/higher resolution +- Open layout: `dimos/assets/foxglove_dashboards/go2.json` + +--- + +## Performance Tuning + +### Symptom: Slow Map Updates + +If you notice: +- Robot appears to "walk across empty space" +- Costmap updates lag behind the robot +- Visualization stutters or freezes + +This happens on lower-end hardware (NUC, older laptops) with large maps. + +### Increase Voxel Size + +Edit [`dimos/robot/unitree_webrtc/unitree_go2_blueprints.py`](/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py) line 82: + +```python +# Before (high detail, slower on large maps) +voxel_mapper(voxel_size=0.05), # 5cm voxels + +# After (lower detail, 8x faster) +voxel_mapper(voxel_size=0.1), # 10cm voxels +``` + +**Trade-off:** +- Larger voxels = fewer voxels = faster updates +- But slightly less detail in the map diff --git a/docs/agents/docs/assets/codeblocks_example.svg b/docs/agents/docs/assets/codeblocks_example.svg new file mode 100644 index 0000000000..3ba6c37a4b --- /dev/null +++ b/docs/agents/docs/assets/codeblocks_example.svg @@ -0,0 +1,47 @@ + + + + + + + + +A + +A + + + +B + +B + + + +A->B + + + + + +C + +C + + + +A->C + + + + + +B->C + + + + + diff --git a/docs/agents/docs/assets/pikchr_basic.svg b/docs/agents/docs/assets/pikchr_basic.svg new file mode 100644 index 0000000000..5410d35577 --- /dev/null +++ b/docs/agents/docs/assets/pikchr_basic.svg @@ -0,0 +1,12 @@ + + +StepĀ 1 + + + +StepĀ 2 + + + +StepĀ 3 + diff --git a/docs/agents/docs/assets/pikchr_branch.svg b/docs/agents/docs/assets/pikchr_branch.svg new file mode 100644 index 0000000000..e7b2b86596 --- /dev/null +++ b/docs/agents/docs/assets/pikchr_branch.svg @@ -0,0 +1,16 @@ + + +Input + + + +Process + + + +PathĀ A + + + +PathĀ B + diff --git a/docs/agents/docs/assets/pikchr_explicit.svg b/docs/agents/docs/assets/pikchr_explicit.svg new file mode 100644 index 0000000000..a6a913fcb4 --- /dev/null +++ b/docs/agents/docs/assets/pikchr_explicit.svg @@ -0,0 +1,8 @@ + + +StepĀ 1 + + + +StepĀ 2 + diff --git a/docs/agents/docs/assets/pikchr_labels.svg b/docs/agents/docs/assets/pikchr_labels.svg new file mode 100644 index 0000000000..b11fe64bca --- /dev/null +++ b/docs/agents/docs/assets/pikchr_labels.svg @@ -0,0 +1,5 @@ + + +Box +labelĀ below + diff --git a/docs/agents/docs/assets/pikchr_sizing.svg b/docs/agents/docs/assets/pikchr_sizing.svg new file mode 100644 index 0000000000..3a0c433cb1 --- /dev/null +++ b/docs/agents/docs/assets/pikchr_sizing.svg @@ -0,0 +1,13 @@ + + +short + + + +.subscribe() + + + +twoĀ lines +ofĀ text + diff --git a/docs/agents/docs/codeblocks.md b/docs/agents/docs/codeblocks.md new file mode 100644 index 0000000000..323f1c0c50 --- /dev/null +++ b/docs/agents/docs/codeblocks.md @@ -0,0 +1,314 @@ +# Executable Code Blocks + +We use [md-babel-py](https://github.com/leshy/md-babel-py/) to execute code blocks in markdown and insert results. + +## Golden Rule + +**All code blocks must be executable.** Never write illustrative/pseudo code blocks. If you're showing an API usage pattern, create a minimal working example that actually runs. This ensures documentation stays correct as the codebase evolves. + +## Running + +```sh skip +md-babel-py run document.md # edit in-place +md-babel-py run document.md --stdout # preview to stdout +md-babel-py run document.md --dry-run # show what would run +``` + +## Supported Languages + +Python, Shell (sh), Node.js, plus visualization: Matplotlib, Graphviz, Pikchr, Asymptote, OpenSCAD, Diagon. + +## Code Block Flags + +Add flags after the language identifier: + +| Flag | Effect | +|------|--------| +| `session=NAME` | Share state between blocks with same session name | +| `output=path.png` | Write output to file instead of inline | +| `no-result` | Execute but don't insert result | +| `skip` | Don't execute this block | +| `expected-error` | Block is expected to fail | + +## Examples + +# md-babel-py + +Execute code blocks in markdown files and insert the results. + +![Demo](assets/screencast.gif) + +**Use cases:** +- Keep documentation examples up-to-date automatically +- Validate code snippets in docs actually work +- Generate diagrams and charts from code in markdown +- Literate programming with executable documentation + +## Languages + +### Shell + +```sh +echo "cwd: $(pwd)" +``` + + +``` +cwd: /work +``` + +### Python + +```python session=example +a = "hello world" +print(a) +``` + + +``` +hello world +``` + +Sessions preserve state between code blocks: + +```python session=example +print(a, "again") +``` + + +``` +hello world again +``` + +### Node.js + +```node +console.log("Hello from Node.js"); +console.log(`Node version: ${process.version}`); +``` + + +``` +Hello from Node.js +Node version: v22.21.1 +``` + +### Matplotlib + +```python output=assets/matplotlib-demo.svg +import matplotlib.pyplot as plt +import numpy as np +plt.style.use('dark_background') +x = np.linspace(0, 4 * np.pi, 200) +plt.figure(figsize=(8, 4)) +plt.plot(x, np.sin(x), label='sin(x)', linewidth=2) +plt.plot(x, np.cos(x), label='cos(x)', linewidth=2) +plt.xlabel('x') +plt.ylabel('y') +plt.legend() +plt.grid(alpha=0.3) +plt.savefig('{output}', transparent=True) +``` + + +![output](assets/matplotlib-demo.svg) + +### Pikchr + +SQLite's diagram language: + +
+diagram source + +```pikchr fold output=assets/pikchr-demo.svg +color = white +fill = none +linewid = 0.4in + +# Input file +In: file "README.md" fit +arrow + +# Processing +Parse: box "Parse" rad 5px fit +arrow +Exec: box "Execute" rad 5px fit + +# Fan out to languages +arrow from Exec.e right 0.3in then up 0.4in then right 0.3in +Sh: oval "Shell" fit +arrow from Exec.e right 0.3in then right 0.3in +Node: oval "Node" fit +arrow from Exec.e right 0.3in then down 0.4in then right 0.3in +Py: oval "Python" fit + +# Merge back +X: dot at (Py.e.x + 0.3in, Node.e.y) invisible +line from Sh.e right until even with X then down to X +line from Node.e to X +line from Py.e right until even with X then up to X +Out: file "README.md" fit with .w at (X.x + 0.3in, X.y) +arrow from X to Out.w +``` + +
+ + +![output](assets/pikchr-demo.svg) + +### Asymptote + +Vector graphics: + +```asymptote output=assets/histogram.svg +import graph; +import stats; + +size(400,200,IgnoreAspect); +defaultpen(white); + +int n=10000; +real[] a=new real[n]; +for(int i=0; i < n; ++i) a[i]=Gaussrand(); + +draw(graph(Gaussian,min(a),max(a)),orange); + +int N=bins(a); + +histogram(a,min(a),max(a),N,normalize=true,low=0,rgb(0.4,0.6,0.8),rgb(0.2,0.4,0.6),bars=true); + +xaxis("$x$",BottomTop,LeftTicks,p=white); +yaxis("$dP/dx$",LeftRight,RightTicks(trailingzero),p=white); +``` + + +![output](assets/histogram.svg) + +### Graphviz + +```dot output=assets/graph.svg +A -> B -> C +A -> C +``` + + +![output](assets/graph.svg) + +### OpenSCAD + +```openscad output=assets/cube-sphere.png +cube([10, 10, 10]); +sphere(r=7); +``` + + +![output](assets/cube-sphere.png) + +### Diagon + +ASCII art diagrams: + +```diagon mode=Math +1 + 1/2 + sum(i,0,10) +``` + + +``` + 10 + ___ + 1 ╲ +1 + ─ + ╱ i + 2 ‾‾‾ + 0 +``` + +```diagon mode=GraphDAG +A -> B -> C +A -> C +``` + + +``` +ā”Œā”€ā”€ā”€ā” +│A │ +ā””ā”¬ā”€ā”¬ā”˜ + ā”‚ā”Œā–½ā” + ││B│ + ā”‚ā””ā”¬ā”˜ +ā”Œā–½ā”€ā–½ā” +│C │ +ā””ā”€ā”€ā”€ā”˜ +``` + +## Install + +### Nix (recommended) + +```sh skip +# Run directly from GitHub +nix run github:leshy/md-babel-py -- run README.md --stdout + +# Or clone and run locally +nix run . -- run README.md --stdout +``` + +### Docker + +```sh skip +# Pull from Docker Hub +docker run -v $(pwd):/work lesh/md-babel-py:main run /work/README.md --stdout + +# Or build locally via Nix +nix build .#docker # builds tarball to ./result +docker load < result # loads image from tarball +docker run -v $(pwd):/work md-babel-py:latest run /work/file.md --stdout +``` + +### pipx + +```sh skip +pipx install md-babel-py +# or: uv pip install md-babel-py +md-babel-py run README.md --stdout +``` + +If not using nix or docker, evaluators require system dependencies: + +| Language | System packages | +|-----------|-----------------------------| +| python | python3 | +| node | nodejs | +| dot | graphviz | +| asymptote | asymptote, texlive, dvisvgm | +| pikchr | pikchr | +| openscad | openscad, xvfb, imagemagick | +| diagon | diagon | + +```sh skip +# Arch Linux +sudo pacman -S python nodejs graphviz asymptote texlive-basic openscad xorg-server-xvfb imagemagick + +# Debian/Ubuntu +sudo apt-get install python3 nodejs graphviz asymptote texlive xvfb imagemagick openscad +``` + +Note: pikchr and diagon may need to be built from source. Use Docker or Nix for full evaluator support. + +## Usage + +```sh skip +# Edit file in-place +md-babel-py run document.md + +# Output to separate file +md-babel-py run document.md --output result.md + +# Print to stdout +md-babel-py run document.md --stdout + +# Only run specific languages +md-babel-py run document.md --lang python,sh + +# Dry run - show what would execute +md-babel-py run document.md --dry-run +``` diff --git a/docs/agents/docs/doclinks.md b/docs/agents/docs/doclinks.md new file mode 100644 index 0000000000..d5533c5983 --- /dev/null +++ b/docs/agents/docs/doclinks.md @@ -0,0 +1,21 @@ +When writing or editing markdown documentation, use `doclinks` tool to resolve file references. + +Full documentation if needed: [`utils/docs/doclinks.md`](/dimos/utils/docs/doclinks.md) + +## Syntax + + +| Pattern | Example | +|-------------|-----------------------------------------------------| +| Code file | `[`service/spec.py`]()` → resolves path | +| With symbol | `Configurable` in `[`spec.py`]()` → adds `#L` | +| Doc link | `[Configuration](.md)` → resolves to doc | + + +## Usage + +```bash +doclinks docs/guide.md # single file +doclinks docs/ # directory +doclinks --dry-run ... # preview only +``` diff --git a/docs/agents/docs/index.md b/docs/agents/docs/index.md new file mode 100644 index 0000000000..bec2ce79e6 --- /dev/null +++ b/docs/agents/docs/index.md @@ -0,0 +1,192 @@ + +# Code Blocks + +**All code blocks must be executable.** +Never write illustrative/pseudo code blocks. +If you're showing an API usage pattern, create a minimal working example that actually runs. This ensures documentation stays correct as the codebase evolves. + +After writing a code block in your markdown file, you can run it by executing +`md-babel-py run document.md` + +more information on this tool is in [codeblocks](/docs/agents/docs_agent/codeblocks.md) + + +# Code or Docs Links + +After adding a link to a doc run + +`doclinks document.md` + +### Code file references +```markdown +See [`service/spec.py`](/dimos/protocol/service/spec.py) for the implementation. +``` + +After running doclinks, becomes: +```markdown +See [`service/spec.py`](/dimos/protocol/service/spec.py) for the implementation. +``` + +### Symbol auto-linking +Mention a symbol on the same line to auto-link to its line number: +```markdown +The `Configurable` class is defined in [`service/spec.py`](/dimos/protocol/service/spec.py#L22). +``` + +Becomes: +```markdown +The `Configurable` class is defined in [`service/spec.py`](/dimos/protocol/service/spec.py#L22). +``` +### Doc-to-doc references +Use `.md` as the link target: +```markdown +See [Configuration](/docs/api/configuration.md) for more details. +``` + +Becomes: +```markdown +See [Configuration](/docs/concepts/configuration.md) for more details. +``` + +More information on this in [doclinks](/docs/agents/docs_agent/doclinks.md) + + +# Pikchr + +[Pikchr](https://pikchr.org/) is a diagram language from SQLite. Use it for flowcharts and architecture diagrams. + +**Important:** Always wrap pikchr blocks in `
` tags so the source is collapsed by default on GitHub. The rendered SVG stays visible outside the fold. Code blocks (Python, etc.) should NOT be folded—they're meant to be read. + +## Basic syntax + +
+diagram source + +```pikchr fold output=assets/pikchr_basic.svg +color = white +fill = none + +A: box "Step 1" rad 5px fit wid 170% ht 170% +arrow right 0.3in +B: box "Step 2" rad 5px fit wid 170% ht 170% +arrow right 0.3in +C: box "Step 3" rad 5px fit wid 170% ht 170% +``` + +
+ + +![output](assets/pikchr_basic.svg) + +## Box sizing + +Use `fit` with percentage scaling to auto-size boxes with padding: + +
+diagram source + +```pikchr fold output=assets/pikchr_sizing.svg +color = white +fill = none + +# fit wid 170% ht 170% = auto-size + padding +A: box "short" rad 5px fit wid 170% ht 170% +arrow right 0.3in +B: box ".subscribe()" rad 5px fit wid 170% ht 170% +arrow right 0.3in +C: box "two lines" "of text" rad 5px fit wid 170% ht 170% +``` + +
+ + +![output](assets/pikchr_sizing.svg) + +The pattern `fit wid 170% ht 170%` means: auto-size to text, then scale width by 170% and height by 170%. + +For explicit sizing (when you need consistent box sizes): + +
+diagram source + +```pikchr fold output=assets/pikchr_explicit.svg +color = white +fill = none + +A: box "Step 1" rad 5px fit wid 170% ht 170% +arrow right 0.3in +B: box "Step 2" rad 5px fit wid 170% ht 170% +``` + +
+ + +![output](assets/pikchr_explicit.svg) + +## Common settings + +Always start with: + +``` +color = white # text color +fill = none # transparent box fill +``` + +## Branching paths + +
+diagram source + +```pikchr fold output=assets/pikchr_branch.svg +color = white +fill = none + +A: box "Input" rad 5px fit wid 170% ht 170% +arrow +B: box "Process" rad 5px fit wid 170% ht 170% + +# Branch up +arrow from B.e right 0.3in then up 0.35in then right 0.3in +C: box "Path A" rad 5px fit wid 170% ht 170% + +# Branch down +arrow from B.e right 0.3in then down 0.35in then right 0.3in +D: box "Path B" rad 5px fit wid 170% ht 170% +``` + +
+ + +![output](assets/pikchr_branch.svg) + +**Tip:** For tree/hierarchy diagrams, prefer left-to-right layout (root on left, children branching right). This reads more naturally and avoids awkward vertical stacking. + +## Adding labels + +
+diagram source + +```pikchr fold output=assets/pikchr_labels.svg +color = white +fill = none + +A: box "Box" rad 5px fit wid 170% ht 170% +text "label below" at (A.x, A.y - 0.4in) +``` + +
+ + +![output](assets/pikchr_labels.svg) + +## Reference + +| Element | Syntax | +|---------|--------| +| Box | `box "text" rad 5px wid Xin ht Yin` | +| Arrow | `arrow right 0.3in` | +| Oval | `oval "text" wid Xin ht Yin` | +| Text | `text "label" at (X, Y)` | +| Named point | `A: box ...` then reference `A.e`, `A.n`, `A.x`, `A.y` | + +See [pikchr.org/home/doc/trunk/doc/userman.md](https://pikchr.org/home/doc/trunk/doc/userman.md) for full documentation. diff --git a/docs/api/assets/transforms.png b/docs/api/assets/transforms.png new file mode 100644 index 0000000000..49dba4ab9a --- /dev/null +++ b/docs/api/assets/transforms.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6597e0008197902e321a3ad3dfb1e838f860fa7ca1277c369ed6ff7da8bf757d +size 101102 diff --git a/docs/api/assets/transforms_chain.svg b/docs/api/assets/transforms_chain.svg new file mode 100644 index 0000000000..3f6c21741b --- /dev/null +++ b/docs/api/assets/transforms_chain.svg @@ -0,0 +1,12 @@ + + +base_link + + + +camera_link + + + +camera_optical + diff --git a/docs/api/assets/transforms_modules.svg b/docs/api/assets/transforms_modules.svg new file mode 100644 index 0000000000..08e7c309a5 --- /dev/null +++ b/docs/api/assets/transforms_modules.svg @@ -0,0 +1,20 @@ + + +world + + + +base_link + + + +camera_link + + + +camera_optical + +RobotBaseModule + +CameraModule + diff --git a/docs/api/assets/transforms_tree.svg b/docs/api/assets/transforms_tree.svg new file mode 100644 index 0000000000..f95f1a6621 --- /dev/null +++ b/docs/api/assets/transforms_tree.svg @@ -0,0 +1,26 @@ + + +world + + + +robot_base + + + +camera_link + + + +camera_optical +mugĀ here + + + +arm_base + + + +gripper +targetĀ here + diff --git a/docs/api/configuration.md b/docs/api/configuration.md new file mode 100644 index 0000000000..2977e8c3c1 --- /dev/null +++ b/docs/api/configuration.md @@ -0,0 +1,90 @@ +# Configuration + +Dimos provides a `Configurable` base class, see [`service/spec.py`](/dimos/protocol/service/spec.py#L22). + +This allows using dataclasses to specify configuration structure and default values per module. + +```python +from dimos.protocol.service import Configurable +from rich import print +from dataclasses import dataclass + +@dataclass +class Config(): + x: int = 3 + hello: str = "world" + +class MyClass(Configurable): + default_config = Config + config: Config + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + +myclass1 = MyClass() +print(myclass1.config) + +# can easily override +myclass2 = MyClass(hello="override") +print(myclass2.config) + +# we will raise an error for unspecified keys +try: + myclass3 = MyClass(something="else") +except TypeError as e: + print(f"Error: {e}") + + +``` + + +``` +Config(x=3, hello='world') +Config(x=3, hello='override') +Error: Config.__init__() got an unexpected keyword argument 'something' +``` + +# Configurable Modules + +[Modules]() inherit from `Configurable`, so all of the above applies. Module configs should inherit from `ModuleConfig` ([`core/module.py`](/dimos/core/module.py#L40)), which includes shared configuration for all modules like transport protocols, frame_ids etc + +```python +from dataclasses import dataclass +from dimos.core import In, Module, Out, rpc, ModuleConfig +from rich import print + +@dataclass +class Config(ModuleConfig): + frame_id: str = "world" + publish_interval: float = 0 + voxel_size: float = 0.05 + device: str = "CUDA:0" + +class MyModule(Module): + default_config = Config + config: Config + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + print(self.config) + + +myModule = MyModule(frame_id="frame_id_override", device="CPU") + +# In production, use dimos.deploy() instead: +# myModule = dimos.deploy(MyModule, frame_id="frame_id_override") + + +``` + + +``` +Config( + rpc_transport=, + tf_transport=, + frame_id_prefix=None, + frame_id='frame_id_override', + publish_interval=0, + voxel_size=0.05, + device='CPU' +) +``` diff --git a/docs/api/sensor_streams/advanced_streams.md b/docs/api/sensor_streams/advanced_streams.md new file mode 100644 index 0000000000..c7db7c98bd --- /dev/null +++ b/docs/api/sensor_streams/advanced_streams.md @@ -0,0 +1,193 @@ +# Advanced Stream Handling + +> **Prerequisite:** Read [ReactiveX Fundamentals](reactivex.md) first for Observable basics. + +## Backpressure and parallel subscribers to hardware + +In robotics, we deal with hardware that produces data at its own pace - a camera outputs 30fps whether you're ready or not. We can't tell the camera to slow down. And we often have multiple consumers: one module wants every frame for recording, another runs slow ML inference and only needs the latest frame. + +**The problem:** A fast producer can overwhelm a slow consumer, causing memory buildup or dropped frames. We might have multiple subscribers to the same hardware that operate at different speeds. + + +
Pikchr + +```pikchr fold output=assets/backpressure.svg +color = white +fill = none + +Fast: box "Camera" "60 fps" rad 5px fit wid 130% ht 130% +arrow right 0.4in +Queue: box "queue" rad 5px fit wid 170% ht 170% +arrow right 0.4in +Slow: box "ML Model" "2 fps" rad 5px fit wid 130% ht 130% + +text "items pile up!" at (Queue.x, Queue.y - 0.45in) +``` + +
+ + +![output](assets/backpressure.svg) + + +**The solution:** The `backpressure()` wrapper handles this by: + +1. **Sharing the source** - Camera runs once, all subscribers share the stream +2. **Per-subscriber speed** - Fast subscribers get every frame, slow ones get the latest when ready +3. **No blocking** - Slow subscribers never block the source or each other + +```python session=bp +import time +import reactivex as rx +from reactivex import operators as ops +from reactivex.scheduler import ThreadPoolScheduler +from dimos.utils.reactive import backpressure + +# we need this scaffolding here, normally dimos handles this +scheduler = ThreadPoolScheduler(max_workers=4) + +# Simulate fast source +source = rx.interval(0.05).pipe(ops.take(20)) +safe = backpressure(source, scheduler=scheduler) + +fast_results = [] +slow_results = [] + +safe.subscribe(lambda x: fast_results.append(x)) + +def slow_handler(x): + time.sleep(0.15) + slow_results.append(x) + +safe.subscribe(slow_handler) + +time.sleep(1.5) +print(f"fast got {len(fast_results)} items: {fast_results[:5]}...") +print(f"slow got {len(slow_results)} items (skipped {len(fast_results) - len(slow_results)})") +scheduler.executor.shutdown(wait=True) +``` + + +``` +fast got 20 items: [0, 1, 2, 3, 4]... +slow got 7 items (skipped 13) +``` + +### How it works + + +
Pikchr + +```pikchr fold output=assets/backpressure_solution.svg +color = white +fill = none +linewid = 0.3in + +Source: box "Camera" "60 fps" rad 5px fit wid 170% ht 170% +arrow +Core: box "backpressure" rad 5px fit wid 170% ht 170% +arrow from Core.e right 0.3in then up 0.35in then right 0.3in +Fast: box "Fast Sub" rad 5px fit wid 170% ht 170% +arrow from Core.e right 0.3in then down 0.35in then right 0.3in +SlowPre: box "LATEST" rad 5px fit wid 170% ht 170% +arrow +Slow: box "Slow Sub" rad 5px fit wid 170% ht 170% +``` + +
+ + +![output](assets/backpressure_solution.svg) + +The `LATEST` strategy means: when the slow subscriber finishes processing, it gets whatever the most recent value is, skipping any values that arrived while it was busy. + +### Usage in modules + +Most module streams offer backpressured observables + +```python session=bp +from dimos.core import Module, In +from dimos.msgs.sensor_msgs import Image + +class MLModel(Module): + color_image: In[Image] + def start(self): + # no reactivex, simple callback + self.color_image.subscribe(...) + # backpressured + self.color_image.observable().subscribe(...) + # non-backpressured - will pile up queue + self.color_image.pure_observable().subscribe(...) + + +``` + + + + + + +## Getting Values Synchronously + +Sometimes you don't want a stream - you just want to call a function and get the latest value. We provide two approaches: + +| | `getter_hot()` | `getter_cold()` | +|------------------|--------------------------------|----------------------------------| +| **Subscription** | Stays active in background | Fresh subscription each call | +| **Read speed** | Instant (value already cached) | Slower (waits for value) | +| **Resources** | Keeps connection open | Opens/closes each call | +| **Use when** | Frequent reads, need latest | Occasional reads, save resources | + +**Prefer `getter_cold()`** when you can afford to wait and warmup isn't expensive. It's simpler (no cleanup needed) and doesn't hold resources. Only use `getter_hot()` when you need instant reads or the source is expensive to start. + +### `getter_hot()` - Background subscription, instant reads + +Subscribes immediately and keeps updating in the background. Each call returns the cached latest value instantly. + +```python session=sync +import time +import reactivex as rx +from reactivex import operators as ops +from dimos.utils.reactive import getter_hot + +source = rx.interval(0.1).pipe(ops.take(10)) +get_val = getter_hot(source, timeout=5.0) + +print("first call:", get_val()) # instant - value already there +time.sleep(0.35) +print("after 350ms:", get_val()) # instant - returns cached latest +time.sleep(0.35) +print("after 700ms:", get_val()) + +get_val.dispose() # Don't forget to clean up! +``` + + +``` +first call: 0 +after 350ms: 3 +after 700ms: 6 +``` + +### `getter_cold()` - Fresh subscription each call + +Each call creates a new subscription, waits for one value, and cleans up. Slower but doesn't hold resources: + +```python session=sync +from dimos.utils.reactive import getter_cold + +source = rx.of(0, 1, 2, 3, 4) +get_val = getter_cold(source, timeout=5.0) + +# Each call creates fresh subscription, gets first value +print("call 1:", get_val()) # subscribes, gets 0, disposes +print("call 2:", get_val()) # subscribes again, gets 0, disposes +print("call 3:", get_val()) # subscribes again, gets 0, disposes +``` + + +``` +call 1: 0 +call 2: 0 +call 3: 0 +``` diff --git a/docs/api/sensor_streams/assets/alignment_flow.svg b/docs/api/sensor_streams/assets/alignment_flow.svg new file mode 100644 index 0000000000..72aeb337f3 --- /dev/null +++ b/docs/api/sensor_streams/assets/alignment_flow.svg @@ -0,0 +1,22 @@ + + +Primary +arrives + + + +Check +secondaries + + + +Emit +match +allĀ found + + + +Buffer +primary +waiting... + diff --git a/docs/api/sensor_streams/assets/alignment_overview.svg b/docs/api/sensor_streams/assets/alignment_overview.svg new file mode 100644 index 0000000000..8abada6d02 --- /dev/null +++ b/docs/api/sensor_streams/assets/alignment_overview.svg @@ -0,0 +1,18 @@ + + +Camera +30Ā fps + + + +align_timestamped + +Lidar +10Ā Hz + + + + + +(image,Ā pointcloud) + diff --git a/docs/api/sensor_streams/assets/alignment_timeline.png b/docs/api/sensor_streams/assets/alignment_timeline.png new file mode 100644 index 0000000000..235ddd7be0 --- /dev/null +++ b/docs/api/sensor_streams/assets/alignment_timeline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfea5a6aac40182b25decb9ddaeb387ed97a7708e2c51a48f47453c8df7adf57 +size 16136 diff --git a/docs/api/sensor_streams/assets/alignment_timeline2.png b/docs/api/sensor_streams/assets/alignment_timeline2.png new file mode 100644 index 0000000000..2bf8ec5eef --- /dev/null +++ b/docs/api/sensor_streams/assets/alignment_timeline2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22b64923637d05f8f40c9f7c0f0597ee894dc4f31a0f10674aeb809101b54765 +size 23471 diff --git a/docs/api/sensor_streams/assets/alignment_timeline3.png b/docs/api/sensor_streams/assets/alignment_timeline3.png new file mode 100644 index 0000000000..61ddc3b54b --- /dev/null +++ b/docs/api/sensor_streams/assets/alignment_timeline3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8e9589dcd5308f511a2ec7d41bd36978204ccfe1441907bd139029b0489d605 +size 9969 diff --git a/docs/api/sensor_streams/assets/backpressure.svg b/docs/api/sensor_streams/assets/backpressure.svg new file mode 100644 index 0000000000..b3d69af6fb --- /dev/null +++ b/docs/api/sensor_streams/assets/backpressure.svg @@ -0,0 +1,15 @@ + + +Camera +60Ā fps + + + +queue + + + +MLĀ Model +2Ā fps +itemsĀ pileĀ up! + diff --git a/docs/api/sensor_streams/assets/backpressure_solution.svg b/docs/api/sensor_streams/assets/backpressure_solution.svg new file mode 100644 index 0000000000..454a8f460b --- /dev/null +++ b/docs/api/sensor_streams/assets/backpressure_solution.svg @@ -0,0 +1,21 @@ + + +Camera +60Ā fps + + + +backpressure + + + +FastĀ Sub + + + +LATEST + + + +SlowĀ Sub + diff --git a/docs/api/sensor_streams/assets/frame_mosaic.jpg b/docs/api/sensor_streams/assets/frame_mosaic.jpg new file mode 100644 index 0000000000..5c3fbf8350 --- /dev/null +++ b/docs/api/sensor_streams/assets/frame_mosaic.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83934e1179651fbca6c9b62cceb7425d1b2f0e8da18a63d4d95bcb4e6ac33ca +size 88206 diff --git a/docs/api/sensor_streams/assets/frame_mosaic2.jpg b/docs/api/sensor_streams/assets/frame_mosaic2.jpg new file mode 100644 index 0000000000..5e3032acf2 --- /dev/null +++ b/docs/api/sensor_streams/assets/frame_mosaic2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d73f683e92fda39bac9d1bb840f1fc375c821b4099714829e81f3e739f4d602 +size 91036 diff --git a/docs/api/sensor_streams/assets/observable_flow.svg b/docs/api/sensor_streams/assets/observable_flow.svg new file mode 100644 index 0000000000..d7e0e021d6 --- /dev/null +++ b/docs/api/sensor_streams/assets/observable_flow.svg @@ -0,0 +1,16 @@ + + +observable + + + +.pipe(ops) + + + +.subscribe() + + + +callback + diff --git a/docs/api/sensor_streams/assets/sharpness_graph.svg b/docs/api/sensor_streams/assets/sharpness_graph.svg new file mode 100644 index 0000000000..3d61d12d7c --- /dev/null +++ b/docs/api/sensor_streams/assets/sharpness_graph.svg @@ -0,0 +1,1414 @@ + + + + + + + + 1980-01-01T00:00:00+00:00 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/api/sensor_streams/assets/sharpness_graph2.svg b/docs/api/sensor_streams/assets/sharpness_graph2.svg new file mode 100644 index 0000000000..37c1032de0 --- /dev/null +++ b/docs/api/sensor_streams/assets/sharpness_graph2.svg @@ -0,0 +1,1429 @@ + + + + + + + + 1980-01-01T00:00:00+00:00 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/api/sensor_streams/index.md b/docs/api/sensor_streams/index.md new file mode 100644 index 0000000000..dc2ce6c91d --- /dev/null +++ b/docs/api/sensor_streams/index.md @@ -0,0 +1,41 @@ +# Sensor Streams + +Dimos uses reactive streams (RxPY) to handle sensor data. This approach naturally fits robotics where multiple sensors emit data asynchronously at different rates, and downstream processors may be slower than the data sources. + +## Guides + +| Guide | Description | +|----------------------------------------------|---------------------------------------------------------------| +| [ReactiveX Fundamentals](reactivex.md) | Observables, subscriptions, and disposables | +| [Advanced Streams](advanced_streams.md) | Backpressure, parallel subscribers, synchronous getters | +| [Quality-Based Filtering](quality_filter.md) | Select highest quality frames when downsampling streams | +| [Temporal Alignment](temporal_alignment.md) | Match messages from multiple sensors by timestamp | +| [Storage & Replay](storage_replay.md) | Record sensor streams to disk and replay with original timing | + +## Quick Example + +```python +from reactivex import operators as ops +from dimos.utils.reactive import backpressure +from dimos.types.timestamped import align_timestamped +from dimos.msgs.sensor_msgs.Image import sharpness_barrier + +# Camera at 30fps, lidar at 10Hz +camera_stream = camera.observable() +lidar_stream = lidar.observable() + +# Pipeline: filter blurry frames -> align with lidar -> handle slow consumers +processed = ( + camera_stream.pipe( + sharpness_barrier(10.0), # Keep sharpest frame per 100ms window (10Hz) + ) +) + +aligned = align_timestamped( + backpressure(processed), # Camera as primary + lidar_stream, # Lidar as secondary + match_tolerance=0.1, +) + +aligned.subscribe(lambda pair: process_frame_with_pointcloud(*pair)) +``` diff --git a/docs/api/sensor_streams/quality_filter.md b/docs/api/sensor_streams/quality_filter.md new file mode 100644 index 0000000000..c9e25d9a6e --- /dev/null +++ b/docs/api/sensor_streams/quality_filter.md @@ -0,0 +1,316 @@ +# Quality-Based Stream Filtering + +When processing sensor streams, you often want to reduce frequency while keeping the best quality data. For discrete data like images that can't be averaged or merged, instead of blindly dropping frames, `quality_barrier` selects the highest quality item within each time window. + +## The Problem + +A camera outputs 30fps, but your ML model only needs 2fps. Simple approaches: + +- **`sample(0.5)`** - Takes whatever frame happens to land on the interval tick +- **`throttle_first(0.5)`** - Takes the first frame, ignores the rest + +Both ignore quality. You might get a blurry frame when a sharp one was available. + +## The Solution: `quality_barrier` + +```python session=qb +import reactivex as rx +from reactivex import operators as ops +from dimos.utils.reactive import quality_barrier + +# Simulated sensor data with quality scores +data = [ + {"id": 1, "quality": 0.3}, + {"id": 2, "quality": 0.9}, # best in first window + {"id": 3, "quality": 0.5}, + {"id": 4, "quality": 0.2}, + {"id": 5, "quality": 0.8}, # best in second window + {"id": 6, "quality": 0.4}, +] + +source = rx.of(*data) + +# Select best quality item per window (2 items per second = 0.5s windows) +result = source.pipe( + quality_barrier(lambda x: x["quality"], target_frequency=2.0), + ops.to_list(), +).run() + +print("Selected:", [r["id"] for r in result]) +print("Qualities:", [r["quality"] for r in result]) +``` + + +``` +Selected: [2] +Qualities: [0.9] +``` + +## Image Sharpness Filtering + +For camera streams, we provide `sharpness_barrier` which uses the image's sharpness score. + +Let's use real camera data from the Unitree Go2 robot to demonstrate. We use the [Sensor Replay](/docs/old/testing_stream_reply.md) toolkit which provides access to recorded robot data: + +```python session=qb +from dimos.utils.testing import TimedSensorReplay +from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier + +# Load recorded Go2 camera frames +video_replay = TimedSensorReplay("unitree_go2_bigoffice/video") + +# Use stream() with seek to skip blank frames, speed=10x to collect faster +input_frames = video_replay.stream(seek=5.0, duration=1.4, speed=10.0).pipe( + ops.to_list() +).run() + +def show_frames(frames): + for i, frame in enumerate(frames[:10]): + print(f" Frame {i}: {frame.sharpness:.3f}") + +print(f"Loaded {len(input_frames)} frames from Go2 camera") +print(f"Frame resolution: {input_frames[0].width}x{input_frames[0].height}") +print("Sharpness scores:") +show_frames(input_frames) +``` + + +``` +Loaded 20 frames from Go2 camera +Frame resolution: 1280x720 +Sharpness scores: + Frame 0: 0.351 + Frame 1: 0.227 + Frame 2: 0.223 + Frame 3: 0.267 + Frame 4: 0.295 + Frame 5: 0.307 + Frame 6: 0.328 + Frame 7: 0.348 + Frame 8: 0.346 + Frame 9: 0.322 +``` + +Using `sharpness_barrier` to select the sharpest frames: + +```python session=qb +# Create a stream from the recorded frames + +sharp_frames = video_replay.stream(seek=5.0, duration=1.5, speed=1.0).pipe( + sharpness_barrier(2.0), + ops.to_list() +).run() + +print(f"Output: {len(sharp_frames)} frame(s) (selected sharpest per window)") +show_frames(sharp_frames) +``` + + +``` +Output: 3 frame(s) (selected sharpest per window) + Frame 0: 0.351 + Frame 1: 0.352 + Frame 2: 0.360 +``` + +
+Visualization helpers + +```python session=qb fold no-result +import matplotlib +import matplotlib.pyplot as plt +import math + +def plot_mosaic(frames, selected, path, cols=5): + matplotlib.use('Agg') + rows = math.ceil(len(frames) / cols) + aspect = frames[0].width / frames[0].height + fig_w, fig_h = 12, 12 * rows / (cols * aspect) + + fig, axes = plt.subplots(rows, cols, figsize=(fig_w, fig_h)) + fig.patch.set_facecolor('black') + for i, ax in enumerate(axes.flat): + if i < len(frames): + ax.imshow(frames[i].data) + for spine in ax.spines.values(): + spine.set_color('lime' if frames[i] in selected else 'black') + spine.set_linewidth(4 if frames[i] in selected else 0) + ax.set_xticks([]); ax.set_yticks([]) + else: + ax.axis('off') + plt.subplots_adjust(wspace=0.02, hspace=0.02, left=0, right=1, top=1, bottom=0) + plt.savefig(path, facecolor='black', dpi=100, bbox_inches='tight', pad_inches=0) + plt.close() + +def plot_sharpness(frames, selected, path): + matplotlib.use('svg') + plt.style.use('dark_background') + sharpness = [f.sharpness for f in frames] + selected_idx = [i for i, f in enumerate(frames) if f in selected] + + plt.figure(figsize=(10, 3)) + plt.plot(sharpness, 'o-', label='All frames', color='#b5e4f4', alpha=0.7) + for i, idx in enumerate(selected_idx): + plt.axvline(x=idx, color='lime', linestyle='--', label='Selected' if i == 0 else None) + plt.xlabel('Frame'); plt.ylabel('Sharpness') + plt.xticks(range(len(sharpness))) + plt.legend(); plt.grid(alpha=0.3); plt.tight_layout() + plt.savefig(path, transparent=True) + plt.close() +``` + +
+ +Visualizing which frames were selected (green border = selected as sharpest in window): + +```python session=qb output=assets/frame_mosaic.jpg +plot_mosaic(input_frames, sharp_frames, '{output}') +``` + + +![output](assets/frame_mosaic.jpg) + +```python session=qb output=assets/sharpness_graph.svg +plot_sharpness(input_frames, sharp_frames, '{output}') +``` + + +![output](assets/sharpness_graph.svg) + +Let's request higher frequency + +```python session=qb +sharp_frames = video_replay.stream(seek=5.0, duration=1.5, speed=1.0).pipe( + sharpness_barrier(4.0), + ops.to_list() +).run() + +print(f"Output: {len(sharp_frames)} frame(s) (selected sharpest per window)") +show_frames(sharp_frames) +``` + + +``` +Output: 6 frame(s) (selected sharpest per window) + Frame 0: 0.351 + Frame 1: 0.348 + Frame 2: 0.346 + Frame 3: 0.352 + Frame 4: 0.360 + Frame 5: 0.329 +``` + +```python session=qb output=assets/frame_mosaic2.jpg +plot_mosaic(input_frames, sharp_frames, '{output}') +``` + + +![output](assets/frame_mosaic2.jpg) + + +```python session=qb output=assets/sharpness_graph2.svg +plot_sharpness(input_frames, sharp_frames, '{output}') +``` + + +![output](assets/sharpness_graph2.svg) + +As we can see the system is trying to strike a balance between requested frequency and quality that's available + +### Usage in Camera Module + +Here's how it's used in the actual camera module: + +```python skip +from dimos.core.module import Module + +class CameraModule(Module): + frequency: float = 2.0 # Target output frequency + @rpc + def start(self) -> None: + stream = self.hardware.image_stream() + + if self.config.frequency > 0: + stream = stream.pipe(sharpness_barrier(self.config.frequency)) + + self._disposables.add( + stream.subscribe(self.color_image.publish), + ) + +``` + +### How Sharpness is Calculated + +The sharpness score (0.0 to 1.0) is computed using Sobel edge detection: + +from [`NumpyImage.py`](/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py) + +```python session=qb +import cv2 + +# Get a frame and show the calculation +img = input_frames[10] +gray = img.to_grayscale() + +# Sobel gradients - use .data to get the underlying numpy array +sx = cv2.Sobel(gray.data, cv2.CV_32F, 1, 0, ksize=5) +sy = cv2.Sobel(gray.data, cv2.CV_32F, 0, 1, ksize=5) +magnitude = cv2.magnitude(sx, sy) + +print(f"Mean gradient magnitude: {magnitude.mean():.2f}") +print(f"Normalized sharpness: {img.sharpness:.3f}") +``` + + +``` +Mean gradient magnitude: 230.00 +Normalized sharpness: 0.332 +``` + +## Custom Quality Functions + +You can use `quality_barrier` with any quality metric: + +```python session=qb +# Example: select by "confidence" field +detections = [ + {"name": "cat", "confidence": 0.7}, + {"name": "dog", "confidence": 0.95}, # best + {"name": "bird", "confidence": 0.6}, +] + +result = rx.of(*detections).pipe( + quality_barrier(lambda d: d["confidence"], target_frequency=2.0), + ops.to_list(), +).run() + +print(f"Selected: {result[0]['name']} (conf: {result[0]['confidence']})") +``` + + +``` +Selected: dog (conf: 0.95) +``` + +## API Reference + +### `quality_barrier(quality_func, target_frequency)` + +RxPY pipe operator that selects the highest quality item within each time window. + +| Parameter | Type | Description | +|--------------------|------------------------|------------------------------------------------------| +| `quality_func` | `Callable[[T], float]` | Function that returns a quality score for each item | +| `target_frequency` | `float` | Output frequency in Hz (e.g., 2.0 for 2 items/second)| + +**Returns:** A pipe operator for use with `.pipe()` + +### `sharpness_barrier(target_frequency)` + +Convenience wrapper for images that uses `image.sharpness` as the quality function. + +| Parameter | Type | Description | +|--------------------|---------|--------------------------| +| `target_frequency` | `float` | Output frequency in Hz | + +**Returns:** A pipe operator for use with `.pipe()` diff --git a/docs/api/sensor_streams/reactivex.md b/docs/api/sensor_streams/reactivex.md new file mode 100644 index 0000000000..1dcbdfe046 --- /dev/null +++ b/docs/api/sensor_streams/reactivex.md @@ -0,0 +1,447 @@ +# ReactiveX (RxPY) Quick Reference + +RxPY provides composable asynchronous data streams. This is a practical guide focused on common patterns in this codebase. + +## Quick Start: Using an Observable + +Given a function that returns an `Observable`, here's how to use it: + +```python session=rx +import reactivex as rx +from reactivex import operators as ops + +# Create an observable that emits 0,1,2,3,4 +source = rx.of(0, 1, 2, 3, 4) + +# Subscribe and print each value +received = [] +source.subscribe(lambda x: received.append(x)) +print("received:", received) +``` + + +``` +received: [0, 1, 2, 3, 4] +``` + +## The `.pipe()` Pattern + +Chain operators using `.pipe()`: + +```python session=rx +# Transform values: multiply by 2, then filter > 4 +result = [] + +# we build another observable, it's passive until subscribe is called +observable = source.pipe( + ops.map(lambda x: x * 2), + ops.filter(lambda x: x > 4), +) + +observable.subscribe(lambda x: result.append(x)) + +print("transformed:", result) +``` + + +``` +transformed: [6, 8] +``` + +## Common Operators + +### Transform: `map` + +```python session=rx +rx.of(1, 2, 3).pipe( + ops.map(lambda x: f"item_{x}") +).subscribe(print) +``` + + +``` +item_1 +item_2 +item_3 + +``` + +### Filter: `filter` + +```python session=rx +rx.of(1, 2, 3, 4, 5).pipe( + ops.filter(lambda x: x % 2 == 0) +).subscribe(print) +``` + + +``` +2 +4 + +``` + +### Limit emissions: `take` + +```python session=rx +rx.of(1, 2, 3, 4, 5).pipe( + ops.take(3) +).subscribe(print) +``` + + +``` +1 +2 +3 + +``` + +### Flatten nested observables: `flat_map` + +```python session=rx +# For each input, emit multiple values +rx.of(1, 2).pipe( + ops.flat_map(lambda x: rx.of(x, x * 10, x * 100)) +).subscribe(print) +``` + + +``` +1 +10 +100 +2 +20 +200 + +``` + +## Rate Limiting + +### `sample(interval)` - Emit latest value every N seconds + +Takes the most recent value at each interval. Good for continuous streams where you want the freshest data. + +```python session=rx +# Use blocking .run() to collect results properly +results = rx.interval(0.05).pipe( + ops.take(10), + ops.sample(0.2), + ops.to_list(), +).run() +print("sample() got:", results) +``` + + +``` +sample() got: [2, 6, 9] +``` + +### `throttle_first(interval)` - Emit first, then block for N seconds + +Takes the first value then ignores subsequent values for the interval. Good for user input debouncing. + +```python session=rx +results = rx.interval(0.05).pipe( + ops.take(10), + ops.throttle_first(0.15), + ops.to_list(), +).run() +print("throttle_first() got:", results) +``` + + +``` +throttle_first() got: [0, 3, 6, 9] +``` + +### Difference between sample and throttle_first + +```python session=rx +# sample: takes LATEST value at each interval tick +# throttle_first: takes FIRST value then blocks + +# With fast emissions (0,1,2,3,4,5,6,7,8,9) every 50ms: +# sample(0.2s) -> gets value at 200ms, 400ms marks -> [2, 6, 9] +# throttle_first(0.15s) -> gets 0, blocks, then 3, blocks, then 6... -> [0,3,6,9] +print("sample: latest value at each tick") +print("throttle_first: first value, then block") +``` + + +``` +sample: latest value at each tick +throttle_first: first value, then block +``` + + +## What is an Observable? + +An Observable is like a list, but instead of holding all values at once, it produces values over time. + +| | List | Iterator | Observable | +|-------------|-----------------------|-----------------------|------------------| +| **Values** | All exist now | Generated on demand | Arrive over time | +| **Control** | You pull (`for x in`) | You pull (`next()`) | Pushed to you | +| **Size** | Finite | Can be infinite | Can be infinite | +| **Async** | No | Yes (with asyncio) | Yes | +| **Cancel** | N/A | Stop calling `next()` | `.dispose()` | + +The key difference from iterators: with an Observable, **you don't control when values arrive**. A camera produces frames at 30fps whether you're ready or not. An iterator waits for you to call `next()`. + +**Observables are lazy.** An Observable is just a description of work to be done - it sits there doing nothing until you call `.subscribe()`. That's when it "wakes up" and starts producing values. + +This means you can build complex pipelines, pass them around, and nothing happens until someone subscribes. + +**The three things an Observable can tell you:** + +1. **"Here's a value"** (`on_next`) - A new value arrived +2. **"Something went wrong"** (`on_error`) - An error occurred, stream stops +3. **"I'm done"** (`on_completed`) - No more values coming + +**The basic pattern:** + +``` +observable.subscribe(what_to_do_with_each_value) +``` + +That's it. You create or receive an Observable, then subscribe to start receiving values. + +When you subscribe, data flows through a pipeline: + +
+diagram source + +```pikchr fold output=assets/observable_flow.svg +color = white +fill = none + +Obs: box "observable" rad 5px fit wid 170% ht 170% +arrow right 0.3in +Pipe: box ".pipe(ops)" rad 5px fit wid 170% ht 170% +arrow right 0.3in +Sub: box ".subscribe()" rad 5px fit wid 170% ht 170% +arrow right 0.3in +Handler: box "callback" rad 5px fit wid 170% ht 170% +``` + +
+ + +![output](assets/observable_flow.svg) + + +**Key property: Observables are lazy.** Nothing happens until you call `.subscribe()`. This means you can build up complex pipelines without any work being done, then start the flow when ready. + +Here's the full subscribe signature with all three callbacks: + +```python session=rx +rx.of(1, 2, 3).subscribe( + on_next=lambda x: print(f"value: {x}"), + on_error=lambda e: print(f"error: {e}"), + on_completed=lambda: print("done") +) +``` + + +``` +value: 1 +value: 2 +value: 3 +done + +``` + +## Disposables: Cancelling Subscriptions + +When you subscribe, you get back a `Disposable`. This is your "cancel button": + +```python session=rx +import reactivex as rx + +source = rx.interval(0.1) # emits 0, 1, 2, ... every 100ms forever +subscription = source.subscribe(lambda x: print(x)) + +# Later, when you're done: +subscription.dispose() # Stop receiving values, clean up resources +print("disposed") +``` + + +``` +disposed +``` + +**Why does this matter?** + +- Observables can be infinite (sensor feeds, websockets, timers) +- Without disposing, you leak memory and keep processing values forever +- Disposing also cleans up any resources the Observable opened (connections, file handles, etc.) + +**Rule of thumb:** Whenever you subscribe, save the disposable because you have to unsubscribe at some point by calling `disposable.dispose()`. + +**In dimos modules:** Every `Module` has a `self._disposables` (a `CompositeDisposable`) that automatically disposes everything when the module closes: + +```python session=rx +import time +from dimos.core import Module + +class MyModule(Module): + def start(self): + source = rx.interval(0.05) + self._disposables.add(source.subscribe(lambda x: print(f"got {x}"))) + +module = MyModule() +module.start() +time.sleep(0.25) + +# unsubscribes disposables +module.stop() +``` + + +``` +got 0 +got 1 +got 2 +got 3 +got 4 +``` + +## Creating Observables + +### From callback-based APIs + +```python session=create +import reactivex as rx +from reactivex import operators as ops +from dimos.utils.reactive import callback_to_observable + +class MockSensor: + def __init__(self): + self._callbacks = [] + def register(self, cb): + self._callbacks.append(cb) + def unregister(self, cb): + self._callbacks.remove(cb) + def emit(self, value): + for cb in self._callbacks: + cb(value) + +sensor = MockSensor() + +obs = callback_to_observable( + start=sensor.register, + stop=sensor.unregister +) + +received = [] +sub = obs.subscribe(lambda x: received.append(x)) + +sensor.emit("reading_1") +sensor.emit("reading_2") +print("received:", received) + +sub.dispose() +print("callbacks after dispose:", len(sensor._callbacks)) +``` + + +``` +received: ['reading_1', 'reading_2'] +callbacks after dispose: 0 +``` + +### From scratch with `rx.create` + +```python session=create +from reactivex.disposable import Disposable + +def custom_subscribe(observer, scheduler=None): + observer.on_next("first") + observer.on_next("second") + observer.on_completed() + return Disposable(lambda: print("cleaned up")) + +obs = rx.create(custom_subscribe) + +results = [] +obs.subscribe( + on_next=lambda x: results.append(x), + on_completed=lambda: results.append("DONE") +) +print("results:", results) +``` + + +``` +cleaned up +results: ['first', 'second', 'DONE'] +``` + +## CompositeDisposable + +As we know we can always dispose subscriptions when done to prevent leaks: + +```python session=dispose +import time +import reactivex as rx +from reactivex import operators as ops + +source = rx.interval(0.1).pipe(ops.take(100)) +received = [] + +subscription = source.subscribe(lambda x: received.append(x)) +time.sleep(0.25) +subscription.dispose() +time.sleep(0.2) + +print(f"received {len(received)} items before dispose") +``` + + +``` +received 2 items before dispose +``` + +For multiple subscriptions, use `CompositeDisposable`: + +```python session=dispose +from reactivex.disposable import CompositeDisposable + +disposables = CompositeDisposable() + +s1 = rx.of(1,2,3).subscribe(lambda x: None) +s2 = rx.of(4,5,6).subscribe(lambda x: None) + +disposables.add(s1) +disposables.add(s2) + +print("subscriptions:", len(disposables)) +disposables.dispose() +print("after dispose:", disposables.is_disposed) +``` + + +``` +subscriptions: 2 +after dispose: True +``` + +## Reference + +| Operator | Purpose | Example | +|-----------------------|------------------------------------------|---------------------------------------| +| `map(fn)` | Transform each value | `ops.map(lambda x: x * 2)` | +| `filter(pred)` | Keep values matching predicate | `ops.filter(lambda x: x > 0)` | +| `take(n)` | Take first n values | `ops.take(10)` | +| `first()` | Take first value only | `ops.first()` | +| `sample(sec)` | Emit latest every interval | `ops.sample(0.5)` | +| `throttle_first(sec)` | Emit first, block for interval | `ops.throttle_first(0.5)` | +| `flat_map(fn)` | Map + flatten nested observables | `ops.flat_map(lambda x: rx.of(x, x))` | +| `observe_on(sched)` | Switch scheduler | `ops.observe_on(pool_scheduler)` | +| `replay(n)` | Cache last n values for late subscribers | `ops.replay(buffer_size=1)` | +| `timeout(sec)` | Error if no value within timeout | `ops.timeout(5.0)` | + +See [RxPY documentation](https://rxpy.readthedocs.io/) for complete operator reference. diff --git a/docs/api/sensor_streams/storage_replay.md b/docs/api/sensor_streams/storage_replay.md new file mode 100644 index 0000000000..47d4ec7e6a --- /dev/null +++ b/docs/api/sensor_streams/storage_replay.md @@ -0,0 +1,231 @@ +# Sensor Storage and Replay + +Record sensor streams to disk and replay them with original timing. Useful for testing, debugging, and creating reproducible datasets. + +## Quick Start + +### Recording + +```python skip +from dimos.utils.testing.replay import TimedSensorStorage + +# Create storage (directory in data folder) +storage = TimedSensorStorage("my_recording") + +# Save frames from a stream +camera_stream.subscribe(storage.save_one) + +# Or save manually +storage.save(frame1, frame2, frame3) +``` + +### Replaying + +```python skip +from dimos.utils.testing.replay import TimedSensorReplay + +# Load recording +replay = TimedSensorReplay("my_recording") + +# Iterate at original speed +for frame in replay.iterate_realtime(): + process(frame) + +# Or as an Observable stream +replay.stream(speed=1.0).subscribe(process) +``` + +## TimedSensorStorage + +Stores sensor data with timestamps as pickle files. Each frame is saved as `000.pickle`, `001.pickle`, etc. + +```python skip +from dimos.utils.testing.replay import TimedSensorStorage + +storage = TimedSensorStorage("lidar_capture") + +# Save individual frames +storage.save_one(lidar_msg) # Returns frame count + +# Save multiple frames +storage.save(frame1, frame2, frame3) + +# Subscribe to a stream +lidar_stream.subscribe(storage.save_one) + +# Or pipe through (emits frame count) +lidar_stream.pipe( + ops.flat_map(storage.save_stream) +).subscribe() +``` + +**Storage location:** Files are saved to the data directory under the given name. The directory must not already contain pickle files (prevents accidental overwrites). + +**What gets stored:** By default, if a frame has a `.raw_msg` attribute, that's pickled instead of the full object. You can customize with the `autocast` parameter: + +```python skip +# Custom serialization +storage = TimedSensorStorage( + "custom_capture", + autocast=lambda frame: frame.to_dict() +) +``` + +## TimedSensorReplay + +Replays stored sensor data with timestamp-aware iteration and seeking. + +### Basic Iteration + +```python skip +from dimos.utils.testing.replay import TimedSensorReplay + +replay = TimedSensorReplay("lidar_capture") + +# Iterate all frames (ignores timing) +for frame in replay.iterate(): + process(frame) + +# Iterate with timestamps +for ts, frame in replay.iterate_ts(): + print(f"Frame at {ts}: {frame}") + +# Iterate with relative timestamps (from start) +for relative_ts, frame in replay.iterate_duration(): + print(f"At {relative_ts:.2f}s: {frame}") +``` + +### Realtime Playback + +```python skip +# Play at original speed (blocks between frames) +for frame in replay.iterate_realtime(): + process(frame) + +# Play at 2x speed +for frame in replay.iterate_realtime(speed=2.0): + process(frame) + +# Play at half speed +for frame in replay.iterate_realtime(speed=0.5): + process(frame) +``` + +### Seeking and Slicing + +```python skip +# Start 10 seconds into the recording +for ts, frame in replay.iterate_ts(seek=10.0): + process(frame) + +# Play only 5 seconds starting at 10s +for ts, frame in replay.iterate_ts(seek=10.0, duration=5.0): + process(frame) + +# Loop forever +for frame in replay.iterate(loop=True): + process(frame) +``` + +### Finding Specific Frames + +```python skip +# Find frame closest to absolute timestamp +frame = replay.find_closest(1704067200.0) + +# Find frame closest to relative time (30s from start) +frame = replay.find_closest_seek(30.0) + +# With tolerance (returns None if no match within 0.1s) +frame = replay.find_closest(timestamp, tolerance=0.1) +``` + +### Observable Stream + +The `.stream()` method returns an Observable that emits frames with original timing: + +```python skip +# Stream at original speed +replay.stream(speed=1.0).subscribe(process) + +# Stream at 2x with seeking +replay.stream( + speed=2.0, + seek=10.0, # Start 10s in + duration=30.0, # Play for 30s + loop=True # Loop forever +).subscribe(process) +``` + +## Usage: Stub Connections for Testing + +A common pattern is creating replay-based connection stubs for testing without hardware. From [`robot/unitree/connection/go2.py`](/dimos/robot/unitree/connection/go2.py#L83): + +This is a bit primitive, we'd like to write a higher order API for recording full module I/O for any module, but this is a work in progress atm. + + +```python skip +class ReplayConnection(UnitreeWebRTCConnection): + dir_name = "unitree_go2_bigoffice" + + def __init__(self, **kwargs) -> None: + get_data(self.dir_name) + self.replay_config = { + "loop": kwargs.get("loop"), + "seek": kwargs.get("seek"), + "duration": kwargs.get("duration"), + } + + def lidar_stream(self): + lidar_store = TimedSensorReplay(f"{self.dir_name}/lidar") + return lidar_store.stream(**self.replay_config) + + def video_stream(self): + video_store = TimedSensorReplay(f"{self.dir_name}/video") + return video_store.stream(**self.replay_config) +``` + +This allows running the full perception pipeline against recorded data: + +```python skip +# Use replay connection instead of real hardware +connection = ReplayConnection(loop=True, seek=5.0) +robot = GO2Connection(connection=connection) +``` + +## Data Format + +Each pickle file contains a tuple `(timestamp, data)`: + +- **timestamp**: Unix timestamp (float) when the frame was captured +- **data**: The sensor data (or result of `autocast` if provided) + +Files are numbered sequentially: `000.pickle`, `001.pickle`, etc. + +Recordings are stored in the `data/` directory. See [Data Loading](/docs/data.md) for how data storage works, including Git LFS handling for large datasets. + +## API Reference + +### TimedSensorStorage + +| Method | Description | +|------------------------------|------------------------------------------| +| `save_one(frame)` | Save a single frame, returns frame count | +| `save(*frames)` | Save multiple frames | +| `save_stream(observable)` | Pipe an observable through storage | +| `consume_stream(observable)` | Subscribe and save without returning | + +### TimedSensorReplay + +| Method | Description | +|--------------------------------------------------|---------------------------------------| +| `iterate(loop=False)` | Iterate frames (no timing) | +| `iterate_ts(seek, duration, loop)` | Iterate with absolute timestamps | +| `iterate_duration(...)` | Iterate with relative timestamps | +| `iterate_realtime(speed, ...)` | Iterate with blocking to match timing | +| `stream(speed, seek, duration, loop)` | Observable with original timing | +| `find_closest(timestamp, tolerance)` | Find frame by absolute timestamp | +| `find_closest_seek(relative_seconds, tolerance)` | Find frame by relative time | +| `first()` | Get first frame | +| `first_timestamp()` | Get first timestamp | +| `load(name)` | Load specific frame by name/index | diff --git a/docs/api/sensor_streams/temporal_alignment.md b/docs/api/sensor_streams/temporal_alignment.md new file mode 100644 index 0000000000..9484da7155 --- /dev/null +++ b/docs/api/sensor_streams/temporal_alignment.md @@ -0,0 +1,313 @@ +# Temporal Message Alignment + +Robots have multiple sensors emitting data at different rates and latencies. A camera might run at 30fps, while lidar scans at 10Hz, and each has different processing delays. For perception tasks like projecting 2D detections into 3D pointclouds, we need to match data from these streams by timestamp. + +`align_timestamped` solves this by buffering messages and matching them within a time tolerance. + +
Pikchr + +```pikchr fold output=assets/alignment_overview.svg +color = white +fill = none + +Cam: box "Camera" "30 fps" rad 5px fit wid 170% ht 170% +arrow from Cam.e right 0.4in then down 0.35in then right 0.4in +Align: box "align_timestamped" rad 5px fit wid 170% ht 170% + +Lidar: box "Lidar" "10 Hz" rad 5px fit wid 170% ht 170% with .s at (Cam.s.x, Cam.s.y - 0.7in) +arrow from Lidar.e right 0.4in then up 0.35in then right 0.4in + +arrow from Align.e right 0.4in +Out: box "(image, pointcloud)" rad 5px fit wid 170% ht 170% +``` + +
+ + +![output](assets/alignment_overview.svg) + + +## Basic Usage + +Below we setup replay of real camera and lidar data from the Unitree Go2 robot, you can check if interested + +
+Stream Setup + +You can read more about [sensor storage here](storage_replay.md) and [LFS data store here](/docs/data.md) + +```python session=align no-result +from reactivex import Subject +from dimos.utils.testing import TimedSensorReplay +from dimos.types.timestamped import Timestamped, align_timestamped +from reactivex import operators as ops +import reactivex as rx + +# Load recorded Go2 sensor streams +video_replay = TimedSensorReplay("unitree_go2_bigoffice/video") +lidar_replay = TimedSensorReplay("unitree_go2_bigoffice/lidar") + +# this is a bit tricky, we find the first video frame timestamp, then add 2 seconds to it +seek_ts = video_replay.first_timestamp() + 2 + +# Lists to collect items as they flow through streams +video_frames = [] +lidar_scans = [] + +# We are using from_timestamp=... and not seek=... because seek seeks through recording +# timestamps, from_timestamp matches actual message timestamp. +# It's possible for sensor data to come in late, but with correct capture time timestamps +video_stream = video_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( + ops.do_action(lambda x: video_frames.append(x)) +) + +lidar_stream = lidar_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( + ops.do_action(lambda x: lidar_scans.append(x)) +) + +``` + + +
+ +Streams would normally come from an actual robot into your module via `IN` inputs, [`detection/module3D.py`](/dimos/perception/detection/module3D.py#L11) is a good example of this. + +Assume we have them, let's align them. + +```python session=align +# Align video (primary) with lidar (secondary) +# match_tolerance: max time difference for a match (seconds) +# buffer_size: how long to keep messages waiting for matches (seconds) +aligned_pairs = align_timestamped( + video_stream, + lidar_stream, + match_tolerance=0.025, # 25ms tolerance + buffer_size=5.0, # how long to wait for match +).pipe(ops.to_list()).run() + +print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") +print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") + +# Show a matched pair +if aligned_pairs: + img, pc = aligned_pairs[0] + dt = abs(img.ts - pc.ts) + print(f"\nFirst matched pair: Δ{dt*1000:.1f}ms") +``` + + +``` +Video: 29 frames, Lidar: 15 scans +Aligned pairs: 11 out of 29 video frames + +First matched pair: Δ11.3ms +``` + +
+Visualization helper + +```python session=align fold no-result +import matplotlib +import matplotlib.pyplot as plt + +def plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, path): + """Single timeline: video above axis, lidar below, green lines for matches.""" + matplotlib.use('Agg') + plt.style.use('dark_background') + + # Get base timestamp for relative times (frames have .ts attribute) + base_ts = video_frames[0].ts + video_ts = [f.ts - base_ts for f in video_frames] + lidar_ts = [s.ts - base_ts for s in lidar_scans] + + # Find matched timestamps + matched_video_ts = set(img.ts for img, _ in aligned_pairs) + matched_lidar_ts = set(pc.ts for _, pc in aligned_pairs) + + fig, ax = plt.subplots(figsize=(12, 2.5)) + + # Video markers above axis (y=0.3) - circles, cyan when matched + for frame in video_frames: + rel_ts = frame.ts - base_ts + matched = frame.ts in matched_video_ts + ax.plot(rel_ts, 0.3, 'o', color='cyan' if matched else '#688', markersize=8) + + # Lidar markers below axis (y=-0.3) - squares, orange when matched + for scan in lidar_scans: + rel_ts = scan.ts - base_ts + matched = scan.ts in matched_lidar_ts + ax.plot(rel_ts, -0.3, 's', color='orange' if matched else '#a86', markersize=8) + + # Green lines connecting matched pairs + for img, pc in aligned_pairs: + img_rel = img.ts - base_ts + pc_rel = pc.ts - base_ts + ax.plot([img_rel, pc_rel], [0.3, -0.3], '-', color='lime', alpha=0.6, linewidth=1) + + # Axis styling + ax.axhline(y=0, color='white', linewidth=0.5, alpha=0.3) + ax.set_xlim(-0.1, max(video_ts + lidar_ts) + 0.1) + ax.set_ylim(-0.6, 0.6) + ax.set_xlabel('Time (s)') + ax.set_yticks([0.3, -0.3]) + ax.set_yticklabels(['Video', 'Lidar']) + ax.set_title(f'{len(aligned_pairs)} matched from {len(video_frames)} video + {len(lidar_scans)} lidar') + plt.tight_layout() + plt.savefig(path, transparent=True) + plt.close() +``` + +
+ +```python session=align output=assets/alignment_timeline.png +plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') +``` + + +![output](assets/alignment_timeline.png) + +if we loosen up our match tolerance we might get multiple pairs matching the same lidar frame + +```python session=align +aligned_pairs = align_timestamped( + video_stream, + lidar_stream, + match_tolerance=0.05, # 50ms tolerance + buffer_size=5.0, # how long to wait for match +).pipe(ops.to_list()).run() + +print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") +print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") +``` + + +``` +Video: 58 frames, Lidar: 30 scans +Aligned pairs: 23 out of 58 video frames +``` + + +```python session=align output=assets/alignment_timeline2.png +plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') +``` + + +![output](assets/alignment_timeline2.png) + +## We can combine frame alignment with a quality filter + +more on [quality filtering here](quality_filter.md) + +```python session=align +from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier + +# Lists to collect items as they flow through streams +video_frames = [] +lidar_scans = [] + +video_stream = video_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( + sharpness_barrier(3.0), + ops.do_action(lambda x: video_frames.append(x)) +) + +lidar_stream = lidar_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( + ops.do_action(lambda x: lidar_scans.append(x)) +) + +aligned_pairs = align_timestamped( + video_stream, + lidar_stream, + match_tolerance=0.025, # 25ms tolerance + buffer_size=5.0, # how long to wait for match +).pipe(ops.to_list()).run() + +print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") +print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") + +``` + + +``` +Video: 6 frames, Lidar: 15 scans +Aligned pairs: 1 out of 6 video frames +``` + +```python session=align output=assets/alignment_timeline3.png +plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') +``` + + +![output](assets/alignment_timeline3.png) + +We are very picky but data is high quality. best frame, with closest lidar match in this window. + +## How It Works + +The primary stream (first argument) drives emissions. When a primary message arrives: + +1. **Immediate match**: If matching secondaries already exist in buffers, emit immediately +2. **Deferred match**: If secondaries are missing, buffer the primary and wait + +When secondary messages arrive: +1. Add to buffer for future primary matches +2. Check buffered primaries - if this completes a match, emit + +
+diagram source + +```pikchr fold output=assets/alignment_flow.svg +color = white +fill = none +linewid = 0.35in + +Primary: box "Primary" "arrives" rad 5px fit wid 170% ht 170% +arrow +Check: box "Check" "secondaries" rad 5px fit wid 170% ht 170% + +arrow from Check.e right 0.35in then up 0.4in then right 0.35in +Emit: box "Emit" "match" rad 5px fit wid 170% ht 170% +text "all found" at (Emit.w.x - 0.4in, Emit.w.y + 0.15in) + +arrow from Check.e right 0.35in then down 0.4in then right 0.35in +Buffer: box "Buffer" "primary" rad 5px fit wid 170% ht 170% +text "waiting..." at (Buffer.w.x - 0.4in, Buffer.w.y - 0.15in) +``` + +
+ + +![output](assets/alignment_flow.svg) + +## Parameters + +| Parameter | Type | Default | Description | +|--------------------------|--------------------|----------|-------------------------------------------------| +| `primary_observable` | `Observable[T]` | required | Primary stream that drives output timing | +| `*secondary_observables` | `Observable[S]...` | required | One or more secondary streams to align | +| `match_tolerance` | `float` | 0.1 | Max time difference for a match (seconds) | +| `buffer_size` | `float` | 1.0 | How long to buffer unmatched messages (seconds) | + + + +## Usage in Modules + +Every module `In` port exposes an `.observable()` method that returns a backpressured stream of incoming messages. This makes it easy to align inputs from multiple sensors. + +From [`detection/module3D.py`](/dimos/perception/detection/module3D.py), projecting 2D detections into 3D pointclouds: + +```python skip +class Detection3DModule(Detection2DModule): + color_image: In[Image] + pointcloud: In[PointCloud2] + + def start(self): + # Align 2D detections with pointcloud data + self.detection_stream_3d = align_timestamped( + backpressure(self.detection_stream_2d()), + self.pointcloud.observable(), + match_tolerance=0.25, + buffer_size=20.0, + ).pipe(ops.map(detection2d_to_3d)) +``` + +The 2D detection stream (camera + ML model) is the primary, matched with raw pointcloud data from lidar. The longer `buffer_size=20.0` accounts for variable ML inference times. diff --git a/docs/api/transforms.md b/docs/api/transforms.md new file mode 100644 index 0000000000..95def6fcea --- /dev/null +++ b/docs/api/transforms.md @@ -0,0 +1,469 @@ +# Transforms + +## The Problem: Everything Measures from Its Own Perspective + +Imagine your robot has an RGB-D camera—a camera that captures both color images and depth (distance to each pixel). These are common in robotics: Intel RealSense, Microsoft Kinect, and similar sensors. + +The camera spots a coffee mug at pixel (320, 240), and the depth sensor says it's 1.2 meters away. You want the robot arm to pick it up—but the arm doesn't understand pixels or camera-relative distances. It needs coordinates in its own workspace: "move to position (0.8, 0.3, 0.1) meters from my base." + +To convert camera measurements to arm coordinates, you need to know: +- The camera's intrinsic parameters (focal length, sensor size) to convert pixels to a 3D direction +- The depth value to get the full 3D position relative to the camera +- Where the camera is mounted relative to the arm, and at what angle + +This chain of conversions—(pixels + depth) → 3D point in camera frame → robot coordinates—is what **transforms** handle. + +
+diagram source + +```pikchr fold output=assets/transforms_tree.svg +color = white +fill = none + +# Root (left side) +W: box "world" rad 5px fit wid 170% ht 170% +arrow right 0.4in +RB: box "robot_base" rad 5px fit wid 170% ht 170% + +# Camera branch (top) +arrow from RB.e right 0.3in then up 0.4in then right 0.3in +CL: box "camera_link" rad 5px fit wid 170% ht 170% +arrow right 0.4in +CO: box "camera_optical" rad 5px fit wid 170% ht 170% +text "mug here" small italic at (CO.s.x, CO.s.y - 0.25in) + +# Arm branch (bottom) +arrow from RB.e right 0.3in then down 0.4in then right 0.3in +AB: box "arm_base" rad 5px fit wid 170% ht 170% +arrow right 0.4in +GR: box "gripper" rad 5px fit wid 170% ht 170% +text "target here" small italic at (GR.s.x, GR.s.y - 0.25in) +``` + +
+ + +![output](assets/transforms_tree.svg) + + +Each arrow in this tree is a transform. To get the mug's position in gripper coordinates, you chain transforms through their common parent: camera → robot_base → arm → gripper. + +## What's a Coordinate Frame? + +A **coordinate frame** is simply a point of view—an origin point and a set of axes (X, Y, Z) from which you measure positions and orientations. + +Think of it like giving directions: +- **GPS** says you're at 37.7749° N, 122.4194° W +- The **coffee shop floor plan** says "table 5 is 3 meters from the entrance" +- Your **friend** says "I'm two tables to your left" + +These all describe positions in the same physical space, but from different reference points. Each is a coordinate frame. + +In a robot: +- The **camera** measures in pixels, or in meters relative to its lens +- The **LIDAR** measures distances from its own mounting point +- The **robot arm** thinks in terms of its base or end-effector position +- The **world** has a fixed coordinate system everything lives in + +Each sensor, joint, and reference point has its own frame. + +## The Transform Class + +The `Transform` class at [`geometry_msgs/Transform.py`](/dimos/msgs/geometry_msgs/Transform.py#L21) represents a spatial transformation with: + +- `frame_id` - The parent frame name +- `child_frame_id` - The child frame name +- `translation` - A `Vector3` (x, y, z) offset +- `rotation` - A `Quaternion` (x, y, z, w) orientation +- `ts` - Timestamp for temporal lookups + +```python +from dimos.msgs.geometry_msgs import Transform, Vector3, Quaternion + +# Camera 0.5m forward and 0.3m up from base, no rotation +camera_transform = Transform( + translation=Vector3(0.5, 0.0, 0.3), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity rotation + frame_id="base_link", + child_frame_id="camera_link", +) +print(camera_transform) +``` + + +``` +base_link -> camera_link + Translation: → Vector Vector([0.5 0. 0.3]) + Rotation: Quaternion(0.000000, 0.000000, 0.000000, 1.000000) +``` + + +### Transform Operations + +Transforms can be composed and inverted: + +```python +from dimos.msgs.geometry_msgs import Transform, Vector3, Quaternion + +# Create two transforms +t1 = Transform( + translation=Vector3(1.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", +) +t2 = Transform( + translation=Vector3(0.0, 0.5, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="camera_link", + child_frame_id="end_effector", +) + +# Compose: base_link -> camera -> end_effector +t3 = t1 + t2 +print(f"Composed: {t3.frame_id} -> {t3.child_frame_id}") +print(f"Translation: ({t3.translation.x}, {t3.translation.y}, {t3.translation.z})") + +# Inverse: if t goes A -> B, -t goes B -> A +t_inverse = -t1 +print(f"Inverse: {t_inverse.frame_id} -> {t_inverse.child_frame_id}") +``` + + +``` +Composed: base_link -> end_effector +Translation: (1.0, 0.5, 0.0) +Inverse: camera_link -> base_link +``` + + +### Converting to Matrix Form + +For integration with libraries like NumPy or OpenCV: + +```python +from dimos.msgs.geometry_msgs import Transform, Vector3, Quaternion + +t = Transform( + translation=Vector3(1.0, 2.0, 3.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), +) +matrix = t.to_matrix() +print("4x4 transformation matrix:") +print(matrix) +``` + + +``` +4x4 transformation matrix: +[[1. 0. 0. 1.] + [0. 1. 0. 2.] + [0. 0. 1. 3.] + [0. 0. 0. 1.]] +``` + + + +## Frame IDs in Modules + +Modules in DimOS automatically get a `frame_id` property. This is controlled by two config options in [`core/module.py`](/dimos/core/module.py#L78): + +- `frame_id` - The base frame name (defaults to the class name) +- `frame_id_prefix` - Optional prefix for namespacing + +```python +from dimos.core import Module, ModuleConfig +from dataclasses import dataclass + +@dataclass +class MyModuleConfig(ModuleConfig): + frame_id: str = "sensor_link" + frame_id_prefix: str | None = None + +class MySensorModule(Module[MyModuleConfig]): + default_config = MyModuleConfig + +# With default config: +sensor = MySensorModule() +print(f"Default frame_id: {sensor.frame_id}") + +# With prefix (useful for multi-robot scenarios): +sensor2 = MySensorModule(frame_id_prefix="robot1") +print(f"With prefix: {sensor2.frame_id}") +``` + + +``` +Default frame_id: sensor_link +With prefix: robot1/sensor_link +``` + + +## The TF Service + +Every module has access to `self.tf`, a transform service that: + +- **Publishes** transforms to the system +- **Looks up** transforms between any two frames +- **Buffers** historical transforms for temporal queries + +The TF service is implemented in [`tf.py`](/dimos/protocol/tf/tf.py) and is lazily initialized on first access. + +### Multi-Module Transform Example + +This example demonstrates how multiple modules publish and receive transforms. Three modules work together: + +1. **RobotBaseModule** - Publishes `world -> base_link` (robot's position in the world) +2. **CameraModule** - Publishes `base_link -> camera_link` (camera mounting position) and `camera_link -> camera_optical` (optical frame convention) +3. **PerceptionModule** - Looks up transforms between any frames + +```python ansi=false +import time +import reactivex as rx +from reactivex import operators as ops +from dimos.core import Module, rpc, start +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 + +class RobotBaseModule(Module): + """Publishes the robot's position in the world frame at 10Hz.""" + def __init__(self, **kwargs: object) -> None: + super().__init__(**kwargs) + + @rpc + def start(self) -> None: + super().start() + + def publish_pose(_): + robot_pose = Transform( + translation=Vector3(2.5, 3.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="world", + child_frame_id="base_link", + ts=time.time(), + ) + self.tf.publish(robot_pose) + + self._disposables.add( + rx.interval(0.1).subscribe(publish_pose) + ) + +class CameraModule(Module): + """Publishes camera transforms at 10Hz.""" + @rpc + def start(self) -> None: + super().start() + + def publish_transforms(_): + camera_mount = Transform( + translation=Vector3(1.0, 0.0, 0.3), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ts=time.time(), + ) + optical_frame = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), + frame_id="camera_link", + child_frame_id="camera_optical", + ts=time.time(), + ) + self.tf.publish(camera_mount, optical_frame) + + self._disposables.add( + rx.interval(0.1).subscribe(publish_transforms) + ) + + +class PerceptionModule(Module): + """Receives transforms and performs lookups.""" + + def start(self) -> None: + # this is just to init transforms system + # touching the property for the first time enables the system for this module. + # transform lookups normally happen in fast loops in IRL modules + _ = self.tf + + @rpc + def lookup(self) -> None: + + # will pretty print information on transforms in the buffer + print(self.tf) + + direct = self.tf.get("world", "base_link") + print(f"Direct: robot is at ({direct.translation.x}, {direct.translation.y})m in world\n") + + # Chained lookup - automatically composes world->base->camera->optical + chained = self.tf.get("world", "camera_optical") + print(f"Chained: {chained}\n") + + # Inverse lookup - automatically inverts direction + inverse = self.tf.get("camera_optical", "world") + print(f"Inverse: {inverse}\n") + + print("Transform tree:") + print(self.tf.graph()) + + +if __name__ == "__main__": + dimos = start(3) + + # Deploy and start modules + robot = dimos.deploy(RobotBaseModule) + camera = dimos.deploy(CameraModule) + perception = dimos.deploy(PerceptionModule) + + robot.start() + camera.start() + perception.start() + + time.sleep(1.0) + + perception.lookup() + + dimos.stop() + +``` + + +``` +Initialized dimos local cluster with 3 workers, memory limit: auto +2025-12-29T12:47:01.433394Z [info ] Deployed module. [dimos/core/__init__.py] module=RobotBaseModule worker_id=1 +2025-12-29T12:47:01.603269Z [info ] Deployed module. [dimos/core/__init__.py] module=CameraModule worker_id=0 +2025-12-29T12:47:01.698970Z [info ] Deployed module. [dimos/core/__init__.py] module=PerceptionModule worker_id=2 +LCMTF(3 buffers): + TBuffer(world -> base_link, 10 msgs, 0.90s [2025-12-29 20:47:01 - 2025-12-29 20:47:02]) + TBuffer(base_link -> camera_link, 9 msgs, 0.80s [2025-12-29 20:47:01 - 2025-12-29 20:47:02]) + TBuffer(camera_link -> camera_optical, 9 msgs, 0.80s [2025-12-29 20:47:01 - 2025-12-29 20:47:02]) +Direct: robot is at (2.5, 3.0)m in world + +Chained: world -> camera_optical + Translation: → Vector Vector([3.5 3. 0.3]) + Rotation: Quaternion(-0.500000, 0.500000, -0.500000, 0.500000) + +Inverse: camera_optical -> world + Translation: → Vector Vector([ 3. 0.3 -3.5]) + Rotation: Quaternion(0.500000, -0.500000, 0.500000, 0.500000) + +Transform tree: +ā”Œā”€ā”€ā”€ā”€ā”€ā” +│world│ +ā””ā”¬ā”€ā”€ā”€ā”€ā”˜ +ā”Œā–½ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│base_link│ +ā””ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +ā”Œā–½ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│camera_link│ +ā””ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +ā”Œā–½ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│camera_optical│ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + + +You can also run `foxglove-studio-bridge` in the next terminal (binary provided by dimos and should be in your py env) and `foxglove-studio` to view these transforms in 3D (TODO we need to update this for rerun) + +![transforms](assets/transforms.png) + +Key points: + +- **Automatic broadcasting**: `self.tf.publish()` broadcasts via LCM to all modules +- **Chained lookups**: TF finds paths through the tree automatically +- **Inverse lookups**: Request transforms in either direction +- **Temporal buffering**: Transforms are timestamped and buffered (default 10s) for sensor fusion + +The transform tree from the example above, showing which module publishes each transform: + +
+diagram source + +```pikchr fold output=assets/transforms_modules.svg +color = white +fill = none + +# Frame boxes +W: box "world" rad 5px fit wid 170% ht 170% +A1: arrow right 0.4in +BL: box "base_link" rad 5px fit wid 170% ht 170% +A2: arrow right 0.4in +CL: box "camera_link" rad 5px fit wid 170% ht 170% +A3: arrow right 0.4in +CO: box "camera_optical" rad 5px fit wid 170% ht 170% + +# RobotBaseModule box - encompasses world->base_link +box width (BL.e.x - W.w.x + 0.15in) height 0.7in \ + at ((W.w.x + BL.e.x)/2, W.y - 0.05in) \ + rad 10px color 0x6699cc fill none +text "RobotBaseModule" italic at ((W.x + BL.x)/2, W.n.y + 0.25in) + +# CameraModule box - encompasses camera_link->camera_optical (starts after base_link) +box width (CO.e.x - BL.e.x + 0.1in) height 0.7in \ + at ((BL.e.x + CO.e.x)/2, CL.y + 0.05in) \ + rad 10px color 0xcc9966 fill none +text "CameraModule" italic at ((CL.x + CO.x)/2, CL.s.y - 0.25in) +``` + + +
+ + +![output](assets/transforms_modules.svg) + + +# Internals + +## Transform Buffer + +`self.tf` on module is a transform buffer. This is a standalone class that maintains a temporal buffer of transforms (default 10 seconds) allowing queries at past timestamps, you can use it directly: + +```python +from dimos.protocol.tf import TF +from dimos.msgs.geometry_msgs import Transform, Vector3, Quaternion +import time + +tf = TF(autostart=False) + +# Simulate transforms at different times +for i in range(5): + t = Transform( + translation=Vector3(float(i), 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ts=time.time() + i * 0.1, + ) + tf.receive_transform(t) + +# Query the latest transform +result = tf.get("base_link", "camera_link") +print(f"Latest transform: x={result.translation.x}") +print(f"Buffer has {len(tf.buffers)} transform pair(s)") +print(tf) +``` + + +``` +Latest transform: x=4.0 +Buffer has 1 transform pair(s) +LCMTF(1 buffers): + TBuffer(base_link -> camera_link, 5 msgs, 0.40s [2025-12-29 18:19:18 - 2025-12-29 18:19:18]) +``` + + +This is essential for sensor fusion where you need to know where the camera was when an image was captured, not where it is now. + + +## Further Reading + +For a visual introduction to transforms and coordinate frames: +- [Coordinate Transforms (YouTube)](https://www.youtube.com/watch?v=NGPn9nvLPmg) + +For the mathematical foundations, the ROS documentation provides detailed background: + +- [ROS tf2 Concepts](http://wiki.ros.org/tf2) +- [ROS REP 103 - Standard Units and Coordinate Conventions](https://www.ros.org/reps/rep-0103.html) +- [ROS REP 105 - Coordinate Frames for Mobile Platforms](https://www.ros.org/reps/rep-0105.html) + +See also: +- [Modules](/docs/concepts/modules/index.md) for understanding the module system +- [Configuration](/docs/concepts/configuration.md) for module configuration patterns diff --git a/docs/assets/get_data_flow.svg b/docs/assets/get_data_flow.svg new file mode 100644 index 0000000000..d875e1dadb --- /dev/null +++ b/docs/assets/get_data_flow.svg @@ -0,0 +1,25 @@ + + +get_data(name) + + + +Check +data/{name} + + + +ReturnĀ path + + + +PullĀ LFS + + + +Decompress + + + +ReturnĀ path + diff --git a/docs/concepts/assets/camera_module.svg b/docs/concepts/assets/camera_module.svg new file mode 100644 index 0000000000..48cc4286db --- /dev/null +++ b/docs/concepts/assets/camera_module.svg @@ -0,0 +1,87 @@ + + + + + + +module + +cluster_outputs + + +cluster_rpcs + +RPCs + + +cluster_skills + +Skills + + + +CameraModule + +CameraModule + + + +out_color_image + + + +color_image:Image + + + +CameraModule->out_color_image + + + + + +out_camera_info + + + +camera_info:CameraInfo + + + +CameraModule->out_camera_info + + + + + +rpc_set_transport + +set_transport(stream_name: str, transport: Transport) -> bool + + + +CameraModule->rpc_set_transport + + + + +skill_video_stream + +video_stream stream=passive reducer=latest_reducer + + + +CameraModule->skill_video_stream + + + + +rpc_start + +start() + + + diff --git a/docs/concepts/assets/go2_agentic.svg b/docs/concepts/assets/go2_agentic.svg new file mode 100644 index 0000000000..f20c1b5ac5 --- /dev/null +++ b/docs/concepts/assets/go2_agentic.svg @@ -0,0 +1,260 @@ + + + + + + +modules + +cluster_agents + +agents + + +cluster_mapping + +mapping + + +cluster_navigation + +navigation + + +cluster_perception + +perception + + +cluster_robot + +robot + + + +HumanInput + +HumanInput + + + +LlmAgent + +LlmAgent + + + +NavigationSkillContainer + +NavigationSkillContainer + + + +SpeakSkill + +SpeakSkill + + + +WebInput + +WebInput + + + +CostMapper + +CostMapper + + + +chan_global_costmap_OccupancyGrid + + + +global_costmap:OccupancyGrid + + + +CostMapper->chan_global_costmap_OccupancyGrid + + + + +VoxelGridMapper + +VoxelGridMapper + + + +chan_global_map_LidarMessage + + + +global_map:LidarMessage + + + +VoxelGridMapper->chan_global_map_LidarMessage + + + + +ReplanningAStarPlanner + +ReplanningAStarPlanner + + + +chan_cmd_vel_Twist + + + +cmd_vel:Twist + + + +ReplanningAStarPlanner->chan_cmd_vel_Twist + + + + +chan_goal_reached_Bool + + + +goal_reached:Bool + + + +ReplanningAStarPlanner->chan_goal_reached_Bool + + + + +WavefrontFrontierExplorer + +WavefrontFrontierExplorer + + + +chan_goal_request_PoseStamped + + + +goal_request:PoseStamped + + + +WavefrontFrontierExplorer->chan_goal_request_PoseStamped + + + + +SpatialMemory + +SpatialMemory + + + +FoxgloveBridge + +FoxgloveBridge + + + +GO2Connection + +GO2Connection + + + +chan_color_image_Image + + + +color_image:Image + + + +GO2Connection->chan_color_image_Image + + + + +chan_lidar_LidarMessage + + + +lidar:LidarMessage + + + +GO2Connection->chan_lidar_LidarMessage + + + + +UnitreeSkillContainer + +UnitreeSkillContainer + + + +chan_cmd_vel_Twist->GO2Connection + + + + + +chan_color_image_Image->NavigationSkillContainer + + + + + +chan_color_image_Image->SpatialMemory + + + + + +chan_global_costmap_OccupancyGrid->ReplanningAStarPlanner + + + + + +chan_global_costmap_OccupancyGrid->WavefrontFrontierExplorer + + + + + +chan_global_map_LidarMessage->CostMapper + + + + + +chan_goal_reached_Bool->WavefrontFrontierExplorer + + + + + +chan_goal_request_PoseStamped->ReplanningAStarPlanner + + + + + +chan_lidar_LidarMessage->VoxelGridMapper + + + + + diff --git a/docs/concepts/assets/go2_nav.svg b/docs/concepts/assets/go2_nav.svg new file mode 100644 index 0000000000..25adae5264 --- /dev/null +++ b/docs/concepts/assets/go2_nav.svg @@ -0,0 +1,183 @@ + + + + + + +modules + +cluster_mapping + +mapping + + +cluster_navigation + +navigation + + +cluster_robot + +robot + + + +CostMapper + +CostMapper + + + +chan_global_costmap_OccupancyGrid + + + +global_costmap:OccupancyGrid + + + +CostMapper->chan_global_costmap_OccupancyGrid + + + + +VoxelGridMapper + +VoxelGridMapper + + + +chan_global_map_LidarMessage + + + +global_map:LidarMessage + + + +VoxelGridMapper->chan_global_map_LidarMessage + + + + +ReplanningAStarPlanner + +ReplanningAStarPlanner + + + +chan_cmd_vel_Twist + + + +cmd_vel:Twist + + + +ReplanningAStarPlanner->chan_cmd_vel_Twist + + + + +chan_goal_reached_Bool + + + +goal_reached:Bool + + + +ReplanningAStarPlanner->chan_goal_reached_Bool + + + + +WavefrontFrontierExplorer + +WavefrontFrontierExplorer + + + +chan_goal_request_PoseStamped + + + +goal_request:PoseStamped + + + +WavefrontFrontierExplorer->chan_goal_request_PoseStamped + + + + +FoxgloveBridge + +FoxgloveBridge + + + +GO2Connection + +GO2Connection + + + +chan_lidar_LidarMessage + + + +lidar:LidarMessage + + + +GO2Connection->chan_lidar_LidarMessage + + + + +chan_cmd_vel_Twist->GO2Connection + + + + + +chan_global_costmap_OccupancyGrid->ReplanningAStarPlanner + + + + + +chan_global_costmap_OccupancyGrid->WavefrontFrontierExplorer + + + + + +chan_global_map_LidarMessage->CostMapper + + + + + +chan_goal_reached_Bool->WavefrontFrontierExplorer + + + + + +chan_goal_request_PoseStamped->ReplanningAStarPlanner + + + + + +chan_lidar_LidarMessage->VoxelGridMapper + + + + + diff --git a/docs/concepts/lcm.md b/docs/concepts/lcm.md new file mode 100644 index 0000000000..345407e23a --- /dev/null +++ b/docs/concepts/lcm.md @@ -0,0 +1,160 @@ + +# LCM Messages + +[LCM (Lightweight Communications and Marshalling)](https://github.com/lcm-proj/lcm) is a message passing system with bindings for many languages (C, C++, Python, Java, Lua, Go). While LCM includes a UDP multicast transport, its real power is the message definition format - classes that can encode themselves to compact binary representation. + +Dimos uses LCM message definitions for all inter-module communication. Because messages serialize to binary, they can be sent over any transport - not just LCM's UDP multicast, but also shared memory, Redis, WebSockets, or any other channel. + +## dimos-lcm Package + +The `dimos-lcm` package provides base message types that mirror [ROS message definitions](https://docs.ros.org/en/melodic/api/sensor_msgs/html/index.html): + +```python session=lcm_demo ansi=false +from dimos_lcm.geometry_msgs import Vector3 as LCMVector3 +from dimos_lcm.sensor_msgs.PointCloud2 import PointCloud2 as LCMPointCloud2 + +# LCM messages can encode to binary +msg = LCMVector3() +msg.x, msg.y, msg.z = 1.0, 2.0, 3.0 + +binary = msg.lcm_encode() +print(f"Encoded to {len(binary)} bytes: {binary.hex()}") + +# And decode back +decoded = LCMVector3.lcm_decode(binary) +print(f"Decoded: x={decoded.x}, y={decoded.y}, z={decoded.z}") +``` + + +``` +Encoded to 24 bytes: 000000000000f03f00000000000000400000000000000840 +Decoded: x=1.0, y=2.0, z=3.0 +``` + +## Dimos Message Overlays + +Dimos subclasses the base LCM types to add Python-friendly features while preserving binary compatibility. For example, `dimos.msgs.geometry_msgs.Vector3` extends the LCM base with: + +- Multiple constructor overloads (from tuples, numpy arrays, etc.) +- Math operations (`+`, `-`, `*`, `/`, dot product, cross product) +- Conversions to numpy, quaternions, etc. + +```python session=lcm_demo ansi=false +from dimos.msgs.geometry_msgs import Vector3 + +# Rich constructors +v1 = Vector3(1, 2, 3) +v2 = Vector3([4, 5, 6]) +v3 = Vector3(v1) # copy + +# Math operations +print(f"v1 + v2 = {(v1 + v2).to_tuple()}") +print(f"v1 dot v2 = {v1.dot(v2)}") +print(f"v1 x v2 = {v1.cross(v2).to_tuple()}") +print(f"|v1| = {v1.length():.3f}") + +# Still encodes to LCM binary +binary = v1.lcm_encode() +print(f"LCM encoded: {len(binary)} bytes") +``` + + +``` +v1 + v2 = (5.0, 7.0, 9.0) +v1 dot v2 = 32.0 +v1 x v2 = (-3.0, 6.0, -3.0) +|v1| = 3.742 +LCM encoded: 24 bytes +``` + +## PointCloud2 with Open3D + +A more complex example is `PointCloud2`, which wraps Open3D point clouds while maintaining LCM binary compatibility: + +```python session=lcm_demo ansi=false +import numpy as np +from dimos.msgs.sensor_msgs import PointCloud2 + +# Create from numpy +points = np.random.rand(100, 3).astype(np.float32) +pc = PointCloud2.from_numpy(points, frame_id="camera") + +print(f"PointCloud: {len(pc)} points, frame={pc.frame_id}") +print(f"Center: {pc.center}") + +# Access as Open3D (for visualization, processing) +o3d_cloud = pc.pointcloud +print(f"Open3D type: {type(o3d_cloud).__name__}") + +# Encode to LCM binary (for transport) +binary = pc.lcm_encode() +print(f"LCM encoded: {len(binary)} bytes") + +# Decode back +pc2 = PointCloud2.lcm_decode(binary) +print(f"Decoded: {len(pc2)} points") +``` + + +``` +PointCloud: 100 points, frame=camera +Center: ↗ Vector (Vector([0.49166839, 0.50896413, 0.48393918])) +Open3D type: PointCloud +LCM encoded: 1716 bytes +Decoded: 100 points +``` + +## Transport Independence + +Since LCM messages encode to bytes, you can use them over any transport: + +```python session=lcm_demo ansi=false +from dimos.msgs.geometry_msgs import Vector3 +from dimos.protocol.pubsub.memory import Memory +from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory + +# Same message works with any transport +msg = Vector3(1, 2, 3) + +# In-memory (same process) +memory = Memory() +received = [] +memory.subscribe("velocity", lambda m, t: received.append(m)) +memory.publish("velocity", msg) +print(f"Memory transport: received {received[0]}") + +# The LCM binary can also be sent raw over any byte-oriented channel +binary = msg.lcm_encode() +# send over websocket, redis, tcp, file, etc. +decoded = Vector3.lcm_decode(binary) +print(f"Raw binary transport: decoded {decoded}") +``` + + +``` +Memory transport: received ↗ Vector (Vector([1. 2. 3.])) +Raw binary transport: decoded ↗ Vector (Vector([1. 2. 3.])) +``` + +## Available Message Types + +Dimos provides overlays for common message types: + +| Package | Messages | +|---------|----------| +| `geometry_msgs` | `Vector3`, `Quaternion`, `Pose`, `Twist`, `Transform` | +| `sensor_msgs` | `Image`, `PointCloud2`, `CameraInfo`, `LaserScan` | +| `nav_msgs` | `Odometry`, `Path`, `OccupancyGrid` | +| `vision_msgs` | `Detection2D`, `Detection3D`, `BoundingBox2D` | + +Base LCM types (without Dimos extensions) are available in `dimos_lcm.*`. + +## Creating Custom Message Types + +To create a new message type: + +1. Define the LCM message in `.lcm` format (or use existing `dimos_lcm` base) +2. Create a Python overlay that subclasses the LCM type +3. Add `lcm_encode()` and `lcm_decode()` methods if custom serialization is needed + +See [`PointCloud2.py`](/dimos/msgs/sensor_msgs/PointCloud2.py) and [`Vector3.py`](/dimos/msgs/geometry_msgs/Vector3.py) for examples. diff --git a/docs/concepts/modules.md b/docs/concepts/modules.md new file mode 100644 index 0000000000..aeaee8c9b9 --- /dev/null +++ b/docs/concepts/modules.md @@ -0,0 +1,176 @@ + +# Dimos Modules + +Modules are subsystems on a robot that operate autonomously and communicate to other subsystems using standardized messages. + +Some examples of modules are: + +- Webcam (outputs image) +- Navigation (inputs a map and a target, outputs a path) +- Detection (takes an image and a vision model like YOLO, outputs a stream of detections) + +Below is an example of a structure for controlling a robot. Black blocks represent modules and colored lines are connections and message types. It's okay if this doesn't make sense now, it will by the end of this document. + +```python output=assets/go2_nav.svg +from dimos.core.introspection import to_svg +from dimos.robot.unitree_webrtc.unitree_go2_blueprints import nav +to_svg(nav, "assets/go2_nav.svg") +``` + + +![output](assets/go2_nav.svg) + +## Camera Module + +Let's learn how to build stuff like the above, starting with a simple camera module. + +```python session=camera_module_demo output=assets/camera_module.svg +from dimos.hardware.sensors.camera.module import CameraModule +from dimos.core.introspection import to_svg +to_svg(CameraModule.module_info(), "assets/camera_module.svg") +``` + + +![output](assets/camera_module.svg) + +We can always also print out Module I/O quickly into console via `.io()` call, we will do this from now on. + +```python session=camera_module_demo ansi=false +print(CameraModule.io()) +``` + + +``` +ā”Œā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ CameraModule │ +ā””ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ā”œā”€ color_image: Image + ā”œā”€ camera_info: CameraInfo + │ + ā”œā”€ RPC set_transport(stream_name: str, transport: Transport) -> bool + ā”œā”€ RPC start() + │ + ā”œā”€ Skill video_stream (stream=passive, reducer=latest_reducer, output=image) +``` + +We can see that camera module outputs two streams: + +- `color_image` with [sensor_msgs.Image](https://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/Image.html) type +- `camera_info` with [sensor_msgs.CameraInfo](https://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/CameraInfo.html) type + +Offers two RPC calls, `start()` and `stop()` + +As well as an agentic [Skill][skills.md] called `video_stream` (more about this later, in [Skills Tutorial][skills.md]) + +We can start this module and explore the output of its streams in real time (this will use your webcam). + +```python session=camera_module_demo ansi=false +import time + +camera = CameraModule() +camera.start() +# now this module runs in our main loop in a thread. we can observe it's outputs + +print(camera.color_image) + +camera.color_image.subscribe(print) +time.sleep(0.5) +camera.stop() +``` + + +``` +Out color_image[Image] @ CameraModule +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:16) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:16) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) +Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) +``` + + +## Connecting modules + +Let's load a standard 2D detector module and hook it up to a camera. + +```python ansi=false session=detection_module +from dimos.perception.detection.module2D import Detection2DModule, Config +print(Detection2DModule.io()) +``` + + +``` + ā”œā”€ image: Image +ā”Œā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Detection2DModule │ +ā””ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ā”œā”€ detections: Detection2DArray + ā”œā”€ annotations: ImageAnnotations + ā”œā”€ detected_image_0: Image + ā”œā”€ detected_image_1: Image + ā”œā”€ detected_image_2: Image + │ + ā”œā”€ RPC set_transport(stream_name: str, transport: Transport) -> bool + ā”œā”€ RPC start() -> None + ā”œā”€ RPC stop() -> None +``` + +TODO: add easy way to print config + +looks like detector just needs an image input, outputs some sort of detection and annotation messages, let's connect it to a camera. + +```pythonx ansi=false +import time +from dimos.perception.detection.module2D import Detection2DModule, Config +from dimos.hardware.sensors.camera.module import CameraModule + +camera = CameraModule() +detector = Detection2DModule() + +detector.image.connect(camera.color_image) + +camera.start() +detector.start() + +detector.detections.subscribe(print) +time.sleep(3) +detector.stop() +camera.stop() +``` + + +``` +Detection(Person(1)) +Detection(Person(1)) +Detection(Person(1)) +Detection(Person(1)) +``` + +## Distributed Execution + +As we build module structures, very quickly we'll want to utilize all cores on the machine (which python doesn't allow as a single process), and potentially distribute modules across machines or even internet. + +For this we use `dimos.core` and dimos transport protocols. + +Defining message exchange protocol and message types also gives us an ability to write models in faster languages. + +## Blueprints + +Blueprint is a pre-defined structure of interconnected modules. You can include blueprints or modules in new blueprints + +Basic unitree go2 blueprint looks like what we saw before, + +```python session=blueprints output=assets/go2_agentic.svg +from dimos.core.introspection import to_svg +from dimos.robot.unitree_webrtc.unitree_go2_blueprints import agentic + +to_svg(agentic, "assets/go2_agentic.svg") +``` + + +![output](assets/go2_agentic.svg) diff --git a/docs/concepts/transports.md b/docs/concepts/transports.md new file mode 100644 index 0000000000..fe06334fe9 --- /dev/null +++ b/docs/concepts/transports.md @@ -0,0 +1,368 @@ + +# Dimos Transports + +Transports enable communication between [modules](modules.md) across process boundaries and networks. When modules run in different processes or on different machines, they need a transport layer to exchange messages. + +While the interface is called "PubSub", transports aren't limited to traditional pub/sub services. A topic can be anything that identifies a communication channel - an IP address and port, a shared memory segment name, a file path, or a Redis channel. The abstraction is flexible enough to support any communication pattern that can publish and subscribe to named channels. + +## The PubSub Interface + +At the core of all transports is the `PubSub` abstract class. Any transport implementation must provide two methods: + +```python session=pubsub_demo ansi=false +from dimos.protocol.pubsub.spec import PubSub + +# The interface every transport must implement: +import inspect +print(inspect.getsource(PubSub.publish)) +print(inspect.getsource(PubSub.subscribe)) +``` + + +``` +Session process exited unexpectedly: +/home/lesh/coding/dimos/.venv/bin/python3: No module named md_babel_py.session_server + +``` + +Key points: +- `publish(topic, message)` - Send a message to all subscribers on a topic +- `subscribe(topic, callback)` - Register a callback, returns an unsubscribe function + +## Implementing a Simple Transport + +The simplest transport is `Memory`, which works within a single process: + +```python session=memory_demo ansi=false +from dimos.protocol.pubsub.memory import Memory + +# Create a memory transport +bus = Memory() + +# Track received messages +received = [] + +# Subscribe to a topic +unsubscribe = bus.subscribe("sensor/data", lambda msg, topic: received.append(msg)) + +# Publish messages +bus.publish("sensor/data", {"temperature": 22.5}) +bus.publish("sensor/data", {"temperature": 23.0}) + +print(f"Received {len(received)} messages:") +for msg in received: + print(f" {msg}") + +# Unsubscribe when done +unsubscribe() +``` + + +``` +Received 2 messages: + {'temperature': 22.5} + {'temperature': 23.0} +``` + +The full implementation is minimal - see [`memory.py`](/dimos/protocol/pubsub/memory.py) for the complete source. + +## Available Transports + +Dimos includes several transport implementations: + +| Transport | Use Case | Process Boundary | Network | +|-----------|----------|------------------|---------| +| `Memory` | Testing, single process | No | No | +| `SharedMemory` | Multi-process on same machine | Yes | No | +| `LCM` | Network communication (UDP multicast) | Yes | Yes | +| `Redis` | Network communication via Redis server | Yes | Yes | + +### SharedMemory Transport + +For inter-process communication on the same machine, `SharedMemory` provides high-performance message passing: + +```python session=shm_demo ansi=false +from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory + +shm = PickleSharedMemory(prefer="cpu") +shm.start() + +received = [] +shm.subscribe("test/topic", lambda msg, topic: received.append(msg)) +shm.publish("test/topic", {"data": [1, 2, 3]}) + +import time +time.sleep(0.1) # Allow message to propagate + +print(f"Received: {received}") +shm.stop() +``` + + +``` +Received: [{'data': [1, 2, 3]}] +``` + +### LCM Transport + +For network communication, LCM uses UDP multicast and supports typed messages: + +```python session=lcm_demo ansi=false +from dimos.protocol.pubsub.lcmpubsub import LCM, Topic +from dimos.msgs.geometry_msgs import Vector3 + +lcm = LCM(autoconf=True) +lcm.start() + +received = [] +topic = Topic(topic="/robot/velocity", lcm_type=Vector3) + +lcm.subscribe(topic, lambda msg, t: received.append(msg)) +lcm.publish(topic, Vector3(1.0, 0.0, 0.5)) + +import time +time.sleep(0.1) + +print(f"Received velocity: x={received[0].x}, y={received[0].y}, z={received[0].z}") +lcm.stop() +``` + + +``` +Received velocity: x=1.0, y=0.0, z=0.5 +``` + +## Encoder Mixins + +Transports can use encoder mixins to serialize messages. The `PubSubEncoderMixin` pattern wraps publish/subscribe to encode/decode automatically: + +```python session=encoder_demo ansi=false +from dimos.protocol.pubsub.spec import PubSubEncoderMixin, PickleEncoderMixin + +# PickleEncoderMixin provides: +# - encode(msg, topic) -> bytes (uses pickle.dumps) +# - decode(bytes, topic) -> msg (uses pickle.loads) + +# Create a transport with pickle encoding by mixing in: +from dimos.protocol.pubsub.memory import Memory + +class PickleMemory(PickleEncoderMixin, Memory): + pass + +bus = PickleMemory() +received = [] +bus.subscribe("data", lambda msg, t: received.append(msg)) +bus.publish("data", {"complex": [1, 2, 3], "nested": {"key": "value"}}) + +print(f"Received: {received[0]}") +``` + + +``` +Received: {'complex': [1, 2, 3], 'nested': {'key': 'value'}} +``` + +## Using Transports with Modules + +Modules use the `Transport` wrapper class which adapts `PubSub` to the stream interface. You can set a transport on any module stream: + +```python session=module_transport ansi=false +from dimos.core.transport import pLCMTransport, pSHMTransport + +# Transport wrappers for module streams: +# - pLCMTransport: Pickle-encoded LCM +# - LCMTransport: Native LCM encoding +# - pSHMTransport: Pickle-encoded SharedMemory +# - SHMTransport: Native SharedMemory +# - JpegShmTransport: JPEG-compressed images via SharedMemory +# - JpegLcmTransport: JPEG-compressed images via LCM + +# Example: Set a transport on a module output +# camera.set_transport("color_image", pSHMTransport("camera/color")) +print("Available transport wrappers in dimos.core.transport:") +from dimos.core import transport +print([name for name in dir(transport) if "Transport" in name]) +``` + + +``` +Available transport wrappers in dimos.core.transport: +['JpegLcmTransport', 'JpegShmTransport', 'LCMTransport', 'PubSubTransport', 'SHMTransport', 'ZenohTransport', 'pLCMTransport', 'pSHMTransport'] +``` + +## Testing Custom Transports + +The test suite in [`pubsub/test_spec.py`](/dimos/protocol/pubsub/test_spec.py) uses pytest parametrization to run the same tests against all transport implementations. To add your custom transport to the test grid: + +```python session=test_grid ansi=false +# The test grid pattern from test_spec.py: +test_pattern = """ +from contextlib import contextmanager + +@contextmanager +def my_transport_context(): + transport = MyCustomTransport() + transport.start() + yield transport + transport.stop() + +# Add to testdata list: +testdata.append( + (my_transport_context, "my_topic", ["value1", "value2", "value3"]) +) +""" +print(test_pattern) +``` + + +``` + +from contextlib import contextmanager + +@contextmanager +def my_transport_context(): + transport = MyCustomTransport() + transport.start() + yield transport + transport.stop() + +# Add to testdata list: +testdata.append( + (my_transport_context, "my_topic", ["value1", "value2", "value3"]) +) + +``` + +The test suite validates: +- Basic publish/subscribe +- Multiple subscribers receiving the same message +- Unsubscribe functionality +- Multiple messages in order +- Async iteration +- High-volume message handling (10,000 messages) + +Run the tests with: +```bash +pytest dimos/protocol/pubsub/test_spec.py -v +``` + +## Creating a Custom Transport + +To implement a new transport: + +1. **Subclass `PubSub`** and implement `publish()` and `subscribe()` +2. **Add encoding** if needed via `PubSubEncoderMixin` +3. **Create a `Transport` wrapper** by subclassing `PubSubTransport` +4. **Add to the test grid** in `test_spec.py` + +Here's a minimal template: + +```python session=custom_transport ansi=false +template = ''' +from dimos.protocol.pubsub.spec import PubSub, PickleEncoderMixin +from dimos.core.transport import PubSubTransport + +class MyPubSub(PubSub[str, bytes]): + """Custom pub/sub implementation.""" + + def __init__(self): + self._subscribers = {} + + def start(self): + # Initialize connection/resources + pass + + def stop(self): + # Cleanup + pass + + def publish(self, topic: str, message: bytes) -> None: + # Send message to all subscribers on topic + for cb in self._subscribers.get(topic, []): + cb(message, topic) + + def subscribe(self, topic, callback): + # Register callback, return unsubscribe function + if topic not in self._subscribers: + self._subscribers[topic] = [] + self._subscribers[topic].append(callback) + + def unsubscribe(): + self._subscribers[topic].remove(callback) + return unsubscribe + + +# With pickle encoding +class MyPicklePubSub(PickleEncoderMixin, MyPubSub): + pass + + +# Transport wrapper for use with modules +class MyTransport(PubSubTransport): + def __init__(self, topic: str): + super().__init__(topic) + self.pubsub = MyPicklePubSub() + + def broadcast(self, _, msg): + self.pubsub.publish(self.topic, msg) + + def subscribe(self, callback, selfstream=None): + return self.pubsub.subscribe(self.topic, lambda msg, t: callback(msg)) +''' +print(template) +``` + + +``` + +from dimos.protocol.pubsub.spec import PubSub, PickleEncoderMixin +from dimos.core.transport import PubSubTransport + +class MyPubSub(PubSub[str, bytes]): + """Custom pub/sub implementation.""" + + def __init__(self): + self._subscribers = {} + + def start(self): + # Initialize connection/resources + pass + + def stop(self): + # Cleanup + pass + + def publish(self, topic: str, message: bytes) -> None: + # Send message to all subscribers on topic + for cb in self._subscribers.get(topic, []): + cb(message, topic) + + def subscribe(self, topic, callback): + # Register callback, return unsubscribe function + if topic not in self._subscribers: + self._subscribers[topic] = [] + self._subscribers[topic].append(callback) + + def unsubscribe(): + self._subscribers[topic].remove(callback) + return unsubscribe + + +# With pickle encoding +class MyPicklePubSub(PickleEncoderMixin, MyPubSub): + pass + + +# Transport wrapper for use with modules +class MyTransport(PubSubTransport): + def __init__(self, topic: str): + super().__init__(topic) + self.pubsub = MyPicklePubSub() + + def broadcast(self, _, msg): + self.pubsub.publish(self.topic, msg) + + def subscribe(self, callback, selfstream=None): + return self.pubsub.subscribe(self.topic, lambda msg, t: callback(msg)) + +``` diff --git a/docs/data.md b/docs/data.md new file mode 100644 index 0000000000..34313098f9 --- /dev/null +++ b/docs/data.md @@ -0,0 +1,206 @@ +# Data Loading + +The [`get_data`](/dimos/utils/data.py) function provides access to test data and model files, handling Git LFS downloads automatically. + +## Basic Usage + +```python +from dimos.utils.data import get_data + +# Get path to a data file/directory +data_path = get_data("cafe.jpg") +print(f"Path: {data_path}") +print(f"Exists: {data_path.exists()}") +``` + + +``` +Path: /home/lesh/coding/dimos/data/cafe.jpg +Exists: True +``` + +## How It Works + +
Pikchr + +```pikchr fold output=assets/get_data_flow.svg +color = white +fill = none + +A: box "get_data(name)" rad 5px fit wid 170% ht 170% +arrow right 0.4in +B: box "Check" "data/{name}" rad 5px fit wid 170% ht 170% + +# Branch: exists +arrow from B.e right 0.3in then up 0.4in then right 0.3in +C: box "Return path" rad 5px fit wid 170% ht 170% + +# Branch: missing +arrow from B.e right 0.3in then down 0.4in then right 0.3in +D: box "Pull LFS" rad 5px fit wid 170% ht 170% +arrow right 0.3in +E: box "Decompress" rad 5px fit wid 170% ht 170% +arrow right 0.3in +F: box "Return path" rad 5px fit wid 170% ht 170% +``` + +
+ + +![output](assets/get_data_flow.svg) + +1. Checks if `data/{name}` already exists locally +2. If missing, pulls the `.tar.gz` archive from Git LFS +3. Decompresses the archive to `data/` +4. Returns the `Path` to the extracted file/directory + +## Common Patterns + +### Loading Images + +```python +from dimos.utils.data import get_data +from dimos.msgs.sensor_msgs import Image + +image = Image.from_file(get_data("cafe.jpg")) +print(f"Image shape: {image.data.shape}") +``` + + +``` +Image shape: (771, 1024, 3) +``` + +### Loading Model Checkpoints + +```python +from dimos.utils.data import get_data + +model_dir = get_data("models_yolo") +checkpoint = model_dir / "yolo11n.pt" +print(f"Checkpoint: {checkpoint.name} ({checkpoint.stat().st_size // 1024}KB)") +``` + + +``` +Checkpoint: yolo11n.pt (5482KB) +``` + +### Loading Recorded Data for Replay + +```python +from dimos.utils.data import get_data +from dimos.utils.testing.replay import TimedSensorReplay + +data_dir = get_data("unitree_office_walk") +replay = TimedSensorReplay(data_dir / "lidar") +print(f"Replay {replay} loaded from: {data_dir.name}") +print(replay.find_closest_seek(1)) +``` + + +``` +Replay loaded from: unitree_office_walk +{'type': 'msg', 'topic': 'rt/utlidar/voxel_map_compressed', 'data': {'stamp': 1751591000.0, 'frame_id': 'odom', 'resolution': 0.05, 'src_size': 77824, 'origin': [-3.625, -3.275, -0.575], 'width': [128, 128, 38], 'data': {'points': array([[ 2.725, -1.025, -0.575], + [ 2.525, -0.275, -0.575], + [ 2.575, -0.275, -0.575], + ..., + [ 2.675, -0.525, 0.775], + [ 2.375, 1.175, 0.775], + [ 2.325, 1.225, 0.775]], shape=(22730, 3))}}} +``` + +### Loading Point Clouds + +```python +from dimos.utils.data import get_data +from dimos.mapping.pointclouds.util import read_pointcloud + +pointcloud = read_pointcloud(get_data("apartment") / "sum.ply") +print(f"Loaded pointcloud with {len(pointcloud.points)} points") +``` + + +``` +Loaded pointcloud with 63672 points +``` + +## Data Directory Structure + +Data files live in `data/` at the repo root. Large files are stored in `data/.lfs/` as `.tar.gz` archives tracked by Git LFS. + +
Diagon + +```diagon fold mode=Tree +data/ + cafe.jpg + apartment/ + sum.ply + .lfs/ + cafe.jpg.tar.gz + apartment.tar.gz +``` + +
+ + +``` +data/ + ā”œā”€ā”€cafe.jpg + ā”œā”€ā”€apartment/ + │ └──sum.ply + └──.lfs/ + ā”œā”€ā”€cafe.jpg.tar.gz + └──apartment.tar.gz +``` + + +## Adding New Data + +### Small Files (< 1MB) + +Commit directly to `data/`: + +```sh skip +cp my_image.jpg data/ + +# 2. Compress and upload to LFS +./bin/lfs_push + +git add data/.lfs/my_image.jpg.tar.gz + +git commit -m "Add test image" +``` + +### Large Files or Directories + +Use the LFS workflow: + +```sh skip +# 1. Copy data to data/ +cp -r my_dataset/ data/ + +# 2. Compress and upload to LFS +./bin/lfs_push + +git add data/.lfs/my_dataset.tar.gz + +# 3. Commit the .tar.gz reference +git commit -m "Add my_dataset test data" +``` + +The [`lfs_push`](/bin/lfs_push) script: +1. Compresses `data/my_dataset/` → `data/.lfs/my_dataset.tar.gz` +2. Uploads to Git LFS +3. Stages the compressed file + +A pre-commit hook ([`bin/hooks/lfs_check`](/bin/hooks/lfs_check#L26)) blocks commits if you have uncompressed directories in `data/` without a corresponding `.tar.gz` in `data/.lfs/`. + +## Location Resolution + +When running from: +- **Git repo**: Uses `{repo}/data/` +- **Installed package**: Clones repo to user data dir: + - Linux: `~/.local/share/dimos/repo/data/` + - macOS: `~/Library/Application Support/dimos/repo/data/` + - Fallback: `/tmp/dimos/repo/data/` diff --git a/docs/development.md b/docs/development.md index 8718144642..838bae6fdb 100644 --- a/docs/development.md +++ b/docs/development.md @@ -22,7 +22,7 @@ Install the *Dev Containers* plug-in for VS Code, Cursor, or your IDE of choice ### Shell only quick start -Terminal within your IDE should use devcontainer transparently given you installed the plugin, but in case you want to run our shell without an IDE, you can use `./bin/dev` +Terminal within your IDE should use devcontainer transparently given you installed the plugin, but in case you want to run our shell without an IDE, you can use `./bin/dev` (it depends on npm/node being installed) ```sh @@ -37,16 +37,16 @@ found 0 vulnerabilities [5299 ms] f0355b6574d9bd277d6eb613e1dc32e3bc18e7493e5b170e335d0e403578bcdb {"outcome":"success","containerId":"f0355b6574d9bd277d6eb613e1dc32e3bc18e7493e5b170e335d0e403578bcdb","remoteUser":"root","remoteWorkspaceFolder":"/workspaces/dimos"} - ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— - ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ - ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ - ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ + ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— + ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ + ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ + ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā• v_unknown:unknown | Wed May 28 09:23:33 PM UTC 2025 -root@dimos:/workspaces/dimos # +root@dimos:/workspaces/dimos # ``` The script will: @@ -58,7 +58,7 @@ You’ll land in the workspace as **root** with all project tooling available. ## Pre-Commit Hooks -We use [pre-commit](https://pre-commit.com) (config in `.pre-commit-config.yaml`) to enforce formatting, licence headers, EOLs, LFS checks, etc. Hooks run in **milliseconds**. +We use [pre-commit](https://pre-commit.com) (config in `.pre-commit-config.yaml`) to enforce formatting, licence headers, EOLs, LFS checks, etc. Hooks run in **milliseconds**. Hooks also run in CI; any auto-fixes are committed back to your PR, so local installation is optional — but gives faster feedback. ```sh @@ -74,11 +74,11 @@ format json..............................................................Passed LFS data.................................................................Passed ``` -Given your editor uses ruff via devcontainers (which it should) actual auto-commit hook won't ever reformat your code - IDE will have already done this. +Given your editor uses ruff via devcontainers (which it should) actual auto-commit hook won't ever reformat your code - IDE will have already done this. ### Running hooks manually -Given your editor uses git via devcontainers (which it should) auto-commit hooks will run automatically, this is in case you want to run them manually. +Given your editor uses git via devcontainers (which it should) auto-commit hooks will run automatically, this is in case you want to run them manually. Inside the dev container (Your IDE will likely run this transparently for each commit if using devcontainer plugin): @@ -140,7 +140,7 @@ Classic development run within a subtree: root@dimos:/workspaces/dimos # cd dimos/robot/unitree_webrtc/ root@dimos:/workspaces/dimos/dimos/robot/unitree_webrtc # pytest -collected 27 items / 22 deselected / 5 selected +collected 27 items / 22 deselected / 5 selected type/test_map.py::test_robot_mapping PASSED type/test_timeseries.py::test_repr PASSED @@ -155,13 +155,13 @@ Showing prints: ```sh root@dimos:/workspaces/dimos/dimos/robot/unitree_webrtc/type # pytest -s test_odometry.py test_odometry.py::test_odometry_conversion_and_count Odom ts(2025-05-30 13:52:03) pos(→ Vector Vector([0.432199 0.108042 0.316589])), rot(↑ Vector Vector([ 7.7200000e-04 -9.1280000e-03 3.006 -8621e+00])) yaw(172.3°) -Odom ts(2025-05-30 13:52:03) pos(→ Vector Vector([0.433629 0.105965 0.316143])), rot(↑ Vector Vector([ 0.003814 -0.006436 2.99591235])) yaw(171.7°) -Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.434459 0.104739 0.314794])), rot(↗ Vector Vector([ 0.005558 -0.004183 3.00068456])) yaw(171.9°) -Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.435621 0.101699 0.315852])), rot(↑ Vector Vector([ 0.005391 -0.006002 3.00246893])) yaw(172.0°) -Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.436457 0.09857 0.315254])), rot(↑ Vector Vector([ 0.003358 -0.006916 3.00347172])) yaw(172.1°) +8621e+00])) yaw(172.3°) +Odom ts(2025-05-30 13:52:03) pos(→ Vector Vector([0.433629 0.105965 0.316143])), rot(↑ Vector Vector([ 0.003814 -0.006436 2.99591235])) yaw(171.7°) +Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.434459 0.104739 0.314794])), rot(↗ Vector Vector([ 0.005558 -0.004183 3.00068456])) yaw(171.9°) +Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.435621 0.101699 0.315852])), rot(↑ Vector Vector([ 0.005391 -0.006002 3.00246893])) yaw(172.0°) +Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.436457 0.09857 0.315254])), rot(↑ Vector Vector([ 0.003358 -0.006916 3.00347172])) yaw(172.1°) Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.435535 0.097022 0.314399])), rot(↑ Vector Vector([ 1.88300000e-03 -8.17800000e-03 3.00573432e+00])) yaw(172.2°) -Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.433739 0.097553 0.313479])), rot(↑ Vector Vector([ 8.10000000e-05 -8.71700000e-03 3.00729616e+00])) yaw(172.3°) +Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.433739 0.097553 0.313479])), rot(↑ Vector Vector([ 8.10000000e-05 -8.71700000e-03 3.00729616e+00])) yaw(172.3°) Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.430924 0.09859 0.31322 ])), rot(↑ Vector Vector([ 1.84000000e-04 -9.68700000e-03 3.00945623e+00])) yaw(172.4°) ... etc ``` @@ -178,5 +178,3 @@ Odom ts(2025-05-30 13:52:04) pos(→ Vector Vector([0.430924 0.09859 0.31322 ]) | Filter tests by name | `pytest -k ""` | | Enable stdout in tests | `pytest -s` | | Run tagged tests | `pytest -m ` | - - diff --git a/docs/modules.md b/docs/modules.md deleted file mode 100644 index 8ce6d0f5f8..0000000000 --- a/docs/modules.md +++ /dev/null @@ -1,167 +0,0 @@ -# Dimensional Modules - -The DimOS Module system enables distributed, multiprocess robotics applications using Dask for compute distribution and LCM (Lightweight Communications and Marshalling) for high-performance IPC. - -## Core Concepts - -### 1. Module Definition -Modules are Python classes that inherit from `dimos.core.Module` and define inputs, outputs, and RPC methods: - -```python -from dimos.core import Module, In, Out, rpc -from dimos.msgs.geometry_msgs import Vector3 - -class MyModule(Module): - # Declare inputs/outputs as class attributes initialized to None - data_in: In[Vector3] = None - data_out: Out[Vector3] = None - - def __init__(): - # Call parent Module init - super().__init__() - - @rpc - def remote_method(self, param): - """Methods decorated with @rpc can be called remotely""" - return param * 2 -``` - -### 2. Module Deployment -Modules are deployed across Dask workers using the `dimos.deploy()` method: - -```python -from dimos import core - -# Start Dask cluster with N workers -dimos = core.start(4) - -# Deploying modules allows for passing initialization parameters. -# In this case param1 and param2 are passed into Module init -module = dimos.deploy(Module, param1="value1", param2=123) -``` - -### 3. Stream Connections -Modules communicate via reactive streams using LCM transport: - -```python -# Configure LCM transport for outputs -module1.data_out.transport = core.LCMTransport("/topic_name", MessageType) - -# Connect module inputs to outputs -module2.data_in.connect(module1.data_out) - -# Access the underlying Observable stream -stream = module1.data_out.observable() -stream.subscribe(lambda msg: print(f"Received: {msg}")) -``` - -### 4. Module Lifecycle -```python -# Start modules to begin processing -module.start() # Calls the @rpc start() method if defined - -# Inspect module I/O configuration -print(module.io().result()) # Shows inputs, outputs, and RPC methods - -# Clean shutdown -dimos.shutdown() -``` - -## Real-World Example: Robot Control System - -```python -# Connection module wraps robot hardware/simulation -connection = dimos.deploy(ConnectionModule, ip=robot_ip) -connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) -connection.video.transport = core.LCMTransport("/video", Image) - -# Perception module processes sensor data -perception = dimos.deploy(PersonTrackingStream, camera_intrinsics=[...]) -perception.video.connect(connection.video) -perception.tracking_data.transport = core.pLCMTransport("/person_tracking") - -# Start processing -connection.start() -perception.start() - -# Enable tracking via RPC -perception.enable_tracking() - -# Get latest tracking data -data = perception.get_tracking_data() -``` - -## LCM Transport Configuration - -```python -# Standard LCM transport for simple types like lidar -connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) - -# Pickle-based transport for complex Python objects / dictionaries -connection.tracking_data.transport = core.pLCMTransport("/person_tracking") - -# Auto-configure LCM system buffers (required in containers) -from dimos.protocol import pubsub -pubsub.lcm.autoconf() -``` - -This architecture enables building complex robotic systems as composable, distributed modules that communicate efficiently via streams and RPC, scaling from single machines to clusters. - -# Dimensional Install -## Python Installation (Ubuntu 22.04) - -```bash -sudo apt install python3-venv - -# Clone the repository (dev branch, no submodules) -git clone -b dev https://github.com/dimensionalOS/dimos.git -cd dimos - -# Create and activate virtual environment -python3 -m venv venv -source venv/bin/activate - -sudo apt install portaudio19-dev python3-pyaudio - -# Install torch and torchvision if not already installed -# Example CUDA 11.7, Pytorch 2.0.1 (replace with your required pytorch version if different) -pip install torch==2.0.1 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 -``` - -### Install dependencies -```bash -# CPU only (reccomended to attempt first) -pip install .[cpu,dev] - -# CUDA install -pip install .[cuda,dev] - -# Copy and configure environment variables -cp default.env .env -``` - -### Test install -```bash -# Run standard tests -pytest -s dimos/ - -# Test modules functionality -pytest -s -m module dimos/ - -# Test LCM communication -pytest -s -m lcm dimos/ -``` - -# Unitree Go2 Quickstart - -To quickly test the modules system, you can run the Unitree Go2 multiprocess example directly: - -```bash -# Make sure you have the required environment variables set -export ROBOT_IP= - -# Run the multiprocess Unitree Go2 example -python dimos/robot/unitree_webrtc/multiprocess/unitree_go2.py -``` - - diff --git a/docs/ci.md b/docs/old/ci.md similarity index 99% rename from docs/ci.md rename to docs/old/ci.md index a041ab08cc..ac9b11115a 100644 --- a/docs/ci.md +++ b/docs/old/ci.md @@ -50,7 +50,7 @@ pytest # run tests ### Current hierarchy - + ``` ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā” │ubuntu│ @@ -64,7 +64,7 @@ pytest # run tests ā”Œā–½ā”€ā”€ā”€ā”€ā”€ā”€ā” │ros-dev│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ -``` +``` * ghcr.io/dimensionalos/ros:dev * ghcr.io/dimensionalos/python:dev @@ -74,7 +74,7 @@ pytest # run tests > **Note**: The diagram shows only currently active images; the system is extensible—new combinations are possible, builds can be run per branch and as parallel as possible - + ``` ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā” │ubuntu│ @@ -135,7 +135,7 @@ Ideally a child job (e.g. **ros-python**) should depend on both: GitHub Actions can’t express ā€œrun only if *both* conditions are true *and* the parent job wasn’t skippedā€. We are using `needs: [check-changes, ros]` to ensure the job runs after the ros build, but if ros build has been skipped we need `if: always()` to ensure that the build runs anyway. -Adding `always` for some reason completely breaks the conditional check, we cannot have OR, AND operators, it just makes the job _always_ run, which means we build python even if we don't need to. +Adding `always` for some reason completely breaks the conditional check, we cannot have OR, AND operators, it just makes the job _always_ run, which means we build python even if we don't need to. This is unfortunate as the build takes ~30 min first time (a few minutes afterwards thanks to caching) and I've spent a lot of time on this, lots of viable seeming options didn't pan out and probably we need to completely rewrite and own the actions runner and not depend on github structure at all. Single job called `CI` or something, within our custom docker image. diff --git a/docs/jetson.MD b/docs/old/jetson.MD similarity index 87% rename from docs/jetson.MD rename to docs/old/jetson.MD index 31da4225d9..a4d06e3255 100644 --- a/docs/jetson.MD +++ b/docs/old/jetson.MD @@ -1,8 +1,8 @@ -# DimOS Jetson Setup Instructions -Tested on Jetpack 6.2, CUDA 12.6 +# DimOS Jetson Setup Instructions +Tested on Jetpack 6.2, CUDA 12.6 -## Required system dependencies -`sudo apt install portaudio19-dev python3-pyaudio` +## Required system dependencies +`sudo apt install portaudio19-dev python3-pyaudio` ## Installing cuSPARSELt https://ninjalabo.ai/blogs/jetson_pytorch.html @@ -17,7 +17,7 @@ ldconfig ``` ## Install Torch and Torchvision wheels -Enter virtualenv +Enter virtualenv ```bash python3 -m venv venv source venv/bin/activate @@ -26,19 +26,19 @@ source venv/bin/activate Wheels for jp6/cu126 https://pypi.jetson-ai-lab.io/jp6/cu126 -Check compatibility: +Check compatibility: https://docs.nvidia.com/deeplearning/frameworks/install-pytorch-jetson-platform-release-notes/pytorch-jetson-rel.html ### Working torch wheel tested on Jetpack 6.2, CUDA 12.6 `pip install --no-cache https://developer.download.nvidia.com/compute/redist/jp/v61/pytorch/torch-2.5.0a0+872d972e41.nv24.08.17622132-cp310-cp310-linux_aarch64.whl` -### Install torchvision from source: +### Install torchvision from source: ```bash -# Set version by checking above torchvision<-->torch compatibility +# Set version by checking above torchvision<-->torch compatibility # We use 0.20.0 export VERSION=20 - + sudo apt-get install libjpeg-dev zlib1g-dev libpython3-dev libopenblas-dev libavcodec-dev libavformat-dev libswscale-dev git clone --branch release/0.$VERSION https://github.com/pytorch/vision torchvision cd torchvision @@ -46,7 +46,7 @@ export BUILD_VERSION=0.$VERSION.0 python3 setup.py install --user # remove --user if installing in virtualenv ``` -### Verify success: +### Verify success: ```bash $ python3 import torch @@ -65,8 +65,8 @@ import torchvision print(torchvision.__version__) ``` -## Install Onnxruntime-gpu +## Install Onnxruntime-gpu Find pre-build wheels here for your specific JP/CUDA version: https://pypi.jetson-ai-lab.io/jp6 -`pip install https://pypi.jetson-ai-lab.io/jp6/cu126/+f/4eb/e6a8902dc7708/onnxruntime_gpu-1.23.0-cp310-cp310-linux_aarch64.whl#sha256=4ebe6a8902dc7708434b2e1541b3fe629ebf434e16ab5537d1d6a622b42c622b` +`pip install https://pypi.jetson-ai-lab.io/jp6/cu126/+f/4eb/e6a8902dc7708/onnxruntime_gpu-1.23.0-cp310-cp310-linux_aarch64.whl#sha256=4ebe6a8902dc7708434b2e1541b3fe629ebf434e16ab5537d1d6a622b42c622b` diff --git a/docs/old/modules.md b/docs/old/modules.md new file mode 100644 index 0000000000..9cdbf586ac --- /dev/null +++ b/docs/old/modules.md @@ -0,0 +1,165 @@ +# Dimensional Modules + +The DimOS Module system enables distributed, multiprocess robotics applications using Dask for compute distribution and LCM (Lightweight Communications and Marshalling) for high-performance IPC. + +## Core Concepts + +### 1. Module Definition +Modules are Python classes that inherit from `dimos.core.Module` and define inputs, outputs, and RPC methods: + +```python +from dimos.core import Module, In, Out, rpc +from dimos.msgs.geometry_msgs import Vector3 + +class MyModule(Module): + # Declare inputs/outputs as class attributes initialized to None + data_in: In[Vector3] = None + data_out: Out[Vector3] = None + + def __init__(): + # Call parent Module init + super().__init__() + + @rpc + def remote_method(self, param): + """Methods decorated with @rpc can be called remotely""" + return param * 2 +``` + +### 2. Module Deployment +Modules are deployed across Dask workers using the `dimos.deploy()` method: + +```python +from dimos import core + +# Start Dask cluster with N workers +dimos = core.start(4) + +# Deploying modules allows for passing initialization parameters. +# In this case param1 and param2 are passed into Module init +module = dimos.deploy(Module, param1="value1", param2=123) +``` + +### 3. Stream Connections +Modules communicate via reactive streams using LCM transport: + +```python +# Configure LCM transport for outputs +module1.data_out.transport = core.LCMTransport("/topic_name", MessageType) + +# Connect module inputs to outputs +module2.data_in.connect(module1.data_out) + +# Access the underlying Observable stream +stream = module1.data_out.observable() +stream.subscribe(lambda msg: print(f"Received: {msg}")) +``` + +### 4. Module Lifecycle +```python +# Start modules to begin processing +module.start() # Calls the @rpc start() method if defined + +# Inspect module I/O configuration +print(module.io().result()) # Shows inputs, outputs, and RPC methods + +# Clean shutdown +dimos.shutdown() +``` + +## Real-World Example: Robot Control System + +```python +# Connection module wraps robot hardware/simulation +connection = dimos.deploy(ConnectionModule, ip=robot_ip) +connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) +connection.video.transport = core.LCMTransport("/video", Image) + +# Perception module processes sensor data +perception = dimos.deploy(PersonTrackingStream, camera_intrinsics=[...]) +perception.video.connect(connection.video) +perception.tracking_data.transport = core.pLCMTransport("/person_tracking") + +# Start processing +connection.start() +perception.start() + +# Enable tracking via RPC +perception.enable_tracking() + +# Get latest tracking data +data = perception.get_tracking_data() +``` + +## LCM Transport Configuration + +```python +# Standard LCM transport for simple types like lidar +connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) + +# Pickle-based transport for complex Python objects / dictionaries +connection.tracking_data.transport = core.pLCMTransport("/person_tracking") + +# Auto-configure LCM system buffers (required in containers) +from dimos.protocol import pubsub +pubsub.lcm.autoconf() +``` + +This architecture enables building complex robotic systems as composable, distributed modules that communicate efficiently via streams and RPC, scaling from single machines to clusters. + +# Dimensional Install +## Python Installation (Ubuntu 22.04) + +```bash +sudo apt install python3-venv + +# Clone the repository (dev branch, no submodules) +git clone -b dev https://github.com/dimensionalOS/dimos.git +cd dimos + +# Create and activate virtual environment +python3 -m venv venv +source venv/bin/activate + +sudo apt install portaudio19-dev python3-pyaudio + +# Install torch and torchvision if not already installed +# Example CUDA 11.7, Pytorch 2.0.1 (replace with your required pytorch version if different) +pip install torch==2.0.1 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 +``` + +### Install dependencies +```bash +# CPU only (reccomended to attempt first) +pip install .[cpu,dev] + +# CUDA install +pip install .[cuda,dev] + +# Copy and configure environment variables +cp default.env .env +``` + +### Test install +```bash +# Run standard tests +pytest -s dimos/ + +# Test modules functionality +pytest -s -m module dimos/ + +# Test LCM communication +pytest -s -m lcm dimos/ +``` + +# Unitree Go2 Quickstart + +To quickly test the modules system, you can run the Unitree Go2 multiprocess example directly: + +```bash +# Make sure you have the required environment variables set +export ROBOT_IP= + +# Run the multiprocess Unitree Go2 example +python dimos/robot/unitree_webrtc/multiprocess/unitree_go2.py +``` diff --git a/docs/modules_CN.md b/docs/old/modules_CN.md similarity index 98% rename from docs/modules_CN.md rename to docs/old/modules_CN.md index d8f088ef59..89e16c7112 100644 --- a/docs/modules_CN.md +++ b/docs/old/modules_CN.md @@ -60,7 +60,7 @@ stream.subscribe(lambda msg: print(f"ęŽ„ę”¶åˆ°: {msg}")) # åÆåŠØęØ”å—ä»„å¼€å§‹å¤„ē† module.start() # å¦‚ęžœå®šä¹‰äŗ† @rpc start() ę–¹ę³•ļ¼Œåˆ™č°ƒē”Øå®ƒ -# ę£€ęŸ„ęØ”å— I/O é…ē½® +# ę£€ęŸ„ęØ”å— I/O é…ē½® print(module.io().result()) # ę˜¾ē¤ŗč¾“å…„ć€č¾“å‡ŗå’Œ RPC 方法 # 优雅关闭 @@ -141,7 +141,7 @@ cp default.env .env ``` ### 测试安装 -```bash +```bash # čæč”Œę ‡å‡†ęµ‹čÆ• pytest -s dimos/ @@ -185,4 +185,4 @@ LCM ä¼ č¾“é’ˆåÆ¹ęœŗå™Øäŗŗåŗ”ē”Øčæ›č”Œäŗ†ä¼˜åŒ–ļ¼š - **é›¶ę‹·č“**ļ¼šå¤§åž‹ę¶ˆęÆēš„é«˜ę•ˆå†…å­˜ä½æē”Ø - **ä½Žå»¶čæŸ**ļ¼šå¾®ē§’ēŗ§ēš„ę¶ˆęÆä¼ é€’ -- **å¤šę’­ę”ÆęŒ**ļ¼šäø€åÆ¹å¤šēš„é«˜ę•ˆé€šäæ” \ No newline at end of file +- **å¤šę’­ę”ÆęŒ**ļ¼šäø€åÆ¹å¤šēš„é«˜ę•ˆé€šäæ” diff --git a/docs/old/ros_navigation.md b/docs/old/ros_navigation.md new file mode 100644 index 0000000000..4a74500b2f --- /dev/null +++ b/docs/old/ros_navigation.md @@ -0,0 +1,284 @@ +# Autonomy Stack API Documentation + +## Prerequisites + +- Ubuntu 24.04 +- [ROS 2 Jazzy Installation](https://docs.ros.org/en/jazzy/Installation.html) + +Add the following line to your `~/.bashrc` to source the ROS 2 Jazzy setup script automatically: + +``` echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc``` + +## MID360 Ethernet Configuration (skip for sim) + +### Step 1: Configure Network Interface + +1. Open Network Settings in Ubuntu +2. Find your Ethernet connection to the MID360 +3. Click the gear icon to edit settings +4. Go to IPv4 tab +5. Change Method from "Automatic (DHCP)" to "Manual" +6. Add the following settings: + - **Address**: 192.168.1.5 + - **Netmask**: 255.255.255.0 + - **Gateway**: 192.168.1.1 +7. Click "Apply" + +### Step 2: Configure MID360 IP in JSON + +1. Find your MID360 serial number (on sticker under QR code) +2. Note the last 2 digits (e.g., if serial ends in 89, use 189) +3. Edit the configuration file: + +```bash +cd ~/autonomy_stack_mecanum_wheel_platform +nano src/utilities/livox_ros_driver2/config/MID360_config.json +``` + +4. Update line 28 with your IP (192.168.1.1xx where xx = last 2 digits): + +```json +"ip" : "192.168.1.1xx", +``` + +5. Save and exit + +### Step 3: Verify Connection + +```bash +ping 192.168.1.1xx # Replace xx with your last 2 digits +``` + +## Robot Configuration + +### Setting Robot Type + +The system supports different robot configurations. Set the `ROBOT_CONFIG_PATH` environment variable to specify which robot configuration to use: + +```bash +# For Unitree G1 (default if not set) +export ROBOT_CONFIG_PATH="unitree/unitree_g1" + +# Add to ~/.bashrc to make permanent +echo 'export ROBOT_CONFIG_PATH="unitree/unitree_g1"' >> ~/.bashrc +``` + +Available robot configurations: +- `unitree/unitree_g1` - Unitree G1 robot (default) +- Add your custom robot configs in `src/base_autonomy/local_planner/config/` + +## Build the system + +You must do this every you make a code change, this is not Python + +```colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release``` + +## System Launch + +### Simulation Mode + +```bash +cd ~/autonomy_stack_mecanum_wheel_platform + +# Base autonomy only +./system_simulation.sh + +# With route planner +./system_simulation_with_route_planner.sh + +# With exploration planner +./system_simulation_with_exploration_planner.sh +``` + +### Real Robot Mode + +```bash +cd ~/autonomy_stack_mecanum_wheel_platform + +# Base autonomy only +./system_real_robot.sh + +# With route planner +./system_real_robot_with_route_planner.sh + +# With exploration planner +./system_real_robot_with_exploration_planner.sh +``` + +## Quick Troubleshooting + +- **Cannot ping MID360**: Check Ethernet cable and network settings +- **SLAM drift**: Press clear-terrain-map button on joystick controller +- **Joystick not recognized**: Unplug and replug USB dongle + + +## ROS Topics + +### Input Topics (Commands) + +| Topic | Type | Description | +|-------|------|-------------| +| `/way_point` | `geometry_msgs/PointStamped` | Send navigation goal (position only) | +| `/goal_pose` | `geometry_msgs/PoseStamped` | Send goal with orientation | +| `/cancel_goal` | `std_msgs/Bool` | Cancel current goal (data: true) | +| `/joy` | `sensor_msgs/Joy` | Joystick input | +| `/stop` | `std_msgs/Int8` | Soft Stop (2=stop all commmand, 0 = release) | +| `/navigation_boundary` | `geometry_msgs/PolygonStamped` | Set navigation boundaries | +| `/added_obstacles` | `sensor_msgs/PointCloud2` | Virtual obstacles | + +### Output Topics (Status) + +| Topic | Type | Description | +|-------|------|-------------| +| `/state_estimation` | `nav_msgs/Odometry` | Robot pose from SLAM | +| `/registered_scan` | `sensor_msgs/PointCloud2` | Aligned lidar point cloud | +| `/terrain_map` | `sensor_msgs/PointCloud2` | Local terrain map | +| `/terrain_map_ext` | `sensor_msgs/PointCloud2` | Extended terrain map | +| `/path` | `nav_msgs/Path` | Local path being followed | +| `/cmd_vel` | `geometry_msgs/Twist` | Velocity commands to motors | +| `/goal_reached` | `std_msgs/Bool` | True when goal reached, false when cancelled/new goal | + +### Map Topics + +| Topic | Type | Description | +|-------|------|-------------| +| `/overall_map` | `sensor_msgs/PointCloud2` | Global map (only in sim)| +| `/registered_scan` | `sensor_msgs/PointCloud2` | Current scan in map frame | +| `/terrain_map` | `sensor_msgs/PointCloud2` | Local obstacle map | + +## Usage Examples + +### Send Goal +```bash +ros2 topic pub /way_point geometry_msgs/msg/PointStamped "{ + header: {frame_id: 'map'}, + point: {x: 5.0, y: 3.0, z: 0.0} +}" --once +``` + +### Cancel Goal +```bash +ros2 topic pub /cancel_goal std_msgs/msg/Bool "data: true" --once +``` + +### Monitor Robot State +```bash +ros2 topic echo /state_estimation +``` + +## Configuration Parameters + +### Vehicle Parameters (`localPlanner`) + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `vehicleLength` | 0.5 | Robot length (m) | +| `vehicleWidth` | 0.5 | Robot width (m) | +| `maxSpeed` | 0.875 | Maximum speed (m/s) | +| `autonomySpeed` | 0.875 | Autonomous mode speed (m/s) | + +### Goal Tolerance Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `goalReachedThreshold` | 0.3-0.5 | Distance to consider goal reached (m) | +| `goalClearRange` | 0.35-0.6 | Extra clearance around goal (m) | +| `goalBehindRange` | 0.35-0.8 | Stop pursuing if goal behind within this distance (m) | +| `omniDirGoalThre` | 1.0 | Distance for omnidirectional approach (m) | + +### Obstacle Avoidance + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `obstacleHeightThre` | 0.1-0.2 | Height threshold for obstacles (m) | +| `adjacentRange` | 3.5 | Sensor range for planning (m) | +| `minRelZ` | -0.4 | Minimum relative height to consider (m) | +| `maxRelZ` | 0.3 | Maximum relative height to consider (m) | + +### Path Planning + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `pathScale` | 0.875 | Path resolution scale | +| `minPathScale` | 0.675 | Minimum path scale when blocked | +| `minPathRange` | 0.8 | Minimum planning range (m) | +| `dirThre` | 90.0 | Direction threshold (degrees) | + +### Control Parameters (`pathFollower`) + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `lookAheadDis` | 0.5 | Look-ahead distance (m) | +| `maxAccel` | 2.0 | Maximum acceleration (m/s²) | +| `slowDwnDisThre` | 0.875 | Slow down distance threshold (m) | + +### SLAM Blind Zones (`feature_extraction_node`) + +| Parameter | Mecanum | Description | +|-----------|---------|-------------| +| `blindFront` | 0.1 | Front blind zone (m) | +| `blindBack` | -0.2 | Back blind zone (m) | +| `blindLeft` | 0.1 | Left blind zone (m) | +| `blindRight` | -0.1 | Right blind zone (m) | +| `blindDiskRadius` | 0.4 | Cylindrical blind zone radius (m) | + +## Operating Modes + +### Mode Control +- **Joystick L2**: Hold for autonomy mode +- **Joystick R2**: Hold to disable obstacle checking + +### Speed Control +The robot automatically adjusts speed based on: +1. Obstacle proximity +2. Path complexity +3. Goal distance + +## Tuning Guide + +### For Tighter Navigation +- Decrease `goalReachedThreshold` (e.g., 0.2) +- Decrease `goalClearRange` (e.g., 0.3) +- Decrease `vehicleLength/Width` slightly + +### For Smoother Navigation +- Increase `goalReachedThreshold` (e.g., 0.5) +- Increase `lookAheadDis` (e.g., 0.7) +- Decrease `maxAccel` (e.g., 1.5) + +### For Aggressive Obstacle Avoidance +- Increase `obstacleHeightThre` (e.g., 0.15) +- Increase `adjacentRange` (e.g., 4.0) +- Increase blind zone parameters + +## Common Issues + +### Robot Oscillates at Goal +- Increase `goalReachedThreshold` +- Increase `goalBehindRange` + +### Robot Stops Too Far from Goal +- Decrease `goalReachedThreshold` +- Decrease `goalClearRange` + +### Robot Hits Low Obstacles +- Decrease `obstacleHeightThre` +- Adjust `minRelZ` to include lower points + +## SLAM Configuration + +### Localization Mode +Set in `livox_mid360.yaml`: +```yaml +local_mode: true +init_x: 0.0 +init_y: 0.0 +init_yaw: 0.0 +``` + +### Mapping Performance +```yaml +mapping_line_resolution: 0.1 # Decrease for higher quality +mapping_plane_resolution: 0.2 # Decrease for higher quality +max_iterations: 5 # Increase for better accuracy +``` diff --git a/docs/running_without_devcontainer.md b/docs/old/running_without_devcontainer.md similarity index 100% rename from docs/running_without_devcontainer.md rename to docs/old/running_without_devcontainer.md diff --git a/docs/testing_stream_reply.md b/docs/old/testing_stream_reply.md similarity index 99% rename from docs/testing_stream_reply.md rename to docs/old/testing_stream_reply.md index f6b76d3ed9..e3189bb5e8 100644 --- a/docs/testing_stream_reply.md +++ b/docs/old/testing_stream_reply.md @@ -24,7 +24,7 @@ A lightweight framework for **recording, storing, and replaying binary data stre * **No repo bloat** – binaries live in GitĀ LFS; the working tree stays trim. * **Symmetric API** – `SensorReplay` ā†”ļøŽ `SensorStorage`; same name, different direction. * **Format agnostic** – replay *anything* you can pickle (protobuf, numpy, JPEG, …). -* **Data type agnostic** – with testData("raw_odometry_rotate_walk") you get a Path object back, can be a raw video file, whole codebase, ML model etc +* **Data type agnostic** – with testData("raw_odometry_rotate_walk") you get a Path object back, can be a raw video file, whole codebase, ML model etc --- @@ -172,4 +172,3 @@ Either delete or run ./bin/lfs_push * `dimos/robot/unitree_webrtc/type/test_odometry.py` * `dimos/robot/unitree_webrtc/type/test_map.py` - diff --git a/docs/package_usage.md b/docs/package_usage.md new file mode 100644 index 0000000000..24584a2e79 --- /dev/null +++ b/docs/package_usage.md @@ -0,0 +1,62 @@ +# Package Usage + +## With `uv` + +Init your repo if not already done: + +```bash +uv init +``` + +Install: + +```bash +uv add dimos[dev,cpu,sim] +``` + +Test the Unitree Go2 robot in the simulator: + +```bash +uv run dimos-robot --simulation run unitree-g1 +``` + +Run your actual robot: + +```bash +uv run dimos-robot --robot-ip=192.168.X.XXX run unitree-g1 +``` + +### Without installing + +With `uv` you can run tools without having to explicitly install: + +```bash +uvx --from dimos dimos-robot --robot-ip=192.168.X.XXX run unitree-g1 +``` + +## With `pip` + +Create an environment if not already done: + +```bash +python -m venv .venv +. .venv/bin/activate +``` + +Install: + +```bash +pip install dimos[dev,cpu,sim] +``` + +Test the Unitree Go2 robot in the simulator: + +```bash +dimos-robot --simulation run unitree-g1 +``` + +Run your actual robot: + +```bash +dimos-robot --robot-ip=192.168.X.XXX run unitree-g1 +``` diff --git a/examples/language-interop/README.md b/examples/language-interop/README.md new file mode 100644 index 0000000000..52ae561ddb --- /dev/null +++ b/examples/language-interop/README.md @@ -0,0 +1,20 @@ +# Language Interop Examples + +Demonstrates controlling a dimos robot from non-Python languages. + +## Usage + +1. Start the robot (in another terminal): + ```bash + cd ../simplerobot + python simplerobot.py + ``` + +2. Run any language example: + - [TypeScript](ts/) - CLI and browser-based web UI + - [C++](cpp/) + - [Lua](lua/) + +3. (Optional) Monitor traffic with `lcmspy` + +![lcmspy](assets/lcmspy.png) diff --git a/examples/language-interop/assets/lcmspy.png b/examples/language-interop/assets/lcmspy.png new file mode 100644 index 0000000000..dc6a824f69 --- /dev/null +++ b/examples/language-interop/assets/lcmspy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87d3af9d9105d048c3e55faff52981c15cc1bfd8168c58a3d8c8f603aa8b7769 +size 5110 diff --git a/examples/language-interop/cpp/.gitignore b/examples/language-interop/cpp/.gitignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/examples/language-interop/cpp/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/examples/language-interop/cpp/CMakeLists.txt b/examples/language-interop/cpp/CMakeLists.txt new file mode 100644 index 0000000000..a0b8481cef --- /dev/null +++ b/examples/language-interop/cpp/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.14) +project(robot_control) + +set(CMAKE_CXX_STANDARD 17) + +include(FetchContent) + +# Fetch dimos-lcm for message headers +FetchContent_Declare(dimos_lcm + GIT_REPOSITORY https://github.com/dimensionalOS/dimos-lcm.git + GIT_TAG main + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(dimos_lcm) + +# Find LCM via pkg-config +find_package(PkgConfig REQUIRED) +pkg_check_modules(LCM REQUIRED lcm) + +add_executable(robot_control main.cpp) + +target_include_directories(robot_control PRIVATE + ${dimos_lcm_SOURCE_DIR}/generated/cpp_lcm_msgs + ${LCM_INCLUDE_DIRS} +) + +target_link_libraries(robot_control PRIVATE ${LCM_LIBRARIES}) +target_link_directories(robot_control PRIVATE ${LCM_LIBRARY_DIRS}) diff --git a/examples/language-interop/cpp/README.md b/examples/language-interop/cpp/README.md new file mode 100644 index 0000000000..ea11cd4505 --- /dev/null +++ b/examples/language-interop/cpp/README.md @@ -0,0 +1,17 @@ +# C++ Robot Control Example + +Subscribes to `/odom` and publishes velocity commands to `/cmd_vel`. + +## Build + +```bash +mkdir build && cd build +cmake .. +make +./robot_control +``` + +## Dependencies + +- [lcm](https://lcm-proj.github.io/) - install via package manager +- Message headers fetched automatically from [dimos-lcm](https://github.com/dimensionalOS/dimos-lcm) diff --git a/examples/language-interop/cpp/main.cpp b/examples/language-interop/cpp/main.cpp new file mode 100644 index 0000000000..fef2607365 --- /dev/null +++ b/examples/language-interop/cpp/main.cpp @@ -0,0 +1,68 @@ +// C++ robot control example +// Subscribes to robot pose and publishes twist commands + +#include +#include +#include +#include +#include +#include + +#include "geometry_msgs/PoseStamped.hpp" +#include "geometry_msgs/Twist.hpp" + +class RobotController { +public: + RobotController() : lcm_(), running_(true) {} + + void onPose(const lcm::ReceiveBuffer*, const std::string&, + const geometry_msgs::PoseStamped* msg) { + const auto& pos = msg->pose.position; + const auto& ori = msg->pose.orientation; + printf("[pose] x=%.2f y=%.2f z=%.2f | qw=%.2f\n", + pos.x, pos.y, pos.z, ori.w); + } + + void run() { + lcm_.subscribe("/odom#geometry_msgs.PoseStamped", &RobotController::onPose, this); + + printf("Robot control started\n"); + printf("Subscribing to /odom, publishing to /cmd_vel\n"); + printf("Press Ctrl+C to stop.\n\n"); + + // Publisher thread + std::thread pub_thread([this]() { + double t = 0; + while (running_) { + geometry_msgs::Twist twist; + twist.linear.x = 0.5; + twist.linear.y = 0; + twist.linear.z = 0; + twist.angular.x = 0; + twist.angular.y = 0; + twist.angular.z = std::sin(t) * 0.3; + + lcm_.publish("/cmd_vel#geometry_msgs.Twist", &twist); + printf("[twist] linear=%.2f angular=%.2f\n", twist.linear.x, twist.angular.z); + t += 0.1; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + }); + + // Handle incoming messages + while (lcm_.handle() == 0) {} + + running_ = false; + pub_thread.join(); + } + +private: + lcm::LCM lcm_; + std::atomic running_; +}; + +int main() { + RobotController controller; + controller.run(); + return 0; +} diff --git a/examples/language-interop/lua/.gitignore b/examples/language-interop/lua/.gitignore new file mode 100644 index 0000000000..f87dd2d125 --- /dev/null +++ b/examples/language-interop/lua/.gitignore @@ -0,0 +1,3 @@ +lcm/ +dimos-lcm/ +msgs/ diff --git a/examples/language-interop/lua/README.md b/examples/language-interop/lua/README.md new file mode 100644 index 0000000000..b804194e55 --- /dev/null +++ b/examples/language-interop/lua/README.md @@ -0,0 +1,38 @@ +# Lua Robot Control Example + +Subscribes to robot odometry and publishes twist commands using LCM. + +## Prerequisites + +- Lua 5.4 +- LuaSocket (`sudo luarocks install luasocket`) +- System dependencies: `glib`, `cmake` + +## Setup + +```bash +./setup.sh +``` + +This will: +1. Clone and build official [LCM](https://github.com/lcm-proj/lcm) Lua bindings +2. Clone [dimos-lcm](https://github.com/dimensionalOS/dimos-lcm) message definitions + +## Run + +```bash +lua main.lua +``` + +## Output + +``` +Robot control started +Subscribing to /odom, publishing to /cmd_vel +Press Ctrl+C to stop. + +[pose] x=15.29 y=9.62 z=0.00 | qw=0.57 +[twist] linear=0.50 angular=0.00 +[pose] x=15.28 y=9.63 z=0.00 | qw=0.57 +... +``` diff --git a/examples/language-interop/lua/main.lua b/examples/language-interop/lua/main.lua new file mode 100644 index 0000000000..f6d2dccca1 --- /dev/null +++ b/examples/language-interop/lua/main.lua @@ -0,0 +1,59 @@ +#!/usr/bin/env lua + +-- Lua robot control example +-- Subscribes to robot pose and publishes twist commands + +-- Add local msgs folder to path +local script_dir = arg[0]:match("(.*/)") or "./" +package.path = script_dir .. "msgs/?.lua;" .. package.path +package.path = script_dir .. "msgs/?/init.lua;" .. package.path + +local lcm = require("lcm") +local PoseStamped = require("geometry_msgs.PoseStamped") +local Twist = require("geometry_msgs.Twist") +local Vector3 = require("geometry_msgs.Vector3") + +local lc = lcm.lcm.new() + +print("Robot control started") +print("Subscribing to /odom, publishing to /cmd_vel") +print("Press Ctrl+C to stop.\n") + +-- Subscribe to pose +lc:subscribe("/odom#geometry_msgs.PoseStamped", function(channel, msg) + msg = PoseStamped.decode(msg) + local pos = msg.pose.position + local ori = msg.pose.orientation + print(string.format("[pose] x=%.2f y=%.2f z=%.2f | qw=%.2f", + pos.x, pos.y, pos.z, ori.w)) +end) + +-- Publisher loop +local t = 0 +local socket = require("socket") +local last_pub = socket.gettime() + +while true do + -- Handle incoming messages + lc:handle() + + -- Publish at ~10 Hz + local now = socket.gettime() + if now - last_pub >= 0.1 then + local twist = Twist:new() + twist.linear = Vector3:new() + twist.linear.x = 0.5 + twist.linear.y = 0 + twist.linear.z = 0 + twist.angular = Vector3:new() + twist.angular.x = 0 + twist.angular.y = 0 + twist.angular.z = math.sin(t) * 0.3 + + lc:publish("/cmd_vel#geometry_msgs.Twist", twist:encode()) + print(string.format("[twist] linear=%.2f angular=%.2f", twist.linear.x, twist.angular.z)) + + t = t + 0.1 + last_pub = now + end +end diff --git a/examples/language-interop/lua/setup.sh b/examples/language-interop/lua/setup.sh new file mode 100755 index 0000000000..682d676183 --- /dev/null +++ b/examples/language-interop/lua/setup.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# Setup script for LCM Lua bindings +# Clones official LCM repo and builds Lua bindings +# +# Tested on: Arch Linux, Ubuntu, macOS (with Homebrew) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LCM_DIR="$SCRIPT_DIR/lcm" + +echo "=== LCM Lua Setup ===" + +# Detect Lua version +if command -v lua &>/dev/null; then + LUA_VERSION=$(lua -v 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1) + echo "Detected Lua version: $LUA_VERSION" +else + echo "Error: lua not found in PATH" + exit 1 +fi + +# Detect Lua paths using pkg-config if available +if command -v pkg-config &>/dev/null && pkg-config --exists "lua$LUA_VERSION" 2>/dev/null; then + LUA_INCLUDE_DIR=$(pkg-config --variable=includedir "lua$LUA_VERSION") + LUA_LIBRARY=$(pkg-config --libs "lua$LUA_VERSION" | grep -oE '/[^ ]+\.so' | head -1 || echo "") +elif command -v pkg-config &>/dev/null && pkg-config --exists lua 2>/dev/null; then + LUA_INCLUDE_DIR=$(pkg-config --variable=includedir lua) + LUA_LIBRARY=$(pkg-config --libs lua | grep -oE '/[^ ]+\.so' | head -1 || echo "") +fi + +# Platform-specific defaults +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS with Homebrew + LUA_INCLUDE_DIR="${LUA_INCLUDE_DIR:-$(brew --prefix lua 2>/dev/null)/include/lua}" + LUA_LIBRARY="${LUA_LIBRARY:-$(brew --prefix lua 2>/dev/null)/lib/liblua.dylib}" + LUA_CPATH_BASE="${LUA_CPATH_BASE:-/usr/local/lib/lua}" +else + # Linux defaults + LUA_INCLUDE_DIR="${LUA_INCLUDE_DIR:-/usr/include}" + LUA_LIBRARY="${LUA_LIBRARY:-/usr/lib/liblua.so}" + LUA_CPATH_BASE="${LUA_CPATH_BASE:-/usr/local/lib/lua}" +fi + +echo "Lua include: $LUA_INCLUDE_DIR" +echo "Lua library: $LUA_LIBRARY" + +# Clone LCM if not present +if [ ! -d "$LCM_DIR" ]; then + echo "Cloning LCM..." + git clone --depth 1 https://github.com/lcm-proj/lcm.git "$LCM_DIR" +else + echo "LCM already cloned" +fi + +# Build Lua bindings using cmake +echo "Building LCM Lua bindings..." +cd "$LCM_DIR" +mkdir -p build && cd build + +# Configure with Lua support +cmake .. \ + -DLCM_ENABLE_LUA=ON \ + -DLCM_ENABLE_PYTHON=OFF \ + -DLCM_ENABLE_JAVA=OFF \ + -DLCM_ENABLE_TESTS=OFF \ + -DLCM_ENABLE_EXAMPLES=OFF \ + -DLUA_INCLUDE_DIR="$LUA_INCLUDE_DIR" \ + -DLUA_LIBRARY="$LUA_LIBRARY" + +# Build just the lua target +make lcm-lua -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + +# Install the lua module +LUA_CPATH_DIR="$LUA_CPATH_BASE/$LUA_VERSION" +echo "Installing lcm.so to $LUA_CPATH_DIR" +sudo mkdir -p "$LUA_CPATH_DIR" +sudo cp lcm-lua/lcm.so "$LUA_CPATH_DIR/" + +# Get dimos-lcm message definitions +DIMOS_LCM_DIR="$SCRIPT_DIR/dimos-lcm" +MSGS_DST="$SCRIPT_DIR/msgs" + +echo "Getting message definitions..." +if [ -d "$DIMOS_LCM_DIR" ]; then + echo "Updating dimos-lcm..." + cd "$DIMOS_LCM_DIR" && git pull +else + echo "Cloning dimos-lcm..." + git clone --depth 1 https://github.com/dimensionalOS/dimos-lcm.git "$DIMOS_LCM_DIR" +fi + +# Link/copy messages +rm -rf "$MSGS_DST" +cp -r "$DIMOS_LCM_DIR/generated/lua_lcm_msgs" "$MSGS_DST" +echo "Messages installed to $MSGS_DST" + +echo "" +echo "=== Setup complete ===" +echo "Run: lua main.lua" diff --git a/examples/language-interop/ts/README.md b/examples/language-interop/ts/README.md new file mode 100644 index 0000000000..373d7df3be --- /dev/null +++ b/examples/language-interop/ts/README.md @@ -0,0 +1,34 @@ +# TypeScript Robot Control Examples + +Subscribes to `/odom` and publishes velocity commands to `/cmd_vel`. + +## CLI Example + +```bash +deno task start +``` + +## Web Example + +Browser-based control with WebSocket bridge: + +```bash +cd web +deno run --allow-net --allow-read --unstable-net server.ts +``` + +Open http://localhost:8080 in your browser. + +Features: +- Real-time pose display +- Arrow keys / WASD for control +- Click buttons to send twist commands + +The browser imports `@dimos/msgs` via [esm.sh](https://esm.sh) and encodes/decodes LCM packets directly - the server just forwards raw binary between WebSocket and UDP multicast. + +## Dependencies + +Main documentation for TS interop: + +- [@dimos/lcm](https://jsr.io/@dimos/lcm) +- [@dimos/msgs](https://jsr.io/@dimos/msgs) diff --git a/examples/language-interop/ts/deno.json b/examples/language-interop/ts/deno.json new file mode 100644 index 0000000000..b64363a64a --- /dev/null +++ b/examples/language-interop/ts/deno.json @@ -0,0 +1,9 @@ +{ + "imports": { + "@dimos/lcm": "jsr:@dimos/lcm", + "@dimos/msgs": "jsr:@dimos/msgs" + }, + "tasks": { + "start": "deno run --allow-net --unstable-net main.ts" + } +} diff --git a/examples/language-interop/ts/deno.lock b/examples/language-interop/ts/deno.lock new file mode 100644 index 0000000000..9529ebdb34 --- /dev/null +++ b/examples/language-interop/ts/deno.lock @@ -0,0 +1,21 @@ +{ + "version": "5", + "specifiers": { + "jsr:@dimos/lcm@*": "0.2.0", + "jsr:@dimos/msgs@*": "0.1.4" + }, + "jsr": { + "@dimos/lcm@0.2.0": { + "integrity": "03399f5e4800f28a0c294981e0210d784232fc65a57707de19052ad805bd5fea" + }, + "@dimos/msgs@0.1.4": { + "integrity": "564bc30b4bc41a562c296c257a15055283ca0cbd66d0627991ede5295832d0c4" + } + }, + "workspace": { + "dependencies": [ + "jsr:@dimos/lcm@*", + "jsr:@dimos/msgs@*" + ] + } +} diff --git a/examples/language-interop/ts/main.ts b/examples/language-interop/ts/main.ts new file mode 100644 index 0000000000..9026304f2c --- /dev/null +++ b/examples/language-interop/ts/main.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env -S deno run --allow-net --unstable-net + +// TypeScript robot control example +// Subscribes to robot odometry and publishes twist commands + +import { LCM } from "@dimos/lcm"; +import { geometry_msgs } from "@dimos/msgs"; + +const lcm = new LCM(); +await lcm.start(); + +console.log("Robot control started"); +console.log("Subscribing to /odom, publishing to /cmd_vel"); +console.log("Press Ctrl+C to stop.\n"); + +// Subscribe to pose - prints robot position +lcm.subscribe("/odom", geometry_msgs.PoseStamped, (msg) => { + const pos = msg.data.pose.position; + const ori = msg.data.pose.orientation; + console.log( + `[pose] x=${pos.x.toFixed(2)} y=${pos.y.toFixed(2)} z=${pos.z.toFixed(2)} | qw=${ori.w.toFixed(2)}` + ); +}); + +// Publish twist commands at 10 Hz - simple forward motion +let t = 0; +const interval = setInterval(async () => { + if (!lcm.isRunning()) { + clearInterval(interval); + return; + } + + const twist = new geometry_msgs.Twist({ + linear: new geometry_msgs.Vector3({ x: 0.5, y: 0, z: 0 }), + angular: new geometry_msgs.Vector3({ x: 0, y: 0, z: Math.sin(t) * 0.3 }), + }); + + await lcm.publish("/cmd_vel", twist); + console.log(`[twist] linear=${twist.linear.x.toFixed(2)} angular=${twist.angular.z.toFixed(2)}`); + t += 0.1; +}, 100); + +await lcm.run(); diff --git a/examples/language-interop/ts/web/index.html b/examples/language-interop/ts/web/index.html new file mode 100644 index 0000000000..1ce89604b6 --- /dev/null +++ b/examples/language-interop/ts/web/index.html @@ -0,0 +1,213 @@ + + + + + + + Robot Control + + + +

Robot Control

+
Connecting...
+ +
+
+

Pose

+
X--
+
Y--
+
Z--
+
Qw--
+
+ +
+

Controls

+
+
+ +
+ + + +
+ +
+
+
+ +
+

Log

+
+
+ + + + diff --git a/examples/language-interop/ts/web/server.ts b/examples/language-interop/ts/web/server.ts new file mode 100644 index 0000000000..5b8ec035be --- /dev/null +++ b/examples/language-interop/ts/web/server.ts @@ -0,0 +1,69 @@ +#!/usr/bin/env -S deno run --allow-net --allow-read --unstable-net + +// LCM to WebSocket Bridge for Robot Control +// Forwards robot pose to browser, receives twist commands from browser + +import { LCM } from "jsr:@dimos/lcm"; +import { decodePacket, geometry_msgs } from "jsr:@dimos/msgs"; + +const PORT = 8080; +const clients = new Set(); + +Deno.serve({ port: PORT }, async (req) => { + const url = new URL(req.url); + + if (req.headers.get("upgrade") === "websocket") { + const { socket, response } = Deno.upgradeWebSocket(req); + socket.onopen = () => { console.log("Client connected"); clients.add(socket); }; + socket.onclose = () => { console.log("Client disconnected"); clients.delete(socket); }; + socket.onerror = () => clients.delete(socket); + + // Forward binary LCM packets from browser directly to UDP + socket.binaryType = "arraybuffer"; + socket.onmessage = async (event) => { + if (event.data instanceof ArrayBuffer) { + const packet = new Uint8Array(event.data); + try { + // we don't need to decode, just showing we can + const { channel, data } = decodePacket(packet); + console.log(`[ws->lcm] ${channel}`, data); + await lcm.publishPacket(packet); + } catch (e) { + console.error("Forward error:", e); + } + } + }; + + return response; + } + + if (url.pathname === "/" || url.pathname === "/index.html") { + const html = await Deno.readTextFile(new URL("./index.html", import.meta.url)); + return new Response(html, { headers: { "content-type": "text/html" } }); + } + + return new Response("Not found", { status: 404 }); +}); + +console.log(`Server: http://localhost:${PORT}`); + +const lcm = new LCM(); +await lcm.start(); + +// Subscribe to pose and just log to show how server can decode messages for itself +lcm.subscribe("/odom", geometry_msgs.PoseStamped, (msg) => { + const pos = msg.data.pose.position; + const ori = msg.data.pose.orientation; + console.log(`[pose] x=${pos.x.toFixed(2)} y=${pos.y.toFixed(2)} z=${pos.z.toFixed(2)}`); +}); + +// Forward all raw packets to browser (we are decoding LCM directly in the browser) +lcm.subscribePacket((packet) => { + for (const client of clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(packet); + } + } +}); + +await lcm.run(); diff --git a/examples/simplerobot/README.md b/examples/simplerobot/README.md new file mode 100644 index 0000000000..bc6686f77c --- /dev/null +++ b/examples/simplerobot/README.md @@ -0,0 +1,50 @@ +# SimpleRobot + +A minimal virtual robot for testing and development. It implements some of the same LCM interface as real robots, making it ideal for testing third-party integrations (see `examples/language-interop/`) or experimeting with dimos Module patterns + +## Interface + +| Topic | Type | Direction | Description | +|------------|---------------|-----------|-----------------------------------------| +| `/cmd_vel` | `Twist` | Subscribe | Velocity commands (linear.x, angular.z) | +| `/odom` | `PoseStamped` | Publish | Current pose at 30Hz | + +Physical robots typically publish multiple poses in a relationship as `TransformStamped` in a TF tree, while SimpleRobot publishes `PoseStamped` directly for simplicity. + +For details on this check [Transforms](/docs/api/transforms.md) + +## Usage + +```bash +# With pygame visualization +python examples/simplerobot/simplerobot.py + +# Headless mode +python examples/simplerobot/simplerobot.py --headless + +# Run self-test demo +python examples/simplerobot/simplerobot.py --headless --selftest +``` + +Use `lcmspy` in another terminal to inspect messages. Press `q` or `Esc` to quit visualization. + +## Sending Commands + +From any language with LCM bindings, publish `Twist` messages to `/cmd_vel`: + +```python +from dimos.core import LCMTransport +from dimos.msgs.geometry_msgs import Twist + +transport = LCMTransport("/cmd_vel", Twist) +transport.publish(Twist(linear=(0.5, 0, 0), angular=(0, 0, 0.3))) +``` + +See `examples/language-interop/` for C++, TypeScript, and Lua examples. + +## Physics + +SimpleRobot uses a 2D unicycle model: +- `linear.x` drives forward/backward +- `angular.z` rotates left/right +- Commands timeout after 0.5s (robot stops if no new commands) diff --git a/examples/simplerobot/simplerobot.py b/examples/simplerobot/simplerobot.py new file mode 100644 index 0000000000..b959fa7d6f --- /dev/null +++ b/examples/simplerobot/simplerobot.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2026 Dimensional Inc. +# SPDX-License-Identifier: Apache-2.0 + +""" +Simple virtual robot demonstrating a dimos Module with In/Out ports. + +Subscribes to Twist commands and publishes PoseStamped. +""" + +from dataclasses import dataclass +import math +import time +from typing import Any + +import reactivex as rx + +from dimos.core import In, Module, ModuleConfig, Out, rpc +from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Twist, Vector3 + + +def apply_twist(pose: Pose, twist: Twist, dt: float) -> Pose: + """Apply a velocity command to a pose (unicycle model).""" + yaw = pose.yaw + twist.angular.z * dt + return Pose( + position=( + pose.x + twist.linear.x * math.cos(yaw) * dt, + pose.y + twist.linear.x * math.sin(yaw) * dt, + pose.z, + ), + orientation=Quaternion.from_euler(Vector3(0, 0, yaw)), + ) + + +@dataclass +class SimpleRobotConfig(ModuleConfig): + frame_id: str = "world" + update_rate: float = 30.0 + cmd_timeout: float = 0.5 + + +class SimpleRobot(Module[SimpleRobotConfig]): + """A 2D robot that integrates velocity commands into pose.""" + + cmd_vel: In[Twist] + pose: Out[PoseStamped] + default_config = SimpleRobotConfig + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._pose = Pose() + self._vel = Twist() + self._vel_time = 0.0 + + @rpc + def start(self) -> None: + self._disposables.add(self.cmd_vel.observable().subscribe(self._on_twist)) + self._disposables.add( + rx.interval(1.0 / self.config.update_rate).subscribe(lambda _: self._update()) + ) + self._disposables.add( + rx.interval(1.0).subscribe(lambda _: print(f"\033[34m{self._pose}\033[0m")) + ) + + def _on_twist(self, twist: Twist) -> None: + self._vel = twist + self._vel_time = time.time() + print(f"\033[32m{twist}\033[0m") + + def _update(self) -> None: + now = time.time() + dt = 1.0 / self.config.update_rate + + vel = self._vel if now - self._vel_time < self.config.cmd_timeout else Twist() + + self._pose = apply_twist(self._pose, vel, dt) + + self.pose.publish( + PoseStamped( + ts=now, + frame_id=self.config.frame_id, + position=self._pose.position, + orientation=self._pose.orientation, + ) + ) + + +if __name__ == "__main__": + import argparse + + from dimos.core import LCMTransport + + parser = argparse.ArgumentParser(description="Simple virtual robot") + parser.add_argument("--headless", action="store_true") + parser.add_argument("--selftest", action="store_true", help="Run demo movements") + args = parser.parse_args() + + # If running in a dimos cluster we'd call + # + # from dimos.core import start + # dimos = start() + # robot = dimos.deploy(SimpleRobot) + # + # but this is a standalone example + # and we don't mind running in the main thread + + robot = SimpleRobot() + robot.pose.transport = LCMTransport("/odom", PoseStamped) + robot.cmd_vel.transport = LCMTransport("/cmd_vel", Twist) + robot.start() + + if not args.headless: + from vis import start_visualization + + start_visualization(robot) + + print("Robot running.") + print(" Publishing: /odom (PoseStamped)") + print(" Subscribing: /cmd_vel (Twist)") + print(" Run 'lcmspy' in another terminal to see LCM messages") + print(" Check /examples/language-interop for sending commands from LUA, C++, TS etc.") + print(" Ctrl+C to exit") + + try: + if args.selftest: + time.sleep(1) + print("Forward...") + for _ in range(8): + robot._on_twist(Twist(linear=(1.0, 0, 0))) + time.sleep(0.25) + print("Turn...") + for _ in range(12): + robot._on_twist(Twist(linear=(0.5, 0, 0), angular=(0, 0, 0.5))) + time.sleep(0.25) + print("Stop") + robot._on_twist(Twist()) + time.sleep(1) + else: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nStopping...") + finally: + robot.stop() diff --git a/examples/simplerobot/vis.py b/examples/simplerobot/vis.py new file mode 100644 index 0000000000..951a6be1b9 --- /dev/null +++ b/examples/simplerobot/vis.py @@ -0,0 +1,95 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pygame visualization for SimpleRobot.""" + +import math +import threading + + +def run_visualization(robot, window_size=(800, 800), meters_per_pixel=0.02): + """Run pygame visualization for a robot. Call from a thread.""" + import pygame + + pygame.init() + screen = pygame.display.set_mode(window_size) + pygame.display.set_caption("Simple Robot") + clock = pygame.time.Clock() + font = pygame.font.Font(None, 24) + + BG = (30, 30, 40) + GRID = (50, 50, 60) + ROBOT = (100, 200, 255) + ARROW = (255, 150, 100) + TEXT = (200, 200, 200) + + w, h = window_size + cx, cy = w // 2, h // 2 + running = True + + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + if event.type == pygame.KEYDOWN and event.key in (pygame.K_ESCAPE, pygame.K_q): + running = False + + pose, vel = robot._pose, robot._vel + + screen.fill(BG) + + # Grid (1m spacing) + grid_spacing = int(1.0 / meters_per_pixel) + for x in range(0, w, grid_spacing): + pygame.draw.line(screen, GRID, (x, 0), (x, h)) + for y in range(0, h, grid_spacing): + pygame.draw.line(screen, GRID, (0, y), (w, y)) + + # Robot position in screen coords + rx = cx + int(pose.x / meters_per_pixel) + ry = cy - int(pose.y / meters_per_pixel) + + # Robot body + pygame.draw.circle(screen, ROBOT, (rx, ry), 20) + + # Direction arrow + ax = rx + int(45 * math.cos(pose.yaw)) + ay = ry - int(45 * math.sin(pose.yaw)) + pygame.draw.line(screen, ARROW, (rx, ry), (ax, ay), 3) + for sign in [-1, 1]: + hx = ax - int(10 * math.cos(pose.yaw + sign * 0.5)) + hy = ay + int(10 * math.sin(pose.yaw + sign * 0.5)) + pygame.draw.line(screen, ARROW, (ax, ay), (hx, hy), 3) + + # Info text + info = [ + f"Position: ({pose.x:.2f}, {pose.y:.2f}) m", + f"Heading: {math.degrees(pose.yaw):.1f}°", + f"Velocity: {vel.linear.x:.2f} m/s", + f"Angular: {math.degrees(vel.angular.z):.1f}°/s", + ] + for i, text in enumerate(info): + screen.blit(font.render(text, True, TEXT), (10, 10 + i * 25)) + + pygame.display.flip() + clock.tick(60) + + pygame.quit() + + +def start_visualization(robot, **kwargs): + """Start visualization in a background thread.""" + thread = threading.Thread(target=run_visualization, args=(robot,), kwargs=kwargs, daemon=True) + thread.start() + return thread diff --git a/flake.lock b/flake.lock index e6d920a293..402f251030 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "diagon": { + "locked": { + "lastModified": 1763299369, + "narHash": "sha256-z/q22EqZfF79vZQh6K/yCmt8iqDvUSkIVTH+Omhv1VE=", + "owner": "petertrotman", + "repo": "nixpkgs", + "rev": "dff059e25eee7aa958c606aeb6b5879ae1c674f0", + "type": "github" + }, + "original": { + "owner": "petertrotman", + "ref": "Diagon", + "repo": "nixpkgs", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -18,6 +34,78 @@ "type": "github" } }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "xome", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1753983724, + "narHash": "sha256-2vlAOJv4lBrE+P1uOGhZ1symyjXTRdn/mz0tZ6faQcg=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "7035020a507ed616e2b20c61491ae3eaa8e5462c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "lib": { + "inputs": { + "flakeUtils": [ + "flake-utils" + ], + "libSource": "libSource" + }, + "locked": { + "lastModified": 1764022662, + "narHash": "sha256-vS3EeyELqCskh88JkUW/ce8A8b3m+iRPLPd4kDRTqPY=", + "owner": "jeff-hykin", + "repo": "quick-nix-toolkits", + "rev": "de1cc174579ecc7b655de5ba9618548d1b72306c", + "type": "github" + }, + "original": { + "owner": "jeff-hykin", + "repo": "quick-nix-toolkits", + "type": "github" + } + }, + "libSource": { + "locked": { + "lastModified": 1766884708, + "narHash": "sha256-x8nyRwtD0HMeYtX60xuIuZJbwwoI7/UKAdCiATnQNz0=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "15177f81ad356040b4460a676838154cbf7f6213", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "libSource_2": { + "locked": { + "lastModified": 1753579242, + "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", + "owner": "divnix", + "repo": "nixpkgs.lib", + "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", + "type": "github" + }, + "original": { + "owner": "divnix", + "repo": "nixpkgs.lib", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1748929857, @@ -36,8 +124,11 @@ }, "root": { "inputs": { + "diagon": "diagon", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "lib": "lib", + "nixpkgs": "nixpkgs", + "xome": "xome" } }, "systems": { @@ -54,6 +145,31 @@ "repo": "default", "type": "github" } + }, + "xome": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "home-manager": "home-manager", + "libSource": "libSource_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765466883, + "narHash": "sha256-c4YxXoS6U9BFcxP4TWZirwycaxT2oFyPMeyVp5vrME8=", + "owner": "jeff-hykin", + "repo": "xome", + "rev": "1f3507c4985e05177bd1a5b57d2862e30bb5da9b", + "type": "github" + }, + "original": { + "owner": "jeff-hykin", + "repo": "xome", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 0061153089..3a70a0bf2f 100644 --- a/flake.nix +++ b/flake.nix @@ -4,9 +4,15 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; + lib.url = "github:jeff-hykin/quick-nix-toolkits"; + lib.inputs.flakeUtils.follows = "flake-utils"; + xome.url = "github:jeff-hykin/xome"; + xome.inputs.nixpkgs.follows = "nixpkgs"; + xome.inputs.flake-utils.follows = "flake-utils"; + diagon.url = "github:petertrotman/nixpkgs/Diagon"; }; - outputs = { self, nixpkgs, flake-utils, ... }: + outputs = { self, nixpkgs, flake-utils, lib, xome, diagon, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; @@ -14,72 +20,271 @@ # ------------------------------------------------------------ # 1. Shared package list (tool-chain + project deps) # ------------------------------------------------------------ - devPackages = with pkgs; [ + # we "flag" each package with what we need it for (e.g. LD_LIBRARY_PATH, nativeBuildInputs vs buildInputs, etc) + aggregation = lib.aggregator [ ### Core shell & utils - bashInteractive coreutils gh - stdenv.cc.cc.lib pcre2 + { vals.pkg=pkgs.bashInteractive; flags={}; } + { vals.pkg=pkgs.coreutils; flags={}; } + { vals.pkg=pkgs.gh; flags={}; } + { vals.pkg=pkgs.stdenv.cc.cc.lib; flags.ldLibraryGroup=true; } + { vals.pkg=pkgs.stdenv.cc; flags.ldLibraryGroup=true; } + { vals.pkg=pkgs.cctools; flags={}; onlyIf=pkgs.stdenv.isDarwin; } # for pip install opencv-python + { vals.pkg=pkgs.pcre2; flags={ ldLibraryGroup=pkgs.stdenv.isDarwin; packageConfGroup=pkgs.stdenv.isDarwin; }; } + { vals.pkg=pkgs.libsysprof-capture; flags.packageConfGroup=true; onlyIf=pkgs.stdenv.isDarwin; } + { vals.pkg=pkgs.xcbuild; flags={}; } + { vals.pkg=pkgs.git-lfs; flags={}; } + { vals.pkg=pkgs.gnugrep; flags={}; } + { vals.pkg=pkgs.gnused; flags={}; } + { vals.pkg=pkgs.iproute2; flags={}; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.pkg-config; flags={}; } + { vals.pkg=pkgs.git; flags={}; } + { vals.pkg=pkgs.opensshWithKerberos;flags={}; } + { vals.pkg=pkgs.unixtools.ifconfig; flags={}; } + { vals.pkg=pkgs.unixtools.netstat; flags={}; } + + # when pip packages call cc with -I/usr/include, that causes problems on some machines, this swaps that out for the nix cc headers + # this is only necessary for pip packages from venv, pip packages from nixpkgs.python312Packages.* already have "-I/usr/include" patched with the nix equivalent + { + vals.pkg=(pkgs.writeShellScriptBin + "cc-no-usr-include" + '' + #!${pkgs.bash}/bin/bash + set -euo pipefail + + real_cc="${pkgs.stdenv.cc}/bin/gcc" + + args=() + for a in "$@"; do + case "$a" in + -I/usr/include|-I/usr/local/include) + # drop these + ;; + *) + args+=("$a") + ;; + esac + done + + exec "$real_cc" "''${args[@]}" + '' + ); + flags={}; + } ### Python + static analysis - python312 python312Packages.pip python312Packages.setuptools - python312Packages.virtualenv pre-commit + { vals.pkg=pkgs.python312; flags={}; vals.pythonMinorVersion="12";} + { vals.pkg=pkgs.python312Packages.pip; flags={}; } + { vals.pkg=pkgs.python312Packages.setuptools; flags={}; } + { vals.pkg=pkgs.python312Packages.virtualenv; flags={}; } + { vals.pkg=pkgs.pre-commit; flags={}; } ### Runtime deps - python312Packages.pyaudio portaudio ffmpeg_6 ffmpeg_6.dev + { vals.pkg=pkgs.portaudio; flags={ldLibraryGroup=true; packageConfGroup=true;}; } + { vals.pkg=pkgs.ffmpeg_6; flags={}; } + { vals.pkg=pkgs.ffmpeg_6.dev; flags={}; } ### Graphics / X11 stack - libGL libGLU mesa glfw - xorg.libX11 xorg.libXi xorg.libXext xorg.libXrandr xorg.libXinerama - xorg.libXcursor xorg.libXfixes xorg.libXrender xorg.libXdamage - xorg.libXcomposite xorg.libxcb xorg.libXScrnSaver xorg.libXxf86vm - - udev SDL2 SDL2.dev zlib + { vals.pkg=pkgs.libGL; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.libGLU; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.mesa; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.glfw; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libX11; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXi; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXext; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXrandr; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXinerama; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXcursor; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXfixes; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXrender; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXdamage; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXcomposite; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libxcb; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXScrnSaver; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXxf86vm; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.udev; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.SDL2; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.SDL2.dev; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.zlib; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } ### GTK / OpenCV helpers - glib gtk3 gdk-pixbuf gobject-introspection - + { vals.pkg=pkgs.glib; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gtk3; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gdk-pixbuf; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gobject-introspection; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + ### GStreamer - gst_all_1.gstreamer gst_all_1.gst-plugins-base gst_all_1.gst-plugins-good - gst_all_1.gst-plugins-bad gst_all_1.gst-plugins-ugly - python312Packages.gst-python + { vals.pkg=pkgs.gst_all_1.gstreamer; flags.ldLibraryGroup=true; flags.giTypelibGroup=true; } + { vals.pkg=pkgs.gst_all_1.gst-plugins-base; flags.ldLibraryGroup=true; flags.giTypelibGroup=true; } + { vals.pkg=pkgs.gst_all_1.gst-plugins-good; flags={}; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gst_all_1.gst-plugins-bad; flags={}; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gst_all_1.gst-plugins-ugly; flags={}; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.python312Packages.gst-python; flags={}; onlyIf=pkgs.stdenv.isLinux; } ### Open3D & build-time - eigen cmake ninja jsoncpp libjpeg libpng - + { vals.pkg=pkgs.eigen; flags={}; } + { vals.pkg=pkgs.cmake; flags={}; } + { vals.pkg=pkgs.ninja; flags={}; } + { vals.pkg=pkgs.jsoncpp; flags={}; } + { vals.pkg=pkgs.libjpeg; flags.ldLibraryGroup=true; } + { vals.pkg=pkgs.libjpeg_turbo; flags.ldLibraryGroup=true; } + { vals.pkg=pkgs.libpng; flags={}; } + + ### Docs generators + { vals.pkg=pkgs.pikchr; flags={}; } + { vals.pkg=pkgs.graphviz; flags={}; } + { vals.pkg=pkgs.imagemagick; flags={}; } + { vals.pkg=diagon.legacyPackages.${system}.diagon; flags={}; } + ### LCM (Lightweight Communications and Marshalling) - lcm + { vals.pkg=pkgs.lcm; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + # lcm works on darwin, but only after two fixes (1. pkg-config, 2. fsync) + { + onlyIf=pkgs.stdenv.isDarwin; + flags.ldLibraryGroup=true; + flags.manualPythonPackages=true; + vals.pkg=pkgs.lcm.overrideAttrs (old: + let + # 1. fix pkg-config on darwin + pkgConfPackages = aggregation.getAll { hasAllFlags=[ "packageConfGroup" ]; attrPath=[ "pkg" ]; }; + packageConfPackagesString = (aggregation.getAll { + hasAllFlags=[ "packageConfGroup" ]; + attrPath=[ "pkg" ]; + strAppend="/lib/pkgconfig"; + strJoin=":"; + }); + in + { + buildInputs = (old.buildInputs or []) ++ pkgConfPackages; + nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.pkg-config pkgs.python312 ]; + # 1. fix pkg-config on darwin + env.PKG_CONFIG_PATH = packageConfPackagesString; + # 2. Fix fsync on darwin + patches = [ + (pkgs.writeText "lcm-darwin-fsync.patch" "--- ./lcm-logger/lcm_logger.c 2025-11-14 09:46:01.000000000 -0600\n+++ ./lcm-logger/lcm_logger.c 2025-11-14 09:47:05.000000000 -0600\n@@ -428,9 +428,13 @@\n if (needs_flushed) {\n fflush(logger->log->f);\n #ifndef WIN32\n+#ifdef __APPLE__\n+ fsync(fileno(logger->log->f));\n+#else\n // Perform a full fsync operation after flush\n fdatasync(fileno(logger->log->f));\n #endif\n+#endif\n logger->last_fflush_time = log_event->timestamp;\n }\n") + ]; + } + ); + } ]; # ------------------------------------------------------------ - # 2. Host interactive shell → `nix develop` + # 2. group / aggregate our packages # ------------------------------------------------------------ - devShell = pkgs.mkShell { - packages = devPackages; - shellHook = '' - export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ - pkgs.stdenv.cc.cc.lib pkgs.libGL pkgs.libGLU pkgs.mesa pkgs.glfw - pkgs.xorg.libX11 pkgs.xorg.libXi pkgs.xorg.libXext pkgs.xorg.libXrandr - pkgs.xorg.libXinerama pkgs.xorg.libXcursor pkgs.xorg.libXfixes - pkgs.xorg.libXrender pkgs.xorg.libXdamage pkgs.xorg.libXcomposite - pkgs.xorg.libxcb pkgs.xorg.libXScrnSaver pkgs.xorg.libXxf86vm - pkgs.udev pkgs.portaudio pkgs.SDL2.dev pkgs.zlib pkgs.glib pkgs.gtk3 - pkgs.gdk-pixbuf pkgs.gobject-introspection pkgs.lcm pkgs.pcre2 - pkgs.gst_all_1.gstreamer pkgs.gst_all_1.gst-plugins-base]}:$LD_LIBRARY_PATH" - - export DISPLAY=:0 - export GI_TYPELIB_PATH="${pkgs.gst_all_1.gstreamer}/lib/girepository-1.0:${pkgs.gst_all_1.gst-plugins-base}/lib/girepository-1.0:$GI_TYPELIB_PATH" - - PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD") - if [ -f "$PROJECT_ROOT/env/bin/activate" ]; then - . "$PROJECT_ROOT/env/bin/activate" - fi - - [ -f "$PROJECT_ROOT/motd" ] && cat "$PROJECT_ROOT/motd" - [ -f "$PROJECT_ROOT/.pre-commit-config.yaml" ] && pre-commit install --install-hooks - ''; + devPackages = aggregation.getAll { attrPath=[ "pkg" ]; }; + ldLibraryPackages = aggregation.getAll { hasAllFlags=[ "ldLibraryGroup" ]; attrPath=[ "pkg" ]; }; + giTypelibPackagesString = aggregation.getAll { + hasAllFlags=[ "giTypelibGroup" ]; + attrPath=[ "pkg" ]; + strAppend="/lib/girepository-1.0"; + strJoin=":"; + }; + packageConfPackagesString = (aggregation.getAll { + hasAllFlags=[ "packageConfGroup" ]; + attrPath=[ "pkg" ]; + strAppend="/lib/pkgconfig"; + strJoin=":"; + }); + manualPythonPackages = (aggregation.getAll { + hasAllFlags=[ "manualPythonPackages" ]; + attrPath=[ "pkg" ]; + strAppend="/lib/python3.${aggregation.mergedVals.pythonMinorVersion}/site-packages"; + strJoin=":"; + }); + + # ------------------------------------------------------------ + # 3. Host interactive shell → `nix develop` + # ------------------------------------------------------------ + shellHook = '' + shopt -s nullglob 2>/dev/null || setopt +o nomatch 2>/dev/null || true # allow globs to be empty without throwing an error + if [ "$OSTYPE" = "linux-gnu" ]; then + export CC="cc-no-usr-include" # basically patching for nix + # Create nvidia-only lib symlinks to avoid glibc conflicts + NVIDIA_LIBS_DIR="/tmp/nix-nvidia-libs" + mkdir -p "$NVIDIA_LIBS_DIR" + for lib in /usr/lib/libcuda.so* /usr/lib/libnvidia*.so* /usr/lib/x86_64-linux-gnu/libnvidia*.so*; do + [ -e "$lib" ] && ln -sf "$lib" "$NVIDIA_LIBS_DIR/" 2>/dev/null + done + fi + export LD_LIBRARY_PATH="$NVIDIA_LIBS_DIR:${pkgs.lib.makeLibraryPath ldLibraryPackages}:$LD_LIBRARY_PATH" + export LIBRARY_PATH="$LD_LIBRARY_PATH" # fixes python find_library for pyaudio + export DISPLAY=:0 + export GI_TYPELIB_PATH="${giTypelibPackagesString}:$GI_TYPELIB_PATH" + export PKG_CONFIG_PATH=${lib.escapeShellArg packageConfPackagesString} + export PYTHONPATH="$PYTHONPATH:"${lib.escapeShellArg manualPythonPackages} + # CC, CFLAGS, and LDFLAGS are bascially all for `pip install pyaudio` + export CFLAGS="$(pkg-config --cflags portaudio-2.0) $CFLAGS" + export LDFLAGS="-L$(pkg-config --variable=libdir portaudio-2.0) $LDFLAGS" + + # without this alias, the pytest uses the non-venv python and fails + alias pytest="python -m pytest" + + PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD") + if [ -f "$PROJECT_ROOT/env/bin/activate" ]; then + . "$PROJECT_ROOT/env/bin/activate" + fi + + [ -f "$PROJECT_ROOT/motd" ] && cat "$PROJECT_ROOT/motd" + [ -f "$PROJECT_ROOT/.pre-commit-config.yaml" ] && pre-commit install --install-hooks + ''; + devShells = { + # basic shell (blends with your current environment) + default = pkgs.mkShell { + buildInputs = devPackages; + shellHook = shellHook; + }; + # strict shell (creates a fake home, only select exteral commands (e.g. sudo) from your system are available) + isolated = (xome.simpleMakeHomeFor { + inherit pkgs; + pure = true; + commandPassthrough = [ "sudo" "nvim" "code" "sysctl" "sw_vers" "git" "vim" "emacs" "openssl" "ssh" "osascript" "otool" "hidutil" "logger" "codesign" ]; # e.g. use external nvim instead of nix's + # commonly needed for MacOS: [ "osascript" "otool" "hidutil" "logger" "codesign" ] + homeSubpathPassthrough = [ "cache/nix/" ]; # share nix cache between projects + homeModule = { + # for home-manager examples, see: + # https://deepwiki.com/nix-community/home-manager/5-configuration-examples + # all home-manager options: + # https://nix-community.github.io/home-manager/options.xhtml + home.homeDirectory = "/tmp/virtual_homes/dimos"; + home.stateVersion = "25.11"; + home.packages = devPackages; + + programs = { + home-manager = { + enable = true; + }; + zsh = { + enable = true; + enableCompletion = true; + autosuggestion.enable = true; + syntaxHighlighting.enable = true; + shellAliases.ll = "ls -la"; + history.size = 100000; + # this is kinda like .zshrc + initContent = '' + # most people expect comments in their shell to to work + setopt interactivecomments + # fix emoji prompt offset issues (this shouldn't lock people into English b/c LANG can be non-english) + export LC_CTYPE=en_US.UTF-8 + ${shellHook} + ''; + }; + starship = { + enable = true; + enableZshIntegration = true; + settings = { + character = { + success_symbol = "[ā–£](bold green)"; + error_symbol = "[ā–£](bold red)"; + }; + }; + }; + }; + }; + }).default; }; # ------------------------------------------------------------ - # 3. Closure copied into the OCI image rootfs + # 4. Closure copied into the OCI image rootfs # ------------------------------------------------------------ imageRoot = pkgs.buildEnv { name = "dimos-image-root"; @@ -89,7 +294,7 @@ in { ## Local dev shell - devShells.default = devShell; + devShells = devShells; ## Layered docker image with DockerTools packages.devcontainer = pkgs.dockerTools.buildLayeredImage { diff --git a/mypy_strict.ini b/mypy_strict.ini deleted file mode 100644 index ed49020e9b..0000000000 --- a/mypy_strict.ini +++ /dev/null @@ -1,30 +0,0 @@ -[mypy] -python_version = 3.10 -strict = True -exclude = ^dimos/models/Detic(/|$)|.*/test_.|.*/conftest.py* - -# Enable all optional error checks individually (redundant with strict=True, but explicit) -warn_unused_configs = True -warn_unused_ignores = True -warn_redundant_casts = True -warn_no_return = True -warn_return_any = True -warn_unreachable = True -disallow_untyped_calls = True -disallow_untyped_defs = True -disallow_incomplete_defs = True -disallow_untyped_decorators = True -disallow_any_generics = True -no_implicit_optional = True -check_untyped_defs = True -strict_optional = True -ignore_missing_imports = False -show_error_context = True -show_column_numbers = True -pretty = True -color_output = True -error_summary = True - -# Performance and caching -incremental = True -cache_dir = .mypy_cache_strict diff --git a/pyproject.toml b/pyproject.toml index 4a7cec2c6c..b0a1e61415 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,22 @@ [build-system] -requires = ["setuptools>=70", "wheel"] +requires = ["setuptools>=70", "wheel", "pybind11>=2.12"] build-backend = "setuptools.build_meta" [tool.setuptools] -include-package-data = true +include-package-data = false [tool.setuptools.packages.find] where = ["."] include = ["dimos*"] +exclude = ["dimos.web.websocket_vis.node_modules*"] [tool.setuptools.package-data] -"*" = ["*.html", "*.css", "*.js", "*.json", "*.txt", "*.yaml", "*.yml"] +"dimos" = ["*.html", "*.css", "*.js", "*.json", "*.txt", "*.yaml", "*.yml"] +"dimos.utils.cli" = ["*.tcss"] +"dimos.robot.unitree.go2" = ["*.urdf"] +"dimos.robot.unitree_webrtc.params" = ["*.yaml", "*.yml"] +"dimos.web.templates" = ["*"] +"dimos.rxpy_backpressure" = ["*.txt"] [project] name = "dimos" @@ -20,111 +26,147 @@ authors = [ version = "0.0.4" description = "Powering agentive generalist robotics" requires-python = ">=3.10" +readme = "README.md" dependencies = [ - # Core requirements - "opencv-python", + # Transport Protocols + "dimos-lcm", + "PyTurboJPEG==1.8.2", + + # Core + "numpy>=1.26.4", + "scipy>=1.15.1", + "reactivex", + "asyncio==3.4.3", + "sortedcontainers==2.4.0", + "pydantic", "python-dotenv", - "openai", - "anthropic>=0.19.0", - "cerebras-cloud-sdk", - "numpy>=1.26.4,<2.0.0", + + # Multiprocess + "dask[complete]==2025.5.1", + "plum-dispatch==2.5.7", + + # Logging + "structlog>=25.5.0,<26", "colorlog==6.9.0", + + # Core Msgs + "opencv-python", + "open3d", + + # CLI + "pydantic-settings>=2.11.0,<3", + "textual==3.7.1", + "terminaltexteffects==0.12.2", + "typer>=0.19.2,<1", + "plotext==5.3.2", + + # Used for calculating the occupancy map. + "numba>=0.60.0", # First version supporting Python 3.12 + "llvmlite>=0.42.0", # Required by numba 0.60+ +] + + +[project.scripts] +lcmspy = "dimos.utils.cli.lcmspy.run_lcmspy:main" +foxglove-bridge = "dimos.utils.cli.foxglove_bridge.run_foxglove_bridge:main" +skillspy = "dimos.utils.cli.skillspy.skillspy:main" +agentspy = "dimos.utils.cli.agentspy.agentspy:main" +humancli = "dimos.utils.cli.human.humanclianim:main" +dimos = "dimos.robot.cli.dimos:main" + +[project.optional-dependencies] +misc = [ + # Core requirements + "cerebras-cloud-sdk", "yapf==0.40.2", "typeguard", "empy==3.3.4", "catkin_pkg", "lark", - "plum-dispatch==2.5.7", - "ffmpeg-python", "tiktoken>=0.8.0", - "Flask>=2.2", "python-multipart==0.0.20", - "reactivex", - "rxpy-backpressure @ git+https://github.com/dimensionalOS/rxpy-backpressure.git", - "asyncio==3.4.3", - "go2-webrtc-connect @ git+https://github.com/dimensionalOS/go2_webrtc_connect.git", "tensorzero==2025.7.5", - - # Web Extensions - "fastapi>=0.115.6", - "sse-starlette>=2.2.1", - "uvicorn>=0.34.0", - - # Agents - "langchain>=0.3.27", - "langchain-chroma>=0.2.5", - "langchain-core>=0.3.72", - "langchain-openai>=0.3.28", - "langchain-text-splitters>=0.3.9", - - # Class Extraction - "pydantic", - + # Developer Specific "ipykernel", - - # Unitree webrtc streaming - "aiortc==1.9.0", - "pycryptodome", - "sounddevice", - "pyaudio", - "requests", - "wasmtime", - # Image - "PyTurboJPEG==1.8.2", - - # Audio - "openai-whisper", - "soundfile", - - # Hugging Face - "transformers[torch]==4.49.0", - # Vector Embedding - "sentence_transformers", - + "sentence_transformers", + # Perception Dependencies - "ultralytics>=8.3.70", - "filterpy>=1.4.5", - "scipy>=1.15.1", "scikit-learn", - "Pillow", - "clip @ git+https://github.com/openai/CLIP.git", + "clip", "timm>=1.0.15", - "lap>=0.5.12", "opencv-contrib-python==4.10.0.84", + # embedding models + "open_clip_torch==3.2.0", + "torchreid==0.2.5", + "gdown==5.2.0", + "tensorboard==2.20.0", + # Mapping - "open3d", "googlemaps>=4.10.0", # Inference "onnx", + "einops==0.8.1", - # Multiprocess - "dask[complete]==2025.5.1", + # Teleop + "pygame>=2.6.1", + # Hardware SDKs + "xarm-python-sdk>=1.17.0", +] - # LCM / DimOS utilities - "dimos-lcm @ git+https://github.com/dimensionalOS/dimos-lcm.git@3aeb724863144a8ba6cf72c9f42761d1007deda4", +visualization = [ + "rerun-sdk>=0.20.0", +] - # CLI - "pydantic-settings>=2.11.0,<3", - "typer>=0.19.2,<1", +agents = [ + "langchain>=1,<2", + "langchain-chroma>=1,<2", + "langchain-core>=1,<2", + "langchain-openai>=1,<2", + "langchain-text-splitters>=1,<2", + "langchain-huggingface>=1,<2", + "langchain-ollama>=1,<2", + "bitsandbytes>=0.48.2,<1.0; sys_platform == 'linux'", + "ollama>=0.6.0", + "anthropic>=0.19.0", + + # Audio + "openai", + "openai-whisper", + "sounddevice", + + # MCP Server + "mcp>=1.0.0", ] -[project.scripts] -lcmspy = "dimos.utils.cli.lcmspy.run_lcmspy:main" -foxglove-bridge = "dimos.utils.cli.foxglove_bridge.run_foxglove_bridge:main" -skillspy = "dimos.utils.cli.skillspy.skillspy:main" -agentspy = "dimos.utils.cli.agentspy.agentspy:main" -human-cli = "dimos.utils.cli.human.humancli:main" -dimos-robot = "dimos.robot.cli.dimos_robot:main" +web = [ + "fastapi>=0.115.6", + "sse-starlette>=2.2.1", + "uvicorn>=0.34.0", + "ffmpeg-python", + "soundfile", +] -[project.optional-dependencies] -manipulation = [ +perception = [ + "ultralytics>=8.3.70", + "filterpy>=1.4.5", + "Pillow", + "lap>=0.5.12", + "transformers[torch]==4.49.0", + "moondream", +] + +unitree = [ + "dimos[agents,web,perception,visualization]", + "unitree-webrtc-connect-leshy>=2.0.7" +] +manipulation = [ # Contact Graspnet Dependencies "h5py>=3.7.0", "pyrender>=0.1.45", @@ -136,7 +178,7 @@ manipulation = [ "pandas>=1.5.2", "tqdm>=4.65.0", "pyyaml>=6.0", - "contact-graspnet-pytorch @ git+https://github.com/dimensionalOS/contact_graspnet_pytorch.git", + "contact-graspnet-pytorch", # piper arm "piper-sdk", @@ -162,7 +204,7 @@ cuda = [ "mmcv>=2.1.0", "xformers>=0.0.20", - # Detic GPU stack + # Detic GPU stack "mss", "dataclasses", "ftfy", @@ -170,25 +212,23 @@ cuda = [ "fasttext", "lvis", "nltk", - "clip @ git+https://github.com/openai/CLIP.git", - "detectron2 @ git+https://github.com/facebookresearch/detectron2.git@v0.6", - - # embedding models - "open_clip_torch>=3.0.0", - "torchreid==0.2.5", + "clip", + "detectron2", ] dev = [ - "ruff==0.11.10", - "mypy==1.18.2", + "ruff==0.14.3", + "mypy==1.19.0", "pre_commit==4.2.0", "pytest==8.3.5", "pytest-asyncio==0.26.0", "pytest-mock==3.15.0", "pytest-env==1.1.5", "pytest-timeout==2.4.0", - "textual==3.7.1", + "coverage>=7.0", # Required for numba compatibility (coverage.types) "requests-mock==1.12.1", + "terminaltexteffects==0.12.2", + "watchdog>=3.0.0", # Types "lxml-stubs>=0.5.1,<1", @@ -215,16 +255,20 @@ sim = [ # Simulation "mujoco>=3.3.4", "playground>=0.0.5", - "pygame>=2.6.1", ] -jetson-jp6-cuda126 = [ - # Jetson Jetpack 6.2 with CUDA 12.6 specific wheels - # Note: Alternative torch wheel from docs: https://developer.download.nvidia.com/compute/redist/jp/v61/pytorch/torch-2.5.0a0+872d972e41.nv24.08.17622132-cp310-cp310-linux_aarch64.whl - "torch @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/564/4d4458f1ba159/torch-2.8.0-cp310-cp310-linux_aarch64.whl", - "torchvision @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/1c0/3de08a69e9554/torchvision-0.23.0-cp310-cp310-linux_aarch64.whl", - "onnxruntime-gpu @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/4eb/e6a8902dc7708/onnxruntime_gpu-1.23.0-cp310-cp310-linux_aarch64.whl", - "xformers @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/731/15133b0ebb2b3/xformers-0.0.33+ac00641.d20250830-cp39-abi3-linux_aarch64.whl", +# NOTE: jetson-jp6-cuda126 extra is disabled due to 404 errors from wheel URLs +# The pypi.jetson-ai-lab.io URLs are currently unavailable. Update with working URLs when available. +# jetson-jp6-cuda126 = [ +# # Jetson Jetpack 6.2 with CUDA 12.6 specific wheels (aarch64 Linux only) +# "torch @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/.../torch-2.8.0-cp310-cp310-linux_aarch64.whl ; platform_machine == 'aarch64' and sys_platform == 'linux'", +# "torchvision @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/.../torchvision-0.23.0-cp310-cp310-linux_aarch64.whl ; platform_machine == 'aarch64' and sys_platform == 'linux'", +# "onnxruntime-gpu @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/.../onnxruntime_gpu-1.23.0-cp310-cp310-linux_aarch64.whl ; platform_machine == 'aarch64' and sys_platform == 'linux'", +# "xformers @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/.../xformers-0.0.33-cp39-abi3-linux_aarch64.whl ; platform_machine == 'aarch64' and sys_platform == 'linux'", +# ] + +drone = [ + "pymavlink" ] [tool.ruff] @@ -250,7 +294,10 @@ exclude = [ [tool.ruff.lint] extend-select = ["E", "W", "F", "B", "UP", "N", "I", "C90", "A", "RUF", "TCH"] # TODO: All of these should be fixed, but it's easier commit autofixes first -ignore = ["A001", "A002", "A004", "B008", "B017", "B018", "B019", "B023", "B024", "B026", "B027", "B904", "C901", "E402", "E501", "E721", "E722", "E741", "F401", "F403", "F405", "F811", "F821", "F821", "F821", "N801", "N802", "N803", "N806", "N812", "N813", "N813", "N816", "N817", "N999", "RUF001", "RUF002", "RUF003", "RUF006", "RUF009", "RUF012", "RUF034", "RUF043", "RUF059", "TC010", "UP007", "UP035"] +ignore = ["A001", "A002", "B008", "B017", "B019", "B023", "B024", "B026", "B904", "C901", "E402", "E501", "E721", "E722", "E741", "F401", "F403", "F811", "F821", "F821", "F821", "N801", "N802", "N803", "N806", "N812", "N813", "N813", "N816", "N817", "N999", "RUF002", "RUF003", "RUF006", "RUF009", "RUF012", "RUF034", "RUF043", "RUF059", "UP007"] + +[tool.ruff.lint.per-file-ignores] +"dimos/models/Detic/*" = ["ALL"] [tool.ruff.lint.isort] known-first-party = ["dimos"] @@ -258,13 +305,41 @@ combine-as-imports = true force-sort-within-sections = true [tool.mypy] -# mypy doesn't understand plum @dispatch decorator -# so we gave up on this check globally -disable_error_code = ["no-redef", "import-untyped", "import-not-found"] -files = [ - "dimos/msgs/**/*.py", - "dimos/protocol/**/*.py" +python_version = "3.12" +incremental = true +strict = true +warn_unused_ignores = false +exclude = "^dimos/models/Detic(/|$)|^dimos/rxpy_backpressure(/|$)|.*/test_.|.*/conftest.py*" + +[[tool.mypy.overrides]] +module = [ + "rclpy.*", + "std_msgs.*", + "geometry_msgs.*", + "sensor_msgs.*", + "nav_msgs.*", + "tf2_msgs.*", + "mujoco", + "mujoco_playground.*", + "etils", + "xarm.*", + "dimos_lcm.*", + "piper_sdk.*", + "plum.*", + "pycuda.*", + "pycuda", + "plotext", + "torchreid", + "open_clip", + "pyzed.*", + "pyzed", + "unitree_webrtc_connect.*", ] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["dimos.rxpy_backpressure", "dimos.rxpy_backpressure.*"] +follow_imports = "skip" [tool.pytest.ini_options] testpaths = ["dimos"] @@ -288,5 +363,22 @@ addopts = "-v -p no:warnings -ra --color=yes -m 'not vis and not benchmark and n asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" +[tool.largefiles] +max_size_kb = 50 +ignore = [ + "uv.lock", + "*/package-lock.json", + "dimos/dashboard/dimos.rbl", + "dimos/web/dimos_interface/themes.json", +] + +[tool.uv] +# Build dependencies for packages that don't declare them properly +extra-build-dependencies = { detectron2 = ["torch"], contact-graspnet-pytorch = ["numpy"] } +default-groups = [] +[tool.uv.sources] +clip = { git = "https://github.com/openai/CLIP.git" } +contact-graspnet-pytorch = { git = "https://github.com/dimensionalOS/contact_graspnet_pytorch.git" } +detectron2 = { git = "https://github.com/facebookresearch/detectron2.git", tag = "v0.6" } diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5faa7c8874..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,96 +0,0 @@ -opencv-python -python-dotenv -openai -anthropic>=0.19.0 -cerebras-cloud-sdk -numpy>=1.26.4,<2.0.0 -colorlog==6.9.0 -yapf==0.40.2 -typeguard -empy==3.3.4 -catkin_pkg -lark -plum-dispatch==2.5.7 - -# pycolmap -ffmpeg-python -pytest -python-dotenv -openai -tiktoken>=0.8.0 -Flask>=2.2 -python-multipart==0.0.20 -reactivex -git+https://github.com/dimensionalOS/rxpy-backpressure.git -pytest-asyncio==0.26.0 -asyncio==3.4.3 --e git+https://github.com/dimensionalOS/go2_webrtc_connect.git#egg=go2_webrtc_connect -# Web Extensions -fastapi>=0.115.6 -sse-starlette>=2.2.1 -uvicorn>=0.34.0 - -# Agent Memory -langchain-chroma>=0.1.4 -langchain-openai>=0.2.14 - -# Class Extraction -pydantic - -# Developer Specific -ipykernel - -# Audio -openai-whisper -soundfile - -#Hugging Face -transformers[torch]==4.49.0 - -#Vector Embedding -sentence_transformers - -# CTransforms GGUF - GPU required -ctransformers[cuda]==0.2.27 - -# Perception Dependencies -ultralytics>=8.3.70 -filterpy>=1.4.5 -scipy>=1.15.1 -opencv-python==4.10.0.84 -opencv-contrib-python==4.10.0.84 -scikit-learn -Pillow -mmengine>=0.10.3 -mmcv>=2.1.0 -timm>=1.0.15 -lap>=0.5.12 -xformers==0.0.20 - -# Detic -opencv-python -mss -timm -dataclasses -ftfy -regex -fasttext -scikit-learn -lvis -nltk -git+https://github.com/openai/CLIP.git -git+https://github.com/facebookresearch/detectron2.git@v0.6 - -# Mapping -open3d - -# Inference (CPU) -onnxruntime -onnx - -# Terminal colors -rich==14.0.0 - -# multiprocess -dask[complete]==2025.5.1 -git+https://github.com/dimensionalOS/python_lcm_msgs@main#egg=lcm_msgs diff --git a/setup.py b/setup.py index 15fa5aa750..013ff731a8 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright 2025 Dimensional Inc. +# Copyright 2025-2026 Dimensional Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,9 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + +from pybind11.setup_helpers import Pybind11Extension, build_ext from setuptools import find_packages, setup +# C++ extensions +ext_modules = [ + Pybind11Extension( + "dimos.navigation.replanning_a_star.min_cost_astar_ext", + [os.path.join("dimos", "navigation", "replanning_a_star", "min_cost_astar_cpp.cpp")], + extra_compile_args=[ + "-O3", # Maximum optimization + "-march=native", # Optimize for current CPU + "-ffast-math", # Fast floating point + ], + define_macros=[ + ("NDEBUG", "1"), + ], + ), +] + setup( packages=find_packages(), package_dir={"": "."}, + ext_modules=ext_modules, + cmdclass={"build_ext": build_ext}, ) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 8b13789179..0000000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/agent_manip_flow_fastapi_test.py b/tests/agent_manip_flow_fastapi_test.py deleted file mode 100644 index f8b6df4244..0000000000 --- a/tests/agent_manip_flow_fastapi_test.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module initializes and manages the video processing pipeline integrated with a web server. -It handles video capture, frame processing, and exposes the processed video streams via HTTP endpoints. -""" - -# ----- -# Standard library imports -import multiprocessing -import os - -from dotenv import load_dotenv - -# Third-party imports -from reactivex import operators as ops -from reactivex.disposable import CompositeDisposable -from reactivex.scheduler import ThreadPoolScheduler - -# Local application imports -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.video_operators import VideoOperators as vops -from dimos.stream.video_provider import VideoProvider -from dimos.web.fastapi_server import FastAPIServer - -# Load environment variables -load_dotenv() - - -def main(): - """ - Initializes and runs the video processing pipeline with web server output. - - This function orchestrates a video processing system that handles capture, processing, - and visualization of video streams. It demonstrates parallel processing capabilities - and various video manipulation techniques across multiple stages including capture - and processing at different frame rates, edge detection, and optical flow analysis. - - Raises: - RuntimeError: If video sources are unavailable or processing fails. - """ - CompositeDisposable() - - processor = FrameProcessor( - output_dir=f"{os.getcwd()}/assets/output/frames", delete_on_init=True - ) - - optimal_thread_count = multiprocessing.cpu_count() # Gets number of CPU cores - thread_pool_scheduler = ThreadPoolScheduler(optimal_thread_count) - - VIDEO_SOURCES = [ - f"{os.getcwd()}/assets/ldru.mp4", - f"{os.getcwd()}/assets/ldru_480p.mp4", - f"{os.getcwd()}/assets/trimmed_video_480p.mov", - f"{os.getcwd()}/assets/video-f30-480p.mp4", - "rtsp://192.168.50.207:8080/h264.sdp", - "rtsp://10.0.0.106:8080/h264.sdp", - ] - - VIDEO_SOURCE_INDEX = 3 - VIDEO_SOURCE_INDEX_2 = 2 - - my_video_provider = VideoProvider("Video File", video_source=VIDEO_SOURCES[VIDEO_SOURCE_INDEX]) - my_video_provider_2 = VideoProvider( - "Video File 2", video_source=VIDEO_SOURCES[VIDEO_SOURCE_INDEX_2] - ) - - video_stream_obs = my_video_provider.capture_video_as_observable(fps=120).pipe( - ops.subscribe_on(thread_pool_scheduler), - # Move downstream operations to thread pool for parallel processing - # Disabled: Evaluating performance impact - # ops.observe_on(thread_pool_scheduler), - vops.with_jpeg_export(processor, suffix="raw"), - vops.with_fps_sampling(fps=30), - vops.with_jpeg_export(processor, suffix="raw_slowed"), - ) - - video_stream_obs_2 = my_video_provider_2.capture_video_as_observable(fps=120).pipe( - ops.subscribe_on(thread_pool_scheduler), - # Move downstream operations to thread pool for parallel processing - # Disabled: Evaluating performance impact - # ops.observe_on(thread_pool_scheduler), - vops.with_jpeg_export(processor, suffix="raw_2"), - vops.with_fps_sampling(fps=30), - vops.with_jpeg_export(processor, suffix="raw_2_slowed"), - ) - - edge_detection_stream_obs = processor.process_stream_edge_detection(video_stream_obs).pipe( - vops.with_jpeg_export(processor, suffix="edge"), - ) - - optical_flow_relevancy_stream_obs = processor.process_stream_optical_flow_with_relevancy( - video_stream_obs - ) - - optical_flow_stream_obs = optical_flow_relevancy_stream_obs.pipe( - ops.do_action(lambda result: print(f"Optical Flow Relevancy Score: {result[1]}")), - vops.with_optical_flow_filtering(threshold=2.0), - ops.do_action(lambda _: print("Optical Flow Passed Threshold.")), - vops.with_jpeg_export(processor, suffix="optical"), - ) - - # - # ====== Agent Orchastrator (Qu.s Awareness, Temporality, Routing) ====== - # - - # Agent 1 - # my_agent = OpenAIAgent( - # "Agent 1", - # query="You are a robot. What do you see? Put a JSON with objects of what you see in the format {object, description}.") - # my_agent.subscribe_to_image_processing(slowed_video_stream_obs) - # disposables.add(my_agent.disposables) - - # # Agent 2 - # my_agent_two = OpenAIAgent( - # "Agent 2", - # query="This is a visualization of dense optical flow. What movement(s) have occured? Put a JSON with mapped directions you see in the format {direction, probability, english_description}.") - # my_agent_two.subscribe_to_image_processing(optical_flow_stream_obs) - # disposables.add(my_agent_two.disposables) - - # - # ====== Create and start the FastAPI server ====== - # - - # Will be visible at http://[host]:[port]/video_feed/[key] - streams = { - "video_one": video_stream_obs, - "video_two": video_stream_obs_2, - "edge_detection": edge_detection_stream_obs, - "optical_flow": optical_flow_stream_obs, - } - fast_api_server = FastAPIServer(port=5555, **streams) - fast_api_server.run() - - -if __name__ == "__main__": - main() diff --git a/tests/agent_manip_flow_flask_test.py b/tests/agent_manip_flow_flask_test.py deleted file mode 100644 index e96c6f2d20..0000000000 --- a/tests/agent_manip_flow_flask_test.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module initializes and manages the video processing pipeline integrated with a web server. -It handles video capture, frame processing, and exposes the processed video streams via HTTP endpoints. -""" - -# ----- -# Standard library imports -import multiprocessing -import os - -from dotenv import load_dotenv - -# Third-party imports -from flask import Flask -from reactivex import interval, operators as ops, zip -from reactivex.disposable import CompositeDisposable -from reactivex.scheduler import ThreadPoolScheduler - -# Local application imports -from dimos.agents.agent import OpenAIAgent -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.video_operators import VideoOperators as vops -from dimos.stream.video_provider import VideoProvider -from dimos.web.flask_server import FlaskServer - -# Load environment variables -load_dotenv() - -app = Flask(__name__) - - -def main(): - """ - Initializes and runs the video processing pipeline with web server output. - - This function orchestrates a video processing system that handles capture, processing, - and visualization of video streams. It demonstrates parallel processing capabilities - and various video manipulation techniques across multiple stages including capture - and processing at different frame rates, edge detection, and optical flow analysis. - - Raises: - RuntimeError: If video sources are unavailable or processing fails. - """ - disposables = CompositeDisposable() - - processor = FrameProcessor( - output_dir=f"{os.getcwd()}/assets/output/frames", delete_on_init=True - ) - - optimal_thread_count = multiprocessing.cpu_count() # Gets number of CPU cores - thread_pool_scheduler = ThreadPoolScheduler(optimal_thread_count) - - VIDEO_SOURCES = [ - f"{os.getcwd()}/assets/ldru.mp4", - f"{os.getcwd()}/assets/ldru_480p.mp4", - f"{os.getcwd()}/assets/trimmed_video_480p.mov", - f"{os.getcwd()}/assets/video-f30-480p.mp4", - f"{os.getcwd()}/assets/video.mov", - "rtsp://192.168.50.207:8080/h264.sdp", - "rtsp://10.0.0.106:8080/h264.sdp", - f"{os.getcwd()}/assets/people_1080p_24fps.mp4", - ] - - VIDEO_SOURCE_INDEX = 4 - - my_video_provider = VideoProvider("Video File", video_source=VIDEO_SOURCES[VIDEO_SOURCE_INDEX]) - - video_stream_obs = my_video_provider.capture_video_as_observable(fps=120).pipe( - ops.subscribe_on(thread_pool_scheduler), - # Move downstream operations to thread pool for parallel processing - # Disabled: Evaluating performance impact - # ops.observe_on(thread_pool_scheduler), - # vops.with_jpeg_export(processor, suffix="raw"), - vops.with_fps_sampling(fps=30), - # vops.with_jpeg_export(processor, suffix="raw_slowed"), - ) - - processor.process_stream_edge_detection(video_stream_obs).pipe( - # vops.with_jpeg_export(processor, suffix="edge"), - ) - - optical_flow_relevancy_stream_obs = processor.process_stream_optical_flow(video_stream_obs) - - optical_flow_stream_obs = optical_flow_relevancy_stream_obs.pipe( - # ops.do_action(lambda result: print(f"Optical Flow Relevancy Score: {result[1]}")), - # vops.with_optical_flow_filtering(threshold=2.0), - # ops.do_action(lambda _: print(f"Optical Flow Passed Threshold.")), - # vops.with_jpeg_export(processor, suffix="optical") - ) - - # - # ====== Agent Orchastrator (Qu.s Awareness, Temporality, Routing) ====== - # - - # Observable that emits every 2 seconds - secondly_emission = interval(2, scheduler=thread_pool_scheduler).pipe( - ops.map(lambda x: f"Second {x + 1}"), - # ops.take(30) - ) - - # Agent 1 - my_agent = OpenAIAgent( - "Agent 1", - query="You are a robot. What do you see? Put a JSON with objects of what you see in the format {object, description}.", - json_mode=False, - ) - - # Create an agent for each subset of questions that it would be theroized to handle. - # Set std. template/blueprints, and devs will add to that likely. - - ai_1_obs = video_stream_obs.pipe( - # vops.with_fps_sampling(fps=30), - # ops.throttle_first(1), - vops.with_jpeg_export(processor, suffix="open_ai_agent_1"), - ops.take(30), - ops.replay(buffer_size=30, scheduler=thread_pool_scheduler), - ) - ai_1_obs.connect() - - ai_1_repeat_obs = ai_1_obs.pipe(ops.repeat()) - - my_agent.subscribe_to_image_processing(ai_1_obs) - disposables.add(my_agent.disposables) - - # Agent 2 - my_agent_two = OpenAIAgent( - "Agent 2", - query="This is a visualization of dense optical flow. What movement(s) have occured? Put a JSON with mapped directions you see in the format {direction, probability, english_description}.", - max_input_tokens_per_request=1000, - max_output_tokens_per_request=300, - json_mode=False, - model_name="gpt-4o-2024-08-06", - ) - - ai_2_obs = optical_flow_stream_obs.pipe( - # vops.with_fps_sampling(fps=30), - # ops.throttle_first(1), - vops.with_jpeg_export(processor, suffix="open_ai_agent_2"), - ops.take(30), - ops.replay(buffer_size=30, scheduler=thread_pool_scheduler), - ) - ai_2_obs.connect() - - ai_2_repeat_obs = ai_2_obs.pipe(ops.repeat()) - - # Combine emissions using zip - ai_1_secondly_repeating_obs = zip(secondly_emission, ai_1_repeat_obs).pipe( - # ops.do_action(lambda s: print(f"AI 1 - Emission Count: {s[0]}")), - ops.map(lambda r: r[1]), - ) - - # Combine emissions using zip - ai_2_secondly_repeating_obs = zip(secondly_emission, ai_2_repeat_obs).pipe( - # ops.do_action(lambda s: print(f"AI 2 - Emission Count: {s[0]}")), - ops.map(lambda r: r[1]), - ) - - my_agent_two.subscribe_to_image_processing(ai_2_obs) - disposables.add(my_agent_two.disposables) - - # - # ====== Create and start the Flask server ====== - # - - # Will be visible at http://[host]:[port]/video_feed/[key] - flask_server = FlaskServer( - # video_one=video_stream_obs, - # edge_detection=edge_detection_stream_obs, - # optical_flow=optical_flow_stream_obs, - OpenAIAgent_1=ai_1_secondly_repeating_obs, - OpenAIAgent_2=ai_2_secondly_repeating_obs, - ) - - flask_server.run(threaded=True) - - -if __name__ == "__main__": - main() diff --git a/tests/agent_memory_test.py b/tests/agent_memory_test.py deleted file mode 100644 index c2c41ad502..0000000000 --- a/tests/agent_memory_test.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -# ----- -from dotenv import load_dotenv - -load_dotenv() - -from dimos.agents.memory.chroma_impl import OpenAISemanticMemory - -agent_memory = OpenAISemanticMemory() -print("Initialization done.") - -agent_memory.add_vector("id0", "Food") -agent_memory.add_vector("id1", "Cat") -agent_memory.add_vector("id2", "Mouse") -agent_memory.add_vector("id3", "Bike") -agent_memory.add_vector("id4", "Dog") -agent_memory.add_vector("id5", "Tricycle") -agent_memory.add_vector("id6", "Car") -agent_memory.add_vector("id7", "Horse") -agent_memory.add_vector("id8", "Vehicle") -agent_memory.add_vector("id6", "Red") -agent_memory.add_vector("id7", "Orange") -agent_memory.add_vector("id8", "Yellow") -print("Adding vectors done.") - -print(agent_memory.get_vector("id1")) -print("Done retrieving sample vector.") - -results = agent_memory.query("Colors") -print(results) -print("Done querying agent memory (basic).") - -results = agent_memory.query("Colors", similarity_threshold=0.2) -print(results) -print("Done querying agent memory (similarity_threshold=0.2).") - -results = agent_memory.query("Colors", n_results=2) -print(results) -print("Done querying agent memory (n_results=2).") - -results = agent_memory.query("Colors", n_results=19, similarity_threshold=0.45) -print(results) -print("Done querying agent memory (n_results=19, similarity_threshold=0.45).") diff --git a/tests/genesissim/stream_camera.py b/tests/genesissim/stream_camera.py deleted file mode 100644 index 9346f58595..0000000000 --- a/tests/genesissim/stream_camera.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.simulation.genesis import GenesisSimulator, GenesisStream - - -def main(): - # Add multiple entities at once - entities = [ - {"type": "primitive", "params": {"shape": "plane"}}, - {"type": "mjcf", "path": "xml/franka_emika_panda/panda.xml"}, - ] - # Initialize simulator - sim = GenesisSimulator(headless=True, entities=entities) - - # You can also add entity individually - sim.add_entity("primitive", shape="box", size=[0.5, 0.5, 0.5], pos=[0, 1, 0.5]) - - # Create stream with custom settings - stream = GenesisStream( - simulator=sim, - width=1280, # Genesis default resolution - height=960, - fps=60, - camera_path="/camera", # Genesis uses simpler camera paths - annotator_type="rgb", # Can be 'rgb' or 'normals' - transport="tcp", - rtsp_url="rtsp://mediamtx:8554/stream", - ) - - # Start streaming - try: - stream.stream() - except KeyboardInterrupt: - print("\n[Stream] Received keyboard interrupt, stopping stream...") - finally: - try: - stream.cleanup() - finally: - sim.close() - - -if __name__ == "__main__": - main() diff --git a/tests/isaacsim/run-isaacsim-docker.sh b/tests/isaacsim/run-isaacsim-docker.sh deleted file mode 100644 index a9ab642236..0000000000 --- a/tests/isaacsim/run-isaacsim-docker.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Run Isaac Sim container with display and GPU support -sudo docker run --network rtsp_net --name isaac-sim --entrypoint bash -it --runtime=nvidia --gpus all -e "ACCEPT_EULA=Y" --rm \ - -e "PRIVACY_CONSENT=Y" \ - -v ~/docker/isaac-sim/cache/kit:/isaac-sim/kit/cache:rw \ - -v ~/docker/isaac-sim/cache/ov:/root/.cache/ov:rw \ - -v ~/docker/isaac-sim/cache/pip:/root/.cache/pip:rw \ - -v ~/docker/isaac-sim/cache/glcache:/root/.cache/nvidia/GLCache:rw \ - -v ~/docker/isaac-sim/cache/computecache:/root/.nv/ComputeCache:rw \ - -v ~/docker/isaac-sim/logs:/root/.nvidia-omniverse/logs:rw \ - -v ~/docker/isaac-sim/data:/root/.local/share/ov/data:rw \ - -v ~/docker/isaac-sim/documents:/root/Documents:rw \ - -v ~/dimos:/dimos:rw \ - nvcr.io/nvidia/isaac-sim:4.2.0 - -/isaac-sim/python.sh -m pip install -r /dimos/tests/isaacsim/requirements.txt -apt-get update -apt-get install -y ffmpeg -/isaac-sim/python.sh /dimos/tests/isaacsim/stream_camera.py \ No newline at end of file diff --git a/tests/isaacsim/setup_ec2.sh b/tests/isaacsim/setup_ec2.sh deleted file mode 100644 index 379891e334..0000000000 --- a/tests/isaacsim/setup_ec2.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -sudo apt-get update -sudo apt install build-essential -y -sudo apt-get install -y nvidia-driver-535 -sudo reboot -sudo apt install -y nvidia-cuda-toolkit -nvidia-smi - - -# Docker installation using the convenience script -curl -fsSL https://get.docker.com -o get-docker.sh -sudo sh get-docker.sh - -# Post-install steps for Docker -sudo groupadd docker -sudo usermod -aG docker $USER -newgrp docker - -#Verify Docker - -# Configure the repository -curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \ - && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ - sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ - sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list \ - && \ - sudo apt-get update - -# Install the NVIDIA Container Toolkit packages -sudo apt-get install -y nvidia-container-toolkit -sudo systemctl restart docker - -# Configure the container runtime -sudo nvidia-ctk runtime configure --runtime=docker -sudo systemctl restart docker - -# Verify NVIDIA Container Toolkit -sudo docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi - -# Full isaac sim container -sudo docker pull nvcr.io/nvidia/isaac-sim:4.2.0 - diff --git a/tests/isaacsim/setup_isaacsim_python.sh b/tests/isaacsim/setup_isaacsim_python.sh deleted file mode 100644 index 3ed5d8e627..0000000000 --- a/tests/isaacsim/setup_isaacsim_python.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -sudo apt install python3.10-venv -python3.10 -m venv env_isaacsim -source env_isaacsim/bin/activate - -# Install pip packages -pip install isaacsim==4.2.0.2 --extra-index-url https://pypi.nvidia.com -pip install isaacsim-extscache-physics==4.2.0.2 -pip install isaacsim-extscache-kit==4.2.0.2 -pip install isaacsim-extscache-kit-sdk==4.2.0.2 --extra-index-url https://pypi.nvidia.com - -export OMNI_KIT_ACCEPT_EULA=YES - diff --git a/tests/isaacsim/setup_ros.sh b/tests/isaacsim/setup_ros.sh deleted file mode 100644 index 976487f299..0000000000 --- a/tests/isaacsim/setup_ros.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -# Add ROS 2 repository -sudo apt update && sudo apt install -y software-properties-common -sudo add-apt-repository universe -y -sudo apt update && sudo apt install curl -y -sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null - -# Update package lists -sudo apt update -sudo apt upgrade -y - -# Install ROS 2 Humble (latest LTS for Ubuntu 22.04) -sudo apt install -y ros-humble-desktop -sudo apt install -y ros-humble-ros-base -sudo apt install -y ros-dev-tools - -# Install additional ROS 2 packages -sudo apt install -y python3-rosdep -sudo apt install -y python3-colcon-common-extensions - -# Initialize rosdep -sudo rosdep init -rosdep update - -# Setup environment variables -echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc -source ~/.bashrc - -# Install additional dependencies that might be useful -sudo apt install -y python3-pip -pip3 install --upgrade pip -pip3 install transforms3d numpy scipy -sudo apt install -y python3.10-venv - -# Create ROS 2 workspace -mkdir -p ~/ros2_ws/src -cd ~/ros2_ws -colcon build - -# Source the workspace -echo "source ~/ros2_ws/install/setup.bash" >> ~/.bashrc -source ~/.bashrc - -# Print success message -echo "ROS 2 Humble installation completed successfully!" diff --git a/tests/isaacsim/stream_camera.py b/tests/isaacsim/stream_camera.py deleted file mode 100644 index 7aa25e7e38..0000000000 --- a/tests/isaacsim/stream_camera.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dimos.simulation.isaac import IsaacSimulator, IsaacStream - - -def main(): - # Initialize simulator - sim = IsaacSimulator(headless=True) - - # Create stream with custom settings - stream = IsaacStream( - simulator=sim, - width=1920, - height=1080, - fps=60, - camera_path="/World/alfred_parent_prim/alfred_base_descr/chest_cam_rgb_camera_frame/chest_cam", - annotator_type="rgb", - transport="tcp", - rtsp_url="rtsp://mediamtx:8554/stream", - usd_path=f"{os.getcwd()}/assets/TestSim3.usda", - ) - - # Start streaming - stream.stream() - - -if __name__ == "__main__": - main() diff --git a/tests/mockdata/costmap.pickle b/tests/mockdata/costmap.pickle deleted file mode 100644 index a29199e841..0000000000 Binary files a/tests/mockdata/costmap.pickle and /dev/null differ diff --git a/tests/mockdata/vegas.pickle b/tests/mockdata/vegas.pickle deleted file mode 100644 index a7da5309c0..0000000000 Binary files a/tests/mockdata/vegas.pickle and /dev/null differ diff --git a/tests/run.py b/tests/run.py deleted file mode 100644 index d64bbb11c0..0000000000 --- a/tests/run.py +++ /dev/null @@ -1,352 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import atexit -import logging -import os -import signal -import threading -import time -import warnings - -from dotenv import load_dotenv -import reactivex as rx -import reactivex.operators as ops - -from dimos.agents.claude_agent import ClaudeAgent -from dimos.perception.object_detection_stream import ObjectDetectionStream - -# from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 -from dimos.skills.kill_skill import KillSkill -from dimos.skills.navigation import Explore, GetPose, NavigateToGoal, NavigateWithText -from dimos.skills.observe import Observe -from dimos.skills.observe_stream import ObserveStream -from dimos.skills.unitree.unitree_speak import UnitreeSpeak -from dimos.stream.audio.pipelines import stt -from dimos.types.vector import Vector -from dimos.utils.reactive import backpressure -from dimos.web.robot_web_interface import RobotWebInterface -from dimos.web.websocket_vis.server import WebsocketVis - -# Filter out known WebRTC warnings that don't affect functionality -warnings.filterwarnings("ignore", message="coroutine.*was never awaited") -warnings.filterwarnings("ignore", message=".*RTCSctpTransport.*") - -# Set up logging to reduce asyncio noise -logging.getLogger("asyncio").setLevel(logging.ERROR) - -# Load API key from environment -load_dotenv() - -# Allow command line arguments to control spatial memory parameters -import argparse - - -def parse_arguments(): - parser = argparse.ArgumentParser( - description="Run the robot with optional spatial memory parameters" - ) - parser.add_argument( - "--new-memory", action="store_true", help="Create a new spatial memory from scratch" - ) - parser.add_argument( - "--spatial-memory-dir", type=str, help="Directory for storing spatial memory data" - ) - return parser.parse_args() - - -args = parse_arguments() - -# Initialize robot with spatial memory parameters - using WebRTC mode instead of "ai" -robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - mode="normal", -) - - -# Add graceful shutdown handling to prevent WebRTC task destruction errors -def cleanup_robot(): - print("Cleaning up robot connection...") - try: - # Make cleanup non-blocking to avoid hangs - def quick_cleanup(): - try: - robot.liedown() - except: - pass - - # Run cleanup in a separate thread with timeout - cleanup_thread = threading.Thread(target=quick_cleanup) - cleanup_thread.daemon = True - cleanup_thread.start() - cleanup_thread.join(timeout=3.0) # Max 3 seconds for cleanup - - # Force stop the robot's WebRTC connection - try: - robot.stop() - except: - pass - - except Exception as e: - print(f"Error during cleanup: {e}") - # Continue anyway - - -atexit.register(cleanup_robot) - - -def signal_handler(signum, frame): - print("Received shutdown signal, cleaning up...") - try: - cleanup_robot() - except: - pass - # Force exit if cleanup hangs - os._exit(0) - - -signal.signal(signal.SIGINT, signal_handler) -signal.signal(signal.SIGTERM, signal_handler) - -# Initialize WebSocket visualization -websocket_vis = WebsocketVis() -websocket_vis.start() -websocket_vis.connect(robot.global_planner.vis_stream()) - - -def msg_handler(msgtype, data): - if msgtype == "click": - print(f"Received click at position: {data['position']}") - - try: - print("Setting goal...") - - # Instead of disabling visualization, make it timeout-safe - original_vis = robot.global_planner.vis - - def safe_vis(name, drawable): - """Visualization wrapper that won't block on timeouts""" - try: - # Use a separate thread for visualization to avoid blocking - def vis_update(): - try: - original_vis(name, drawable) - except Exception as e: - print(f"Visualization update failed (non-critical): {e}") - - vis_thread = threading.Thread(target=vis_update) - vis_thread.daemon = True - vis_thread.start() - # Don't wait for completion - let it run asynchronously - except Exception as e: - print(f"Visualization setup failed (non-critical): {e}") - - robot.global_planner.vis = safe_vis - robot.global_planner.set_goal(Vector(data["position"])) - robot.global_planner.vis = original_vis - - print("Goal set successfully") - except Exception as e: - print(f"Error setting goal: {e}") - import traceback - - traceback.print_exc() - - -def threaded_msg_handler(msgtype, data): - print(f"Processing message: {msgtype}") - - # Create a dedicated event loop for goal setting to avoid conflicts - def run_with_dedicated_loop(): - try: - # Use asyncio.run which creates and manages its own event loop - # This won't conflict with the robot's or websocket's event loops - async def async_msg_handler(): - msg_handler(msgtype, data) - - asyncio.run(async_msg_handler()) - print("Goal setting completed successfully") - except Exception as e: - print(f"Error in goal setting thread: {e}") - import traceback - - traceback.print_exc() - - thread = threading.Thread(target=run_with_dedicated_loop) - thread.daemon = True - thread.start() - - -websocket_vis.msg_handler = threaded_msg_handler - - -def newmap(msg): - return ["costmap", robot.map.costmap.smudge()] - - -websocket_vis.connect(robot.map_stream.pipe(ops.map(newmap))) -websocket_vis.connect(robot.odom_stream().pipe(ops.map(lambda pos: ["robot_pos", pos.pos.to_2d()]))) - -# Create a subject for agent responses -agent_response_subject = rx.subject.Subject() -agent_response_stream = agent_response_subject.pipe(ops.share()) -local_planner_viz_stream = robot.local_planner_viz_stream.pipe(ops.share()) -audio_subject = rx.subject.Subject() - -# Initialize object detection stream -min_confidence = 0.6 -class_filter = None # No class filtering - -# Create video stream from robot's camera -video_stream = backpressure(robot.get_video_stream()) # WebRTC doesn't use ROS video stream - -# # Initialize ObjectDetectionStream with robot -object_detector = ObjectDetectionStream( - camera_intrinsics=robot.camera_intrinsics, - class_filter=class_filter, - get_pose=robot.get_pose, - video_stream=video_stream, - draw_masks=True, -) - -# # Create visualization stream for web interface -viz_stream = backpressure(object_detector.get_stream()).pipe( - ops.share(), - ops.map(lambda x: x["viz_frame"] if x is not None else None), - ops.filter(lambda x: x is not None), -) - -# # Get the formatted detection stream -formatted_detection_stream = object_detector.get_formatted_stream().pipe( - ops.filter(lambda x: x is not None) -) - - -# Create a direct mapping that combines detection data with locations -def combine_with_locations(object_detections): - # Get locations from spatial memory - try: - spatial_memory = robot.get_spatial_memory() - if spatial_memory is None: - # If spatial memory is disabled, just return the object detections - return object_detections - - locations = spatial_memory.get_robot_locations() - - # Format the locations section - locations_text = "\n\nSaved Robot Locations:\n" - if locations: - for loc in locations: - locations_text += f"- {loc.name}: Position ({loc.position[0]:.2f}, {loc.position[1]:.2f}, {loc.position[2]:.2f}), " - locations_text += f"Rotation ({loc.rotation[0]:.2f}, {loc.rotation[1]:.2f}, {loc.rotation[2]:.2f})\n" - else: - locations_text += "None\n" - - # Simply concatenate the strings - return object_detections + locations_text - except Exception as e: - print(f"Error adding locations: {e}") - return object_detections - - -# Create the combined stream with a simple pipe operation -enhanced_data_stream = formatted_detection_stream.pipe(ops.map(combine_with_locations), ops.share()) - -streams = { - "unitree_video": robot.get_video_stream(), # Changed from get_ros_video_stream to get_video_stream for WebRTC - "local_planner_viz": local_planner_viz_stream, - "object_detection": viz_stream, # Uncommented object detection -} -text_streams = { - "agent_responses": agent_response_stream, -} - -web_interface = RobotWebInterface( - port=5555, text_streams=text_streams, audio_subject=audio_subject, **streams -) - -stt_node = stt() -stt_node.consume_audio(audio_subject.pipe(ops.share())) - -# Read system query from prompt.txt file -with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets/agent/prompt.txt")) as f: - system_query = f.read() - -# Create a ClaudeAgent instance -agent = ClaudeAgent( - dev_name="test_agent", - input_query_stream=stt_node.emit_text(), - # input_query_stream=web_interface.query_stream, - input_data_stream=enhanced_data_stream, - skills=robot.get_skills(), - system_query=system_query, - model_name="claude-3-5-haiku-latest", - thinking_budget_tokens=0, - max_output_tokens_per_request=8192, - # model_name="llama-4-scout-17b-16e-instruct", -) - -# tts_node = tts() -# tts_node.consume_text(agent.get_response_observable()) - -robot_skills = robot.get_skills() -robot_skills.add(ObserveStream) -robot_skills.add(Observe) -robot_skills.add(KillSkill) -robot_skills.add(NavigateWithText) -# robot_skills.add(FollowHuman) # TODO: broken -robot_skills.add(GetPose) -robot_skills.add(UnitreeSpeak) # Re-enable Speak skill -robot_skills.add(NavigateToGoal) -robot_skills.add(Explore) - -robot_skills.create_instance("ObserveStream", robot=robot, agent=agent) -robot_skills.create_instance("Observe", robot=robot, agent=agent) -robot_skills.create_instance("KillSkill", robot=robot, skill_library=robot_skills) -robot_skills.create_instance("NavigateWithText", robot=robot) -# robot_skills.create_instance("FollowHuman", robot=robot) -robot_skills.create_instance("GetPose", robot=robot) -robot_skills.create_instance("NavigateToGoal", robot=robot) -robot_skills.create_instance("Explore", robot=robot) -robot_skills.create_instance("UnitreeSpeak", robot=robot) # Now only needs robot instance - -# Subscribe to agent responses and send them to the subject -agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) - -print("ObserveStream and Kill skills registered and ready for use") -print("Created memory.txt file") - -# Start web interface in a separate thread to avoid blocking -web_thread = threading.Thread(target=web_interface.run) -web_thread.daemon = True -web_thread.start() - -try: - while True: - # Main loop - can add robot movement or other logic here - time.sleep(0.01) - -except KeyboardInterrupt: - print("Stopping robot") - robot.liedown() -except Exception as e: - print(f"Unexpected error in main loop: {e}") - import traceback - - traceback.print_exc() -finally: - print("Cleaning up...") - cleanup_robot() diff --git a/tests/run_go2_ros.py b/tests/run_go2_ros.py deleted file mode 100644 index bc083a3a57..0000000000 --- a/tests/run_go2_ros.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import time - -from dimos.robot.unitree.unitree_go2 import UnitreeGo2, WebRTCConnectionMethod -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl - - -def get_env_var(var_name, default=None, required=False): - """Get environment variable with validation.""" - value = os.getenv(var_name, default) - if value == "": - value = default - if required and not value: - raise ValueError(f"{var_name} environment variable is required") - return value - - -if __name__ == "__main__": - # Get configuration from environment variables - robot_ip = get_env_var("ROBOT_IP") - connection_method = get_env_var("CONNECTION_METHOD", "LocalSTA") - serial_number = get_env_var("SERIAL_NUMBER", None) - output_dir = get_env_var("ROS_OUTPUT_DIR", os.path.join(os.getcwd(), "assets/output/ros")) - - # Ensure output directory exists - os.makedirs(output_dir, exist_ok=True) - print(f"Ensuring output directory exists: {output_dir}") - - use_ros = True - use_webrtc = False - # Convert connection method string to enum - connection_method = getattr(WebRTCConnectionMethod, connection_method) - - print("Initializing UnitreeGo2...") - print("Configuration:") - print(f" IP: {robot_ip}") - print(f" Connection Method: {connection_method}") - print(f" Serial Number: {serial_number if serial_number else 'Not provided'}") - print(f" Output Directory: {output_dir}") - - if use_ros: - ros_control = UnitreeROSControl(node_name="unitree_go2", use_raw=True) - else: - ros_control = None - - robot = UnitreeGo2( - ip=robot_ip, - connection_method=connection_method, - serial_number=serial_number, - output_dir=output_dir, - ros_control=ros_control, - use_ros=use_ros, - use_webrtc=use_webrtc, - ) - time.sleep(5) - try: - # Start perception - print("\nStarting perception system...") - - # Get the processed stream - processed_stream = robot.get_ros_video_stream(fps=30) - - # Create frame counter for unique filenames - frame_count = 0 - - # Create a subscriber to handle the frames - def handle_frame(frame): - global frame_count - frame_count += 1 - - try: - # Save frame to output directory if desired for debugging frame streaming - # MAKE SURE TO CHANGE OUTPUT DIR depending on if running in ROS or local - # frame_path = os.path.join(output_dir, f"frame_{frame_count:04d}.jpg") - # success = cv2.imwrite(frame_path, frame) - # print(f"Frame #{frame_count} {'saved successfully' if success else 'failed to save'} to {frame_path}") - pass - - except Exception as e: - print(f"Error in handle_frame: {e}") - import traceback - - print(traceback.format_exc()) - - def handle_error(error): - print(f"Error in stream: {error}") - - def handle_completion(): - print("Stream completed") - - # Subscribe to the stream - print("Creating subscription...") - try: - subscription = processed_stream.subscribe( - on_next=handle_frame, - on_error=lambda e: print(f"Subscription error: {e}"), - on_completed=lambda: print("Subscription completed"), - ) - print("Subscription created successfully") - except Exception as e: - print(f"Error creating subscription: {e}") - - time.sleep(5) - - # First put the robot in a good starting state - print("Running recovery stand...") - robot.webrtc_req(api_id=1006) # RecoveryStand - - # Queue 20 WebRTC requests back-to-back - print("\nšŸ¤– QUEUEING WEBRTC COMMANDS BACK-TO-BACK FOR TESTING UnitreeGo2šŸ¤–\n") - - # Dance 1 - robot.webrtc_req(api_id=1033) - print("Queued: WiggleHips (1033)") - - robot.reverse(distance=0.2, speed=0.5) - print("Queued: Reverse 0.5m at 0.5m/s") - - # Wiggle Hips - robot.webrtc_req(api_id=1033) - print("Queued: WiggleHips (1033)") - - robot.move(distance=0.2, speed=0.5) - print("Queued: Move forward 1.0m at 0.5m/s") - - robot.webrtc_req(api_id=1017) - print("Queued: Stretch (1017)") - - robot.move(distance=0.2, speed=0.5) - print("Queued: Move forward 1.0m at 0.5m/s") - - robot.webrtc_req(api_id=1017) - print("Queued: Stretch (1017)") - - robot.reverse(distance=0.2, speed=0.5) - print("Queued: Reverse 0.5m at 0.5m/s") - - robot.webrtc_req(api_id=1017) - print("Queued: Stretch (1017)") - robot.spin(degrees=-90.0, speed=45.0) - print("Queued: Spin right 90 degrees at 45 degrees/s") - - robot.spin(degrees=90.0, speed=45.0) - print("Queued: Spin left 90 degrees at 45 degrees/s") - - # To prevent termination - while True: - time.sleep(0.1) - - except KeyboardInterrupt: - print("\nStopping perception...") - if "subscription" in locals(): - subscription.dispose() - except Exception as e: - print(f"Error in main loop: {e}") - finally: - # Cleanup - print("Cleaning up resources...") - if "subscription" in locals(): - subscription.dispose() - del robot - print("Cleanup complete.") diff --git a/tests/run_navigation_only.py b/tests/run_navigation_only.py deleted file mode 100644 index 947da9c3a2..0000000000 --- a/tests/run_navigation_only.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import asyncio -import atexit -import logging -import os -import signal -import threading -import time -import warnings - -from dotenv import load_dotenv -import reactivex.operators as ops - -from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 -from dimos.types.vector import Vector -from dimos.web.robot_web_interface import RobotWebInterface -from dimos.web.websocket_vis.server import WebsocketVis - -# logging.basicConfig(level=logging.DEBUG) - -# Filter out known WebRTC warnings that don't affect functionality -warnings.filterwarnings("ignore", message="coroutine.*was never awaited") -warnings.filterwarnings("ignore", message=".*RTCSctpTransport.*") - -# Set up logging to reduce asyncio noise -logging.getLogger("asyncio").setLevel(logging.ERROR) - -load_dotenv() -robot = UnitreeGo2(ip=os.getenv("ROBOT_IP"), mode="normal", enable_perception=False) - - -# Add graceful shutdown handling to prevent WebRTC task destruction errors -def cleanup_robot(): - print("Cleaning up robot connection...") - try: - # Make cleanup non-blocking to avoid hangs - def quick_cleanup(): - try: - robot.liedown() - except: - pass - - # Run cleanup in a separate thread with timeout - cleanup_thread = threading.Thread(target=quick_cleanup) - cleanup_thread.daemon = True - cleanup_thread.start() - cleanup_thread.join(timeout=3.0) # Max 3 seconds for cleanup - - # Force stop the robot's WebRTC connection - try: - robot.stop() - except: - pass - - except Exception as e: - print(f"Error during cleanup: {e}") - # Continue anyway - - -atexit.register(cleanup_robot) - - -def signal_handler(signum, frame): - print("Received shutdown signal, cleaning up...") - try: - cleanup_robot() - except: - pass - # Force exit if cleanup hangs - os._exit(0) - - -signal.signal(signal.SIGINT, signal_handler) -signal.signal(signal.SIGTERM, signal_handler) - -websocket_vis = WebsocketVis() -websocket_vis.start() -websocket_vis.connect(robot.global_planner.vis_stream()) - - -def msg_handler(msgtype, data): - if msgtype == "click": - print(f"Received click at position: {data['position']}") - - try: - print("Setting goal...") - - # Instead of disabling visualization, make it timeout-safe - original_vis = robot.global_planner.vis - - def safe_vis(name, drawable): - """Visualization wrapper that won't block on timeouts""" - try: - # Use a separate thread for visualization to avoid blocking - def vis_update(): - try: - original_vis(name, drawable) - except Exception as e: - print(f"Visualization update failed (non-critical): {e}") - - vis_thread = threading.Thread(target=vis_update) - vis_thread.daemon = True - vis_thread.start() - # Don't wait for completion - let it run asynchronously - except Exception as e: - print(f"Visualization setup failed (non-critical): {e}") - - robot.global_planner.vis = safe_vis - robot.global_planner.set_goal(Vector(data["position"])) - robot.global_planner.vis = original_vis - - print("Goal set successfully") - except Exception as e: - print(f"Error setting goal: {e}") - import traceback - - traceback.print_exc() - - -def threaded_msg_handler(msgtype, data): - print(f"Processing message: {msgtype}") - - # Create a dedicated event loop for goal setting to avoid conflicts - def run_with_dedicated_loop(): - try: - # Use asyncio.run which creates and manages its own event loop - # This won't conflict with the robot's or websocket's event loops - async def async_msg_handler(): - msg_handler(msgtype, data) - - asyncio.run(async_msg_handler()) - print("Goal setting completed successfully") - except Exception as e: - print(f"Error in goal setting thread: {e}") - import traceback - - traceback.print_exc() - - thread = threading.Thread(target=run_with_dedicated_loop) - thread.daemon = True - thread.start() - - -websocket_vis.msg_handler = threaded_msg_handler - -print("standing up") -robot.standup() -print("robot is up") - - -def newmap(msg): - return ["costmap", robot.map.costmap.smudge()] - - -websocket_vis.connect(robot.map_stream.pipe(ops.map(newmap))) -websocket_vis.connect(robot.odom_stream().pipe(ops.map(lambda pos: ["robot_pos", pos.pos.to_2d()]))) - -local_planner_viz_stream = robot.local_planner_viz_stream.pipe(ops.share()) - -# Add RobotWebInterface with video stream -streams = {"unitree_video": robot.get_video_stream(), "local_planner_viz": local_planner_viz_stream} -web_interface = RobotWebInterface(port=5555, **streams) -web_interface.run() - -try: - while True: - # robot.move_vel(Vector(0.1, 0.1, 0.1)) - time.sleep(0.01) - -except KeyboardInterrupt: - print("Stopping robot") - robot.liedown() -except Exception as e: - print(f"Unexpected error in main loop: {e}") - import traceback - - traceback.print_exc() -finally: - print("Cleaning up...") - cleanup_robot() diff --git a/tests/simple_agent_test.py b/tests/simple_agent_test.py deleted file mode 100644 index f2cf8493d4..0000000000 --- a/tests/simple_agent_test.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dimos.agents.agent import OpenAIAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills - -# Initialize robot -robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), ros_control=UnitreeROSControl(), skills=MyUnitreeSkills() -) - -# Initialize agent -agent = OpenAIAgent( - dev_name="UnitreeExecutionAgent", - input_video_stream=robot.get_ros_video_stream(), - skills=robot.get_skills(), - system_query="Wiggle when you see a person! Jump when you see a person waving!", -) - -try: - input("Press ESC to exit...") -except KeyboardInterrupt: - print("\nExiting...") diff --git a/tests/test_agent.py b/tests/test_agent.py deleted file mode 100644 index e91345ff6a..0000000000 --- a/tests/test_agent.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -# ----- -from dotenv import load_dotenv - - -# Sanity check for dotenv -def test_dotenv(): - print("test_dotenv:") - load_dotenv() - openai_api_key = os.getenv("OPENAI_API_KEY") - print("\t\tOPENAI_API_KEY: ", openai_api_key) - - -# Sanity check for openai connection -def test_openai_connection(): - from openai import OpenAI - - client = OpenAI() - print("test_openai_connection:") - response = client.chat.completions.create( - model="gpt-4o-mini", - messages=[ - { - "role": "user", - "content": [ - {"type": "text", "text": "What's in this image?"}, - { - "type": "image_url", - "image_url": { - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", - }, - }, - ], - } - ], - max_tokens=300, - ) - print("\t\tOpenAI Response: ", response.choices[0]) - - -test_dotenv() -test_openai_connection() diff --git a/tests/test_agent_alibaba.py b/tests/test_agent_alibaba.py deleted file mode 100644 index fa4dfe80bf..0000000000 --- a/tests/test_agent_alibaba.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from openai import OpenAI - -from dimos.agents.agent import OpenAIAgent -from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.stream.video_provider import VideoProvider -from dimos.utils.threadpool import get_scheduler - -# Initialize video stream -video_stream = VideoProvider( - dev_name="VideoProvider", - # video_source=f"{os.getcwd()}/assets/framecount.mp4", - video_source=f"{os.getcwd()}/assets/trimmed_video_office.mov", - pool_scheduler=get_scheduler(), -).capture_video_as_observable(realtime=False, fps=1) - -# Specify the OpenAI client for Alibaba -qwen_client = OpenAI( - base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - api_key=os.getenv("ALIBABA_API_KEY"), -) - -# Initialize Unitree skills -myUnitreeSkills = MyUnitreeSkills() -myUnitreeSkills.initialize_skills() - -# Initialize agent -agent = OpenAIAgent( - dev_name="AlibabaExecutionAgent", - openai_client=qwen_client, - model_name="qwen2.5-vl-72b-instruct", - tokenizer=HuggingFaceTokenizer(model_name="Qwen/Qwen2.5-VL-72B-Instruct"), - max_output_tokens_per_request=8192, - input_video_stream=video_stream, - # system_query="Tell me the number in the video. Find me the center of the number spotted, and print the coordinates to the console using an appropriate function call. Then provide me a deep history of the number in question and its significance in history. Additionally, tell me what model and version of language model you are.", - system_query="Tell me about any objects seen. Print the coordinates for center of the objects seen to the console using an appropriate function call. Then provide me a deep history of the number in question and its significance in history. Additionally, tell me what model and version of language model you are.", - skills=myUnitreeSkills, -) - -try: - input("Press ESC to exit...") -except KeyboardInterrupt: - print("\nExiting...") diff --git a/tests/test_agent_ctransformers_gguf.py b/tests/test_agent_ctransformers_gguf.py deleted file mode 100644 index 389a9c74c5..0000000000 --- a/tests/test_agent_ctransformers_gguf.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent_ctransformers_gguf import CTransformersGGUFAgent - -system_query = "You are a robot with the following functions. Move(), Reverse(), Left(), Right(), Stop(). Given the following user comands return the correct function." - -# Initialize agent -agent = CTransformersGGUFAgent( - dev_name="GGUF-Agent", - model_name="TheBloke/Llama-2-7B-GGUF", - model_file="llama-2-7b.Q4_K_M.gguf", - model_type="llama", - system_query=system_query, - gpu_layers=50, - max_input_tokens_per_request=250, - max_output_tokens_per_request=10, -) - -test_query = "User: Travel forward 10 meters" - -agent.run_observable_query(test_query).subscribe( - on_next=lambda response: print(f"One-off query response: {response}"), - on_error=lambda error: print(f"Error: {error}"), - on_completed=lambda: print("Query completed"), -) - -try: - input("Press ESC to exit...") -except KeyboardInterrupt: - print("\nExiting...") diff --git a/tests/test_agent_huggingface_local.py b/tests/test_agent_huggingface_local.py deleted file mode 100644 index eb88dd9847..0000000000 --- a/tests/test_agent_huggingface_local.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dimos.agents.agent_huggingface_local import HuggingFaceLocalAgent -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.stream.data_provider import QueryDataProvider -from dimos.stream.video_provider import VideoProvider -from dimos.utils.threadpool import get_scheduler - -# Initialize video stream -video_stream = VideoProvider( - dev_name="VideoProvider", - # video_source=f"{os.getcwd()}/assets/framecount.mp4", - video_source=f"{os.getcwd()}/assets/trimmed_video_office.mov", - pool_scheduler=get_scheduler(), -).capture_video_as_observable(realtime=False, fps=1) - -# Initialize Unitree skills -myUnitreeSkills = MyUnitreeSkills() -myUnitreeSkills.initialize_skills() - -# Initialize query stream -query_provider = QueryDataProvider() - -system_query = "You are a robot with the following functions. Move(), Reverse(), Left(), Right(), Stop(). Given the following user comands return ONLY the correct function." - -# Initialize agent -agent = HuggingFaceLocalAgent( - dev_name="HuggingFaceLLMAgent", - model_name="Qwen/Qwen2.5-3B", - agent_type="HF-LLM", - system_query=system_query, - input_query_stream=query_provider.data_stream, - process_all_inputs=False, - max_input_tokens_per_request=250, - max_output_tokens_per_request=20, - # output_dir=self.output_dir, - # skills=skills_instance, - # frame_processor=frame_processor, -) - -# Start the query stream. -# Queries will be pushed every 1 second, in a count from 100 to 5000. -# This will cause listening agents to consume the queries and respond -# to them via skill execution and provide 1-shot responses. -query_provider.start_query_stream( - query_template="{query}; User: travel forward by 10 meters", - frequency=10, - start_count=1, - end_count=10000, - step=1, -) - -try: - input("Press ESC to exit...") -except KeyboardInterrupt: - print("\nExiting...") diff --git a/tests/test_agent_huggingface_local_jetson.py b/tests/test_agent_huggingface_local_jetson.py deleted file mode 100644 index 883a05be54..0000000000 --- a/tests/test_agent_huggingface_local_jetson.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dimos.agents.agent_huggingface_local import HuggingFaceLocalAgent -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.stream.data_provider import QueryDataProvider -from dimos.stream.video_provider import VideoProvider -from dimos.utils.threadpool import get_scheduler - -# Initialize video stream -video_stream = VideoProvider( - dev_name="VideoProvider", - # video_source=f"{os.getcwd()}/assets/framecount.mp4", - video_source=f"{os.getcwd()}/assets/trimmed_video_office.mov", - pool_scheduler=get_scheduler(), -).capture_video_as_observable(realtime=False, fps=1) - -# Initialize Unitree skills -myUnitreeSkills = MyUnitreeSkills() -myUnitreeSkills.initialize_skills() - -# Initialize query stream -query_provider = QueryDataProvider() - -system_query = "You are a helpful assistant." - -# Initialize agent -agent = HuggingFaceLocalAgent( - dev_name="HuggingFaceLLMAgent", - model_name="Qwen/Qwen2.5-0.5B", - # model_name="HuggingFaceTB/SmolLM2-135M", - agent_type="HF-LLM", - system_query=system_query, - input_query_stream=query_provider.data_stream, - process_all_inputs=False, - max_input_tokens_per_request=250, - max_output_tokens_per_request=20, - # output_dir=self.output_dir, - # skills=skills_instance, - # frame_processor=frame_processor, -) - -# Start the query stream. -# Queries will be pushed every 1 second, in a count from 100 to 5000. -# This will cause listening agents to consume the queries and respond -# to them via skill execution and provide 1-shot responses. -query_provider.start_query_stream( - query_template="{query}; User: Hello how are you!", - frequency=30, - start_count=1, - end_count=10000, - step=1, -) - -try: - input("Press ESC to exit...") -except KeyboardInterrupt: - print("\nExiting...") diff --git a/tests/test_agent_huggingface_remote.py b/tests/test_agent_huggingface_remote.py deleted file mode 100644 index ed99faa8a4..0000000000 --- a/tests/test_agent_huggingface_remote.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.agents.agent_huggingface_remote import HuggingFaceRemoteAgent -from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer -from dimos.stream.data_provider import QueryDataProvider - -# Initialize video stream -# video_stream = VideoProvider( -# dev_name="VideoProvider", -# # video_source=f"{os.getcwd()}/assets/framecount.mp4", -# video_source=f"{os.getcwd()}/assets/trimmed_video_office.mov", -# pool_scheduler=get_scheduler(), -# ).capture_video_as_observable(realtime=False, fps=1) - -# Initialize Unitree skills -# myUnitreeSkills = MyUnitreeSkills() -# myUnitreeSkills.initialize_skills() - -# Initialize query stream -query_provider = QueryDataProvider() - -# Initialize agent -agent = HuggingFaceRemoteAgent( - dev_name="HuggingFaceRemoteAgent", - model_name="meta-llama/Meta-Llama-3-8B-Instruct", - tokenizer=HuggingFaceTokenizer(model_name="meta-llama/Meta-Llama-3-8B-Instruct"), - max_output_tokens_per_request=8192, - input_query_stream=query_provider.data_stream, - # input_video_stream=video_stream, - system_query="You are a helpful assistant that can answer questions and help with tasks.", -) - -# Start the query stream. -# Queries will be pushed every 1 second, in a count from 100 to 5000. -query_provider.start_query_stream( - query_template="{query}; Denote the number at the beginning of this query before the semicolon as the 'reference number'. Provide the reference number, without any other text in your response.", - frequency=5, - start_count=1, - end_count=10000, - step=1, -) - -try: - input("Press ESC to exit...") -except KeyboardInterrupt: - print("\nExiting...") diff --git a/tests/test_audio_agent.py b/tests/test_audio_agent.py deleted file mode 100644 index d79d2040c2..0000000000 --- a/tests/test_audio_agent.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import OpenAIAgent -from dimos.stream.audio.pipelines import stt, tts -from dimos.stream.audio.utils import keepalive -from dimos.utils.threadpool import get_scheduler - - -def main(): - stt_node = stt() - - agent = OpenAIAgent( - dev_name="UnitreeExecutionAgent", - input_query_stream=stt_node.emit_text(), - system_query="You are a helpful robot named daneel that does my bidding", - pool_scheduler=get_scheduler(), - ) - - tts_node = tts() - tts_node.consume_text(agent.get_response_observable()) - - # Keep the main thread alive - keepalive() - - -if __name__ == "__main__": - main() diff --git a/tests/test_audio_robot_agent.py b/tests/test_audio_robot_agent.py deleted file mode 100644 index 27340fcd80..0000000000 --- a/tests/test_audio_robot_agent.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dimos.agents.agent import OpenAIAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.stream.audio.pipelines import stt, tts -from dimos.stream.audio.utils import keepalive -from dimos.utils.threadpool import get_scheduler - - -def main(): - stt_node = stt() - tts_node = tts() - - robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - ros_control=UnitreeROSControl(), - skills=MyUnitreeSkills(), - ) - - # Initialize agent with main thread pool scheduler - agent = OpenAIAgent( - dev_name="UnitreeExecutionAgent", - input_query_stream=stt_node.emit_text(), - system_query="You are a helpful robot named daneel that does my bidding", - pool_scheduler=get_scheduler(), - skills=robot.get_skills(), - ) - - tts_node.consume_text(agent.get_response_observable()) - - # Keep the main thread alive - keepalive() - - -if __name__ == "__main__": - main() diff --git a/tests/test_cerebras_unitree_ros.py b/tests/test_cerebras_unitree_ros.py deleted file mode 100644 index 60890a3d5c..0000000000 --- a/tests/test_cerebras_unitree_ros.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dotenv import load_dotenv -import reactivex as rx -import reactivex.operators as ops - -from dimos.agents.cerebras_agent import CerebrasAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.kill_skill import KillSkill -from dimos.skills.navigation import GetPose, NavigateToGoal, NavigateWithText -from dimos.skills.observe_stream import ObserveStream -from dimos.skills.speak import Speak -from dimos.skills.visual_navigation_skills import FollowHuman -from dimos.stream.audio.pipelines import stt, tts -from dimos.web.robot_web_interface import RobotWebInterface - -# Load API key from environment -load_dotenv() - -# robot = MockRobot() -robot_skills = MyUnitreeSkills() - -robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - ros_control=UnitreeROSControl(), - skills=robot_skills, - mock_connection=False, - new_memory=True, -) - -# Create a subject for agent responses -agent_response_subject = rx.subject.Subject() -agent_response_stream = agent_response_subject.pipe(ops.share()) - -streams = { - "unitree_video": robot.get_ros_video_stream(), -} -text_streams = { - "agent_responses": agent_response_stream, -} - -web_interface = RobotWebInterface( - port=5555, - text_streams=text_streams, - **streams, -) - -stt_node = stt() - -# Create a CerebrasAgent instance -agent = CerebrasAgent( - dev_name="test_cerebras_agent", - input_query_stream=stt_node.emit_text(), - # input_query_stream=web_interface.query_stream, - skills=robot_skills, - system_query="""You are an agent controlling a virtual robot. When given a query, respond by using the appropriate tool calls if needed to execute commands on the robot. - -IMPORTANT INSTRUCTIONS: -1. Each tool call must include the exact function name and appropriate parameters -2. If a function needs parameters like 'distance' or 'angle', be sure to include them -3. If you're unsure which tool to use, choose the most appropriate one based on the user's query -4. Parse the user's instructions carefully to determine correct parameter values - -When you need to call a skill or tool, ALWAYS respond ONLY with a JSON object in this exact format: {"name": "SkillName", "arguments": {"arg1": "value1", "arg2": "value2"}} - -Example: If the user asks to spin right by 90 degrees, output ONLY the following: {"name": "SpinRight", "arguments": {"degrees": 90}}""", - model_name="llama-4-scout-17b-16e-instruct", -) - -tts_node = tts() -tts_node.consume_text(agent.get_response_observable()) - -robot_skills.add(ObserveStream) -robot_skills.add(KillSkill) -robot_skills.add(NavigateWithText) -robot_skills.add(FollowHuman) -robot_skills.add(GetPose) -robot_skills.add(Speak) -robot_skills.add(NavigateToGoal) -robot_skills.create_instance("ObserveStream", robot=robot, agent=agent) -robot_skills.create_instance("KillSkill", robot=robot, skill_library=robot_skills) -robot_skills.create_instance("NavigateWithText", robot=robot) -robot_skills.create_instance("FollowHuman", robot=robot) -robot_skills.create_instance("GetPose", robot=robot) -robot_skills.create_instance("NavigateToGoal", robot=robot) - - -robot_skills.create_instance("Speak", tts_node=tts_node) - -# Subscribe to agent responses and send them to the subject -agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) - -# print(f"Registered skills: {', '.join([skill.__name__ for skill in robot_skills.skills])}") -print("Cerebras agent demo initialized. You can now interact with the agent via the web interface.") - -web_interface.run() diff --git a/tests/test_claude_agent_query.py b/tests/test_claude_agent_query.py deleted file mode 100644 index 05893a6b9d..0000000000 --- a/tests/test_claude_agent_query.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dotenv import load_dotenv - -from dimos.agents.claude_agent import ClaudeAgent - -# Load API key from environment -load_dotenv() - -# Create a ClaudeAgent instance -agent = ClaudeAgent(dev_name="test_agent", query="What is the capital of France?") - -# Use the stream_query method to get a response -response = agent.run_observable_query("What is the capital of France?").run() - -print(f"Response from Claude Agent: {response}") diff --git a/tests/test_claude_agent_skills_query.py b/tests/test_claude_agent_skills_query.py deleted file mode 100644 index bb5753d2db..0000000000 --- a/tests/test_claude_agent_skills_query.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import threading - -from dotenv import load_dotenv -import reactivex as rx -import reactivex.operators as ops - -from dimos.agents.claude_agent import ClaudeAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.kill_skill import KillSkill -from dimos.skills.navigation import BuildSemanticMap, GetPose, Navigate, NavigateToGoal -from dimos.skills.observe_stream import ObserveStream -from dimos.skills.speak import Speak -from dimos.skills.visual_navigation_skills import FollowHuman, NavigateToObject -from dimos.stream.audio.pipelines import stt, tts -from dimos.types.vector import Vector -from dimos.web.robot_web_interface import RobotWebInterface -from dimos.web.websocket_vis.server import WebsocketVis - -# Load API key from environment -load_dotenv() - -robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - ros_control=UnitreeROSControl(), - skills=MyUnitreeSkills(), - mock_connection=False, -) - -# Create a subject for agent responses -agent_response_subject = rx.subject.Subject() -agent_response_stream = agent_response_subject.pipe(ops.share()) -local_planner_viz_stream = robot.local_planner_viz_stream.pipe(ops.share()) - -streams = { - "unitree_video": robot.get_ros_video_stream(), - "local_planner_viz": local_planner_viz_stream, -} -text_streams = { - "agent_responses": agent_response_stream, -} - -web_interface = RobotWebInterface(port=5555, text_streams=text_streams, **streams) - -stt_node = stt() - -# Create a ClaudeAgent instance -agent = ClaudeAgent( - dev_name="test_agent", - input_query_stream=stt_node.emit_text(), - # input_query_stream=web_interface.query_stream, - skills=robot.get_skills(), - system_query="""You are an agent controlling a virtual robot. When given a query, respond by using the appropriate tool calls if needed to execute commands on the robot. - -IMPORTANT INSTRUCTIONS: -1. Each tool call must include the exact function name and appropriate parameters -2. If a function needs parameters like 'distance' or 'angle', be sure to include them -3. If you're unsure which tool to use, choose the most appropriate one based on the user's query -4. Parse the user's instructions carefully to determine correct parameter values - -Example: If the user asks to move forward 1 meter, call the Move function with distance=1""", - model_name="claude-3-7-sonnet-latest", - thinking_budget_tokens=2000, -) - -tts_node = tts() -# tts_node.consume_text(agent.get_response_observable()) - -robot_skills = robot.get_skills() -robot_skills.add(ObserveStream) -robot_skills.add(KillSkill) -robot_skills.add(Navigate) -robot_skills.add(BuildSemanticMap) -robot_skills.add(NavigateToObject) -robot_skills.add(FollowHuman) -robot_skills.add(GetPose) -robot_skills.add(Speak) -robot_skills.add(NavigateToGoal) -robot_skills.create_instance("ObserveStream", robot=robot, agent=agent) -robot_skills.create_instance("KillSkill", robot=robot, skill_library=robot_skills) -robot_skills.create_instance("Navigate", robot=robot) -robot_skills.create_instance("BuildSemanticMap", robot=robot) -robot_skills.create_instance("NavigateToObject", robot=robot) -robot_skills.create_instance("FollowHuman", robot=robot) -robot_skills.create_instance("GetPose", robot=robot) -robot_skills.create_instance("NavigateToGoal", robot=robot) -robot_skills.create_instance("Speak", tts_node=tts_node) - -# Subscribe to agent responses and send them to the subject -agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) - -print("ObserveStream and Kill skills registered and ready for use") -print("Created memory.txt file") - -websocket_vis = WebsocketVis() -websocket_vis.start() -websocket_vis.connect(robot.global_planner.vis_stream()) - - -def msg_handler(msgtype, data): - if msgtype == "click": - target = Vector(data["position"]) - try: - robot.global_planner.set_goal(target) - except Exception as e: - print(f"Error setting goal: {e}") - return - - -def threaded_msg_handler(msgtype, data): - thread = threading.Thread(target=msg_handler, args=(msgtype, data)) - thread.daemon = True - thread.start() - - -websocket_vis.msg_handler = threaded_msg_handler - -web_interface.run() diff --git a/tests/test_command_pose_unitree.py b/tests/test_command_pose_unitree.py deleted file mode 100644 index f67b8c969f..0000000000 --- a/tests/test_command_pose_unitree.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import sys - -# Add the parent directory to the Python path -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import os -import time - -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills - -# Initialize robot -robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), ros_control=UnitreeROSControl(), skills=MyUnitreeSkills() -) - - -# Helper function to send pose commands continuously for a duration -def send_pose_for_duration(roll, pitch, yaw, duration, hz=10): - """Send the same pose command repeatedly at specified frequency for the given duration""" - start_time = time.time() - while time.time() - start_time < duration: - robot.pose_command(roll=roll, pitch=pitch, yaw=yaw) - time.sleep(1.0 / hz) # Sleep to achieve the desired frequency - - -# Test pose commands - -# First, make sure the robot is in a stable position -print("Setting default pose...") -send_pose_for_duration(0.0, 0.0, 0.0, 1) - -# Test roll angle (lean left/right) -print("Testing roll angle - lean right...") -send_pose_for_duration(0.5, 0.0, 0.0, 1.5) # Lean right - -print("Testing roll angle - lean left...") -send_pose_for_duration(-0.5, 0.0, 0.0, 1.5) # Lean left - -# Test pitch angle (lean forward/backward) -print("Testing pitch angle - lean forward...") -send_pose_for_duration(0.0, 0.5, 0.0, 1.5) # Lean forward - -print("Testing pitch angle - lean backward...") -send_pose_for_duration(0.0, -0.5, 0.0, 1.5) # Lean backward - -# Test yaw angle (rotate body without moving feet) -print("Testing yaw angle - rotate clockwise...") -send_pose_for_duration(0.0, 0.0, 0.5, 1.5) # Rotate body clockwise - -print("Testing yaw angle - rotate counterclockwise...") -send_pose_for_duration(0.0, 0.0, -0.5, 1.5) # Rotate body counterclockwise - -# Reset to default pose -print("Resetting to default pose...") -send_pose_for_duration(0.0, 0.0, 0.0, 2) - -print("Pose command test completed") - -# Keep the program running (optional) -print("Press Ctrl+C to exit") -try: - while True: - time.sleep(1) -except KeyboardInterrupt: - print("Test terminated by user") diff --git a/tests/test_header.py b/tests/test_header.py deleted file mode 100644 index 05e6c3e21c..0000000000 --- a/tests/test_header.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test utilities for identifying caller information and path setup. - -This module provides functionality to determine which file called the current -script and sets up the Python path to include the parent directory, allowing -tests to import from the main application. -""" - -import inspect -import os -import sys - -# Add the parent directory of 'tests' to the Python path -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - - -def get_caller_info(): - """Identify the filename of the caller in the stack. - - Examines the call stack to find the first non-internal file that called - this module. Skips the current file and Python internal files. - - Returns: - str: The basename of the caller's filename, or "unknown" if not found. - """ - current_file = os.path.abspath(__file__) - - # Look through the call stack to find the first file that's not this one - for frame in inspect.stack()[1:]: - filename = os.path.abspath(frame.filename) - # Skip this file and Python internals - if filename != current_file and " 0: - best_score = max(grasp.get("score", 0.0) for grasp in grasps) - print(f" Best grasp score: {best_score:.3f}") - last_grasp_count = current_count - last_update_time = current_time - else: - # Show periodic "still waiting" message - if current_time - last_update_time > 10.0: - print(f" Still waiting for grasps... ({time.strftime('%H:%M:%S')})") - last_update_time = current_time - - time.sleep(1.0) # Check every second - - except Exception as e: - print(f" Error in grasp monitor: {e}") - time.sleep(2.0) - - -def main(): - """Test point cloud filtering with grasp generation using ManipulationPipeline.""" - print(" Testing point cloud filtering + grasp generation with ManipulationPipeline...") - - # Configuration - min_confidence = 0.6 - web_port = 5555 - grasp_server_url = "ws://18.224.39.74:8000/ws/grasp" - - try: - # Initialize ZED camera stream - zed_stream = ZEDCameraStream(resolution=sl.RESOLUTION.HD1080, fps=10) - - # Get camera intrinsics - camera_intrinsics_dict = zed_stream.get_camera_info() - camera_intrinsics = [ - camera_intrinsics_dict["fx"], - camera_intrinsics_dict["fy"], - camera_intrinsics_dict["cx"], - camera_intrinsics_dict["cy"], - ] - - # Create the concurrent manipulation pipeline WITH grasp generation - pipeline = ManipulationPipeline( - camera_intrinsics=camera_intrinsics, - min_confidence=min_confidence, - max_objects=10, - grasp_server_url=grasp_server_url, - enable_grasp_generation=True, # Enable grasp generation - ) - - # Create ZED stream - zed_frame_stream = zed_stream.create_stream().pipe(ops.share()) - - # Create concurrent processing streams - streams = pipeline.create_streams(zed_frame_stream) - detection_viz_stream = streams["detection_viz"] - pointcloud_viz_stream = streams["pointcloud_viz"] - grasps_stream = streams.get("grasps") # Get grasp stream if available - grasp_overlay_stream = streams.get("grasp_overlay") # Get grasp overlay stream if available - - except ImportError: - print("Error: ZED SDK not installed. Please install pyzed package.") - sys.exit(1) - except RuntimeError as e: - print(f"Error: Failed to open ZED camera: {e}") - sys.exit(1) - - try: - # Set up web interface with concurrent visualization streams - print("Initializing web interface...") - web_interface = RobotWebInterface( - port=web_port, - object_detection=detection_viz_stream, - pointcloud_stream=pointcloud_viz_stream, - grasp_overlay_stream=grasp_overlay_stream, - ) - - # Start grasp monitoring in background thread - grasp_monitor_thread = threading.Thread( - target=monitor_grasps, args=(pipeline,), daemon=True - ) - grasp_monitor_thread.start() - - print("\n Point Cloud + Grasp Generation Test Running:") - print(f" Web Interface: http://localhost:{web_port}") - print(" Object Detection View: RGB with bounding boxes") - print(" Point Cloud View: Depth with colored point clouds and 3D bounding boxes") - print(f" Confidence threshold: {min_confidence}") - print(f" Grasp server: {grasp_server_url}") - print(f" Available streams: {list(streams.keys())}") - print("\nPress Ctrl+C to stop the test\n") - - # Start web server (blocking call) - web_interface.run() - - except KeyboardInterrupt: - print("\nTest interrupted by user") - except Exception as e: - print(f"Error during test: {e}") - finally: - print("Cleaning up resources...") - if "zed_stream" in locals(): - zed_stream.cleanup() - if "pipeline" in locals(): - pipeline.cleanup() - print("Test completed") - - -if __name__ == "__main__": - main() diff --git a/tests/test_manipulation_perception_pipeline.py.py b/tests/test_manipulation_perception_pipeline.py.py deleted file mode 100644 index 6f8755d3da..0000000000 --- a/tests/test_manipulation_perception_pipeline.py.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys -import threading -import time - -from pyzed import sl -from reactivex import operators as ops - -from dimos.manipulation.manip_aio_pipeline import ManipulationPipeline -from dimos.stream.stereo_camera_streams.zed import ZEDCameraStream -from dimos.web.robot_web_interface import RobotWebInterface - - -def monitor_grasps(pipeline): - """Monitor and print grasp updates in a separate thread.""" - print(" Grasp monitor started...") - - last_grasp_count = 0 - last_update_time = time.time() - - while True: - try: - # Get latest grasps using the getter function - grasps = pipeline.get_latest_grasps(timeout=0.5) - current_time = time.time() - - if grasps is not None: - current_count = len(grasps) - if current_count != last_grasp_count: - print(f" Grasps received: {current_count} (at {time.strftime('%H:%M:%S')})") - if current_count > 0: - best_score = max(grasp.get("score", 0.0) for grasp in grasps) - print(f" Best grasp score: {best_score:.3f}") - last_grasp_count = current_count - last_update_time = current_time - else: - # Show periodic "still waiting" message - if current_time - last_update_time > 10.0: - print(f" Still waiting for grasps... ({time.strftime('%H:%M:%S')})") - last_update_time = current_time - - time.sleep(1.0) # Check every second - - except Exception as e: - print(f" Error in grasp monitor: {e}") - time.sleep(2.0) - - -def main(): - """Test point cloud filtering with grasp generation using ManipulationPipeline.""" - print(" Testing point cloud filtering + grasp generation with ManipulationPipeline...") - - # Configuration - min_confidence = 0.6 - web_port = 5555 - grasp_server_url = "ws://18.224.39.74:8000/ws/grasp" - - try: - # Initialize ZED camera stream - zed_stream = ZEDCameraStream(resolution=sl.RESOLUTION.HD1080, fps=10) - - # Get camera intrinsics - camera_intrinsics_dict = zed_stream.get_camera_info() - camera_intrinsics = [ - camera_intrinsics_dict["fx"], - camera_intrinsics_dict["fy"], - camera_intrinsics_dict["cx"], - camera_intrinsics_dict["cy"], - ] - - # Create the concurrent manipulation pipeline WITH grasp generation - pipeline = ManipulationPipeline( - camera_intrinsics=camera_intrinsics, - min_confidence=min_confidence, - max_objects=10, - grasp_server_url=grasp_server_url, - enable_grasp_generation=True, # Enable grasp generation - ) - - # Create ZED stream - zed_frame_stream = zed_stream.create_stream().pipe(ops.share()) - - # Create concurrent processing streams - streams = pipeline.create_streams(zed_frame_stream) - detection_viz_stream = streams["detection_viz"] - pointcloud_viz_stream = streams["pointcloud_viz"] - grasps_stream = streams.get("grasps") # Get grasp stream if available - grasp_overlay_stream = streams.get("grasp_overlay") # Get grasp overlay stream if available - - except ImportError: - print("Error: ZED SDK not installed. Please install pyzed package.") - sys.exit(1) - except RuntimeError as e: - print(f"Error: Failed to open ZED camera: {e}") - sys.exit(1) - - try: - # Set up web interface with concurrent visualization streams - print("Initializing web interface...") - web_interface = RobotWebInterface( - port=web_port, - object_detection=detection_viz_stream, - pointcloud_stream=pointcloud_viz_stream, - grasp_overlay_stream=grasp_overlay_stream, - ) - - # Start grasp monitoring in background thread - grasp_monitor_thread = threading.Thread( - target=monitor_grasps, args=(pipeline,), daemon=True - ) - grasp_monitor_thread.start() - - print("\n Point Cloud + Grasp Generation Test Running:") - print(f" Web Interface: http://localhost:{web_port}") - print(" Object Detection View: RGB with bounding boxes") - print(" Point Cloud View: Depth with colored point clouds and 3D bounding boxes") - print(f" Confidence threshold: {min_confidence}") - print(f" Grasp server: {grasp_server_url}") - print(f" Available streams: {list(streams.keys())}") - print("\nPress Ctrl+C to stop the test\n") - - # Start web server (blocking call) - web_interface.run() - - except KeyboardInterrupt: - print("\nTest interrupted by user") - except Exception as e: - print(f"Error during test: {e}") - finally: - print("Cleaning up resources...") - if "zed_stream" in locals(): - zed_stream.cleanup() - if "pipeline" in locals(): - pipeline.cleanup() - print("Test completed") - - -if __name__ == "__main__": - main() diff --git a/tests/test_manipulation_pipeline_single_frame.py b/tests/test_manipulation_pipeline_single_frame.py deleted file mode 100644 index c29b2b2607..0000000000 --- a/tests/test_manipulation_pipeline_single_frame.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test manipulation processor with direct visualization and grasp data output.""" - -import argparse -import os - -import cv2 -import matplotlib -import numpy as np - -from dimos.utils.data import get_data - -# Try to use TkAgg backend for live display, fallback to Agg if not available -try: - matplotlib.use("TkAgg") -except: - try: - matplotlib.use("Qt5Agg") - except: - matplotlib.use("Agg") # Fallback to non-interactive -import matplotlib.pyplot as plt -import open3d as o3d - -from dimos.manipulation.manip_aio_processer import ManipulationProcessor -from dimos.perception.grasp_generation.utils import create_grasp_overlay, visualize_grasps_3d -from dimos.perception.pointcloud.utils import ( - combine_object_pointclouds, - load_camera_matrix_from_yaml, - visualize_clustered_point_clouds, - visualize_pcd, - visualize_voxel_grid, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_pipeline_viz") - - -def load_first_frame(data_dir: str): - """Load first RGB-D frame and camera intrinsics.""" - # Load images - color_img = cv2.imread(os.path.join(data_dir, "color", "00000.png")) - color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB) - - depth_img = cv2.imread(os.path.join(data_dir, "depth", "00000.png"), cv2.IMREAD_ANYDEPTH) - if depth_img.dtype == np.uint16: - depth_img = depth_img.astype(np.float32) / 1000.0 - # Load intrinsics - camera_matrix = load_camera_matrix_from_yaml(os.path.join(data_dir, "color_camera_info.yaml")) - intrinsics = [ - camera_matrix[0, 0], - camera_matrix[1, 1], - camera_matrix[0, 2], - camera_matrix[1, 2], - ] - - return color_img, depth_img, intrinsics - - -def create_point_cloud(color_img, depth_img, intrinsics): - """Create Open3D point cloud.""" - fx, fy, cx, cy = intrinsics - height, width = depth_img.shape - - o3d_intrinsics = o3d.camera.PinholeCameraIntrinsic(width, height, fx, fy, cx, cy) - color_o3d = o3d.geometry.Image(color_img) - depth_o3d = o3d.geometry.Image((depth_img * 1000).astype(np.uint16)) - - rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth( - color_o3d, depth_o3d, depth_scale=1000.0, convert_rgb_to_intensity=False - ) - - return o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, o3d_intrinsics) - - -def run_processor(color_img, depth_img, intrinsics, grasp_server_url=None): - """Run processor and collect results.""" - processor_kwargs = { - "camera_intrinsics": intrinsics, - "enable_grasp_generation": True, - "enable_segmentation": True, - } - - if grasp_server_url: - processor_kwargs["grasp_server_url"] = grasp_server_url - - processor = ManipulationProcessor(**processor_kwargs) - - # Process frame without grasp generation - results = processor.process_frame(color_img, depth_img, generate_grasps=False) - - # Run grasp generation separately - grasps = processor.run_grasp_generation(results["all_objects"], results["full_pointcloud"]) - results["grasps"] = grasps - results["grasp_overlay"] = create_grasp_overlay(color_img, grasps, intrinsics) - - processor.cleanup() - return results - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--data-dir", default=get_data("rgbd_frames")) - parser.add_argument("--wait-time", type=float, default=5.0) - parser.add_argument( - "--grasp-server-url", - default="ws://18.224.39.74:8000/ws/grasp", - help="WebSocket URL for Dimensional Grasp server", - ) - args = parser.parse_args() - - # Load data - color_img, depth_img, intrinsics = load_first_frame(args.data_dir) - logger.info(f"Loaded images: color {color_img.shape}, depth {depth_img.shape}") - - # Run processor - results = run_processor(color_img, depth_img, intrinsics, args.grasp_server_url) - - # Print results summary - print(f"Processing time: {results.get('processing_time', 0):.3f}s") - print(f"Detection objects: {len(results.get('detected_objects', []))}") - print(f"All objects processed: {len(results.get('all_objects', []))}") - - # Print grasp summary - grasp_data = results["grasps"] - total_grasps = len(grasp_data) if isinstance(grasp_data, list) else 0 - best_score = max(grasp["score"] for grasp in grasp_data) if grasp_data else 0 - - print(f"Grasps: {total_grasps} total (best score: {best_score:.3f})") - - # Create visualizations - plot_configs = [] - if results["detection_viz"] is not None: - plot_configs.append(("detection_viz", "Object Detection")) - if results["segmentation_viz"] is not None: - plot_configs.append(("segmentation_viz", "Semantic Segmentation")) - if results["pointcloud_viz"] is not None: - plot_configs.append(("pointcloud_viz", "All Objects Point Cloud")) - if results["detected_pointcloud_viz"] is not None: - plot_configs.append(("detected_pointcloud_viz", "Detection Objects Point Cloud")) - if results["misc_pointcloud_viz"] is not None: - plot_configs.append(("misc_pointcloud_viz", "Misc/Background Points")) - if results["grasp_overlay"] is not None: - plot_configs.append(("grasp_overlay", "Grasp Overlay")) - - # Create subplot layout - num_plots = len(plot_configs) - if num_plots <= 3: - fig, axes = plt.subplots(1, num_plots, figsize=(6 * num_plots, 5)) - else: - rows = 2 - cols = (num_plots + 1) // 2 - _fig, axes = plt.subplots(rows, cols, figsize=(6 * cols, 5 * rows)) - - if num_plots == 1: - axes = [axes] - elif num_plots > 2: - axes = axes.flatten() - - # Plot each result - for i, (key, title) in enumerate(plot_configs): - axes[i].imshow(results[key]) - axes[i].set_title(title) - axes[i].axis("off") - - # Hide unused subplots - if num_plots > 3: - for i in range(num_plots, len(axes)): - axes[i].axis("off") - - plt.tight_layout() - plt.savefig("manipulation_results.png", dpi=150, bbox_inches="tight") - plt.show(block=True) - plt.close() - - point_clouds = [obj["point_cloud"] for obj in results["all_objects"]] - colors = [obj["color"] for obj in results["all_objects"]] - combined_pcd = combine_object_pointclouds(point_clouds, colors) - - # 3D Grasp visualization - if grasp_data: - # Convert grasp format to visualization format for 3D display - viz_grasps = [] - for grasp in grasp_data: - translation = grasp.get("translation", [0, 0, 0]) - rotation_matrix = np.array(grasp.get("rotation_matrix", np.eye(3).tolist())) - score = grasp.get("score", 0.0) - width = grasp.get("width", 0.08) - - viz_grasp = { - "translation": translation, - "rotation_matrix": rotation_matrix, - "width": width, - "score": score, - } - viz_grasps.append(viz_grasp) - - # Use unified 3D visualization - visualize_grasps_3d(combined_pcd, viz_grasps) - - # Visualize full point cloud - visualize_pcd( - results["full_pointcloud"], - window_name="Full Scene Point Cloud", - point_size=2.0, - show_coordinate_frame=True, - ) - - # Visualize all objects point cloud - visualize_pcd( - combined_pcd, - window_name="All Objects Point Cloud", - point_size=3.0, - show_coordinate_frame=True, - ) - - # Visualize misc clusters - visualize_clustered_point_clouds( - results["misc_clusters"], - window_name="Misc/Background Clusters (DBSCAN)", - point_size=3.0, - show_coordinate_frame=True, - ) - - # Visualize voxel grid - visualize_voxel_grid( - results["misc_voxel_grid"], - window_name="Misc/Background Voxel Grid", - show_coordinate_frame=True, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/test_manipulation_pipeline_single_frame_lcm.py b/tests/test_manipulation_pipeline_single_frame_lcm.py deleted file mode 100644 index 0c2f2bc591..0000000000 --- a/tests/test_manipulation_pipeline_single_frame_lcm.py +++ /dev/null @@ -1,419 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test manipulation processor with LCM topic subscription.""" - -import argparse -import pickle -import threading - -import cv2 -import matplotlib -import numpy as np - -# Try to use TkAgg backend for live display, fallback to Agg if not available -try: - matplotlib.use("TkAgg") -except: - try: - matplotlib.use("Qt5Agg") - except: - matplotlib.use("Agg") # Fallback to non-interactive - -# LCM imports -import lcm -from lcm_msgs.sensor_msgs import CameraInfo as LCMCameraInfo, Image as LCMImage -import open3d as o3d - -from dimos.manipulation.manip_aio_processer import ManipulationProcessor -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_pipeline_lcm") - - -class LCMDataCollector: - """Collects one message from each required LCM topic.""" - - def __init__(self, lcm_url: str = "udpm://239.255.76.67:7667?ttl=1"): - self.lcm = lcm.LCM(lcm_url) - - # Data storage - self.rgb_data: np.ndarray | None = None - self.depth_data: np.ndarray | None = None - self.camera_intrinsics: list[float] | None = None - - # Synchronization - self.data_lock = threading.Lock() - self.data_ready_event = threading.Event() - - # Flags to track received messages - self.rgb_received = False - self.depth_received = False - self.camera_info_received = False - - # Subscribe to topics - self.lcm.subscribe("head_cam_rgb#sensor_msgs.Image", self._handle_rgb_message) - self.lcm.subscribe("head_cam_depth#sensor_msgs.Image", self._handle_depth_message) - self.lcm.subscribe("head_cam_info#sensor_msgs.CameraInfo", self._handle_camera_info_message) - - logger.info("LCM Data Collector initialized") - logger.info("Subscribed to topics:") - logger.info(" - head_cam_rgb#sensor_msgs.Image") - logger.info(" - head_cam_depth#sensor_msgs.Image") - logger.info(" - head_cam_info#sensor_msgs.CameraInfo") - - def _handle_rgb_message(self, channel: str, data: bytes): - """Handle RGB image message.""" - if self.rgb_received: - return # Already got one, ignore subsequent messages - - try: - msg = LCMImage.decode(data) - - # Convert message data to numpy array - if msg.encoding == "rgb8": - # RGB8 format: 3 bytes per pixel - rgb_array = np.frombuffer(msg.data[: msg.data_length], dtype=np.uint8) - rgb_image = rgb_array.reshape((msg.height, msg.width, 3)) - - with self.data_lock: - self.rgb_data = rgb_image - self.rgb_received = True - logger.info( - f"RGB message received: {msg.width}x{msg.height}, encoding: {msg.encoding}" - ) - self._check_all_data_received() - - else: - logger.warning(f"Unsupported RGB encoding: {msg.encoding}") - - except Exception as e: - logger.error(f"Error processing RGB message: {e}") - - def _handle_depth_message(self, channel: str, data: bytes): - """Handle depth image message.""" - if self.depth_received: - return # Already got one, ignore subsequent messages - - try: - msg = LCMImage.decode(data) - - # Convert message data to numpy array - if msg.encoding == "32FC1": - # 32FC1 format: 4 bytes (float32) per pixel - depth_array = np.frombuffer(msg.data[: msg.data_length], dtype=np.float32) - depth_image = depth_array.reshape((msg.height, msg.width)) - - with self.data_lock: - self.depth_data = depth_image - self.depth_received = True - logger.info( - f"Depth message received: {msg.width}x{msg.height}, encoding: {msg.encoding}" - ) - logger.info( - f"Depth range: {depth_image.min():.3f} - {depth_image.max():.3f} meters" - ) - self._check_all_data_received() - - else: - logger.warning(f"Unsupported depth encoding: {msg.encoding}") - - except Exception as e: - logger.error(f"Error processing depth message: {e}") - - def _handle_camera_info_message(self, channel: str, data: bytes): - """Handle camera info message.""" - if self.camera_info_received: - return # Already got one, ignore subsequent messages - - try: - msg = LCMCameraInfo.decode(data) - - # Extract intrinsics from K matrix: [fx, 0, cx, 0, fy, cy, 0, 0, 1] - K = msg.K - fx = K[0] # K[0,0] - fy = K[4] # K[1,1] - cx = K[2] # K[0,2] - cy = K[5] # K[1,2] - - intrinsics = [fx, fy, cx, cy] - - with self.data_lock: - self.camera_intrinsics = intrinsics - self.camera_info_received = True - logger.info(f"Camera info received: {msg.width}x{msg.height}") - logger.info(f"Intrinsics: fx={fx:.1f}, fy={fy:.1f}, cx={cx:.1f}, cy={cy:.1f}") - self._check_all_data_received() - - except Exception as e: - logger.error(f"Error processing camera info message: {e}") - - def _check_all_data_received(self): - """Check if all required data has been received.""" - if self.rgb_received and self.depth_received and self.camera_info_received: - logger.info("āœ… All required data received!") - self.data_ready_event.set() - - def wait_for_data(self, timeout: float = 30.0) -> bool: - """Wait for all data to be received.""" - logger.info("Waiting for RGB, depth, and camera info messages...") - - # Start LCM handling in a separate thread - lcm_thread = threading.Thread(target=self._lcm_handle_loop, daemon=True) - lcm_thread.start() - - # Wait for data with timeout - return self.data_ready_event.wait(timeout) - - def _lcm_handle_loop(self): - """LCM message handling loop.""" - try: - while not self.data_ready_event.is_set(): - self.lcm.handle_timeout(100) # 100ms timeout - except Exception as e: - logger.error(f"Error in LCM handling loop: {e}") - - def get_data(self): - """Get the collected data.""" - with self.data_lock: - return self.rgb_data, self.depth_data, self.camera_intrinsics - - -def create_point_cloud(color_img, depth_img, intrinsics): - """Create Open3D point cloud.""" - fx, fy, cx, cy = intrinsics - height, width = depth_img.shape - - o3d_intrinsics = o3d.camera.PinholeCameraIntrinsic(width, height, fx, fy, cx, cy) - color_o3d = o3d.geometry.Image(color_img) - depth_o3d = o3d.geometry.Image((depth_img * 1000).astype(np.uint16)) - - rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth( - color_o3d, depth_o3d, depth_scale=1000.0, convert_rgb_to_intensity=False - ) - - return o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, o3d_intrinsics) - - -def run_processor(color_img, depth_img, intrinsics): - """Run processor and collect results.""" - # Create processor - processor = ManipulationProcessor( - camera_intrinsics=intrinsics, - grasp_server_url="ws://18.224.39.74:8000/ws/grasp", - enable_grasp_generation=False, - enable_segmentation=True, - ) - - # Process single frame directly - results = processor.process_frame(color_img, depth_img) - - # Debug: print available results - print(f"Available results: {list(results.keys())}") - - processor.cleanup() - - return results - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - "--lcm-url", default="udpm://239.255.76.67:7667?ttl=1", help="LCM URL for subscription" - ) - parser.add_argument( - "--timeout", type=float, default=30.0, help="Timeout in seconds to wait for messages" - ) - parser.add_argument( - "--save-images", action="store_true", help="Save received RGB and depth images to files" - ) - args = parser.parse_args() - - # Create data collector - collector = LCMDataCollector(args.lcm_url) - - # Wait for data - if not collector.wait_for_data(args.timeout): - logger.error(f"Timeout waiting for data after {args.timeout} seconds") - logger.error("Make sure Unity is running and publishing to the LCM topics") - return - - # Get the collected data - color_img, depth_img, intrinsics = collector.get_data() - - logger.info(f"Loaded images: color {color_img.shape}, depth {depth_img.shape}") - logger.info(f"Intrinsics: {intrinsics}") - - # Save images if requested - if args.save_images: - try: - cv2.imwrite("received_rgb.png", cv2.cvtColor(color_img, cv2.COLOR_RGB2BGR)) - # Save depth as 16-bit for visualization - depth_viz = (np.clip(depth_img * 1000, 0, 65535)).astype(np.uint16) - cv2.imwrite("received_depth.png", depth_viz) - logger.info("Saved received_rgb.png and received_depth.png") - except Exception as e: - logger.warning(f"Failed to save images: {e}") - - # Run processor - results = run_processor(color_img, depth_img, intrinsics) - - # Debug: Print what we received - print("\nāœ… Processor Results:") - print(f" Available results: {list(results.keys())}") - print(f" Processing time: {results.get('processing_time', 0):.3f}s") - - # Show timing breakdown if available - if "timing_breakdown" in results: - breakdown = results["timing_breakdown"] - print(" Timing breakdown:") - print(f" - Detection: {breakdown.get('detection', 0):.3f}s") - print(f" - Segmentation: {breakdown.get('segmentation', 0):.3f}s") - print(f" - Point cloud: {breakdown.get('pointcloud', 0):.3f}s") - print(f" - Misc extraction: {breakdown.get('misc_extraction', 0):.3f}s") - - # Print object information - detected_count = len(results.get("detected_objects", [])) - all_count = len(results.get("all_objects", [])) - - print(f" Detection objects: {detected_count}") - print(f" All objects processed: {all_count}") - - # Print misc clusters information - if results.get("misc_clusters"): - cluster_count = len(results["misc_clusters"]) - total_misc_points = sum( - len(np.asarray(cluster.points)) for cluster in results["misc_clusters"] - ) - print(f" Misc clusters: {cluster_count} clusters with {total_misc_points} total points") - else: - print(" Misc clusters: None") - - # Print grasp summary - if results.get("grasps"): - total_grasps = 0 - best_score = 0 - for grasp in results["grasps"]: - score = grasp.get("score", 0) - if score > best_score: - best_score = score - total_grasps += 1 - print(f" Grasps generated: {total_grasps} (best score: {best_score:.3f})") - else: - print(" Grasps: None generated") - - # Save results to pickle file - pickle_path = "manipulation_results.pkl" - print(f"\nSaving results to pickle file: {pickle_path}") - - def serialize_point_cloud(pcd): - """Convert Open3D PointCloud to serializable format.""" - if pcd is None: - return None - data = { - "points": np.asarray(pcd.points).tolist() if hasattr(pcd, "points") else [], - "colors": np.asarray(pcd.colors).tolist() - if hasattr(pcd, "colors") and pcd.colors - else [], - } - return data - - def serialize_voxel_grid(voxel_grid): - """Convert Open3D VoxelGrid to serializable format.""" - if voxel_grid is None: - return None - - # Extract voxel data - voxels = voxel_grid.get_voxels() - data = { - "voxel_size": voxel_grid.voxel_size, - "origin": np.asarray(voxel_grid.origin).tolist(), - "voxels": [ - ( - v.grid_index[0], - v.grid_index[1], - v.grid_index[2], - v.color[0], - v.color[1], - v.color[2], - ) - for v in voxels - ], - } - return data - - # Create a copy of results with non-picklable objects converted - pickle_data = { - "color_img": color_img, - "depth_img": depth_img, - "intrinsics": intrinsics, - "results": {}, - } - - # Convert and store all results, properly handling Open3D objects - for key, value in results.items(): - if key.endswith("_viz") or key in [ - "processing_time", - "timing_breakdown", - "detection2d_objects", - "segmentation2d_objects", - ]: - # These are already serializable - pickle_data["results"][key] = value - elif key == "full_pointcloud": - # Serialize PointCloud object - pickle_data["results"][key] = serialize_point_cloud(value) - print(f"Serialized {key}") - elif key == "misc_voxel_grid": - # Serialize VoxelGrid object - pickle_data["results"][key] = serialize_voxel_grid(value) - print(f"Serialized {key}") - elif key == "misc_clusters": - # List of PointCloud objects - if value: - serialized_clusters = [serialize_point_cloud(cluster) for cluster in value] - pickle_data["results"][key] = serialized_clusters - print(f"Serialized {key} ({len(serialized_clusters)} clusters)") - elif key == "detected_objects" or key == "all_objects": - # Objects with PointCloud attributes - serialized_objects = [] - for obj in value: - obj_dict = {k: v for k, v in obj.items() if k != "point_cloud"} - if "point_cloud" in obj: - obj_dict["point_cloud"] = serialize_point_cloud(obj.get("point_cloud")) - serialized_objects.append(obj_dict) - pickle_data["results"][key] = serialized_objects - print(f"Serialized {key} ({len(serialized_objects)} objects)") - else: - try: - # Try to pickle as is - pickle_data["results"][key] = value - print(f"Preserved {key} as is") - except (TypeError, ValueError): - print(f"Warning: Could not serialize {key}, skipping") - - with open(pickle_path, "wb") as f: - pickle.dump(pickle_data, f) - - print("Results saved successfully with all 3D data serialized!") - print(f"Pickled data keys: {list(pickle_data['results'].keys())}") - - # Visualization code has been moved to visualization_script.py - # The results have been pickled and can be loaded from there - print("\nVisualization code has been moved to visualization_script.py") - print("Run 'python visualization_script.py' to visualize the results") - - -if __name__ == "__main__": - main() diff --git a/tests/test_move_vel_unitree.py b/tests/test_move_vel_unitree.py deleted file mode 100644 index 4700c056aa..0000000000 --- a/tests/test_move_vel_unitree.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import time - -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills - -# Initialize robot -robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), ros_control=UnitreeROSControl(), skills=MyUnitreeSkills() -) - -# Move the robot forward -robot.move_vel(x=0.5, y=0, yaw=0, duration=5) - -while True: - time.sleep(1) diff --git a/tests/test_navigate_to_object_robot.py b/tests/test_navigate_to_object_robot.py deleted file mode 100644 index ecf4fd4956..0000000000 --- a/tests/test_navigate_to_object_robot.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import os -import threading -import time - -from reactivex import operators as RxOps - -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.navigation import Navigate -from dimos.utils.logging_config import logger -from dimos.web.robot_web_interface import RobotWebInterface - - -def parse_args(): - parser = argparse.ArgumentParser(description="Navigate to an object using Qwen vision.") - parser.add_argument( - "--object", - type=str, - default="chair", - help="Name of the object to navigate to (default: chair)", - ) - parser.add_argument( - "--distance", - type=float, - default=1.0, - help="Desired distance to maintain from object in meters (default: 0.8)", - ) - parser.add_argument( - "--timeout", - type=float, - default=60.0, - help="Maximum navigation time in seconds (default: 30.0)", - ) - return parser.parse_args() - - -def main(): - # Get command line arguments - args = parse_args() - object_name = args.object # Object to navigate to - distance = args.distance # Desired distance to object - timeout = args.timeout # Maximum navigation time - - print(f"Initializing Unitree Go2 robot for navigating to a {object_name}...") - - # Initialize the robot with ROS control and skills - robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - ros_control=UnitreeROSControl(), - skills=MyUnitreeSkills(), - ) - - # Add and create instance of NavigateToObject skill - robot_skills = robot.get_skills() - robot_skills.add(Navigate) - robot_skills.create_instance("Navigate", robot=robot) - - # Set up tracking and visualization streams - object_tracking_stream = robot.object_tracking_stream - viz_stream = object_tracking_stream.pipe( - RxOps.share(), - RxOps.map(lambda x: x["viz_frame"] if x is not None else None), - RxOps.filter(lambda x: x is not None), - ) - - # The local planner visualization stream is created during robot initialization - local_planner_stream = robot.local_planner_viz_stream - - local_planner_stream = local_planner_stream.pipe( - RxOps.share(), - RxOps.map(lambda x: x if x is not None else None), - RxOps.filter(lambda x: x is not None), - ) - - try: - # Set up web interface - logger.info("Initializing web interface") - streams = { - # "robot_video": video_stream, - "object_tracking": viz_stream, - "local_planner": local_planner_stream, - } - - web_interface = RobotWebInterface(port=5555, **streams) - - # Wait for camera and tracking to initialize - print("Waiting for camera and tracking to initialize...") - time.sleep(3) - - def navigate_to_object(): - try: - result = robot_skills.call( - "Navigate", robot=robot, query=object_name, timeout=timeout - ) - print(f"Navigation result: {result}") - except Exception as e: - print(f"Error during navigation: {e}") - - navigate_thread = threading.Thread(target=navigate_to_object, daemon=True) - navigate_thread.start() - - print( - f"Navigating to {object_name} with desired distance {distance}m and timeout {timeout}s..." - ) - print("Web interface available at http://localhost:5555") - - # Start web server (blocking call) - web_interface.run() - - except KeyboardInterrupt: - print("\nInterrupted by user") - except Exception as e: - print(f"Error during navigation test: {e}") - finally: - print("Test completed") - robot.cleanup() - - -if __name__ == "__main__": - main() diff --git a/tests/test_navigation_skills.py b/tests/test_navigation_skills.py deleted file mode 100644 index 93497de691..0000000000 --- a/tests/test_navigation_skills.py +++ /dev/null @@ -1,265 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Simple test script for semantic / spatial memory skills. - -This script is a simplified version that focuses only on making the workflow work. - -Usage: - # Build and query in one run: - python simple_navigation_test.py --query "kitchen" - - # Skip build and just query: - python simple_navigation_test.py --skip-build --query "kitchen" -""" - -import argparse -import os -import threading -import time - -from reactivex import operators as RxOps - -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.navigation import BuildSemanticMap, Navigate -from dimos.utils.logging_config import setup_logger -from dimos.web.robot_web_interface import RobotWebInterface - -# Setup logging -logger = setup_logger("simple_navigation_test") - - -def parse_args(): - spatial_memory_dir = os.path.abspath( - os.path.join(os.path.dirname(__file__), "../assets/spatial_memory_vegas") - ) - - parser = argparse.ArgumentParser(description="Simple test for semantic map skills.") - parser.add_argument( - "--skip-build", - action="store_true", - help="Skip building the map and run navigation with existing semantic and visual memory", - ) - parser.add_argument( - "--query", type=str, default="kitchen", help="Text query for navigation (default: kitchen)" - ) - parser.add_argument( - "--db-path", - type=str, - default=os.path.join(spatial_memory_dir, "chromadb_data"), - help="Path to ChromaDB database", - ) - parser.add_argument("--justgo", type=str, help="Globally navigate to location") - parser.add_argument( - "--visual-memory-dir", - type=str, - default=spatial_memory_dir, - help="Directory for visual memory", - ) - parser.add_argument( - "--visual-memory-file", - type=str, - default="visual_memory.pkl", - help="Filename for visual memory", - ) - parser.add_argument( - "--port", type=int, default=5555, help="Port for web visualization interface" - ) - return parser.parse_args() - - -def build_map(robot, args): - logger.info("Starting to build spatial memory...") - - # Create the BuildSemanticMap skill - build_skill = BuildSemanticMap( - robot=robot, - db_path=args.db_path, - visual_memory_dir=args.visual_memory_dir, - visual_memory_file=args.visual_memory_file, - ) - - # Start the skill - build_skill() - - # Wait for user to press Ctrl+C - logger.info("Press Ctrl+C to stop mapping and proceed to navigation...") - - try: - while True: - time.sleep(0.5) - except KeyboardInterrupt: - logger.info("Stopping map building...") - - # Stop the skill - build_skill.stop() - logger.info("Map building complete.") - - -def query_map(robot, args): - logger.info(f"Querying spatial memory for: '{args.query}'") - - # Create the Navigate skill - nav_skill = Navigate( - robot=robot, - query=args.query, - db_path=args.db_path, - visual_memory_path=os.path.join(args.visual_memory_dir, args.visual_memory_file), - ) - - # Query the map - result = nav_skill() - - # Display the result - if isinstance(result, dict) and result.get("success", False): - position = result.get("position", (0, 0, 0)) - similarity = result.get("similarity", 0) - logger.info(f"Found '{args.query}' at position: {position}") - logger.info(f"Similarity score: {similarity:.4f}") - return position - - else: - logger.error(f"Navigation query failed: {result}") - return False - - -def setup_visualization(robot, port=5555): - """Set up visualization streams for the web interface""" - logger.info(f"Setting up visualization streams on port {port}") - - # Get video stream from robot - video_stream = robot.video_stream_ros.pipe( - RxOps.share(), - RxOps.map(lambda frame: frame), - RxOps.filter(lambda frame: frame is not None), - ) - - # Get local planner visualization stream - local_planner_stream = robot.local_planner_viz_stream.pipe( - RxOps.share(), - RxOps.map(lambda frame: frame), - RxOps.filter(lambda frame: frame is not None), - ) - - # Create web interface with streams - streams = {"robot_video": video_stream, "local_planner": local_planner_stream} - - web_interface = RobotWebInterface(port=port, **streams) - - return web_interface - - -def run_navigation(robot, target): - """Run navigation in a separate thread""" - logger.info(f"Starting navigation to target: {target}") - return robot.global_planner.set_goal(target) - - -def main(): - args = parse_args() - - # Ensure directories exist - if not args.justgo: - os.makedirs(args.db_path, exist_ok=True) - os.makedirs(args.visual_memory_dir, exist_ok=True) - - # Initialize robot - logger.info("Initializing robot...") - ros_control = UnitreeROSControl(node_name="simple_nav_test", mock_connection=False) - robot = UnitreeGo2(ros_control=ros_control, ip=os.getenv("ROBOT_IP"), skills=MyUnitreeSkills()) - - # Set up visualization - web_interface = None - try: - # Set up visualization first if the robot has video capabilities - if hasattr(robot, "video_stream_ros") and robot.video_stream_ros is not None: - web_interface = setup_visualization(robot, port=args.port) - # Start web interface in a separate thread - viz_thread = threading.Thread(target=web_interface.run, daemon=True) - viz_thread.start() - logger.info(f"Web visualization available at http://localhost:{args.port}") - # Wait a moment for the web interface to initialize - time.sleep(2) - - if args.justgo: - # Just go to the specified location - coords = list(map(float, args.justgo.split(","))) - logger.info(f"Navigating to coordinates: {coords}") - - # Run navigation - navigate_thread = threading.Thread( - target=lambda: run_navigation(robot, coords), daemon=True - ) - navigate_thread.start() - - # Wait for navigation to complete or user to interrupt - try: - while navigate_thread.is_alive(): - time.sleep(0.5) - logger.info("Navigation completed") - except KeyboardInterrupt: - logger.info("Navigation interrupted by user") - else: - # Build map if not skipped - if not args.skip_build: - build_map(robot, args) - - # Query the map - target = query_map(robot, args) - - if not target: - logger.error("No target found for navigation.") - return - - # Run navigation - navigate_thread = threading.Thread( - target=lambda: run_navigation(robot, target), daemon=True - ) - navigate_thread.start() - - # Wait for navigation to complete or user to interrupt - try: - while navigate_thread.is_alive(): - time.sleep(0.5) - logger.info("Navigation completed") - except KeyboardInterrupt: - logger.info("Navigation interrupted by user") - - # If web interface is running, keep the main thread alive - if web_interface: - logger.info( - "Navigation completed. Visualization still available. Press Ctrl+C to exit." - ) - try: - while True: - time.sleep(0.5) - except KeyboardInterrupt: - logger.info("Exiting...") - - finally: - # Clean up - logger.info("Cleaning up resources...") - try: - robot.cleanup() - except Exception as e: - logger.error(f"Error during cleanup: {e}") - - logger.info("Test completed successfully") - - -if __name__ == "__main__": - main() diff --git a/tests/test_object_detection_agent_data_query_stream.py b/tests/test_object_detection_agent_data_query_stream.py deleted file mode 100644 index ca5671f78e..0000000000 --- a/tests/test_object_detection_agent_data_query_stream.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import os -import sys -import threading - -from dotenv import load_dotenv -from reactivex import operators as ops - -from dimos.agents.claude_agent import ClaudeAgent -from dimos.perception.detection2d.detic_2d_det import Detic2DDetector -from dimos.perception.object_detection_stream import ObjectDetectionStream -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.stream.video_provider import VideoProvider -from dimos.utils.reactive import backpressure -from dimos.web.robot_web_interface import RobotWebInterface - - -def parse_args(): - parser = argparse.ArgumentParser( - description="Test ObjectDetectionStream for object detection and position estimation" - ) - parser.add_argument( - "--mode", - type=str, - default="webcam", - choices=["robot", "webcam"], - help='Mode to run: "robot" or "webcam" (default: webcam)', - ) - return parser.parse_args() - - -load_dotenv() - - -def main(): - # Get command line arguments - args = parse_args() - - # Set default parameters - min_confidence = 0.6 - class_filter = None # No class filtering - web_port = 5555 - - # Initialize detector - detector = Detic2DDetector(vocabulary=None, threshold=min_confidence) - - # Initialize based on mode - if args.mode == "robot": - print("Initializing in robot mode...") - - # Get robot IP from environment - robot_ip = os.getenv("ROBOT_IP") - if not robot_ip: - print("Error: ROBOT_IP environment variable not set.") - sys.exit(1) - - # Initialize robot - robot = UnitreeGo2( - ip=robot_ip, - ros_control=UnitreeROSControl(), - skills=MyUnitreeSkills(), - ) - # Create video stream from robot's camera - video_stream = robot.video_stream_ros - - # Initialize ObjectDetectionStream with robot and transform function - object_detector = ObjectDetectionStream( - camera_intrinsics=robot.camera_intrinsics, - min_confidence=min_confidence, - class_filter=class_filter, - transform_to_map=robot.ros_control.transform_pose, - detector=detector, - video_stream=video_stream, - ) - - else: # webcam mode - print("Initializing in webcam mode...") - - # Define camera intrinsics for the webcam - # These are approximate values for a typical 640x480 webcam - width, height = 640, 480 - focal_length_mm = 3.67 # mm (typical webcam) - sensor_width_mm = 4.8 # mm (1/4" sensor) - - # Calculate focal length in pixels - focal_length_x_px = width * focal_length_mm / sensor_width_mm - focal_length_y_px = height * focal_length_mm / sensor_width_mm - - # Principal point (center of image) - cx, cy = width / 2, height / 2 - - # Camera intrinsics in [fx, fy, cx, cy] format - camera_intrinsics = [focal_length_x_px, focal_length_y_px, cx, cy] - - # Initialize video provider and ObjectDetectionStream - video_provider = VideoProvider("test_camera", video_source=0) # Default camera - # Create video stream - video_stream = backpressure( - video_provider.capture_video_as_observable(realtime=True, fps=30) - ) - - object_detector = ObjectDetectionStream( - camera_intrinsics=camera_intrinsics, - min_confidence=min_confidence, - class_filter=class_filter, - detector=detector, - video_stream=video_stream, - ) - - # Set placeholder robot for cleanup - robot = None - - # Create visualization stream for web interface - viz_stream = object_detector.get_stream().pipe( - ops.share(), - ops.map(lambda x: x["viz_frame"] if x is not None else None), - ops.filter(lambda x: x is not None), - ) - - # Create object data observable for Agent using the formatted stream - object_data_stream = object_detector.get_formatted_stream().pipe( - ops.share(), ops.filter(lambda x: x is not None) - ) - - # Create stop event for clean shutdown - stop_event = threading.Event() - - try: - # Set up web interface - print("Initializing web interface...") - web_interface = RobotWebInterface(port=web_port, object_detection=viz_stream) - - agent = ClaudeAgent( - dev_name="test_agent", - # input_query_stream=stt_node.emit_text(), - input_query_stream=web_interface.query_stream, - input_data_stream=object_data_stream, - system_query="Tell me what you see", - model_name="claude-3-7-sonnet-latest", - thinking_budget_tokens=0, - ) - - # Print configuration information - print("\nObjectDetectionStream Test Running:") - print(f"Mode: {args.mode}") - print(f"Web Interface: http://localhost:{web_port}") - print("\nPress Ctrl+C to stop the test\n") - - # Start web server (blocking call) - web_interface.run() - - except KeyboardInterrupt: - print("\nTest interrupted by user") - except Exception as e: - print(f"Error during test: {e}") - finally: - # Clean up resources - print("Cleaning up resources...") - stop_event.set() - - if args.mode == "robot" and robot: - robot.cleanup() - elif args.mode == "webcam": - if "video_provider" in locals(): - video_provider.dispose_all() - - print("Test completed") - - -if __name__ == "__main__": - main() diff --git a/tests/test_object_detection_stream.py b/tests/test_object_detection_stream.py deleted file mode 100644 index 2d45c261d5..0000000000 --- a/tests/test_object_detection_stream.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import os -import sys -import threading -import time -from typing import Any - -from dotenv import load_dotenv -from reactivex import operators as ops - -from dimos.perception.object_detection_stream import ObjectDetectionStream -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.stream.video_provider import VideoProvider -from dimos.utils.reactive import backpressure -from dimos.web.robot_web_interface import RobotWebInterface - - -def parse_args(): - parser = argparse.ArgumentParser( - description="Test ObjectDetectionStream for object detection and position estimation" - ) - parser.add_argument( - "--mode", - type=str, - default="webcam", - choices=["robot", "webcam"], - help='Mode to run: "robot" or "webcam" (default: webcam)', - ) - return parser.parse_args() - - -load_dotenv() - - -class ResultPrinter: - def __init__(self, print_interval: float = 1.0): - """ - Initialize a result printer that limits console output frequency. - - Args: - print_interval: Minimum time between console prints in seconds - """ - self.print_interval = print_interval - self.last_print_time = 0 - - def print_results(self, objects: list[dict[str, Any]]): - """Print object detection results to console with rate limiting.""" - current_time = time.time() - - # Only print results at the specified interval - if current_time - self.last_print_time >= self.print_interval: - self.last_print_time = current_time - - if not objects: - print("\n[No objects detected]") - return - - print("\n" + "=" * 50) - print(f"Detected {len(objects)} objects at {time.strftime('%H:%M:%S')}:") - print("=" * 50) - - for i, obj in enumerate(objects): - pos = obj["position"] - rot = obj["rotation"] - size = obj["size"] - - print( - f"{i + 1}. {obj['label']} (ID: {obj['object_id']}, Conf: {obj['confidence']:.2f})" - ) - print(f" Position: x={pos.x:.2f}, y={pos.y:.2f}, z={pos.z:.2f} m") - print(f" Rotation: yaw={rot.z:.2f} rad") - print(f" Size: width={size['width']:.2f}, height={size['height']:.2f} m") - print(f" Depth: {obj['depth']:.2f} m") - print("-" * 30) - - -def main(): - # Get command line arguments - args = parse_args() - - # Set up the result printer for console output - result_printer = ResultPrinter(print_interval=1.0) - - # Set default parameters - min_confidence = 0.6 - class_filter = None # No class filtering - web_port = 5555 - - # Initialize based on mode - if args.mode == "robot": - print("Initializing in robot mode...") - - # Get robot IP from environment - robot_ip = os.getenv("ROBOT_IP") - if not robot_ip: - print("Error: ROBOT_IP environment variable not set.") - sys.exit(1) - - # Initialize robot - robot = UnitreeGo2( - ip=robot_ip, - ros_control=UnitreeROSControl(), - skills=MyUnitreeSkills(), - ) - # Create video stream from robot's camera - video_stream = robot.video_stream_ros - - # Initialize ObjectDetectionStream with robot and transform function - object_detector = ObjectDetectionStream( - camera_intrinsics=robot.camera_intrinsics, - min_confidence=min_confidence, - class_filter=class_filter, - transform_to_map=robot.ros_control.transform_pose, - detector=detector, - video_stream=video_stream, - disable_depth=False, - ) - - else: # webcam mode - print("Initializing in webcam mode...") - - # Define camera intrinsics for the webcam - # These are approximate values for a typical 640x480 webcam - width, height = 640, 480 - focal_length_mm = 3.67 # mm (typical webcam) - sensor_width_mm = 4.8 # mm (1/4" sensor) - - # Calculate focal length in pixels - focal_length_x_px = width * focal_length_mm / sensor_width_mm - focal_length_y_px = height * focal_length_mm / sensor_width_mm - - # Principal point (center of image) - cx, cy = width / 2, height / 2 - - # Camera intrinsics in [fx, fy, cx, cy] format - camera_intrinsics = [focal_length_x_px, focal_length_y_px, cx, cy] - - # Initialize video provider and ObjectDetectionStream - video_provider = VideoProvider("test_camera", video_source=0) # Default camera - # Create video stream - video_stream = backpressure( - video_provider.capture_video_as_observable(realtime=True, fps=30) - ) - - object_detector = ObjectDetectionStream( - camera_intrinsics=camera_intrinsics, - min_confidence=min_confidence, - class_filter=class_filter, - video_stream=video_stream, - disable_depth=False, - draw_masks=True, - ) - - # Set placeholder robot for cleanup - robot = None - - # Create visualization stream for web interface - viz_stream = object_detector.get_stream().pipe( - ops.share(), - ops.map(lambda x: x["viz_frame"] if x is not None else None), - ops.filter(lambda x: x is not None), - ) - - # Create stop event for clean shutdown - stop_event = threading.Event() - - # Define subscription callback to print results - def on_next(result): - if stop_event.is_set(): - return - - # Print detected objects to console - if "objects" in result: - result_printer.print_results(result["objects"]) - - def on_error(error): - print(f"Error in detection stream: {error}") - stop_event.set() - - def on_completed(): - print("Detection stream completed") - stop_event.set() - - try: - # Subscribe to the detection stream - subscription = object_detector.get_stream().subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed - ) - - # Set up web interface - print("Initializing web interface...") - web_interface = RobotWebInterface(port=web_port, object_detection=viz_stream) - - # Print configuration information - print("\nObjectDetectionStream Test Running:") - print(f"Mode: {args.mode}") - print(f"Web Interface: http://localhost:{web_port}") - print("\nPress Ctrl+C to stop the test\n") - - # Start web server (blocking call) - web_interface.run() - - except KeyboardInterrupt: - print("\nTest interrupted by user") - except Exception as e: - print(f"Error during test: {e}") - finally: - # Clean up resources - print("Cleaning up resources...") - stop_event.set() - - if subscription: - subscription.dispose() - - if args.mode == "robot" and robot: - robot.cleanup() - elif args.mode == "webcam": - if "video_provider" in locals(): - video_provider.dispose_all() - - print("Test completed") - - -if __name__ == "__main__": - main() diff --git a/tests/test_object_tracking_module.py b/tests/test_object_tracking_module.py deleted file mode 100755 index 4fc1adac83..0000000000 --- a/tests/test_object_tracking_module.py +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test script for Object Tracking module with ZED camera.""" - -import asyncio - -import cv2 -from dimos_lcm.sensor_msgs import CameraInfo - -from dimos import core -from dimos.hardware.zed_camera import ZEDModule -from dimos.msgs.geometry_msgs import PoseStamped - -# Import message types -from dimos.msgs.sensor_msgs import Image -from dimos.perception.object_tracker import ObjectTracking -from dimos.protocol import pubsub -from dimos.protocol.pubsub.lcmpubsub import LCM, Topic -from dimos.robot.foxglove_bridge import FoxgloveBridge -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_object_tracking_module") - -# Suppress verbose Foxglove bridge warnings -import logging - -logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) -logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) - - -class TrackingVisualization: - """Handles visualization and user interaction for object tracking.""" - - def __init__(self): - self.lcm = LCM() - self.latest_color = None - - # Mouse interaction state - self.selecting_bbox = False - self.bbox_start = None - self.current_bbox = None - self.tracking_active = False - - # Subscribe to color image topic only - self.color_topic = Topic("/zed/color_image", Image) - - def start(self): - """Start the visualization node.""" - self.lcm.start() - - # Subscribe to color image only - self.lcm.subscribe(self.color_topic, self._on_color_image) - - logger.info("Visualization started, subscribed to color image topic") - - def _on_color_image(self, msg: Image, _: str): - """Handle color image messages.""" - try: - # Convert dimos Image to OpenCV format (BGR) for display - self.latest_color = msg.to_opencv() - logger.debug(f"Received color image: {msg.width}x{msg.height}, format: {msg.format}") - except Exception as e: - logger.error(f"Error processing color image: {e}") - - def mouse_callback(self, event, x, y, _, param): - """Handle mouse events for bbox selection.""" - tracker_module = param.get("tracker") - - if event == cv2.EVENT_LBUTTONDOWN: - self.selecting_bbox = True - self.bbox_start = (x, y) - self.current_bbox = None - - elif event == cv2.EVENT_MOUSEMOVE and self.selecting_bbox: - # Update current selection for visualization - x1, y1 = self.bbox_start - self.current_bbox = [min(x1, x), min(y1, y), max(x1, x), max(y1, y)] - - elif event == cv2.EVENT_LBUTTONUP and self.selecting_bbox: - self.selecting_bbox = False - if self.bbox_start: - x1, y1 = self.bbox_start - x2, y2 = x, y - # Ensure valid bbox - bbox = [min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)] - - # Check if bbox is valid (has area) - if bbox[2] > bbox[0] and bbox[3] > bbox[1]: - # Call track RPC on the tracker module - if tracker_module: - result = tracker_module.track(bbox) - logger.info(f"Tracking initialized: {result}") - self.tracking_active = True - self.current_bbox = None - - def draw_interface(self, frame): - """Draw UI elements on the frame.""" - # Draw bbox selection if in progress - if self.selecting_bbox and self.current_bbox: - x1, y1, x2, y2 = self.current_bbox - cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 255), 2) - - # Draw instructions - cv2.putText( - frame, - "Click and drag to select object", - (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (255, 255, 255), - 2, - ) - cv2.putText( - frame, - "Press 's' to stop tracking, 'q' to quit", - (10, 60), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (255, 255, 255), - 2, - ) - - # Show tracking status - if self.tracking_active: - status = "Tracking Active" - color = (0, 255, 0) - else: - status = "No Target" - color = (0, 0, 255) - cv2.putText(frame, f"Status: {status}", (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) - - return frame - - -async def test_object_tracking_module(): - """Test object tracking with ZED camera module.""" - logger.info("Starting Object Tracking Module test") - - # Start Dimos - dimos = core.start(2) - - # Enable LCM auto-configuration - pubsub.lcm.autoconf() - - viz = None - tracker = None - zed = None - foxglove_bridge = None - - try: - # Deploy ZED module - logger.info("Deploying ZED module...") - zed = dimos.deploy( - ZEDModule, - camera_id=0, - resolution="HD720", - depth_mode="NEURAL", - fps=30, - enable_tracking=True, - publish_rate=15.0, - frame_id="zed_camera_link", - ) - - # Configure ZED LCM transports - zed.color_image.transport = core.LCMTransport("/zed/color_image", Image) - zed.depth_image.transport = core.LCMTransport("/zed/depth_image", Image) - zed.camera_info.transport = core.LCMTransport("/zed/camera_info", CameraInfo) - zed.pose.transport = core.LCMTransport("/zed/pose", PoseStamped) - - # Start ZED to begin publishing - zed.start() - await asyncio.sleep(2) # Wait for camera to initialize - - # Deploy Object Tracking module - logger.info("Deploying Object Tracking module...") - tracker = dimos.deploy( - ObjectTracking, - camera_intrinsics=None, # Will get from camera_info topic - reid_threshold=5, - reid_fail_tolerance=10, - frame_id="zed_camera_link", - ) - - # Configure tracking LCM transports - tracker.color_image.transport = core.LCMTransport("/zed/color_image", Image) - tracker.depth.transport = core.LCMTransport("/zed/depth_image", Image) - tracker.camera_info.transport = core.LCMTransport("/zed/camera_info", CameraInfo) - - # Configure output transports - from dimos_lcm.vision_msgs import Detection2DArray, Detection3DArray - - tracker.detection2darray.transport = core.LCMTransport( - "/detection2darray", Detection2DArray - ) - tracker.detection3darray.transport = core.LCMTransport( - "/detection3darray", Detection3DArray - ) - tracker.tracked_overlay.transport = core.LCMTransport("/tracked_overlay", Image) - - # Connect inputs - tracker.color_image.connect(zed.color_image) - tracker.depth.connect(zed.depth_image) - tracker.camera_info.connect(zed.camera_info) - - # Start tracker - tracker.start() - - # Create visualization - viz = TrackingVisualization() - viz.start() - - # Start Foxglove bridge for visualization - foxglove_bridge = FoxgloveBridge() - foxglove_bridge.acquire() - - # Give modules time to initialize - await asyncio.sleep(1) - - # Create OpenCV window and set mouse callback - cv2.namedWindow("Object Tracking") - cv2.setMouseCallback("Object Tracking", viz.mouse_callback, {"tracker": tracker}) - - logger.info("System ready. Click and drag to select an object to track.") - logger.info("Foxglove visualization available at http://localhost:8765") - - # Main visualization loop - while True: - # Get the color frame to display - if viz.latest_color is not None: - display_frame = viz.latest_color.copy() - else: - # Wait for frames - await asyncio.sleep(0.03) - continue - - # Draw UI elements - display_frame = viz.draw_interface(display_frame) - - # Show frame - cv2.imshow("Object Tracking", display_frame) - - # Handle keyboard input - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - logger.info("Quit requested") - break - elif key == ord("s"): - # Stop tracking - if tracker: - tracker.stop_track() - viz.tracking_active = False - logger.info("Tracking stopped") - - await asyncio.sleep(0.03) # ~30 FPS - - except Exception as e: - logger.error(f"Error in test: {e}") - import traceback - - traceback.print_exc() - - finally: - # Clean up - cv2.destroyAllWindows() - - if tracker: - tracker.stop() - if zed: - zed.stop() - if foxglove_bridge: - foxglove_bridge.release() - - dimos.close() - logger.info("Test completed") - - -if __name__ == "__main__": - asyncio.run(test_object_tracking_module()) diff --git a/tests/test_object_tracking_webcam.py b/tests/test_object_tracking_webcam.py deleted file mode 100644 index 8fcfe7bacd..0000000000 --- a/tests/test_object_tracking_webcam.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import queue -import threading - -import cv2 - -from dimos.perception.object_tracker import ObjectTrackingStream -from dimos.stream.video_provider import VideoProvider - -# Global variables for bounding box selection -selecting_bbox = False -bbox_points = [] -current_bbox = None -tracker_initialized = False -object_size = 0.30 # Hardcoded object size in meters (adjust based on your tracking target) - - -def mouse_callback(event, x, y, flags, param): - global selecting_bbox, bbox_points, current_bbox, tracker_initialized, tracker_stream - - if event == cv2.EVENT_LBUTTONDOWN: - # Start bbox selection - selecting_bbox = True - bbox_points = [(x, y)] - current_bbox = None - tracker_initialized = False - - elif event == cv2.EVENT_MOUSEMOVE and selecting_bbox: - # Update current selection for visualization - current_bbox = [bbox_points[0][0], bbox_points[0][1], x, y] - - elif event == cv2.EVENT_LBUTTONUP: - # End bbox selection - selecting_bbox = False - if bbox_points: - bbox_points.append((x, y)) - x1, y1 = bbox_points[0] - x2, y2 = bbox_points[1] - # Ensure x1,y1 is top-left and x2,y2 is bottom-right - current_bbox = [min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)] - # Add the bbox to the tracking queue - if param.get("bbox_queue") and not tracker_initialized: - param["bbox_queue"].put((current_bbox, object_size)) - tracker_initialized = True - - -def main(): - global tracker_initialized - - # Create queues for thread communication - frame_queue = queue.Queue(maxsize=5) - bbox_queue = queue.Queue() - stop_event = threading.Event() - - # Logitech C920e camera parameters at 480p - # Convert physical parameters to pixel-based intrinsics - width, height = 640, 480 - focal_length_mm = 3.67 # mm - sensor_width_mm = 4.8 # mm (1/4" sensor) - sensor_height_mm = 3.6 # mm - - # Calculate focal length in pixels - focal_length_x_px = width * focal_length_mm / sensor_width_mm - focal_length_y_px = height * focal_length_mm / sensor_height_mm - - # Principal point (assuming center of image) - cx = width / 2 - cy = height / 2 - - # Final camera intrinsics in [fx, fy, cx, cy] format - camera_intrinsics = [focal_length_x_px, focal_length_y_px, cx, cy] - - # Initialize video provider and object tracking stream - video_provider = VideoProvider("test_camera", video_source=0) - tracker_stream = ObjectTrackingStream( - camera_intrinsics=camera_intrinsics, - camera_pitch=0.0, # Adjust if your camera is tilted - camera_height=0.5, # Height of camera from ground in meters (adjust as needed) - ) - - # Create video stream - video_stream = video_provider.capture_video_as_observable(realtime=True, fps=30) - tracking_stream = tracker_stream.create_stream(video_stream) - - # Define callbacks for the tracking stream - def on_next(result): - if stop_event.is_set(): - return - - # Get the visualization frame - viz_frame = result["viz_frame"] - - # If we're selecting a bbox, draw the current selection - if selecting_bbox and current_bbox is not None: - x1, y1, x2, y2 = current_bbox - cv2.rectangle(viz_frame, (x1, y1), (x2, y2), (0, 255, 255), 2) - - # Add instructions - cv2.putText( - viz_frame, - "Click and drag to select object", - (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (255, 255, 255), - 2, - ) - cv2.putText( - viz_frame, - f"Object size: {object_size:.2f}m", - (10, 60), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (255, 255, 255), - 2, - ) - - # Show tracking status - status = "Tracking" if tracker_initialized else "Not tracking" - cv2.putText( - viz_frame, - f"Status: {status}", - (10, 90), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (0, 255, 0) if tracker_initialized else (0, 0, 255), - 2, - ) - - # Put frame in queue for main thread to display - try: - frame_queue.put_nowait(viz_frame) - except queue.Full: - # Skip frame if queue is full - pass - - def on_error(error): - print(f"Error: {error}") - stop_event.set() - - def on_completed(): - print("Stream completed") - stop_event.set() - - # Start the subscription - subscription = None - - try: - # Subscribe to start processing in background thread - subscription = tracking_stream.subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed - ) - - print("Object tracking started. Click and drag to select an object. Press 'q' to exit.") - - # Create window and set mouse callback - cv2.namedWindow("Object Tracker") - cv2.setMouseCallback("Object Tracker", mouse_callback, {"bbox_queue": bbox_queue}) - - # Main thread loop for displaying frames and handling bbox selection - while not stop_event.is_set(): - # Check if there's a new bbox to track - try: - new_bbox, size = bbox_queue.get_nowait() - print(f"New object selected: {new_bbox}, size: {size}m") - # Initialize tracker with the new bbox and size - tracker_stream.track(new_bbox, size=size) - except queue.Empty: - pass - - try: - # Get frame with timeout - viz_frame = frame_queue.get(timeout=1.0) - - # Display the frame - cv2.imshow("Object Tracker", viz_frame) - # Check for exit key - if cv2.waitKey(1) & 0xFF == ord("q"): - print("Exit key pressed") - break - - except queue.Empty: - # No frame available, check if we should continue - if cv2.waitKey(1) & 0xFF == ord("q"): - print("Exit key pressed") - break - continue - - except KeyboardInterrupt: - print("\nKeyboard interrupt received. Stopping...") - finally: - # Signal threads to stop - stop_event.set() - - # Clean up resources - if subscription: - subscription.dispose() - - video_provider.dispose_all() - tracker_stream.cleanup() - cv2.destroyAllWindows() - print("Cleanup complete") - - -if __name__ == "__main__": - main() diff --git a/tests/test_object_tracking_with_qwen.py b/tests/test_object_tracking_with_qwen.py deleted file mode 100644 index e8fcd86a2b..0000000000 --- a/tests/test_object_tracking_with_qwen.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import queue -import threading - -import cv2 - -from dimos.models.qwen.video_query import get_bbox_from_qwen -from dimos.perception.object_tracker import ObjectTrackingStream -from dimos.stream.video_provider import VideoProvider - -# Global variables for tracking control -object_size = 0.30 # Hardcoded object size in meters (adjust based on your tracking target) -tracking_object_name = "object" # Will be updated by Qwen -object_name = "hairbrush" # Example object name for Qwen - -global tracker_initialized, detection_in_progress - -# Create queues for thread communication -frame_queue = queue.Queue(maxsize=5) -stop_event = threading.Event() - -# Logitech C920e camera parameters at 480p -width, height = 640, 480 -focal_length_mm = 3.67 # mm -sensor_width_mm = 4.8 # mm (1/4" sensor) -sensor_height_mm = 3.6 # mm - -# Calculate focal length in pixels -focal_length_x_px = width * focal_length_mm / sensor_width_mm -focal_length_y_px = height * focal_length_mm / sensor_height_mm -cx, cy = width / 2, height / 2 - -# Final camera intrinsics in [fx, fy, cx, cy] format -camera_intrinsics = [focal_length_x_px, focal_length_y_px, cx, cy] - -# Initialize video provider and object tracking stream -video_provider = VideoProvider("webcam", video_source=0) -tracker_stream = ObjectTrackingStream( - camera_intrinsics=camera_intrinsics, camera_pitch=0.0, camera_height=0.5 -) - -# Create video streams -video_stream = video_provider.capture_video_as_observable(realtime=True, fps=10) -tracking_stream = tracker_stream.create_stream(video_stream) - -# Check if display is available -if "DISPLAY" not in os.environ: - raise RuntimeError( - "No display available. Please set DISPLAY environment variable or run in headless mode." - ) - - -# Define callbacks for the tracking stream -def on_next(result): - global tracker_initialized, detection_in_progress - if stop_event.is_set(): - return - - # Get the visualization frame - viz_frame = result["viz_frame"] - - # Add information to the visualization - cv2.putText( - viz_frame, - f"Tracking {tracking_object_name}", - (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (255, 255, 255), - 2, - ) - cv2.putText( - viz_frame, - f"Object size: {object_size:.2f}m", - (10, 60), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (255, 255, 255), - 2, - ) - - # Show tracking status - status = "Tracking" if tracker_initialized else "Waiting for detection" - color = (0, 255, 0) if tracker_initialized else (0, 0, 255) - cv2.putText(viz_frame, f"Status: {status}", (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) - - # If detection is in progress, show a message - if detection_in_progress: - cv2.putText( - viz_frame, "Querying Qwen...", (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2 - ) - - # Put frame in queue for main thread to display - try: - frame_queue.put_nowait(viz_frame) - except queue.Full: - pass - - -def on_error(error): - print(f"Error: {error}") - stop_event.set() - - -def on_completed(): - print("Stream completed") - stop_event.set() - - -# Start the subscription -subscription = None - -try: - # Initialize global flags - tracker_initialized = False - detection_in_progress = False - # Subscribe to start processing in background thread - subscription = tracking_stream.subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed - ) - - print("Object tracking with Qwen started. Press 'q' to exit.") - print("Waiting for initial object detection...") - - # Main thread loop for displaying frames and updating tracking - while not stop_event.is_set(): - # Check if we need to update tracking - - if not detection_in_progress: - detection_in_progress = True - print("Requesting object detection from Qwen...") - - print("detection_in_progress: ", detection_in_progress) - print("tracker_initialized: ", tracker_initialized) - - def detection_task(): - global detection_in_progress, tracker_initialized, tracking_object_name, object_size - try: - result = get_bbox_from_qwen(video_stream, object_name=object_name) - print(f"Got result from Qwen: {result}") - - if result: - bbox, size = result - print(f"Detected object at {bbox} with size {size}") - tracker_stream.track(bbox, size=size) - tracker_initialized = True - return - - print("No object detected by Qwen") - tracker_initialized = False - tracker_stream.stop_track() - - except Exception as e: - print(f"Error in update_tracking: {e}") - tracker_initialized = False - tracker_stream.stop_track() - finally: - detection_in_progress = False - - # Run detection task in a separate thread - threading.Thread(target=detection_task, daemon=True).start() - - try: - # Get frame with timeout - viz_frame = frame_queue.get(timeout=0.1) - - # Display the frame - cv2.imshow("Object Tracking with Qwen", viz_frame) - - # Check for exit key - if cv2.waitKey(1) & 0xFF == ord("q"): - print("Exit key pressed") - break - - except queue.Empty: - # No frame available, check if we should continue - if cv2.waitKey(1) & 0xFF == ord("q"): - print("Exit key pressed") - break - continue - -except KeyboardInterrupt: - print("\nKeyboard interrupt received. Stopping...") -finally: - # Signal threads to stop - stop_event.set() - - # Clean up resources - if subscription: - subscription.dispose() - - video_provider.dispose_all() - tracker_stream.cleanup() - cv2.destroyAllWindows() - print("Cleanup complete") diff --git a/tests/test_person_following_robot.py b/tests/test_person_following_robot.py deleted file mode 100644 index f7ee6eaf0d..0000000000 --- a/tests/test_person_following_robot.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import time - -from reactivex import operators as RxOps - -from dimos.models.qwen.video_query import query_single_frame_observable -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.utils.logging_config import logger -from dimos.web.robot_web_interface import RobotWebInterface - - -def main(): - # Hardcoded parameters - timeout = 60.0 # Maximum time to follow a person (seconds) - distance = 0.5 # Desired distance to maintain from target (meters) - - print("Initializing Unitree Go2 robot...") - - # Initialize the robot with ROS control and skills - robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - ros_control=UnitreeROSControl(), - skills=MyUnitreeSkills(), - ) - - tracking_stream = robot.person_tracking_stream - viz_stream = tracking_stream.pipe( - RxOps.share(), - RxOps.map(lambda x: x["viz_frame"] if x is not None else None), - RxOps.filter(lambda x: x is not None), - ) - video_stream = robot.get_ros_video_stream() - - try: - # Set up web interface - logger.info("Initializing web interface") - streams = {"unitree_video": video_stream, "person_tracking": viz_stream} - - web_interface = RobotWebInterface(port=5555, **streams) - - # Wait for camera and tracking to initialize - print("Waiting for camera and tracking to initialize...") - time.sleep(5) - # Get initial point from Qwen - - max_retries = 5 - delay = 3 - - for attempt in range(max_retries): - try: - qwen_point = eval( - query_single_frame_observable( - video_stream, - "Look at this frame and point to the person shirt. Return ONLY their center coordinates as a tuple (x,y).", - ) - .pipe(RxOps.take(1)) - .run() - ) # Get first response and convert string tuple to actual tuple - logger.info(f"Found person at coordinates {qwen_point}") - break # If successful, break out of retry loop - except Exception as e: - if attempt < max_retries - 1: - logger.error( - f"Person not found. Attempt {attempt + 1}/{max_retries} failed. Retrying in {delay}s... Error: {e}" - ) - time.sleep(delay) - else: - logger.error(f"Person not found after {max_retries} attempts. Last error: {e}") - return - - # Start following human in a separate thread - import threading - - follow_thread = threading.Thread( - target=lambda: robot.follow_human(timeout=timeout, distance=distance, point=qwen_point), - daemon=True, - ) - follow_thread.start() - - print(f"Following human at point {qwen_point} for {timeout} seconds...") - print("Web interface available at http://localhost:5555") - - # Start web server (blocking call) - web_interface.run() - - except KeyboardInterrupt: - print("\nInterrupted by user") - except Exception as e: - print(f"Error during test: {e}") - finally: - print("Test completed") - robot.cleanup() - - -if __name__ == "__main__": - main() diff --git a/tests/test_person_following_webcam.py b/tests/test_person_following_webcam.py deleted file mode 100644 index 20a6a7ca4d..0000000000 --- a/tests/test_person_following_webcam.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import queue -import threading - -import cv2 -import numpy as np - -from dimos.perception.person_tracker import PersonTrackingStream -from dimos.perception.visual_servoing import VisualServoing -from dimos.stream.video_provider import VideoProvider - - -def main(): - # Create a queue for thread communication (limit to prevent memory issues) - frame_queue = queue.Queue(maxsize=5) - result_queue = queue.Queue(maxsize=5) # For tracking results - stop_event = threading.Event() - - # Logitech C920e camera parameters at 480p - # Convert physical parameters to intrinsics [fx, fy, cx, cy] - resolution = (640, 480) # 480p resolution - focal_length_mm = 3.67 # mm - sensor_size_mm = (4.8, 3.6) # mm (1/4" sensor) - - # Calculate focal length in pixels - fx = (resolution[0] * focal_length_mm) / sensor_size_mm[0] - fy = (resolution[1] * focal_length_mm) / sensor_size_mm[1] - - # Principal point (typically at image center) - cx = resolution[0] / 2 - cy = resolution[1] / 2 - - # Camera intrinsics in [fx, fy, cx, cy] format - camera_intrinsics = [fx, fy, cx, cy] - - # Camera mounted parameters - camera_pitch = np.deg2rad(-5) # negative for downward pitch - camera_height = 1.4 # meters - - # Initialize video provider and person tracking stream - video_provider = VideoProvider("test_camera", video_source=0) - person_tracker = PersonTrackingStream( - camera_intrinsics=camera_intrinsics, camera_pitch=camera_pitch, camera_height=camera_height - ) - - # Create streams - video_stream = video_provider.capture_video_as_observable(realtime=False, fps=20) - person_tracking_stream = person_tracker.create_stream(video_stream) - - # Create visual servoing object - visual_servoing = VisualServoing( - tracking_stream=person_tracking_stream, - max_linear_speed=0.5, - max_angular_speed=0.75, - desired_distance=2.5, - ) - - # Track if we have selected a person to follow - selected_point = None - tracking_active = False - - # Define callbacks for the tracking stream - def on_next(result): - if stop_event.is_set(): - return - - # Get the visualization frame which already includes person detections - # with bounding boxes, tracking IDs, and distance/angle information - viz_frame = result["viz_frame"] - - # Store the result for the main thread to use with visual servoing - try: - result_queue.put_nowait(result) - except queue.Full: - # Skip if queue is full - pass - - # Put frame in queue for main thread to display (non-blocking) - try: - frame_queue.put_nowait(viz_frame) - except queue.Full: - # Skip frame if queue is full - pass - - def on_error(error): - print(f"Error: {error}") - stop_event.set() - - def on_completed(): - print("Stream completed") - stop_event.set() - - # Mouse callback for selecting a person to track - def mouse_callback(event, x, y, flags, param): - nonlocal selected_point, tracking_active - - if event == cv2.EVENT_LBUTTONDOWN: - # Store the clicked point - selected_point = (x, y) - tracking_active = False # Will be set to True if start_tracking succeeds - print(f"Selected point: {selected_point}") - - # Start the subscription - subscription = None - - try: - # Subscribe to start processing in background thread - subscription = person_tracking_stream.subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed - ) - - print("Person tracking visualization started.") - print("Click on a person to start visual servoing. Press 'q' to exit.") - - # Set up mouse callback - cv2.namedWindow("Person Tracking") - cv2.setMouseCallback("Person Tracking", mouse_callback) - - # Main thread loop for displaying frames - while not stop_event.is_set(): - try: - # Get frame with timeout (allows checking stop_event periodically) - frame = frame_queue.get(timeout=1.0) - - # Call the visual servoing if we have a selected point - if selected_point is not None: - # If not actively tracking, try to start tracking - if not tracking_active: - tracking_active = visual_servoing.start_tracking(point=selected_point) - if not tracking_active: - print("Failed to start tracking") - selected_point = None - - # If tracking is active, update tracking - if tracking_active: - servoing_result = visual_servoing.updateTracking() - - # Display visual servoing output on the frame - linear_vel = servoing_result.get("linear_vel", 0.0) - angular_vel = servoing_result.get("angular_vel", 0.0) - running = visual_servoing.running - - status_color = ( - (0, 255, 0) if running else (0, 0, 255) - ) # Green if running, red if not - - # Add velocity text to frame - cv2.putText( - frame, - f"Linear: {linear_vel:.2f} m/s", - (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - status_color, - 2, - ) - cv2.putText( - frame, - f"Angular: {angular_vel:.2f} rad/s", - (10, 60), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - status_color, - 2, - ) - cv2.putText( - frame, - f"Tracking: {'ON' if running else 'OFF'}", - (10, 90), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - status_color, - 2, - ) - - # If tracking is lost, reset selected_point and tracking_active - if not running: - selected_point = None - tracking_active = False - - # Display the frame in main thread - cv2.imshow("Person Tracking", frame) - - # Check for exit key - if cv2.waitKey(1) & 0xFF == ord("q"): - print("Exit key pressed") - break - - except queue.Empty: - # No frame available, check if we should continue - if cv2.waitKey(1) & 0xFF == ord("q"): - print("Exit key pressed") - break - continue - - except KeyboardInterrupt: - print("\nKeyboard interrupt received. Stopping...") - finally: - # Signal threads to stop - stop_event.set() - - # Clean up resources - if subscription: - subscription.dispose() - - visual_servoing.cleanup() - video_provider.dispose_all() - person_tracker.cleanup() - cv2.destroyAllWindows() - print("Cleanup complete") - - -if __name__ == "__main__": - main() diff --git a/tests/test_pick_and_place_module.py b/tests/test_pick_and_place_module.py deleted file mode 100644 index 1bce414a6e..0000000000 --- a/tests/test_pick_and_place_module.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Run script for Piper Arm robot with pick and place functionality. -Subscribes to visualization images and handles mouse/keyboard input. -""" - -import asyncio -import sys -import threading -import time - -import cv2 -import numpy as np - -try: - import pyzed.sl as sl -except ImportError: - print("Error: ZED SDK not installed.") - sys.exit(1) - -# Import LCM message types -from dimos_lcm.sensor_msgs import Image - -from dimos.protocol.pubsub.lcmpubsub import LCM, Topic -from dimos.robot.agilex.piper_arm import PiperArmRobot -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.tests.test_pick_and_place_module") - -# Global for mouse events -mouse_click = None -camera_mouse_click = None -current_window = None -pick_location = None # Store pick location -place_location = None # Store place location -place_mode = False # Track if we're in place selection mode - - -def mouse_callback(event, x, y, _flags, param): - global mouse_click, camera_mouse_click - window_name = param - if event == cv2.EVENT_LBUTTONDOWN: - if window_name == "Camera Feed": - camera_mouse_click = (x, y) - else: - mouse_click = (x, y) - - -class VisualizationNode: - """Node that subscribes to visualization images and handles user input.""" - - def __init__(self, robot: PiperArmRobot): - self.lcm = LCM() - self.latest_viz = None - self.latest_camera = None - self._running = False - self.robot = robot - - # Subscribe to visualization topic - self.viz_topic = Topic("/manipulation/viz", Image) - self.camera_topic = Topic("/zed/color_image", Image) - - def start(self): - """Start the visualization node.""" - self._running = True - self.lcm.start() - - # Subscribe to visualization topic - self.lcm.subscribe(self.viz_topic, self._on_viz_image) - # Subscribe to camera topic for point selection - self.lcm.subscribe(self.camera_topic, self._on_camera_image) - - logger.info("Visualization node started") - - def stop(self): - """Stop the visualization node.""" - self._running = False - cv2.destroyAllWindows() - - def _on_viz_image(self, msg: Image, topic: str): - """Handle visualization image messages.""" - try: - # Convert LCM message to numpy array - data = np.frombuffer(msg.data, dtype=np.uint8) - if msg.encoding == "rgb8": - image = data.reshape((msg.height, msg.width, 3)) - # Convert RGB to BGR for OpenCV - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - self.latest_viz = image - except Exception as e: - logger.error(f"Error processing viz image: {e}") - - def _on_camera_image(self, msg: Image, topic: str): - """Handle camera image messages.""" - try: - # Convert LCM message to numpy array - data = np.frombuffer(msg.data, dtype=np.uint8) - if msg.encoding == "rgb8": - image = data.reshape((msg.height, msg.width, 3)) - # Convert RGB to BGR for OpenCV - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - self.latest_camera = image - except Exception as e: - logger.error(f"Error processing camera image: {e}") - - def run_visualization(self): - """Run the visualization loop with user interaction.""" - global mouse_click, camera_mouse_click, pick_location, place_location, place_mode - - # Setup windows - cv2.namedWindow("Pick and Place") - cv2.setMouseCallback("Pick and Place", mouse_callback, "Pick and Place") - - cv2.namedWindow("Camera Feed") - cv2.setMouseCallback("Camera Feed", mouse_callback, "Camera Feed") - - print("=== Piper Arm Robot - Pick and Place ===") - print("Control mode: Module-based with LCM communication") - print("\nPICK AND PLACE WORKFLOW:") - print("1. Click on an object to select PICK location") - print("2. Click again to select PLACE location (auto pick & place)") - print("3. OR press 'p' after first click for pick-only task") - print("\nCONTROLS:") - print(" 'p' - Execute pick-only task (after selecting pick location)") - print(" 'r' - Reset everything") - print(" 'q' - Quit") - print(" 's' - SOFT STOP (emergency stop)") - print(" 'g' - RELEASE GRIPPER (open gripper)") - print(" 'SPACE' - EXECUTE target pose (manual override)") - print("\nNOTE: Click on objects in the Camera Feed window!") - - while self._running: - # Show camera feed with status overlay - if self.latest_camera is not None: - display_image = self.latest_camera.copy() - - # Add status text - status_text = "" - if pick_location is None: - status_text = "Click to select PICK location" - color = (0, 255, 0) - elif place_location is None: - status_text = "Click to select PLACE location (or press 'p' for pick-only)" - color = (0, 255, 255) - else: - status_text = "Executing pick and place..." - color = (255, 0, 255) - - cv2.putText( - display_image, status_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2 - ) - - # Draw pick location marker if set - if pick_location is not None: - # Simple circle marker - cv2.circle(display_image, pick_location, 10, (0, 255, 0), 2) - cv2.circle(display_image, pick_location, 2, (0, 255, 0), -1) - - # Simple label - cv2.putText( - display_image, - "PICK", - (pick_location[0] + 15, pick_location[1] + 5), - cv2.FONT_HERSHEY_SIMPLEX, - 0.6, - (0, 255, 0), - 2, - ) - - # Draw place location marker if set - if place_location is not None: - # Simple circle marker - cv2.circle(display_image, place_location, 10, (0, 255, 255), 2) - cv2.circle(display_image, place_location, 2, (0, 255, 255), -1) - - # Simple label - cv2.putText( - display_image, - "PLACE", - (place_location[0] + 15, place_location[1] + 5), - cv2.FONT_HERSHEY_SIMPLEX, - 0.6, - (0, 255, 255), - 2, - ) - - # Draw simple arrow between pick and place - if pick_location is not None: - cv2.arrowedLine( - display_image, - pick_location, - place_location, - (255, 255, 0), - 2, - tipLength=0.05, - ) - - cv2.imshow("Camera Feed", display_image) - - # Show visualization if available - if self.latest_viz is not None: - cv2.imshow("Pick and Place", self.latest_viz) - - # Handle keyboard input - key = cv2.waitKey(1) & 0xFF - if key != 255: # Key was pressed - if key == ord("q"): - logger.info("Quit requested") - self._running = False - break - elif key == ord("r"): - # Reset everything - pick_location = None - place_location = None - place_mode = False - logger.info("Reset pick and place selections") - # Also send reset to robot - action = self.robot.handle_keyboard_command("r") - if action: - logger.info(f"Action: {action}") - elif key == ord("p"): - # Execute pick-only task if pick location is set - if pick_location is not None: - logger.info(f"Executing pick-only task at {pick_location}") - result = self.robot.pick_and_place( - pick_location[0], - pick_location[1], - None, # No place location - None, - ) - logger.info(f"Pick task started: {result}") - # Clear selection after sending - pick_location = None - place_location = None - else: - logger.warning("Please select a pick location first!") - else: - # Send keyboard command to robot - if key in [82, 84]: # Arrow keys - action = self.robot.handle_keyboard_command(str(key)) - else: - action = self.robot.handle_keyboard_command(chr(key)) - if action: - logger.info(f"Action: {action}") - - # Handle mouse clicks - if camera_mouse_click: - x, y = camera_mouse_click - - if pick_location is None: - # First click - set pick location - pick_location = (x, y) - logger.info(f"Pick location set at ({x}, {y})") - elif place_location is None: - # Second click - set place location and execute - place_location = (x, y) - logger.info(f"Place location set at ({x}, {y})") - logger.info(f"Executing pick at {pick_location} and place at ({x}, {y})") - - # Start pick and place task with both locations - result = self.robot.pick_and_place(pick_location[0], pick_location[1], x, y) - logger.info(f"Pick and place task started: {result}") - - # Clear all points after sending mission - pick_location = None - place_location = None - - camera_mouse_click = None - - # Handle mouse click from Pick and Place window (if viz is running) - elif mouse_click and self.latest_viz is not None: - # Similar logic for viz window clicks - x, y = mouse_click - - if pick_location is None: - # First click - set pick location - pick_location = (x, y) - logger.info(f"Pick location set at ({x}, {y}) from viz window") - elif place_location is None: - # Second click - set place location and execute - place_location = (x, y) - logger.info(f"Place location set at ({x}, {y}) from viz window") - logger.info(f"Executing pick at {pick_location} and place at ({x}, {y})") - - # Start pick and place task with both locations - result = self.robot.pick_and_place(pick_location[0], pick_location[1], x, y) - logger.info(f"Pick and place task started: {result}") - - # Clear all points after sending mission - pick_location = None - place_location = None - - mouse_click = None - - time.sleep(0.03) # ~30 FPS - - -async def run_piper_arm_with_viz(): - """Run the Piper Arm robot with visualization.""" - logger.info("Starting Piper Arm Robot") - - # Create robot instance - robot = PiperArmRobot() - - try: - # Start the robot - await robot.start() - - # Give modules time to fully initialize - await asyncio.sleep(2) - - # Create and start visualization node - viz_node = VisualizationNode(robot) - viz_node.start() - - # Run visualization in separate thread - viz_thread = threading.Thread(target=viz_node.run_visualization, daemon=True) - viz_thread.start() - - # Keep running until visualization stops - while viz_node._running: - await asyncio.sleep(0.1) - - # Stop visualization - viz_node.stop() - - except Exception as e: - logger.error(f"Error running robot: {e}") - import traceback - - traceback.print_exc() - - finally: - # Clean up - robot.stop() - logger.info("Robot stopped") - - -if __name__ == "__main__": - # Run the robot - asyncio.run(run_piper_arm_with_viz()) diff --git a/tests/test_pick_and_place_skill.py b/tests/test_pick_and_place_skill.py deleted file mode 100644 index 78eeb761fb..0000000000 --- a/tests/test_pick_and_place_skill.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Run script for Piper Arm robot with pick and place functionality. -Uses hardcoded points and the PickAndPlace skill. -""" - -import asyncio -import sys - -try: - import pyzed.sl as sl # Required for ZED camera -except ImportError: - print("Error: ZED SDK not installed.") - sys.exit(1) - -from dimos.robot.agilex.piper_arm import PiperArmRobot -from dimos.skills.manipulation.pick_and_place import PickAndPlace -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.robot.agilex.run_robot") - - -async def run_piper_arm(): - """Run the Piper Arm robot with pick and place skill.""" - logger.info("Starting Piper Arm Robot") - - # Create robot instance - robot = PiperArmRobot() - - try: - # Start the robot - await robot.start() - - # Give modules time to fully initialize - await asyncio.sleep(3) - - # Add the PickAndPlace skill to the robot's skill library - robot.skill_library.add(PickAndPlace) - - logger.info("Robot initialized successfully") - print("\n=== Piper Arm Robot - Pick and Place Demo ===") - print("This demo uses hardcoded pick and place points.") - print("\nCommands:") - print(" 1. Run pick and place with hardcoded points") - print(" 2. Run pick-only with hardcoded point") - print(" r. Reset robot to idle") - print(" q. Quit") - print("") - - running = True - while running: - try: - # Get user input - command = input("\nEnter command: ").strip().lower() - - if command == "q": - logger.info("Quit requested") - running = False - break - - elif command == "r" or command == "s": - logger.info("Resetting robot") - robot.handle_keyboard_command(command) - - elif command == "1": - # Hardcoded pick and place points - # These should be adjusted based on your camera view - print("\nExecuting pick and place with hardcoded points...") - - # Create and execute the skill - skill = PickAndPlace( - robot=robot, - object_query="labubu doll", # Will use visual detection - target_query="on the keyboard", # Will use visual detection - ) - - result = skill() - - if result["success"]: - print(f"āœ“ {result['message']}") - else: - print(f"āœ— Failed: {result.get('error', 'Unknown error')}") - - elif command == "2": - # Pick-only with hardcoded point - print("\nExecuting pick-only with hardcoded point...") - - # Create and execute the skill for pick-only - skill = PickAndPlace( - robot=robot, - object_query="labubu doll", # Will use visual detection - target_query=None, # No place target - pick only - ) - - result = skill() - - if result["success"]: - print(f"āœ“ {result['message']}") - else: - print(f"āœ— Failed: {result.get('error', 'Unknown error')}") - - else: - print("Invalid command. Please try again.") - - # Small delay to prevent CPU spinning - await asyncio.sleep(0.1) - - except KeyboardInterrupt: - logger.info("Keyboard interrupt received") - running = False - break - except Exception as e: - logger.error(f"Error in command loop: {e}") - print(f"Error: {e}") - - except Exception as e: - logger.error(f"Error running robot: {e}") - import traceback - - traceback.print_exc() - - finally: - # Clean up - logger.info("Shutting down robot...") - await robot.stop() - logger.info("Robot stopped") - - -def main(): - """Main entry point.""" - print("Starting Piper Arm Robot...") - print("Note: The robot will use Qwen VLM to identify objects and locations") - print("based on the queries specified in the code.") - - # Run the robot - asyncio.run(run_piper_arm()) - - -if __name__ == "__main__": - main() diff --git a/tests/test_planning_agent_web_interface.py b/tests/test_planning_agent_web_interface.py deleted file mode 100644 index 6c88919110..0000000000 --- a/tests/test_planning_agent_web_interface.py +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Planning agent demo with FastAPI server and robot integration. - -Connects a planning agent, execution agent, and robot with a web interface. - -Environment Variables: - OPENAI_API_KEY: Required. OpenAI API key. - ROBOT_IP: Required. IP address of the robot. - CONN_TYPE: Required. Connection method to the robot. - ROS_OUTPUT_DIR: Optional. Directory for ROS output files. -""" - -import os -import sys - -# ----- -from textwrap import dedent -import time - -import reactivex as rx -import reactivex.operators as ops - -# Local application imports -from dimos.agents.agent import OpenAIAgent -from dimos.agents.planning_agent import PlanningAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.utils.logging_config import logger -from dimos.utils.threadpool import make_single_thread_scheduler - -# from dimos.web.fastapi_server import FastAPIServer -from dimos.web.robot_web_interface import RobotWebInterface - - -def main(): - # Get environment variables - robot_ip = os.getenv("ROBOT_IP") - if not robot_ip: - raise ValueError("ROBOT_IP environment variable is required") - connection_method = os.getenv("CONN_TYPE") or "webrtc" - output_dir = os.getenv("ROS_OUTPUT_DIR", os.path.join(os.getcwd(), "assets/output/ros")) - - # Initialize components as None for proper cleanup - robot = None - web_interface = None - planner = None - executor = None - - try: - # Initialize robot - logger.info("Initializing Unitree Robot") - robot = UnitreeGo2( - ip=robot_ip, - connection_method=connection_method, - output_dir=output_dir, - mock_connection=False, - skills=MyUnitreeSkills(), - ) - # Set up video stream - logger.info("Starting video stream") - video_stream = robot.get_ros_video_stream() - - # Initialize robot skills - logger.info("Initializing robot skills") - - # Create subjects for planner and executor responses - logger.info("Creating response streams") - planner_response_subject = rx.subject.Subject() - planner_response_stream = planner_response_subject.pipe(ops.share()) - - executor_response_subject = rx.subject.Subject() - executor_response_stream = executor_response_subject.pipe(ops.share()) - - # Web interface mode with FastAPI server - logger.info("Initializing FastAPI server") - streams = {"unitree_video": video_stream} - text_streams = { - "planner_responses": planner_response_stream, - "executor_responses": executor_response_stream, - } - - web_interface = RobotWebInterface(port=5555, text_streams=text_streams, **streams) - - logger.info("Starting planning agent with web interface") - planner = PlanningAgent( - dev_name="TaskPlanner", - model_name="gpt-4o", - input_query_stream=web_interface.query_stream, - skills=robot.get_skills(), - ) - - # Get planner's response observable - logger.info("Setting up agent response streams") - planner_responses = planner.get_response_observable() - - # Connect planner to its subject - planner_responses.subscribe(lambda x: planner_response_subject.on_next(x)) - - planner_responses.subscribe( - on_next=lambda x: logger.info(f"Planner response: {x}"), - on_error=lambda e: logger.error(f"Planner error: {e}"), - on_completed=lambda: logger.info("Planner completed"), - ) - - # Initialize execution agent with robot skills - logger.info("Starting execution agent") - system_query = dedent( - """ - You are a robot execution agent that can execute tasks on a virtual - robot. The sole text you will be given is the task to execute. - You will be given a list of skills that you can use to execute the task. - ONLY OUTPUT THE SKILLS TO EXECUTE, NOTHING ELSE. - """ - ) - executor = OpenAIAgent( - dev_name="StepExecutor", - input_query_stream=planner_responses, - output_dir=output_dir, - skills=robot.get_skills(), - system_query=system_query, - pool_scheduler=make_single_thread_scheduler(), - ) - - # Get executor's response observable - executor_responses = executor.get_response_observable() - - # Subscribe to responses for logging - executor_responses.subscribe( - on_next=lambda x: logger.info(f"Executor response: {x}"), - on_error=lambda e: logger.error(f"Executor error: {e}"), - on_completed=lambda: logger.info("Executor completed"), - ) - - # Connect executor to its subject - executor_responses.subscribe(lambda x: executor_response_subject.on_next(x)) - - # Start web server (blocking call) - logger.info("Starting FastAPI server") - web_interface.run() - - except KeyboardInterrupt: - print("Stopping demo...") - except Exception as e: - logger.error(f"Error: {e}") - return 1 - finally: - # Clean up all components - logger.info("Cleaning up components") - if executor: - executor.dispose_all() - if planner: - planner.dispose_all() - if web_interface: - web_interface.dispose_all() - if robot: - robot.cleanup() - # Halt execution forever - while True: - time.sleep(1) - - -if __name__ == "__main__": - sys.exit(main()) - -# Example Task: Move the robot forward by 1 meter, then turn 90 degrees clockwise, then move backward by 1 meter, then turn a random angle counterclockwise, then repeat this sequence 5 times. diff --git a/tests/test_planning_robot_agent.py b/tests/test_planning_robot_agent.py deleted file mode 100644 index aa16a7cac7..0000000000 --- a/tests/test_planning_robot_agent.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Planning agent demo with FastAPI server and robot integration. - -Connects a planning agent, execution agent, and robot with a web interface. - -Environment Variables: - OPENAI_API_KEY: Required. OpenAI API key. - ROBOT_IP: Required. IP address of the robot. - CONN_TYPE: Required. Connection method to the robot. - ROS_OUTPUT_DIR: Optional. Directory for ROS output files. - USE_TERMINAL: Optional. If set to "true", use terminal interface instead of web. -""" - -import os -import sys - -# ----- -from textwrap import dedent -import time - -# Local application imports -from dimos.agents.agent import OpenAIAgent -from dimos.agents.planning_agent import PlanningAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.utils.logging_config import logger -from dimos.utils.threadpool import make_single_thread_scheduler -from dimos.web.robot_web_interface import RobotWebInterface - - -def main(): - # Get environment variables - robot_ip = os.getenv("ROBOT_IP") - if not robot_ip: - raise ValueError("ROBOT_IP environment variable is required") - connection_method = os.getenv("CONN_TYPE") or "webrtc" - output_dir = os.getenv("ROS_OUTPUT_DIR", os.path.join(os.getcwd(), "assets/output/ros")) - use_terminal = os.getenv("USE_TERMINAL", "").lower() == "true" - - use_terminal = True - # Initialize components as None for proper cleanup - robot = None - web_interface = None - planner = None - executor = None - - try: - # Initialize robot - logger.info("Initializing Unitree Robot") - robot = UnitreeGo2( - ip=robot_ip, - connection_method=connection_method, - output_dir=output_dir, - mock_connection=True, - ) - - # Set up video stream - logger.info("Starting video stream") - video_stream = robot.get_ros_video_stream() - - # Initialize robot skills - logger.info("Initializing robot skills") - skills_instance = MyUnitreeSkills(robot=robot) - - if use_terminal: - # Terminal mode - no web interface needed - logger.info("Starting planning agent in terminal mode") - planner = PlanningAgent( - dev_name="TaskPlanner", - model_name="gpt-4o", - use_terminal=True, - skills=skills_instance, - ) - else: - # Web interface mode - logger.info("Initializing FastAPI server") - streams = {"unitree_video": video_stream} - web_interface = RobotWebInterface(port=5555, **streams) - - logger.info("Starting planning agent with web interface") - planner = PlanningAgent( - dev_name="TaskPlanner", - model_name="gpt-4o", - input_query_stream=web_interface.query_stream, - skills=skills_instance, - ) - - # Get planner's response observable - logger.info("Setting up agent response streams") - planner_responses = planner.get_response_observable() - - # Initialize execution agent with robot skills - logger.info("Starting execution agent") - system_query = dedent( - """ - You are a robot execution agent that can execute tasks on a virtual - robot. You are given a task to execute and a list of skills that - you can use to execute the task. ONLY OUTPUT THE SKILLS TO EXECUTE, - NOTHING ELSE. - """ - ) - executor = OpenAIAgent( - dev_name="StepExecutor", - input_query_stream=planner_responses, - output_dir=output_dir, - skills=skills_instance, - system_query=system_query, - pool_scheduler=make_single_thread_scheduler(), - ) - - # Get executor's response observable - executor_responses = executor.get_response_observable() - - # Subscribe to responses for logging - executor_responses.subscribe( - on_next=lambda x: logger.info(f"Executor response: {x}"), - on_error=lambda e: logger.error(f"Executor error: {e}"), - on_completed=lambda: logger.info("Executor completed"), - ) - - if use_terminal: - # In terminal mode, just wait for the planning session to complete - logger.info("Waiting for planning session to complete") - while not planner.plan_confirmed: - pass - logger.info("Planning session completed") - else: - # Start web server (blocking call) - logger.info("Starting FastAPI server") - web_interface.run() - - # Keep the main thread alive - logger.error("NOTE: Keeping main thread alive") - while True: - time.sleep(1) - - except KeyboardInterrupt: - print("Stopping demo...") - except Exception as e: - logger.error(f"Error: {e}") - return 1 - finally: - # Clean up all components - logger.info("Cleaning up components") - if executor: - executor.dispose_all() - if planner: - planner.dispose_all() - if web_interface: - web_interface.dispose_all() - if robot: - robot.cleanup() - # Halt execution forever - while True: - time.sleep(1) - - -if __name__ == "__main__": - sys.exit(main()) - -# Example Task: Move the robot forward by 1 meter, then turn 90 degrees clockwise, then move backward by 1 meter, then turn a random angle counterclockwise, then repeat this sequence 5 times. diff --git a/tests/test_pointcloud_filtering.py b/tests/test_pointcloud_filtering.py deleted file mode 100644 index 8a9eb8665f..0000000000 --- a/tests/test_pointcloud_filtering.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys - -from pyzed import sl -from reactivex import operators as ops - -from dimos.manipulation.manip_aio_pipeline import ManipulationPipeline -from dimos.stream.stereo_camera_streams.zed import ZEDCameraStream -from dimos.web.robot_web_interface import RobotWebInterface - - -def main(): - """Test point cloud filtering using the concurrent stream-based ManipulationPipeline.""" - print("Testing point cloud filtering with ManipulationPipeline...") - - # Configuration - min_confidence = 0.6 - web_port = 5555 - - try: - # Initialize ZED camera stream - zed_stream = ZEDCameraStream(resolution=sl.RESOLUTION.HD1080, fps=10) - - # Get camera intrinsics - camera_intrinsics_dict = zed_stream.get_camera_info() - camera_intrinsics = [ - camera_intrinsics_dict["fx"], - camera_intrinsics_dict["fy"], - camera_intrinsics_dict["cx"], - camera_intrinsics_dict["cy"], - ] - - # Create the concurrent manipulation pipeline - pipeline = ManipulationPipeline( - camera_intrinsics=camera_intrinsics, - min_confidence=min_confidence, - max_objects=10, - ) - - # Create ZED stream - zed_frame_stream = zed_stream.create_stream().pipe(ops.share()) - - # Create concurrent processing streams - streams = pipeline.create_streams(zed_frame_stream) - detection_viz_stream = streams["detection_viz"] - pointcloud_viz_stream = streams["pointcloud_viz"] - - except ImportError: - print("Error: ZED SDK not installed. Please install pyzed package.") - sys.exit(1) - except RuntimeError as e: - print(f"Error: Failed to open ZED camera: {e}") - sys.exit(1) - - try: - # Set up web interface with concurrent visualization streams - print("Initializing web interface...") - web_interface = RobotWebInterface( - port=web_port, - object_detection=detection_viz_stream, - pointcloud_stream=pointcloud_viz_stream, - ) - - print("\nPoint Cloud Filtering Test Running:") - print(f"Web Interface: http://localhost:{web_port}") - print("Object Detection View: RGB with bounding boxes") - print("Point Cloud View: Depth with colored point clouds and 3D bounding boxes") - print(f"Confidence threshold: {min_confidence}") - print("\nPress Ctrl+C to stop the test\n") - - # Start web server (blocking call) - web_interface.run() - - except KeyboardInterrupt: - print("\nTest interrupted by user") - except Exception as e: - print(f"Error during test: {e}") - finally: - print("Cleaning up resources...") - if "zed_stream" in locals(): - zed_stream.cleanup() - if "pipeline" in locals(): - pipeline.cleanup() - print("Test completed") - - -if __name__ == "__main__": - main() diff --git a/tests/test_qwen_image_query.py b/tests/test_qwen_image_query.py deleted file mode 100644 index 6a3aa9d8c6..0000000000 --- a/tests/test_qwen_image_query.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test the Qwen image query functionality.""" - -import os - -import cv2 -import numpy as np -from PIL import Image - -from dimos.models.qwen.video_query import query_single_frame - - -def test_qwen_image_query(): - """Test querying Qwen with a single image.""" - # Skip if no API key - if not os.getenv("ALIBABA_API_KEY"): - print("ALIBABA_API_KEY not set") - return - - # Load test image - image_path = os.path.join(os.getcwd(), "assets", "test_spatial_memory", "frame_038.jpg") - pil_image = Image.open(image_path) - - # Convert PIL image to numpy array in RGB format - image_array = np.array(pil_image) - if image_array.shape[-1] == 3: - # Ensure it's in RGB format (PIL loads as RGB by default) - image = image_array - else: - # Handle grayscale images - image = cv2.cvtColor(image_array, cv2.COLOR_GRAY2RGB) - - # Test basic object detection query - response = query_single_frame( - image=image, - query="What objects do you see in this image? Return as a comma-separated list.", - ) - print(response) - - # Test coordinate query - response = query_single_frame( - image=image, - query="Return the center coordinates of any person in the image as a tuple (x,y)", - ) - print(response) - - -if __name__ == "__main__": - test_qwen_image_query() diff --git a/tests/test_robot.py b/tests/test_robot.py deleted file mode 100644 index 63439ce3d9..0000000000 --- a/tests/test_robot.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import threading -import time - -from reactivex import operators as RxOps - -from dimos.robot.local_planner.local_planner import navigate_to_goal_local -from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 -from dimos.web.robot_web_interface import RobotWebInterface - - -def main(): - print("Initializing Unitree Go2 robot with local planner visualization...") - - # Initialize the robot with webrtc interface - robot = UnitreeGo2(ip=os.getenv("ROBOT_IP"), mode="ai") - - # Get the camera stream - video_stream = robot.get_video_stream() - - # The local planner visualization stream is created during robot initialization - local_planner_stream = robot.local_planner_viz_stream - - local_planner_stream = local_planner_stream.pipe( - RxOps.share(), - RxOps.map(lambda x: x if x is not None else None), - RxOps.filter(lambda x: x is not None), - ) - - goal_following_thread = None - try: - # Set up web interface with both streams - streams = {"camera": video_stream, "local_planner": local_planner_stream} - - # Create and start the web interface - web_interface = RobotWebInterface(port=5555, **streams) - - # Wait for initialization - print("Waiting for camera and systems to initialize...") - time.sleep(2) - - # Start the goal following test in a separate thread - print("Starting navigation to local goal (2m ahead) in a separate thread...") - goal_following_thread = threading.Thread( - target=navigate_to_goal_local, - kwargs={"robot": robot, "goal_xy_robot": (3.0, 0.0), "distance": 0.0, "timeout": 300}, - daemon=True, - ) - goal_following_thread.start() - - print("Robot streams running") - print("Web interface available at http://localhost:5555") - print("Press Ctrl+C to exit") - - # Start web server (blocking call) - web_interface.run() - - except KeyboardInterrupt: - print("\nInterrupted by user") - except Exception as e: - print(f"Error during test: {e}") - finally: - print("Cleaning up...") - # Make sure the robot stands down safely - try: - robot.liedown() - except: - pass - print("Test completed") - - -if __name__ == "__main__": - main() diff --git a/tests/test_rtsp_video_provider.py b/tests/test_rtsp_video_provider.py deleted file mode 100644 index fb0f075750..0000000000 --- a/tests/test_rtsp_video_provider.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import numpy as np -import reactivex as rx -from reactivex import operators as ops - -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.rtsp_video_provider import RtspVideoProvider -from dimos.stream.video_operators import VideoOperators as vops -from dimos.stream.video_provider import get_scheduler -from dimos.utils.logging_config import setup_logger -from dimos.web.robot_web_interface import RobotWebInterface - -logger = setup_logger("tests.test_rtsp_video_provider") - -import os -import sys - -# Load environment variables from .env file -from dotenv import load_dotenv - -load_dotenv() - -# RTSP URL must be provided as a command-line argument or environment variable -RTSP_URL = os.environ.get("TEST_RTSP_URL", "") -if len(sys.argv) > 1: - RTSP_URL = sys.argv[1] # Allow overriding with command-line argument -elif RTSP_URL == "": - print("Please provide an RTSP URL for testing.") - print( - "You can set the TEST_RTSP_URL environment variable or pass it as a command-line argument." - ) - print("Example: python -m dimos.stream.rtsp_video_provider rtsp://...") - sys.exit(1) - -logger.info("Attempting to connect to provided RTSP URL.") -provider = RtspVideoProvider(dev_name="TestRtspCam", rtsp_url=RTSP_URL) - -logger.info("Creating observable...") -video_stream_observable = provider.capture_video_as_observable() - -logger.info("Subscribing to observable...") -frame_counter = 0 -start_time = time.monotonic() # Re-initialize start_time -last_log_time = start_time # Keep this for interval timing - -# Create a subject for ffmpeg responses -ffmpeg_response_subject = rx.subject.Subject() -ffmpeg_response_stream = ffmpeg_response_subject.pipe(ops.observe_on(get_scheduler()), ops.share()) - - -def process_frame(frame: np.ndarray): - """Callback function executed for each received frame.""" - global frame_counter, last_log_time, start_time # Add start_time to global - frame_counter += 1 - current_time = time.monotonic() - # Log stats periodically (e.g., every 5 seconds) - if current_time - last_log_time >= 5.0: - total_elapsed_time = current_time - start_time # Calculate total elapsed time - avg_fps = frame_counter / total_elapsed_time if total_elapsed_time > 0 else 0 - logger.info(f"Received frame {frame_counter}. Shape: {frame.shape}. Avg FPS: {avg_fps:.2f}") - ffmpeg_response_subject.on_next( - f"Received frame {frame_counter}. Shape: {frame.shape}. Avg FPS: {avg_fps:.2f}" - ) - last_log_time = current_time # Update log time for the next interval - - -def handle_error(error: Exception): - """Callback function executed if the observable stream errors.""" - logger.error(f"Stream error: {error}", exc_info=True) # Log with traceback - - -def handle_completion(): - """Callback function executed when the observable stream completes.""" - logger.info("Stream completed.") - - -# Subscribe to the observable stream -processor = FrameProcessor() -subscription = video_stream_observable.pipe( - # ops.subscribe_on(get_scheduler()), - ops.observe_on(get_scheduler()), - ops.share(), - vops.with_jpeg_export(processor, suffix="reolink_", save_limit=30, loop=True), -).subscribe(on_next=process_frame, on_error=handle_error, on_completed=handle_completion) - -streams = {"reolink_video": video_stream_observable} -text_streams = { - "ffmpeg_responses": ffmpeg_response_stream, -} - -web_interface = RobotWebInterface(port=5555, text_streams=text_streams, **streams) - -web_interface.run() # This may block the main thread - -# TODO: Redo disposal / keep-alive loop - -# Keep the main thread alive to receive frames (e.g., for 60 seconds) -print("Stream running. Press Ctrl+C to stop...") -try: - # Keep running indefinitely until interrupted - while True: - time.sleep(1) - # Optional: Check if subscription is still active - # if not subscription.is_disposed: - # # logger.debug("Subscription active...") - # pass - # else: - # logger.warning("Subscription was disposed externally.") - # break - -except KeyboardInterrupt: - print("KeyboardInterrupt received. Shutting down...") -finally: - # Ensure resources are cleaned up regardless of how the loop exits - print("Disposing subscription...") - # subscription.dispose() - print("Disposing provider resources...") - provider.dispose_all() - print("Cleanup finished.") - -# Final check (optional, for debugging) -time.sleep(1) # Give background threads a moment -final_process = provider._ffmpeg_process -if final_process and final_process.poll() is None: - print(f"WARNING: ffmpeg process (PID: {final_process.pid}) may still be running after cleanup!") -else: - print("ffmpeg process appears terminated.") diff --git a/tests/test_semantic_seg_robot.py b/tests/test_semantic_seg_robot.py deleted file mode 100644 index 0a78bc371b..0000000000 --- a/tests/test_semantic_seg_robot.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import queue -import sys -import threading - -import cv2 -import numpy as np - -# Add the parent directory to the Python path -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from reactivex import operators as RxOps - -from dimos.perception.semantic_seg import SemanticSegmentationStream -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.video_operators import Operators as MyOps -from dimos.web.robot_web_interface import RobotWebInterface - - -def main(): - # Create a queue for thread communication (limit to prevent memory issues) - frame_queue = queue.Queue(maxsize=5) - stop_event = threading.Event() - - # Unitree Go2 camera parameters at 1080p - camera_params = { - "resolution": (1920, 1080), # 1080p resolution - "focal_length": 3.2, # mm - "sensor_size": (4.8, 3.6), # mm (1/4" sensor) - } - - # Initialize video provider and segmentation stream - # video_provider = VideoProvider("test_camera", video_source=0) - robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - ros_control=UnitreeROSControl(), - ) - - seg_stream = SemanticSegmentationStream( - enable_mono_depth=False, camera_params=camera_params, gt_depth_scale=512.0 - ) - - # Create streams - video_stream = robot.get_ros_video_stream(fps=5) - segmentation_stream = seg_stream.create_stream(video_stream) - - # Define callbacks for the segmentation stream - def on_next(segmentation): - if stop_event.is_set(): - return - # Get the frame and visualize - vis_frame = segmentation.metadata["viz_frame"] - depth_viz = segmentation.metadata["depth_viz"] - # Get the image dimensions - height, width = vis_frame.shape[:2] - depth_height, depth_width = depth_viz.shape[:2] - - # Resize depth visualization to match segmentation height - # (maintaining aspect ratio if needed) - depth_resized = cv2.resize(depth_viz, (int(depth_width * height / depth_height), height)) - - # Create a combined frame for side-by-side display - combined_viz = np.hstack((vis_frame, depth_resized)) - - # Add labels - font = cv2.FONT_HERSHEY_SIMPLEX - cv2.putText(combined_viz, "Semantic Segmentation", (10, 30), font, 0.8, (255, 255, 255), 2) - cv2.putText( - combined_viz, "Depth Estimation", (width + 10, 30), font, 0.8, (255, 255, 255), 2 - ) - - # Put frame in queue for main thread to display (non-blocking) - try: - frame_queue.put_nowait(combined_viz) - except queue.Full: - # Skip frame if queue is full - pass - - def on_error(error): - print(f"Error: {error}") - stop_event.set() - - def on_completed(): - print("Stream completed") - stop_event.set() - - # Start the subscription - subscription = None - - try: - # Subscribe to start processing in background thread - print_emission_args = { - "enabled": True, - "dev_name": "SemanticSegmentation", - "counts": {}, - } - - FrameProcessor(delete_on_init=True) - subscription = segmentation_stream.pipe( - MyOps.print_emission(id="A", **print_emission_args), - RxOps.share(), - MyOps.print_emission(id="B", **print_emission_args), - RxOps.map(lambda x: x.metadata["viz_frame"] if x is not None else None), - MyOps.print_emission(id="C", **print_emission_args), - RxOps.filter(lambda x: x is not None), - MyOps.print_emission(id="D", **print_emission_args), - # MyVideoOps.with_jpeg_export(frame_processor=frame_processor, suffix="_frame_"), - MyOps.print_emission(id="E", **print_emission_args), - ) - - print("Semantic segmentation visualization started. Press 'q' to exit.") - - streams = { - "segmentation_stream": subscription, - } - fast_api_server = RobotWebInterface(port=5555, **streams) - fast_api_server.run() - - except KeyboardInterrupt: - print("\nKeyboard interrupt received. Stopping...") - finally: - # Signal threads to stop - stop_event.set() - - # Clean up resources - if subscription: - subscription.dispose() - - seg_stream.cleanup() - cv2.destroyAllWindows() - print("Cleanup complete") - - -if __name__ == "__main__": - main() diff --git a/tests/test_semantic_seg_robot_agent.py b/tests/test_semantic_seg_robot_agent.py deleted file mode 100644 index f35fdb53d4..0000000000 --- a/tests/test_semantic_seg_robot_agent.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import cv2 -from reactivex import Subject, operators as RxOps - -from dimos.agents.agent import OpenAIAgent -from dimos.perception.semantic_seg import SemanticSegmentationStream -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.video_operators import VideoOperators as MyVideoOps -from dimos.utils.threadpool import get_scheduler -from dimos.web.robot_web_interface import RobotWebInterface - - -def main(): - # Unitree Go2 camera parameters at 1080p - camera_params = { - "resolution": (1920, 1080), # 1080p resolution - "focal_length": 3.2, # mm - "sensor_size": (4.8, 3.6), # mm (1/4" sensor) - } - - robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), ros_control=UnitreeROSControl(), skills=MyUnitreeSkills() - ) - - seg_stream = SemanticSegmentationStream( - enable_mono_depth=True, camera_params=camera_params, gt_depth_scale=512.0 - ) - - # Create streams - video_stream = robot.get_ros_video_stream(fps=5) - segmentation_stream = seg_stream.create_stream( - video_stream.pipe(MyVideoOps.with_fps_sampling(fps=0.5)) - ) - # Throttling to slowdown SegmentationAgent calls - # TODO: add Agent parameter to handle this called api_call_interval - - FrameProcessor(delete_on_init=True) - seg_stream = segmentation_stream.pipe( - RxOps.share(), - RxOps.map(lambda x: x.metadata["viz_frame"] if x is not None else None), - RxOps.filter(lambda x: x is not None), - # MyVideoOps.with_jpeg_export(frame_processor=frame_processor, suffix="_frame_"), # debugging - ) - - depth_stream = segmentation_stream.pipe( - RxOps.share(), - RxOps.map(lambda x: x.metadata["depth_viz"] if x is not None else None), - RxOps.filter(lambda x: x is not None), - ) - - object_stream = segmentation_stream.pipe( - RxOps.share(), - RxOps.map(lambda x: x.metadata["objects"] if x is not None else None), - RxOps.filter(lambda x: x is not None), - RxOps.map( - lambda objects: "\n".join( - f"Object {obj['object_id']}: {obj['label']} (confidence: {obj['prob']:.2f})" - + (f", depth: {obj['depth']:.2f}m" if "depth" in obj else "") - for obj in objects - ) - if objects - else "No objects detected." - ), - ) - - text_query_stream = Subject() - - # Combine text query with latest object data when a new text query arrives - enriched_query_stream = text_query_stream.pipe( - RxOps.with_latest_from(object_stream), - RxOps.map( - lambda combined: { - "query": combined[0], - "objects": combined[1] if len(combined) > 1 else "No object data available", - } - ), - RxOps.map(lambda data: f"{data['query']}\n\nCurrent objects detected:\n{data['objects']}"), - RxOps.do_action( - lambda x: print(f"\033[34mEnriched query: {x.split(chr(10))[0]}\033[0m") - or [print(f"\033[34m{line}\033[0m") for line in x.split(chr(10))[1:]] - ), - ) - - segmentation_agent = OpenAIAgent( - dev_name="SemanticSegmentationAgent", - model_name="gpt-4o", - system_query="You are a helpful assistant that can control a virtual robot with semantic segmentation / distnace data as a guide. Only output skill calls, no other text", - input_query_stream=enriched_query_stream, - process_all_inputs=False, - pool_scheduler=get_scheduler(), - skills=robot.get_skills(), - ) - agent_response_stream = segmentation_agent.get_response_observable() - - print("Semantic segmentation visualization started. Press 'q' to exit.") - - streams = { - "raw_stream": video_stream, - "depth_stream": depth_stream, - "seg_stream": seg_stream, - } - text_streams = { - "object_stream": object_stream, - "enriched_query_stream": enriched_query_stream, - "agent_response_stream": agent_response_stream, - } - - try: - fast_api_server = RobotWebInterface(port=5555, text_streams=text_streams, **streams) - fast_api_server.query_stream.subscribe(lambda x: text_query_stream.on_next(x)) - fast_api_server.run() - except KeyboardInterrupt: - print("\nKeyboard interrupt received. Stopping...") - finally: - seg_stream.cleanup() - cv2.destroyAllWindows() - print("Cleanup complete") - - -if __name__ == "__main__": - main() diff --git a/tests/test_semantic_seg_webcam.py b/tests/test_semantic_seg_webcam.py deleted file mode 100644 index b7fc57073b..0000000000 --- a/tests/test_semantic_seg_webcam.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import queue -import sys -import threading - -import cv2 -import numpy as np - -# Add the parent directory to the Python path -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from dimos.perception.semantic_seg import SemanticSegmentationStream -from dimos.stream.video_provider import VideoProvider - - -def main(): - # Create a queue for thread communication (limit to prevent memory issues) - frame_queue = queue.Queue(maxsize=5) - stop_event = threading.Event() - - # Logitech C920e camera parameters at 480p - camera_params = { - "resolution": (640, 480), # 480p resolution - "focal_length": 3.67, # mm - "sensor_size": (4.8, 3.6), # mm (1/4" sensor) - } - - # Initialize video provider and segmentation stream - video_provider = VideoProvider("test_camera", video_source=0) - seg_stream = SemanticSegmentationStream( - enable_mono_depth=True, camera_params=camera_params, gt_depth_scale=512.0 - ) - - # Create streams - video_stream = video_provider.capture_video_as_observable(realtime=False, fps=5) - segmentation_stream = seg_stream.create_stream(video_stream) - - # Define callbacks for the segmentation stream - def on_next(segmentation): - if stop_event.is_set(): - return - - # Get the frame and visualize - vis_frame = segmentation.metadata["viz_frame"] - depth_viz = segmentation.metadata["depth_viz"] - # Get the image dimensions - height, width = vis_frame.shape[:2] - depth_height, depth_width = depth_viz.shape[:2] - - # Resize depth visualization to match segmentation height - # (maintaining aspect ratio if needed) - depth_resized = cv2.resize(depth_viz, (int(depth_width * height / depth_height), height)) - - # Create a combined frame for side-by-side display - combined_viz = np.hstack((vis_frame, depth_resized)) - - # Add labels - font = cv2.FONT_HERSHEY_SIMPLEX - cv2.putText(combined_viz, "Semantic Segmentation", (10, 30), font, 0.8, (255, 255, 255), 2) - cv2.putText( - combined_viz, "Depth Estimation", (width + 10, 30), font, 0.8, (255, 255, 255), 2 - ) - - # Put frame in queue for main thread to display (non-blocking) - try: - frame_queue.put_nowait(combined_viz) - except queue.Full: - # Skip frame if queue is full - pass - - def on_error(error): - print(f"Error: {error}") - stop_event.set() - - def on_completed(): - print("Stream completed") - stop_event.set() - - # Start the subscription - subscription = None - - try: - # Subscribe to start processing in background thread - subscription = segmentation_stream.subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed - ) - - print("Semantic segmentation visualization started. Press 'q' to exit.") - - # Main thread loop for displaying frames - while not stop_event.is_set(): - try: - # Get frame with timeout (allows checking stop_event periodically) - combined_viz = frame_queue.get(timeout=1.0) - - # Display the frame in main thread - cv2.imshow("Semantic Segmentation", combined_viz) - # Check for exit key - if cv2.waitKey(1) & 0xFF == ord("q"): - print("Exit key pressed") - break - - except queue.Empty: - # No frame available, check if we should continue - if cv2.waitKey(1) & 0xFF == ord("q"): - print("Exit key pressed") - break - continue - - except KeyboardInterrupt: - print("\nKeyboard interrupt received. Stopping...") - finally: - # Signal threads to stop - stop_event.set() - - # Clean up resources - if subscription: - subscription.dispose() - - video_provider.dispose_all() - seg_stream.cleanup() - cv2.destroyAllWindows() - print("Cleanup complete") - - -if __name__ == "__main__": - main() diff --git a/tests/test_skills.py b/tests/test_skills.py deleted file mode 100644 index 139a4efe59..0000000000 --- a/tests/test_skills.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for the skills module in the dimos package.""" - -import unittest -from unittest import mock - -from dimos.agents.agent import OpenAIAgent -from dimos.robot.robot import MockRobot -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.skills import AbstractSkill - - -class TestSkill(AbstractSkill): - """A test skill that tracks its execution for testing purposes.""" - - _called: bool = False - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._called = False - - def __call__(self): - self._called = True - return "TestSkill executed successfully" - - -class SkillLibraryTest(unittest.TestCase): - """Tests for the SkillLibrary functionality.""" - - def setUp(self): - """Set up test fixtures before each test method.""" - self.robot = MockRobot() - self.skill_library = MyUnitreeSkills(robot=self.robot) - self.skill_library.initialize_skills() - - def test_skill_iteration(self): - """Test that skills can be properly iterated in the skill library.""" - skills_count = 0 - for skill in self.skill_library: - skills_count += 1 - self.assertTrue(hasattr(skill, "__name__")) - self.assertTrue(issubclass(skill, AbstractSkill)) - - self.assertGreater(skills_count, 0, "Skill library should contain at least one skill") - - def test_skill_registration(self): - """Test that skills can be properly registered in the skill library.""" - # Clear existing skills for isolated test - self.skill_library = MyUnitreeSkills(robot=self.robot) - original_count = len(list(self.skill_library)) - - # Add a custom test skill - test_skill = TestSkill - self.skill_library.add(test_skill) - - # Verify the skill was added - new_count = len(list(self.skill_library)) - self.assertEqual(new_count, original_count + 1) - - # Check if the skill can be found by name - found = False - for skill in self.skill_library: - if skill.__name__ == "TestSkill": - found = True - break - self.assertTrue(found, "Added skill should be found in skill library") - - def test_skill_direct_execution(self): - """Test that a skill can be executed directly.""" - test_skill = TestSkill() - self.assertFalse(test_skill._called) - result = test_skill() - self.assertTrue(test_skill._called) - self.assertEqual(result, "TestSkill executed successfully") - - def test_skill_library_execution(self): - """Test that a skill can be executed through the skill library.""" - # Add our test skill to the library - test_skill = TestSkill - self.skill_library.add(test_skill) - - # Create an instance to confirm it was executed - with mock.patch.object(TestSkill, "__call__", return_value="Success") as mock_call: - result = self.skill_library.call("TestSkill") - mock_call.assert_called_once() - self.assertEqual(result, "Success") - - def test_skill_not_found(self): - """Test that calling a non-existent skill raises an appropriate error.""" - with self.assertRaises(ValueError): - self.skill_library.call("NonExistentSkill") - - -class SkillWithAgentTest(unittest.TestCase): - """Tests for skills used with an agent.""" - - def setUp(self): - """Set up test fixtures before each test method.""" - self.robot = MockRobot() - self.skill_library = MyUnitreeSkills(robot=self.robot) - self.skill_library.initialize_skills() - - # Add a test skill - self.skill_library.add(TestSkill) - - # Create the agent - self.agent = OpenAIAgent( - dev_name="SkillTestAgent", - system_query="You are a skill testing agent. When prompted to perform an action, use the appropriate skill.", - skills=self.skill_library, - ) - - @mock.patch("dimos.agents.agent.OpenAIAgent.run_observable_query") - def test_agent_skill_identification(self, mock_query): - """Test that the agent can identify skills based on natural language.""" - # Mock the agent response - mock_response = mock.MagicMock() - mock_response.run.return_value = "I found the TestSkill and executed it." - mock_query.return_value = mock_response - - # Run the test - response = self.agent.run_observable_query("Please run the test skill").run() - - # Assertions - mock_query.assert_called_once_with("Please run the test skill") - self.assertEqual(response, "I found the TestSkill and executed it.") - - @mock.patch.object(TestSkill, "__call__") - @mock.patch("dimos.agents.agent.OpenAIAgent.run_observable_query") - def test_agent_skill_execution(self, mock_query, mock_skill_call): - """Test that the agent can execute skills properly.""" - # Mock the agent and skill call - mock_skill_call.return_value = "TestSkill executed successfully" - mock_response = mock.MagicMock() - mock_response.run.return_value = "Executed TestSkill successfully." - mock_query.return_value = mock_response - - # Run the test - response = self.agent.run_observable_query("Execute the TestSkill skill").run() - - # We can't directly verify the skill was called since our mocking setup - # doesn't capture the internal skill execution of the agent, but we can - # verify the agent was properly called - mock_query.assert_called_once_with("Execute the TestSkill skill") - self.assertEqual(response, "Executed TestSkill successfully.") - - def test_agent_multi_skill_registration(self): - """Test that multiple skills can be registered with an agent.""" - - # Create a new skill - class AnotherTestSkill(AbstractSkill): - def __call__(self): - return "Another test skill executed" - - # Register the new skill - initial_count = len(list(self.skill_library)) - self.skill_library.add(AnotherTestSkill) - - # Verify two distinct skills now exist - self.assertEqual(len(list(self.skill_library)), initial_count + 1) - - # Verify both skills are found by name - skill_names = [skill.__name__ for skill in self.skill_library] - self.assertIn("TestSkill", skill_names) - self.assertIn("AnotherTestSkill", skill_names) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_skills_rest.py b/tests/test_skills_rest.py deleted file mode 100644 index a9493e3c79..0000000000 --- a/tests/test_skills_rest.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from textwrap import dedent - -from dotenv import load_dotenv -import reactivex as rx -import reactivex.operators as ops - -from dimos.agents.claude_agent import ClaudeAgent -from dimos.skills.rest.rest import GenericRestSkill -from dimos.skills.skills import SkillLibrary -from dimos.web.robot_web_interface import RobotWebInterface - -# Load API key from environment -load_dotenv() - -# Create a skill library and add the GenericRestSkill -skills = SkillLibrary() -skills.add(GenericRestSkill) - -# Create a subject for agent responses -agent_response_subject = rx.subject.Subject() -agent_response_stream = agent_response_subject.pipe(ops.share()) - -# Create a text stream for agent responses in the web interface -text_streams = { - "agent_responses": agent_response_stream, -} -web_interface = RobotWebInterface(port=5555, text_streams=text_streams) - -# Create a ClaudeAgent instance -agent = ClaudeAgent( - dev_name="test_agent", - input_query_stream=web_interface.query_stream, - skills=skills, - system_query=dedent( - """ - You are a virtual agent. When given a query, respond by using - the appropriate tool calls if needed to execute commands on the robot. - - IMPORTANT: - Only return the response directly asked of the user. E.G. if the user asks for the time, - only return the time. If the user asks for the weather, only return the weather. - """ - ), - model_name="claude-3-7-sonnet-latest", - thinking_budget_tokens=2000, -) - -# Subscribe to agent responses and send them to the subject -agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) - -# Start the web interface -web_interface.run() - -# Run this query in the web interface: -# -# Make a web request to nist to get the current time. -# You should use http://worldclockapi.com/api/json/utc/now -# diff --git a/tests/test_spatial_memory.py b/tests/test_spatial_memory.py deleted file mode 100644 index e9c8c623b9..0000000000 --- a/tests/test_spatial_memory.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import time - -import chromadb -import cv2 -from matplotlib.patches import Circle -import matplotlib.pyplot as plt -import reactivex -from reactivex import operators as ops - -from dimos.agents.memory.visual_memory import VisualMemory -from dimos.msgs.geometry_msgs import Quaternion, Vector3 - -# from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 # Uncomment when properly configured -from dimos.perception.spatial_perception import SpatialMemory - - -def extract_pose_data(transform): - """Extract position and rotation from a transform message""" - if transform is None: - return None, None - - pos = transform.transform.translation - rot = transform.transform.rotation - - # Convert to Vector3 objects expected by SpatialMemory - position = Vector3(x=pos.x, y=pos.y, z=pos.z) - - # Convert quaternion to euler angles for rotation vector - quat = Quaternion(x=rot.x, y=rot.y, z=rot.z, w=rot.w) - euler = quat.to_euler() - rotation = Vector3(x=euler.x, y=euler.y, z=euler.z) - - return position, rotation - - -def setup_persistent_chroma_db(db_path="chromadb_data"): - """ - Set up a persistent ChromaDB database at the specified path. - - Args: - db_path: Path to store the ChromaDB database - - Returns: - The ChromaDB client instance - """ - # Create a persistent ChromaDB client - full_db_path = os.path.join("/home/stash/dimensional/dimos/assets/test_spatial_memory", db_path) - print(f"Setting up persistent ChromaDB at: {full_db_path}") - - # Ensure the directory exists - os.makedirs(full_db_path, exist_ok=True) - - return chromadb.PersistentClient(path=full_db_path) - - -def main(): - print("Starting spatial memory test...") - - # Create counters for tracking - frame_count = 0 - transform_count = 0 - stored_count = 0 - - print("Note: This test requires proper robot connection setup.") - print("Please ensure video_stream and transform_stream are properly configured.") - - # These need to be set up based on your specific robot configuration - video_stream = None # TODO: Set up video stream from robot - transform_stream = None # TODO: Set up transform stream from robot - - if video_stream is None or transform_stream is None: - print("\nWARNING: Video or transform streams not configured.") - print("Exiting test. Please configure streams properly.") - return - - # Setup output directory for visual memory - visual_memory_dir = "/home/stash/dimensional/dimos/assets/test_spatial_memory" - os.makedirs(visual_memory_dir, exist_ok=True) - - # Setup persistent storage path for visual memory - visual_memory_path = os.path.join(visual_memory_dir, "visual_memory.pkl") - - # Try to load existing visual memory if it exists - if os.path.exists(visual_memory_path): - try: - print(f"Loading existing visual memory from {visual_memory_path}...") - visual_memory = VisualMemory.load(visual_memory_path, output_dir=visual_memory_dir) - print(f"Loaded {visual_memory.count()} images from previous runs") - except Exception as e: - print(f"Error loading visual memory: {e}") - visual_memory = VisualMemory(output_dir=visual_memory_dir) - else: - print("No existing visual memory found. Starting with empty visual memory.") - visual_memory = VisualMemory(output_dir=visual_memory_dir) - - # Setup a persistent database for ChromaDB - db_client = setup_persistent_chroma_db() - - # Create spatial perception instance with persistent storage - print("Creating SpatialMemory with persistent vector database...") - spatial_memory = SpatialMemory( - collection_name="test_spatial_memory", - min_distance_threshold=1, # Store frames every 1 meter - min_time_threshold=1, # Store frames at least every 1 second - chroma_client=db_client, # Use the persistent client - visual_memory=visual_memory, # Use the visual memory we loaded or created - ) - - # Combine streams using combine_latest - # This will pair up items properly without buffering - combined_stream = reactivex.combine_latest(video_stream, transform_stream).pipe( - ops.map( - lambda pair: { - "frame": pair[0], # First element is the frame - "position": extract_pose_data(pair[1])[0], # Position as Vector3 - "rotation": extract_pose_data(pair[1])[1], # Rotation as Vector3 - } - ), - ops.filter(lambda data: data["position"] is not None and data["rotation"] is not None), - ) - - # Process with spatial memory - result_stream = spatial_memory.process_stream(combined_stream) - - # Simple callback to track stored frames and save them to the assets directory - def on_stored_frame(result): - nonlocal stored_count - # Only count actually stored frames (not debug frames) - if not not result.get("stored", True): - stored_count += 1 - pos = result["position"] - if isinstance(pos, tuple): - print( - f"\nStored frame #{stored_count} at ({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f})" - ) - else: - print(f"\nStored frame #{stored_count} at position {pos}") - - # Save the frame to the assets directory - if "frame" in result: - frame_filename = f"/home/stash/dimensional/dimos/assets/test_spatial_memory/frame_{stored_count:03d}.jpg" - cv2.imwrite(frame_filename, result["frame"]) - print(f"Saved frame to {frame_filename}") - - # Subscribe to results - print("Subscribing to spatial perception results...") - result_subscription = result_stream.subscribe(on_stored_frame) - - print("\nRunning until interrupted...") - try: - while True: - time.sleep(1.0) - print(f"Running: {stored_count} frames stored so far", end="\r") - except KeyboardInterrupt: - print("\nTest interrupted by user") - finally: - # Clean up resources - print("\nCleaning up...") - if "result_subscription" in locals(): - result_subscription.dispose() - - # Visualize spatial memory with multiple object queries - visualize_spatial_memory_with_objects( - spatial_memory, - objects=[ - "kitchen", - "conference room", - "vacuum", - "office", - "bathroom", - "boxes", - "telephone booth", - ], - output_filename="spatial_memory_map.png", - ) - - # Save visual memory to disk for later use - saved_path = spatial_memory.vector_db.visual_memory.save("visual_memory.pkl") - print(f"Saved {spatial_memory.vector_db.visual_memory.count()} images to disk at {saved_path}") - - spatial_memory.stop() - - print("Test completed successfully") - - -def visualize_spatial_memory_with_objects( - spatial_memory, objects, output_filename="spatial_memory_map.png" -): - """ - Visualize a spatial memory map with multiple labeled objects. - - Args: - spatial_memory: SpatialMemory instance - objects: List of object names to query and visualize (e.g. ["kitchen", "office"]) - output_filename: Filename to save the visualization - """ - # Define colors for different objects - will cycle through these - colors = ["red", "green", "orange", "purple", "brown", "cyan", "magenta", "yellow"] - - # Get all stored locations for background - locations = spatial_memory.vector_db.get_all_locations() - if not locations: - print("No locations stored in spatial memory.") - return - - # Extract coordinates from all stored locations - x_coords = [] - y_coords = [] - for loc in locations: - if isinstance(loc, dict): - x_coords.append(loc.get("pos_x", 0)) - y_coords.append(loc.get("pos_y", 0)) - elif isinstance(loc, tuple | list) and len(loc) >= 2: - x_coords.append(loc[0]) - y_coords.append(loc[1]) - else: - print(f"Unknown location format: {loc}") - - # Create figure - plt.figure(figsize=(12, 10)) - - # Plot all points in blue - plt.scatter(x_coords, y_coords, c="blue", s=50, alpha=0.5, label="All Frames") - - # Container for all object coordinates - object_coords = {} - - # Query for each object and store the result - for i, obj in enumerate(objects): - color = colors[i % len(colors)] # Cycle through colors - print(f"\nProcessing {obj} query for visualization...") - - # Get best match for this object - results = spatial_memory.query_by_text(obj, limit=1) - if not results: - print(f"No results found for '{obj}'") - continue - - # Get the first (best) result - result = results[0] - metadata = result["metadata"] - - # Extract coordinates from the first metadata item - if isinstance(metadata, list) and metadata: - metadata = metadata[0] - - if isinstance(metadata, dict): - # New metadata format uses pos_x, pos_y - x = metadata.get("pos_x", metadata.get("x", 0)) - y = metadata.get("pos_y", metadata.get("y", 0)) - - # Store coordinates for this object - object_coords[obj] = (x, y) - - # Plot this object's position - plt.scatter([x], [y], c=color, s=100, alpha=0.8, label=obj.title()) - - # Add annotation - obj_abbrev = obj[0].upper() if len(obj) > 0 else "X" - plt.annotate( - f"{obj_abbrev}", (x, y), textcoords="offset points", xytext=(0, 10), ha="center" - ) - - # Save the image to a file using the object name - if "image" in result and result["image"] is not None: - # Clean the object name to make it suitable for a filename - clean_name = obj.replace(" ", "_").lower() - output_img_filename = f"{clean_name}_result.jpg" - cv2.imwrite(output_img_filename, result["image"]) - print(f"Saved {obj} image to {output_img_filename}") - - # Finalize the plot - plt.title("Spatial Memory Map with Query Results") - plt.xlabel("X Position (m)") - plt.ylabel("Y Position (m)") - plt.grid(True) - plt.axis("equal") - plt.legend() - - # Add origin circle - plt.gca().add_patch(Circle((0, 0), 1.0, fill=False, color="blue", linestyle="--")) - - # Save the visualization - plt.savefig(output_filename, dpi=300) - print(f"Saved enhanced map visualization to {output_filename}") - - return object_coords - - -if __name__ == "__main__": - main() diff --git a/tests/test_spatial_memory_query.py b/tests/test_spatial_memory_query.py deleted file mode 100644 index 539f5f5eb0..0000000000 --- a/tests/test_spatial_memory_query.py +++ /dev/null @@ -1,295 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Test script for querying an existing spatial memory database - -Usage: - python test_spatial_memory_query.py --query "kitchen table" --limit 5 --threshold 0.7 --save-all - python test_spatial_memory_query.py --query "robot" --limit 3 --save-one -""" - -import argparse -from datetime import datetime -import os - -import chromadb -import cv2 -import matplotlib.pyplot as plt - -from dimos.agents.memory.visual_memory import VisualMemory -from dimos.perception.spatial_perception import SpatialMemory - - -def setup_persistent_chroma_db(db_path): - """Set up a persistent ChromaDB client at the specified path.""" - print(f"Setting up persistent ChromaDB at: {db_path}") - os.makedirs(db_path, exist_ok=True) - return chromadb.PersistentClient(path=db_path) - - -def parse_args(): - """Parse command-line arguments.""" - parser = argparse.ArgumentParser(description="Query spatial memory database.") - parser.add_argument( - "--query", type=str, default=None, help="Text query to search for (e.g., 'kitchen table')" - ) - parser.add_argument("--limit", type=int, default=3, help="Maximum number of results to return") - parser.add_argument( - "--threshold", - type=float, - default=None, - help="Similarity threshold (0.0-1.0). Only return results above this threshold.", - ) - parser.add_argument("--save-all", action="store_true", help="Save all result images") - parser.add_argument("--save-one", action="store_true", help="Save only the best matching image") - parser.add_argument( - "--visualize", - action="store_true", - help="Create a visualization of all stored memory locations", - ) - parser.add_argument( - "--db-path", - type=str, - default="/home/stash/dimensional/dimos/assets/test_spatial_memory/chromadb_data", - help="Path to ChromaDB database", - ) - parser.add_argument( - "--visual-memory-path", - type=str, - default="/home/stash/dimensional/dimos/assets/test_spatial_memory/visual_memory.pkl", - help="Path to visual memory file", - ) - return parser.parse_args() - - -def main(): - args = parse_args() - print("Loading existing spatial memory database for querying...") - - # Setup the persistent ChromaDB client - db_client = setup_persistent_chroma_db(args.db_path) - - # Setup output directory for any saved results - output_dir = os.path.dirname(args.visual_memory_path) - - # Load the visual memory - print(f"Loading visual memory from {args.visual_memory_path}...") - if os.path.exists(args.visual_memory_path): - visual_memory = VisualMemory.load(args.visual_memory_path, output_dir=output_dir) - print(f"Loaded {visual_memory.count()} images from visual memory") - else: - visual_memory = VisualMemory(output_dir=output_dir) - print("No existing visual memory found. Query results won't include images.") - - # Create SpatialMemory with the existing database and visual memory - spatial_memory = SpatialMemory( - collection_name="test_spatial_memory", chroma_client=db_client, visual_memory=visual_memory - ) - - # Create a visualization if requested - if args.visualize: - print("\nCreating visualization of spatial memory...") - common_objects = [ - "kitchen", - "conference room", - "vacuum", - "office", - "bathroom", - "boxes", - "telephone booth", - ] - visualize_spatial_memory_with_objects( - spatial_memory, objects=common_objects, output_filename="spatial_memory_map.png" - ) - - # Handle query if provided - if args.query: - query = args.query - limit = args.limit - print(f"\nQuerying for: '{query}' (limit: {limit})...") - - # Run the query - results = spatial_memory.query_by_text(query, limit=limit) - - if not results: - print(f"No results found for query: '{query}'") - return - - # Filter by threshold if specified - if args.threshold is not None: - print(f"Filtering results with similarity threshold: {args.threshold}") - filtered_results = [] - for result in results: - # Distance is inverse of similarity (0 is perfect match) - # Convert to similarity score (1.0 is perfect match) - similarity = 1.0 - ( - result.get("distance", 0) if result.get("distance") is not None else 0 - ) - if similarity >= args.threshold: - filtered_results.append((result, similarity)) - - # Sort by similarity (highest first) - filtered_results.sort(key=lambda x: x[1], reverse=True) - - if not filtered_results: - print(f"No results met the similarity threshold of {args.threshold}") - return - - print(f"Found {len(filtered_results)} results above threshold") - results_with_scores = filtered_results - else: - # Add similarity scores for all results - results_with_scores = [] - for result in results: - similarity = 1.0 - ( - result.get("distance", 0) if result.get("distance") is not None else 0 - ) - results_with_scores.append((result, similarity)) - - # Process and display results - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - - for i, (result, similarity) in enumerate(results_with_scores): - metadata = result.get("metadata", {}) - if isinstance(metadata, list) and metadata: - metadata = metadata[0] - - # Display result information - print(f"\nResult {i + 1} for '{query}':") - print(f"Similarity: {similarity:.4f} (distance: {1.0 - similarity:.4f})") - - # Extract and display position information - if isinstance(metadata, dict): - x = metadata.get("x", 0) - y = metadata.get("y", 0) - z = metadata.get("z", 0) - print(f"Position: ({x:.2f}, {y:.2f}, {z:.2f})") - if "timestamp" in metadata: - print(f"Timestamp: {metadata['timestamp']}") - if "frame_id" in metadata: - print(f"Frame ID: {metadata['frame_id']}") - - # Save image if requested and available - if "image" in result and result["image"] is not None: - # Only save first image, or all images based on flags - if args.save_one and i > 0: - continue - if not (args.save_all or args.save_one): - continue - - # Create a descriptive filename - clean_query = query.replace(" ", "_").replace("/", "_").lower() - output_filename = f"{clean_query}_result_{i + 1}_{timestamp}.jpg" - - # Save the image - cv2.imwrite(output_filename, result["image"]) - print(f"Saved image to {output_filename}") - elif "image" in result and result["image"] is None: - print("Image data not available for this result") - else: - print('No query specified. Use --query "text to search for" to run a query.') - print("Use --help to see all available options.") - - print("\nQuery completed successfully!") - - -def visualize_spatial_memory_with_objects( - spatial_memory, objects, output_filename="spatial_memory_map.png" -): - """Visualize spatial memory with labeled objects.""" - # Define colors for different objects - colors = ["red", "green", "orange", "purple", "brown", "cyan", "magenta", "yellow"] - - # Get all stored locations for background - locations = spatial_memory.vector_db.get_all_locations() - if not locations: - print("No locations stored in spatial memory.") - return - - # Extract coordinates - if len(locations[0]) >= 3: - x_coords = [loc[0] for loc in locations] - y_coords = [loc[1] for loc in locations] - else: - x_coords, y_coords = zip(*locations, strict=False) - - # Create figure - plt.figure(figsize=(12, 10)) - plt.scatter(x_coords, y_coords, c="blue", s=50, alpha=0.5, label="All Frames") - - # Container for object coordinates - object_coords = {} - - # Query for each object - for i, obj in enumerate(objects): - color = colors[i % len(colors)] - print(f"Processing {obj} query for visualization...") - - # Get best match - results = spatial_memory.query_by_text(obj, limit=1) - if not results: - print(f"No results found for '{obj}'") - continue - - # Process result - result = results[0] - metadata = result["metadata"] - - if isinstance(metadata, list) and metadata: - metadata = metadata[0] - - if isinstance(metadata, dict) and "x" in metadata and "y" in metadata: - x = metadata.get("x", 0) - y = metadata.get("y", 0) - - # Store coordinates - object_coords[obj] = (x, y) - - # Plot position - plt.scatter([x], [y], c=color, s=100, alpha=0.8, label=obj.title()) - - # Add annotation - obj_abbrev = obj[0].upper() if len(obj) > 0 else "X" - plt.annotate( - f"{obj_abbrev}", (x, y), textcoords="offset points", xytext=(0, 10), ha="center" - ) - - # Save image if available - if "image" in result and result["image"] is not None: - clean_name = obj.replace(" ", "_").lower() - output_img_filename = f"{clean_name}_result.jpg" - cv2.imwrite(output_img_filename, result["image"]) - print(f"Saved {obj} image to {output_img_filename}") - - # Finalize plot - plt.title("Spatial Memory Map with Query Results") - plt.xlabel("X Position (m)") - plt.ylabel("Y Position (m)") - plt.grid(True) - plt.axis("equal") - plt.legend() - - # Add origin marker - plt.gca().add_patch(plt.Circle((0, 0), 1.0, fill=False, color="blue", linestyle="--")) - - # Save visualization - plt.savefig(output_filename, dpi=300) - print(f"Saved visualization to {output_filename}") - - return object_coords - - -if __name__ == "__main__": - main() diff --git a/tests/test_standalone_chromadb.py b/tests/test_standalone_chromadb.py deleted file mode 100644 index d6e59e5237..0000000000 --- a/tests/test_standalone_chromadb.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -# ----- -from langchain_chroma import Chroma -from langchain_openai import OpenAIEmbeddings - -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") -if not OPENAI_API_KEY: - raise Exception("OpenAI key not specified.") - -collection_name = "my_collection" - -embeddings = OpenAIEmbeddings( - model="text-embedding-3-large", - dimensions=1024, - api_key=OPENAI_API_KEY, -) - -db_connection = Chroma( - collection_name=collection_name, - embedding_function=embeddings, -) - - -def add_vector(vector_id, vector_data): - """Add a vector to the ChromaDB collection.""" - if not db_connection: - raise Exception("Collection not initialized. Call connect() first.") - db_connection.add_texts( - ids=[vector_id], - texts=[vector_data], - metadatas=[{"name": vector_id}], - ) - - -add_vector("id0", "Food") -add_vector("id1", "Cat") -add_vector("id2", "Mouse") -add_vector("id3", "Bike") -add_vector("id4", "Dog") -add_vector("id5", "Tricycle") -add_vector("id6", "Car") -add_vector("id7", "Horse") -add_vector("id8", "Vehicle") -add_vector("id6", "Red") -add_vector("id7", "Orange") -add_vector("id8", "Yellow") - - -def get_vector(vector_id): - """Retrieve a vector from the ChromaDB by its identifier.""" - result = db_connection.get(include=["embeddings"], ids=[vector_id]) - return result - - -print(get_vector("id1")) -# print(get_vector("id3")) -# print(get_vector("id0")) -# print(get_vector("id2")) - - -def query(query_texts, n_results=2): - """Query the collection with a specific text and return up to n results.""" - if not db_connection: - raise Exception("Collection not initialized. Call connect() first.") - return db_connection.similarity_search(query=query_texts, k=n_results) - - -results = query("Colors") -print(results) diff --git a/tests/test_standalone_fastapi.py b/tests/test_standalone_fastapi.py deleted file mode 100644 index eb7a9a060a..0000000000 --- a/tests/test_standalone_fastapi.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os - -logging.basicConfig(level=logging.DEBUG) - -import cv2 -from fastapi import FastAPI -from starlette.responses import StreamingResponse -import uvicorn - -app = FastAPI() - -# Note: Chrome does not allow for loading more than 6 simultaneous -# video streams. Use Safari or another browser for utilizing -# multiple simultaneous streams. Possibly build out functionality -# that will stop live streams. - - -@app.get("/") -async def root(): - pid = os.getpid() # Get the current process ID - return {"message": f"Video Streaming Server, PID: {pid}"} - - -def video_stream_generator(): - pid = os.getpid() - print(f"Stream initiated by worker with PID: {pid}") # Log the PID when the generator is called - - # Use the correct path for your video source - cap = cv2.VideoCapture( - f"{os.getcwd()}/assets/trimmed_video_480p.mov" - ) # Change 0 to a filepath for video files - - if not cap.isOpened(): - yield (b"--frame\r\nContent-Type: text/plain\r\n\r\n" + b"Could not open video source\r\n") - return - - try: - while True: - ret, frame = cap.read() - # If frame is read correctly ret is True - if not ret: - print(f"Reached the end of the video, restarting... PID: {pid}") - cap.set( - cv2.CAP_PROP_POS_FRAMES, 0 - ) # Set the position of the next video frame to 0 (the beginning) - continue - _, buffer = cv2.imencode(".jpg", frame) - yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + buffer.tobytes() + b"\r\n") - finally: - cap.release() - - -@app.get("/video") -async def video_endpoint(): - logging.debug("Attempting to open video stream.") - response = StreamingResponse( - video_stream_generator(), media_type="multipart/x-mixed-replace; boundary=frame" - ) - logging.debug("Streaming response set up.") - return response - - -if __name__ == "__main__": - uvicorn.run("__main__:app", host="0.0.0.0", port=5555, workers=20) diff --git a/tests/test_standalone_hugging_face.py b/tests/test_standalone_hugging_face.py deleted file mode 100644 index ad5f02d510..0000000000 --- a/tests/test_standalone_hugging_face.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# from transformers import AutoModelForCausalLM, AutoTokenizer -# model_name = "Qwen/QwQ-32B" -# model = AutoModelForCausalLM.from_pretrained( -# model_name, -# torch_dtype="auto", -# device_map="auto" -# ) -# tokenizer = AutoTokenizer.from_pretrained(model_name) -# prompt = "How many r's are in the word \"strawberry\"" -# messages = [ -# {"role": "user", "content": prompt} -# ] -# text = tokenizer.apply_chat_template( -# messages, -# tokenize=False, -# add_generation_prompt=True -# ) -# model_inputs = tokenizer([text], return_tensors="pt").to(model.device) -# generated_ids = model.generate( -# **model_inputs, -# max_new_tokens=32768 -# ) -# generated_ids = [ -# output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) -# ] -# response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] -# print(response) -# ----------------------------------------------------------------------------- -# import requests -# import json -# API_URL = "https://api-inference.huggingface.co/models/Qwen/QwQ-32B" -# api_key = os.getenv('HUGGINGFACE_ACCESS_TOKEN') -# HEADERS = {"Authorization": f"Bearer {api_key}"} -# prompt = "How many r's are in the word \"strawberry\"" -# messages = [ -# {"role": "user", "content": prompt} -# ] -# # Format the prompt in the desired chat format -# chat_template = ( -# f"{messages[0]['content']}\n" -# "Assistant:" -# ) -# payload = { -# "inputs": chat_template, -# "parameters": { -# "max_new_tokens": 32768, -# "temperature": 0.7 -# } -# } -# # API request -# response = requests.post(API_URL, headers=HEADERS, json=payload) -# # Handle response -# if response.status_code == 200: -# output = response.json()[0]['generated_text'] -# print(output.strip()) -# else: -# print(f"Error {response.status_code}: {response.text}") -# ----------------------------------------------------------------------------- -# import os -# import requests -# import time -# API_URL = "https://api-inference.huggingface.co/models/Qwen/QwQ-32B" -# api_key = os.getenv('HUGGINGFACE_ACCESS_TOKEN') -# HEADERS = {"Authorization": f"Bearer {api_key}"} -# def query_with_retries(payload, max_retries=5, delay=15): -# for attempt in range(max_retries): -# response = requests.post(API_URL, headers=HEADERS, json=payload) -# if response.status_code == 200: -# return response.json()[0]['generated_text'] -# elif response.status_code == 500: # Service unavailable -# print(f"Attempt {attempt + 1}/{max_retries}: Model busy. Retrying in {delay} seconds...") -# time.sleep(delay) -# else: -# print(f"Error {response.status_code}: {response.text}") -# break -# return "Failed after multiple retries." -# prompt = "How many r's are in the word \"strawberry\"" -# messages = [{"role": "user", "content": prompt}] -# chat_template = f"{messages[0]['content']}\nAssistant:" -# payload = { -# "inputs": chat_template, -# "parameters": {"max_new_tokens": 32768, "temperature": 0.7} -# } -# output = query_with_retries(payload) -# print(output.strip()) -# ----------------------------------------------------------------------------- -import os - -from huggingface_hub import InferenceClient - -# Use environment variable for API key -api_key = os.getenv("HUGGINGFACE_ACCESS_TOKEN") - -client = InferenceClient( - provider="hf-inference", - api_key=api_key, -) - -messages = [{"role": "user", "content": 'How many r\'s are in the word "strawberry"'}] - -completion = client.chat.completions.create( - model="Qwen/QwQ-32B", - messages=messages, - max_tokens=150, -) - -print(completion.choices[0].message) diff --git a/tests/test_standalone_openai_json.py b/tests/test_standalone_openai_json.py deleted file mode 100644 index fe1a67ad78..0000000000 --- a/tests/test_standalone_openai_json.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -# ----- -import dotenv - -dotenv.load_dotenv() - -import json -from textwrap import dedent - -from openai import OpenAI -from pydantic import BaseModel - -MODEL = "gpt-4o-2024-08-06" - -math_tutor_prompt = """ - You are a helpful math tutor. You will be provided with a math problem, - and your goal will be to output a step by step solution, along with a final answer. - For each step, just provide the output as an equation use the explanation field to detail the reasoning. -""" - -bad_prompt = """ - Follow the instructions. -""" - -client = OpenAI() - - -class MathReasoning(BaseModel): - class Step(BaseModel): - explanation: str - output: str - - steps: list[Step] - final_answer: str - - -def get_math_solution(question: str): - completion = client.beta.chat.completions.parse( - model=MODEL, - messages=[ - {"role": "system", "content": dedent(bad_prompt)}, - {"role": "user", "content": question}, - ], - response_format=MathReasoning, - ) - return completion.choices[0].message - - -# Web Server -import http.server -import socketserver -import urllib.parse - -PORT = 5555 - - -class CustomHandler(http.server.SimpleHTTPRequestHandler): - def do_GET(self): - # Parse query parameters from the URL - parsed_path = urllib.parse.urlparse(self.path) - query_params = urllib.parse.parse_qs(parsed_path.query) - - # Check for a specific query parameter, e.g., 'problem' - problem = query_params.get("problem", [""])[ - 0 - ] # Default to an empty string if 'problem' isn't provided - - if problem: - print(f"Problem: {problem}") - solution = get_math_solution(problem) - - if solution.refusal: - print(f"Refusal: {solution.refusal}") - - print(f"Solution: {solution}") - self.send_response(200) - else: - solution = json.dumps( - {"error": "Please provide a math problem using the 'problem' query parameter."} - ) - self.send_response(400) - - self.send_header("Content-type", "application/json; charset=utf-8") - self.end_headers() - - # Write the message content - self.wfile.write(str(solution).encode()) - - -with socketserver.TCPServer(("", PORT), CustomHandler) as httpd: - print(f"Serving at port {PORT}") - httpd.serve_forever() diff --git a/tests/test_standalone_openai_json_struct.py b/tests/test_standalone_openai_json_struct.py deleted file mode 100644 index b22f064e35..0000000000 --- a/tests/test_standalone_openai_json_struct.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -# ----- - -import dotenv - -dotenv.load_dotenv() - -from textwrap import dedent - -from openai import OpenAI -from pydantic import BaseModel - -MODEL = "gpt-4o-2024-08-06" - -math_tutor_prompt = """ - You are a helpful math tutor. You will be provided with a math problem, - and your goal will be to output a step by step solution, along with a final answer. - For each step, just provide the output as an equation use the explanation field to detail the reasoning. -""" - -general_prompt = """ - Follow the instructions. Output a step by step solution, along with a final answer. Use the explanation field to detail the reasoning. -""" - -client = OpenAI() - - -class MathReasoning(BaseModel): - class Step(BaseModel): - explanation: str - output: str - - steps: list[Step] - final_answer: str - - -def get_math_solution(question: str): - prompt = general_prompt - completion = client.beta.chat.completions.parse( - model=MODEL, - messages=[ - {"role": "system", "content": dedent(prompt)}, - {"role": "user", "content": question}, - ], - response_format=MathReasoning, - ) - return completion.choices[0].message - - -# Define Problem -problem = "What is the derivative of 3x^2" -print(f"Problem: {problem}") - -# Query for result -solution = get_math_solution(problem) - -# If the query was refused -if solution.refusal: - print(f"Refusal: {solution.refusal}") - exit() - -# If we were able to successfully parse the response back -parsed_solution = solution.parsed -if not parsed_solution: - print("Unable to Parse Solution") - exit() - -# Print solution from class definitions -print(f"Parsed: {parsed_solution}") - -steps = parsed_solution.steps -print(f"Steps: {steps}") - -final_answer = parsed_solution.final_answer -print(f"Final Answer: {final_answer}") diff --git a/tests/test_standalone_openai_json_struct_func.py b/tests/test_standalone_openai_json_struct_func.py deleted file mode 100644 index 36f158cd20..0000000000 --- a/tests/test_standalone_openai_json_struct_func.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -# ----- - -import dotenv - -dotenv.load_dotenv() - -import json -from textwrap import dedent - -from openai import OpenAI, pydantic_function_tool -from pydantic import BaseModel, Field -import requests - -MODEL = "gpt-4o-2024-08-06" - -math_tutor_prompt = """ - You are a helpful math tutor. You will be provided with a math problem, - and your goal will be to output a step by step solution, along with a final answer. - For each step, just provide the output as an equation use the explanation field to detail the reasoning. -""" - -general_prompt = """ - Follow the instructions. Output a step by step solution, along with a final answer. Use the explanation field to detail the reasoning. -""" - -client = OpenAI() - - -class MathReasoning(BaseModel): - class Step(BaseModel): - explanation: str - output: str - - steps: list[Step] - final_answer: str - - -# region Function Calling -class GetWeather(BaseModel): - latitude: str = Field(..., description="latitude e.g. BogotĆ”, Colombia") - longitude: str = Field(..., description="longitude e.g. BogotĆ”, Colombia") - - -def get_weather(latitude, longitude): - response = requests.get( - f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m&temperature_unit=fahrenheit" - ) - data = response.json() - return data["current"]["temperature_2m"] - - -def get_tools(): - return [pydantic_function_tool(GetWeather)] - - -tools = get_tools() - - -def call_function(name, args): - if name == "get_weather": - print(f"Running function: {name}") - print(f"Arguments are: {args}") - return get_weather(**args) - elif name == "GetWeather": - print(f"Running function: {name}") - print(f"Arguments are: {args}") - return get_weather(**args) - else: - return f"Local function not found: {name}" - - -def callback(message, messages, response_message, tool_calls): - if message is None or message.tool_calls is None: - print("No message or tools were called.") - return - - has_called_tools = False - for tool_call in message.tool_calls: - messages.append(response_message) - - has_called_tools = True - name = tool_call.function.name - args = json.loads(tool_call.function.arguments) - - result = call_function(name, args) - print(f"Function Call Results: {result}") - - messages.append( - {"role": "tool", "tool_call_id": tool_call.id, "content": str(result), "name": name} - ) - - # Complete the second call, after the functions have completed. - if has_called_tools: - print("Sending Second Query.") - completion_2 = client.beta.chat.completions.parse( - model=MODEL, - messages=messages, - response_format=MathReasoning, - tools=tools, - ) - print(f"Message: {completion_2.choices[0].message}") - return completion_2.choices[0].message - else: - print("No Need for Second Query.") - return None - - -# endregion Function Calling - - -def get_math_solution(question: str): - prompt = general_prompt - messages = [ - {"role": "system", "content": dedent(prompt)}, - {"role": "user", "content": question}, - ] - response = client.beta.chat.completions.parse( - model=MODEL, messages=messages, response_format=MathReasoning, tools=tools - ) - - response_message = response.choices[0].message - tool_calls = response_message.tool_calls - - new_response = callback(response.choices[0].message, messages, response_message, tool_calls) - - return new_response or response.choices[0].message - - -# Define Problem -problems = ["What is the derivative of 3x^2", "What's the weather like in San Fran today?"] -problem = problems[0] - -for problem in problems: - print("================") - print(f"Problem: {problem}") - - # Query for result - solution = get_math_solution(problem) - - # If the query was refused - if solution.refusal: - print(f"Refusal: {solution.refusal}") - break - - # If we were able to successfully parse the response back - parsed_solution = solution.parsed - if not parsed_solution: - print("Unable to Parse Solution") - print(f"Solution: {solution}") - break - - # Print solution from class definitions - print(f"Parsed: {parsed_solution}") - - steps = parsed_solution.steps - print(f"Steps: {steps}") - - final_answer = parsed_solution.final_answer - print(f"Final Answer: {final_answer}") diff --git a/tests/test_standalone_openai_json_struct_func_playground.py b/tests/test_standalone_openai_json_struct_func_playground.py deleted file mode 100644 index 8dd687148d..0000000000 --- a/tests/test_standalone_openai_json_struct_func_playground.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# ----- -# # Milestone 1 -# from typing import List, Dict, Optional -# import requests -# import json -# from pydantic import BaseModel, Field -# from openai import OpenAI, pydantic_function_tool -# # Environment setup -# import dotenv -# dotenv.load_dotenv() -# # Constants and prompts -# MODEL = "gpt-4o-2024-08-06" -# GENERAL_PROMPT = ''' -# Follow the instructions. Output a step by step solution, along with a final answer. -# Use the explanation field to detail the reasoning. -# ''' -# # Initialize OpenAI client -# client = OpenAI() -# # Models and functions -# class Step(BaseModel): -# explanation: str -# output: str -# class MathReasoning(BaseModel): -# steps: List[Step] -# final_answer: str -# class GetWeather(BaseModel): -# latitude: str = Field(..., description="Latitude e.g., BogotĆ”, Colombia") -# longitude: str = Field(..., description="Longitude e.g., BogotĆ”, Colombia") -# def fetch_weather(latitude: str, longitude: str) -> Dict: -# url = f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m&temperature_unit=fahrenheit" -# response = requests.get(url) -# return response.json().get('current', {}) -# # Tool management -# def get_tools() -> List[BaseModel]: -# return [pydantic_function_tool(GetWeather)] -# def handle_function_call(tool_call: Dict) -> Optional[str]: -# if tool_call['name'] == "get_weather": -# result = fetch_weather(**tool_call['args']) -# return f"Temperature is {result['temperature_2m']}°F" -# return None -# # Communication and processing with OpenAI -# def process_message_with_openai(question: str) -> MathReasoning: -# messages = [ -# {"role": "system", "content": GENERAL_PROMPT.strip()}, -# {"role": "user", "content": question} -# ] -# response = client.beta.chat.completions.parse( -# model=MODEL, -# messages=messages, -# response_format=MathReasoning, -# tools=get_tools() -# ) -# return response.choices[0].message -# def get_math_solution(question: str) -> MathReasoning: -# solution = process_message_with_openai(question) -# return solution -# # Example usage -# def main(): -# problems = [ -# "What is the derivative of 3x^2", -# "What's the weather like in San Francisco today?" -# ] -# problem = problems[1] -# print(f"Problem: {problem}") -# solution = get_math_solution(problem) -# if not solution: -# print("Failed to get a solution.") -# return -# if not solution.parsed: -# print("Failed to get a parsed solution.") -# print(f"Solution: {solution}") -# return -# print(f"Steps: {solution.parsed.steps}") -# print(f"Final Answer: {solution.parsed.final_answer}") -# if __name__ == "__main__": -# main() -# # Milestone 1 -# Milestone 2 -import json - -from dotenv import load_dotenv -import requests - -load_dotenv() - -from openai import OpenAI - -client = OpenAI() - - -def get_current_weather(latitude, longitude): - """Get the current weather in a given latitude and longitude using the 7Timer API""" - base = "http://www.7timer.info/bin/api.pl" - request_url = f"{base}?lon={longitude}&lat={latitude}&product=civillight&output=json" - response = requests.get(request_url) - - # Parse response to extract the main weather data - weather_data = response.json() - current_data = weather_data.get("dataseries", [{}])[0] - - result = { - "latitude": latitude, - "longitude": longitude, - "temp": current_data.get("temp2m", {"max": "Unknown", "min": "Unknown"}), - "humidity": "Unknown", - } - - # Convert the dictionary to JSON string to match the given structure - return json.dumps(result) - - -def run_conversation(content): - messages = [{"role": "user", "content": content}] - tools = [ - { - "type": "function", - "function": { - "name": "get_current_weather", - "description": "Get the current weather in a given latitude and longitude", - "parameters": { - "type": "object", - "properties": { - "latitude": { - "type": "string", - "description": "The latitude of a place", - }, - "longitude": { - "type": "string", - "description": "The longitude of a place", - }, - }, - "required": ["latitude", "longitude"], - }, - }, - } - ] - response = client.chat.completions.create( - model="gpt-3.5-turbo-0125", - messages=messages, - tools=tools, - tool_choice="auto", - ) - response_message = response.choices[0].message - tool_calls = response_message.tool_calls - - if tool_calls: - messages.append(response_message) - - available_functions = { - "get_current_weather": get_current_weather, - } - for tool_call in tool_calls: - print(f"Function: {tool_call.function.name}") - print(f"Params:{tool_call.function.arguments}") - function_name = tool_call.function.name - function_to_call = available_functions[function_name] - function_args = json.loads(tool_call.function.arguments) - function_response = function_to_call( - latitude=function_args.get("latitude"), - longitude=function_args.get("longitude"), - ) - print(f"API: {function_response}") - messages.append( - { - "tool_call_id": tool_call.id, - "role": "tool", - "name": function_name, - "content": function_response, - } - ) - - second_response = client.chat.completions.create( - model="gpt-3.5-turbo-0125", messages=messages, stream=True - ) - return second_response - - -if __name__ == "__main__": - question = "What's the weather like in Paris and San Francisco?" - response = run_conversation(question) - for chunk in response: - print(chunk.choices[0].delta.content or "", end="", flush=True) -# Milestone 2 diff --git a/tests/test_standalone_project_out.py b/tests/test_standalone_project_out.py deleted file mode 100644 index 9a924f7f42..0000000000 --- a/tests/test_standalone_project_out.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# ----- -import ast -import inspect -import sys - - -def extract_function_info(filename): - with open(filename) as f: - source = f.read() - tree = ast.parse(source, filename=filename) - - function_info = [] - - # Use a dictionary to track functions - module_globals = {} - - # Add the source to the locals (useful if you use local functions) - exec(source, module_globals) - - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef): - docstring = ast.get_docstring(node) or "" - - # Attempt to get the callable object from the globals - try: - if node.name in module_globals: - func_obj = module_globals[node.name] - signature = inspect.signature(func_obj) - function_info.append( - {"name": node.name, "signature": str(signature), "docstring": docstring} - ) - else: - function_info.append( - { - "name": node.name, - "signature": "Could not get signature", - "docstring": docstring, - } - ) - except TypeError as e: - print( - f"Could not get function signature for {node.name} in {filename}: {e}", - file=sys.stderr, - ) - function_info.append( - { - "name": node.name, - "signature": "Could not get signature", - "docstring": docstring, - } - ) - - class_info = [] - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef): - docstring = ast.get_docstring(node) or "" - methods = [] - for method in node.body: - if isinstance(method, ast.FunctionDef | ast.AsyncFunctionDef): - method_docstring = ast.get_docstring(method) or "" - try: - if node.name in module_globals: - class_obj = module_globals[node.name] - method_obj = getattr(class_obj, method.name) - signature = inspect.signature(method_obj) - methods.append( - { - "name": method.name, - "signature": str(signature), - "docstring": method_docstring, - } - ) - else: - methods.append( - { - "name": method.name, - "signature": "Could not get signature", - "docstring": method_docstring, - } - ) - except AttributeError as e: - print( - f"Could not get method signature for {node.name}.{method.name} in {filename}: {e}", - file=sys.stderr, - ) - methods.append( - { - "name": method.name, - "signature": "Could not get signature", - "docstring": method_docstring, - } - ) - except TypeError as e: - print( - f"Could not get method signature for {node.name}.{method.name} in {filename}: {e}", - file=sys.stderr, - ) - methods.append( - { - "name": method.name, - "signature": "Could not get signature", - "docstring": method_docstring, - } - ) - class_info.append({"name": node.name, "docstring": docstring, "methods": methods}) - - return {"function_info": function_info, "class_info": class_info} - - -# Usage: -file_path = "./dimos/agents/memory/base.py" -extracted_info = extract_function_info(file_path) -print(extracted_info) - -file_path = "./dimos/agents/memory/chroma_impl.py" -extracted_info = extract_function_info(file_path) -print(extracted_info) - -file_path = "./dimos/agents/agent.py" -extracted_info = extract_function_info(file_path) -print(extracted_info) diff --git a/tests/test_standalone_rxpy_01.py b/tests/test_standalone_rxpy_01.py deleted file mode 100644 index 9be48f3eab..0000000000 --- a/tests/test_standalone_rxpy_01.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import multiprocessing -from threading import Event - -# ----- -import reactivex -from reactivex import operators as ops -from reactivex.scheduler import ThreadPoolScheduler - -which_test = 2 -if which_test == 1: - """ - Test 1: Periodic Emission Test - - This test creates a ThreadPoolScheduler that leverages as many threads as there are CPU - cores available, optimizing the execution across multiple threads. The core functionality - revolves around an observable, secondly_emission, which emits a value every second. - Each emission is an incrementing integer, which is then mapped to a message indicating - the number of seconds since the test began. The sequence is limited to 30 emissions, - each logged as it occurs, and accompanied by an additional message via the - emission_process function to indicate the value's emission. The test subscribes to the - observable to print each emitted value, handle any potential errors, and confirm - completion of the emissions after 30 seconds. - - Key Components: - • ThreadPoolScheduler: Manages concurrency with multiple threads. - • Observable Sequence: Emits every second, indicating progression with a specific - message format. - • Subscription: Monitors and logs emissions, errors, and the completion event. - """ - - # Create a scheduler that uses as many threads as there are CPUs available - optimal_thread_count = multiprocessing.cpu_count() - pool_scheduler = ThreadPoolScheduler(optimal_thread_count) - - def emission_process(value): - print(f"Emitting: {value}") - - # Create an observable that emits every second - secondly_emission = reactivex.interval(1.0, scheduler=pool_scheduler).pipe( - ops.map(lambda x: f"Value {x} emitted after {x + 1} second(s)"), - ops.do_action(emission_process), - ops.take(30), # Limit the emission to 30 times - ) - - # Subscribe to the observable to start emitting - secondly_emission.subscribe( - on_next=lambda x: print(x), - on_error=lambda e: print(e), - on_completed=lambda: print("Emission completed."), - scheduler=pool_scheduler, - ) - -elif which_test == 2: - """ - Test 2: Combined Emission Test - - In this test, a similar ThreadPoolScheduler setup is used to handle tasks across multiple - CPU cores efficiently. This setup includes two observables. The first, secondly_emission, - emits an incrementing integer every second, indicating the passage of time. The second - observable, immediate_emission, emits a predefined sequence of characters (['a', 'b', - 'c', 'd', 'e']) repeatedly and immediately. These two streams are combined using the zip - operator, which synchronizes their emissions into pairs. Each combined pair is formatted - and logged, indicating both the time elapsed and the immediate value emitted at that - second. - - A synchronization mechanism via an Event (completed_event) ensures that the main program - thread waits until all planned emissions are completed before exiting. This test not only - checks the functionality of zipping different rhythmic emissions but also demonstrates - handling of asynchronous task completion in Python using event-driven programming. - - Key Components: - • Combined Observable Emissions: Synchronizes periodic and immediate emissions into - a single stream. - • Event Synchronization: Uses a threading event to manage program lifecycle and - ensure that all emissions are processed before shutdown. - • Complex Subscription Management: Handles errors and completion, including - setting an event to signal the end of task processing. - """ - - # Create a scheduler with optimal threads - optimal_thread_count = multiprocessing.cpu_count() - pool_scheduler = ThreadPoolScheduler(optimal_thread_count) - - # Define an event to wait for the observable to complete - completed_event = Event() - - def emission_process(value): - print(f"Emitting: {value}") - - # Observable that emits every second - secondly_emission = reactivex.interval(1.0, scheduler=pool_scheduler).pipe( - ops.map(lambda x: f"Second {x + 1}"), ops.take(30) - ) - - # Observable that emits values immediately and repeatedly - immediate_emission = reactivex.from_(["a", "b", "c", "d", "e"]).pipe(ops.repeat()) - - # Combine emissions using zip - combined_emissions = reactivex.zip(secondly_emission, immediate_emission).pipe( - ops.map(lambda combined: f"{combined[0]} - Value: {combined[1]}"), - ops.do_action(lambda s: print(f"Combined emission: {s}")), - ) - - # Subscribe to the combined emissions - combined_emissions.subscribe( - on_next=lambda x: print(x), - on_error=lambda e: print(f"Error: {e}"), - on_completed=lambda: { - print("Combined emission completed."), - completed_event.set(), # Set the event to signal completion - }, - scheduler=pool_scheduler, - ) - - # Wait for the observable to complete - completed_event.wait() diff --git a/tests/test_unitree_agent.py b/tests/test_unitree_agent.py deleted file mode 100644 index 5c4b6acb7b..0000000000 --- a/tests/test_unitree_agent.py +++ /dev/null @@ -1,317 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import time - -from dimos.web.fastapi_server import FastAPIServer - -print(f"Current working directory: {os.getcwd()}") - -# ----- - -from dimos.agents.agent import OpenAIAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.stream.data_provider import QueryDataProvider - -MOCK_CONNECTION = True - - -class UnitreeAgentDemo: - def __init__(self): - self.robot_ip = None - self.connection_method = None - self.serial_number = None - self.output_dir = None - self._fetch_env_vars() - - def _fetch_env_vars(self): - print("Fetching environment variables") - - def get_env_var(var_name, default=None, required=False): - """Get environment variable with validation.""" - value = os.getenv(var_name, default) - if required and not value: - raise ValueError(f"{var_name} environment variable is required") - return value - - self.robot_ip = get_env_var("ROBOT_IP", required=True) - self.connection_method = get_env_var("CONN_TYPE") - self.serial_number = get_env_var("SERIAL_NUMBER") - self.output_dir = get_env_var( - "ROS_OUTPUT_DIR", os.path.join(os.getcwd(), "assets/output/ros") - ) - - def _initialize_robot(self, with_video_stream=True): - print( - f"Initializing Unitree Robot {'with' if with_video_stream else 'without'} Video Stream" - ) - self.robot = UnitreeGo2( - ip=self.robot_ip, - connection_method=self.connection_method, - serial_number=self.serial_number, - output_dir=self.output_dir, - disable_video_stream=(not with_video_stream), - mock_connection=MOCK_CONNECTION, - ) - print(f"Robot initialized: {self.robot}") - - # ----- - - def run_with_queries(self): - # Initialize robot - self._initialize_robot(with_video_stream=False) - - # Initialize query stream - query_provider = QueryDataProvider() - - # Create the skills available to the agent. - # By default, this will create all skills in this class and make them available. - skills_instance = MyUnitreeSkills(robot=self.robot) - - print("Starting Unitree Perception Agent") - self.UnitreePerceptionAgent = OpenAIAgent( - dev_name="UnitreePerceptionAgent", - agent_type="Perception", - input_query_stream=query_provider.data_stream, - output_dir=self.output_dir, - skills=skills_instance, - # frame_processor=frame_processor, - ) - - # Start the query stream. - # Queries will be pushed every 1 second, in a count from 100 to 5000. - # This will cause listening agents to consume the queries and respond - # to them via skill execution and provide 1-shot responses. - query_provider.start_query_stream( - query_template="{query}; Denote the number at the beginning of this query before the semicolon as the 'reference number'. Provide the reference number, without any other text in your response. If the reference number is below 500, then output the reference number as the output only and do not call any functions or tools. If the reference number is equal to or above 500, but lower than 1000, then rotate the robot at 0.5 rad/s for 1 second. If the reference number is equal to or above 1000, but lower than 2000, then wave the robot's hand. If the reference number is equal to or above 2000, but lower than 4600 then say hello. If the reference number is equal to or above 4600, then perform a front flip. IF YOU DO NOT FOLLOW THESE INSTRUCTIONS EXACTLY, YOU WILL DIE!!!", - frequency=0.01, - start_count=1, - end_count=10000, - step=1, - ) - - def run_with_test_video(self): - # Initialize robot - self._initialize_robot(with_video_stream=False) - - # Initialize test video stream - from dimos.stream.video_provider import VideoProvider - - self.video_stream = VideoProvider( - dev_name="UnitreeGo2", video_source=f"{os.getcwd()}/assets/framecount.mp4" - ).capture_video_as_observable(realtime=False, fps=1) - - # Get Skills - # By default, this will create all skills in this class and make them available to the agent. - skills_instance = MyUnitreeSkills(robot=self.robot) - - print("Starting Unitree Perception Agent (Test Video)") - self.UnitreePerceptionAgent = OpenAIAgent( - dev_name="UnitreePerceptionAgent", - agent_type="Perception", - input_video_stream=self.video_stream, - output_dir=self.output_dir, - query="Denote the number you see in the image as the 'reference number'. Only provide the reference number, without any other text in your response. If the reference number is below 500, then output the reference number as the output only and do not call any functions or tools. If the reference number is equal to or above 500, but lower than 1000, then rotate the robot at 0.5 rad/s for 1 second. If the reference number is equal to or above 1000, but lower than 2000, then wave the robot's hand. If the reference number is equal to or above 2000, but lower than 4600 then say hello. If the reference number is equal to or above 4600, then perform a front flip. IF YOU DO NOT FOLLOW THESE INSTRUCTIONS EXACTLY, YOU WILL DIE!!!", - image_detail="high", - skills=skills_instance, - # frame_processor=frame_processor, - ) - - def run_with_ros_video(self): - # Initialize robot - self._initialize_robot() - - # Initialize ROS video stream - print("Starting Unitree Perception Stream") - self.video_stream = self.robot.get_ros_video_stream() - - # Get Skills - # By default, this will create all skills in this class and make them available to the agent. - skills_instance = MyUnitreeSkills(robot=self.robot) - - # Run recovery stand - print("Running recovery stand") - self.robot.webrtc_req(api_id=1006) - - # Wait for 1 second - time.sleep(1) - - # Switch to sport mode - print("Switching to sport mode") - self.robot.webrtc_req(api_id=1011, parameter='{"gait_type": "sport"}') - - # Wait for 1 second - time.sleep(1) - - print("Starting Unitree Perception Agent (ROS Video)") - self.UnitreePerceptionAgent = OpenAIAgent( - dev_name="UnitreePerceptionAgent", - agent_type="Perception", - input_video_stream=self.video_stream, - output_dir=self.output_dir, - query="Based on the image, execute the command seen in the image AND ONLY THE COMMAND IN THE IMAGE. IF YOU DO NOT FOLLOW THESE INSTRUCTIONS EXACTLY, YOU WILL DIE!!!", - # WORKING MOVEMENT DEMO VVV - # query="Move() 5 meters foward. Then spin 360 degrees to the right, and then Reverse() 5 meters, and then Move forward 3 meters", - image_detail="high", - skills=skills_instance, - # frame_processor=frame_processor, - ) - - def run_with_multiple_query_and_test_video_agents(self): - # Initialize robot - self._initialize_robot(with_video_stream=False) - - # Initialize query stream - query_provider = QueryDataProvider() - - # Initialize test video stream - from dimos.stream.video_provider import VideoProvider - - self.video_stream = VideoProvider( - dev_name="UnitreeGo2", video_source=f"{os.getcwd()}/assets/framecount.mp4" - ).capture_video_as_observable(realtime=False, fps=1) - - # Create the skills available to the agent. - # By default, this will create all skills in this class and make them available. - skills_instance = MyUnitreeSkills(robot=self.robot) - - print("Starting Unitree Perception Agent") - self.UnitreeQueryPerceptionAgent = OpenAIAgent( - dev_name="UnitreeQueryPerceptionAgent", - agent_type="Perception", - input_query_stream=query_provider.data_stream, - output_dir=self.output_dir, - skills=skills_instance, - # frame_processor=frame_processor, - ) - - print("Starting Unitree Perception Agent Two") - self.UnitreeQueryPerceptionAgentTwo = OpenAIAgent( - dev_name="UnitreeQueryPerceptionAgentTwo", - agent_type="Perception", - input_query_stream=query_provider.data_stream, - output_dir=self.output_dir, - skills=skills_instance, - # frame_processor=frame_processor, - ) - - print("Starting Unitree Perception Agent (Test Video)") - self.UnitreeVideoPerceptionAgent = OpenAIAgent( - dev_name="UnitreeVideoPerceptionAgent", - agent_type="Perception", - input_video_stream=self.video_stream, - output_dir=self.output_dir, - query="Denote the number you see in the image as the 'reference number'. Only provide the reference number, without any other text in your response. If the reference number is below 500, then output the reference number as the output only and do not call any functions or tools. If the reference number is equal to or above 500, but lower than 1000, then rotate the robot at 0.5 rad/s for 1 second. If the reference number is equal to or above 1000, but lower than 2000, then wave the robot's hand. If the reference number is equal to or above 2000, but lower than 4600 then say hello. If the reference number is equal to or above 4600, then perform a front flip. IF YOU DO NOT FOLLOW THESE INSTRUCTIONS EXACTLY, YOU WILL DIE!!!", - image_detail="high", - skills=skills_instance, - # frame_processor=frame_processor, - ) - - print("Starting Unitree Perception Agent Two (Test Video)") - self.UnitreeVideoPerceptionAgentTwo = OpenAIAgent( - dev_name="UnitreeVideoPerceptionAgentTwo", - agent_type="Perception", - input_video_stream=self.video_stream, - output_dir=self.output_dir, - query="Denote the number you see in the image as the 'reference number'. Only provide the reference number, without any other text in your response. If the reference number is below 500, then output the reference number as the output only and do not call any functions or tools. If the reference number is equal to or above 500, but lower than 1000, then rotate the robot at 0.5 rad/s for 1 second. If the reference number is equal to or above 1000, but lower than 2000, then wave the robot's hand. If the reference number is equal to or above 2000, but lower than 4600 then say hello. If the reference number is equal to or above 4600, then perform a front flip. IF YOU DO NOT FOLLOW THESE INSTRUCTIONS EXACTLY, YOU WILL DIE!!!", - image_detail="high", - skills=skills_instance, - # frame_processor=frame_processor, - ) - - # Start the query stream. - # Queries will be pushed every 1 second, in a count from 100 to 5000. - # This will cause listening agents to consume the queries and respond - # to them via skill execution and provide 1-shot responses. - query_provider.start_query_stream( - query_template="{query}; Denote the number at the beginning of this query before the semicolon as the 'reference number'. Provide the reference number, without any other text in your response. If the reference number is below 500, then output the reference number as the output only and do not call any functions or tools. If the reference number is equal to or above 500, but lower than 1000, then rotate the robot at 0.5 rad/s for 1 second. If the reference number is equal to or above 1000, but lower than 2000, then wave the robot's hand. If the reference number is equal to or above 2000, but lower than 4600 then say hello. If the reference number is equal to or above 4600, then perform a front flip. IF YOU DO NOT FOLLOW THESE INSTRUCTIONS EXACTLY, YOU WILL DIE!!!", - frequency=0.01, - start_count=1, - end_count=10000000, - step=1, - ) - - def run_with_queries_and_fast_api(self): - # Initialize robot - self._initialize_robot(with_video_stream=True) - - # Initialize ROS video stream - print("Starting Unitree Perception Stream") - self.video_stream = self.robot.get_ros_video_stream() - - # Initialize test video stream - # from dimos.stream.video_provider import VideoProvider - # self.video_stream = VideoProvider( - # dev_name="UnitreeGo2", - # video_source=f"{os.getcwd()}/assets/framecount.mp4" - # ).capture_video_as_observable(realtime=False, fps=1) - - # Will be visible at http://[host]:[port]/video_feed/[key] - streams = { - "unitree_video": self.video_stream, - } - fast_api_server = FastAPIServer(port=5555, **streams) - - # Create the skills available to the agent. - skills_instance = MyUnitreeSkills(robot=self.robot) - - print("Starting Unitree Perception Agent") - self.UnitreeQueryPerceptionAgent = OpenAIAgent( - dev_name="UnitreeQueryPerceptionAgent", - agent_type="Perception", - input_query_stream=fast_api_server.query_stream, - output_dir=self.output_dir, - skills=skills_instance, - ) - - # Run the FastAPI server (this will block) - fast_api_server.run() - - # ----- - - def stop(self): - print("Stopping Unitree Agent") - self.robot.cleanup() - - -if __name__ == "__main__": - myUnitreeAgentDemo = UnitreeAgentDemo() - - test_to_run = 4 - - if test_to_run == 0: - myUnitreeAgentDemo.run_with_queries() - elif test_to_run == 1: - myUnitreeAgentDemo.run_with_test_video() - elif test_to_run == 2: - myUnitreeAgentDemo.run_with_ros_video() - elif test_to_run == 3: - myUnitreeAgentDemo.run_with_multiple_query_and_test_video_agents() - elif test_to_run == 4: - myUnitreeAgentDemo.run_with_queries_and_fast_api() - elif test_to_run < 0 or test_to_run >= 5: - raise AssertionError(f"Invalid test number: {test_to_run}") - - # Keep the program running to allow the Unitree Agent Demo to operate continuously - try: - print("\nRunning Unitree Agent Demo (Press Ctrl+C to stop)...") - while True: - time.sleep(0.1) - except KeyboardInterrupt: - print("\nStopping Unitree Agent Demo") - myUnitreeAgentDemo.stop() - except Exception as e: - print(f"Error in main loop: {e}") diff --git a/tests/test_unitree_agent_queries_fastapi.py b/tests/test_unitree_agent_queries_fastapi.py deleted file mode 100644 index 0671a53135..0000000000 --- a/tests/test_unitree_agent_queries_fastapi.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unitree Go2 robot agent demo with FastAPI server integration. - -Connects a Unitree Go2 robot to an OpenAI agent with a web interface. - -Environment Variables: - OPENAI_API_KEY: Required. OpenAI API key. - ROBOT_IP: Required. IP address of the Unitree robot. - CONN_TYPE: Required. Connection method to the robot. - ROS_OUTPUT_DIR: Optional. Directory for ROS output files. -""" - -import os -import sys - -import reactivex as rx -import reactivex.operators as ops - -# Local application imports -from dimos.agents.agent import OpenAIAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.utils.logging_config import logger -from dimos.web.fastapi_server import FastAPIServer - - -def main(): - # Get environment variables - robot_ip = os.getenv("ROBOT_IP") - if not robot_ip: - raise ValueError("ROBOT_IP environment variable is required") - connection_method = os.getenv("CONN_TYPE") or "webrtc" - output_dir = os.getenv("ROS_OUTPUT_DIR", os.path.join(os.getcwd(), "assets/output/ros")) - - try: - # Initialize robot - logger.info("Initializing Unitree Robot") - robot = UnitreeGo2( - ip=robot_ip, - connection_method=connection_method, - output_dir=output_dir, - skills=MyUnitreeSkills(), - ) - - # Set up video stream - logger.info("Starting video stream") - video_stream = robot.get_ros_video_stream() - - # Create FastAPI server with video stream and text streams - logger.info("Initializing FastAPI server") - streams = {"unitree_video": video_stream} - - # Create a subject for agent responses - agent_response_subject = rx.subject.Subject() - agent_response_stream = agent_response_subject.pipe(ops.share()) - - text_streams = { - "agent_responses": agent_response_stream, - } - - web_interface = FastAPIServer(port=5555, text_streams=text_streams, **streams) - - logger.info("Starting action primitive execution agent") - agent = OpenAIAgent( - dev_name="UnitreeQueryExecutionAgent", - input_query_stream=web_interface.query_stream, - output_dir=output_dir, - skills=robot.get_skills(), - ) - - # Subscribe to agent responses and send them to the subject - agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) - - # Start server (blocking call) - logger.info("Starting FastAPI server") - web_interface.run() - - except KeyboardInterrupt: - print("Stopping demo...") - except Exception as e: - logger.error(f"Error: {e}") - return 1 - finally: - if robot: - robot.cleanup() - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/test_unitree_ros_v0.0.4.py b/tests/test_unitree_ros_v0.0.4.py deleted file mode 100644 index efb39be2bf..0000000000 --- a/tests/test_unitree_ros_v0.0.4.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dotenv import load_dotenv -import reactivex as rx -import reactivex.operators as ops - -from dimos.agents.claude_agent import ClaudeAgent -from dimos.perception.detection2d.detic_2d_det import Detic2DDetector -from dimos.perception.object_detection_stream import ObjectDetectionStream -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.kill_skill import KillSkill -from dimos.skills.navigation import GetPose, NavigateWithText -from dimos.skills.observe_stream import ObserveStream -from dimos.skills.speak import Speak -from dimos.skills.visual_navigation_skills import FollowHuman -from dimos.stream.audio.pipelines import stt, tts -from dimos.utils.reactive import backpressure -from dimos.web.robot_web_interface import RobotWebInterface - -# Load API key from environment -load_dotenv() - -# Allow command line arguments to control spatial memory parameters -import argparse - - -def parse_arguments(): - parser = argparse.ArgumentParser( - description="Run the robot with optional spatial memory parameters" - ) - parser.add_argument( - "--voice", - action="store_true", - help="Use voice input from microphone instead of web interface", - ) - return parser.parse_args() - - -args = parse_arguments() - -# Initialize robot with spatial memory parameters -robot = UnitreeGo2( - ip=os.getenv("ROBOT_IP"), - skills=MyUnitreeSkills(), - mock_connection=False, - new_memory=True, -) - -# Create a subject for agent responses -agent_response_subject = rx.subject.Subject() -agent_response_stream = agent_response_subject.pipe(ops.share()) -local_planner_viz_stream = robot.local_planner_viz_stream.pipe(ops.share()) - -# Initialize object detection stream -min_confidence = 0.6 -class_filter = None # No class filtering -detector = Detic2DDetector(vocabulary=None, threshold=min_confidence) - -# Create video stream from robot's camera -video_stream = backpressure(robot.get_ros_video_stream()) - -# Initialize ObjectDetectionStream with robot -object_detector = ObjectDetectionStream( - camera_intrinsics=robot.camera_intrinsics, - min_confidence=min_confidence, - class_filter=class_filter, - transform_to_map=robot.ros_control.transform_pose, - detector=detector, - video_stream=video_stream, -) - -# Create visualization stream for web interface -viz_stream = backpressure(object_detector.get_stream()).pipe( - ops.share(), - ops.map(lambda x: x["viz_frame"] if x is not None else None), - ops.filter(lambda x: x is not None), -) - -# Get the formatted detection stream -formatted_detection_stream = object_detector.get_formatted_stream().pipe( - ops.filter(lambda x: x is not None) -) - - -# Create a direct mapping that combines detection data with locations -def combine_with_locations(object_detections): - # Get locations from spatial memory - try: - locations = robot.get_spatial_memory().get_robot_locations() - - # Format the locations section - locations_text = "\n\nSaved Robot Locations:\n" - if locations: - for loc in locations: - locations_text += f"- {loc.name}: Position ({loc.position[0]:.2f}, {loc.position[1]:.2f}, {loc.position[2]:.2f}), " - locations_text += f"Rotation ({loc.rotation[0]:.2f}, {loc.rotation[1]:.2f}, {loc.rotation[2]:.2f})\n" - else: - locations_text += "None\n" - - # Simply concatenate the strings - return object_detections + locations_text - except Exception as e: - print(f"Error adding locations: {e}") - return object_detections - - -# Create the combined stream with a simple pipe operation -enhanced_data_stream = formatted_detection_stream.pipe(ops.map(combine_with_locations), ops.share()) - -streams = { - "unitree_video": robot.get_ros_video_stream(), - "local_planner_viz": local_planner_viz_stream, - "object_detection": viz_stream, -} -text_streams = { - "agent_responses": agent_response_stream, -} - -web_interface = RobotWebInterface(port=5555, text_streams=text_streams, **streams) - -stt_node = stt() - -# Read system query from prompt.txt file -with open( - os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "agent", "prompt.txt") -) as f: - system_query = f.read() - -# Create a ClaudeAgent instance with either voice input or web interface input based on flag -input_stream = stt_node.emit_text() if args.voice else web_interface.query_stream -print(f"Using {'voice input' if args.voice else 'web interface input'} for queries") - -agent = ClaudeAgent( - dev_name="test_agent", - input_query_stream=input_stream, - input_data_stream=enhanced_data_stream, # Add the enhanced data stream - skills=robot.get_skills(), - system_query=system_query, - model_name="claude-3-7-sonnet-latest", - thinking_budget_tokens=0, -) - -# Initialize TTS node only if voice flag is set -tts_node = None -if args.voice: - print("Voice mode: Enabling TTS for speech output") - tts_node = tts() - tts_node.consume_text(agent.get_response_observable()) -else: - print("Web interface mode: Disabling TTS to avoid audio issues") - -robot_skills = robot.get_skills() -robot_skills.add(ObserveStream) -robot_skills.add(KillSkill) -robot_skills.add(NavigateWithText) -robot_skills.add(FollowHuman) -robot_skills.add(GetPose) -# Add Speak skill only if voice flag is set -if args.voice: - robot_skills.add(Speak) -# robot_skills.add(NavigateToGoal) -robot_skills.create_instance("ObserveStream", robot=robot, agent=agent) -robot_skills.create_instance("KillSkill", robot=robot, skill_library=robot_skills) -robot_skills.create_instance("NavigateWithText", robot=robot) -robot_skills.create_instance("FollowHuman", robot=robot) -robot_skills.create_instance("GetPose", robot=robot) -# robot_skills.create_instance("NavigateToGoal", robot=robot) -# Create Speak skill instance only if voice flag is set -if args.voice: - robot_skills.create_instance("Speak", tts_node=tts_node) - -# Subscribe to agent responses and send them to the subject -agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) - -print("ObserveStream and Kill skills registered and ready for use") -print("Created memory.txt file") - -web_interface.run() diff --git a/tests/test_webrtc_queue.py b/tests/test_webrtc_queue.py deleted file mode 100644 index 5e09ec1f9d..0000000000 --- a/tests/test_webrtc_queue.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import time - -from dimos.robot.unitree.unitree_go2 import UnitreeGo2, WebRTCConnectionMethod -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl - - -def main(): - """Test WebRTC request queue with a sequence of 20 back-to-back commands""" - - print("Initializing UnitreeGo2...") - - # Get configuration from environment variables - - robot_ip = os.getenv("ROBOT_IP") - connection_method = getattr(WebRTCConnectionMethod, os.getenv("CONNECTION_METHOD", "LocalSTA")) - - # Initialize ROS control - ros_control = UnitreeROSControl(node_name="unitree_go2_test", use_raw=True) - - # Initialize robot - robot = UnitreeGo2( - ip=robot_ip, - connection_method=connection_method, - ros_control=ros_control, - use_ros=True, - use_webrtc=False, # Using queue instead of direct WebRTC - ) - - # Wait for initialization - print("Waiting for robot to initialize...") - time.sleep(5) - - # First put the robot in a good starting state - print("Running recovery stand...") - robot.webrtc_req(api_id=1006) # RecoveryStand - - # Queue 20 WebRTC requests back-to-back - print("\nšŸ¤– QUEUEING 20 COMMANDS BACK-TO-BACK šŸ¤–\n") - - # Dance 1 - robot.webrtc_req(api_id=1022) # Dance1 - print("Queued: Dance1 (1022)") - - # Wiggle Hips - robot.webrtc_req(api_id=1033) # WiggleHips - print("Queued: WiggleHips (1033)") - - # Stretch - robot.webrtc_req(api_id=1017) # Stretch - print("Queued: Stretch (1017)") - - # Hello - robot.webrtc_req(api_id=1016) # Hello - print("Queued: Hello (1016)") - - # Dance 2 - robot.webrtc_req(api_id=1023) # Dance2 - print("Queued: Dance2 (1023)") - - # Wallow - robot.webrtc_req(api_id=1021) # Wallow - print("Queued: Wallow (1021)") - - # Scrape - robot.webrtc_req(api_id=1029) # Scrape - print("Queued: Scrape (1029)") - - # Finger Heart - robot.webrtc_req(api_id=1036) # FingerHeart - print("Queued: FingerHeart (1036)") - - # Recovery Stand (base position) - robot.webrtc_req(api_id=1006) # RecoveryStand - print("Queued: RecoveryStand (1006)") - - # Hello again - robot.webrtc_req(api_id=1016) # Hello - print("Queued: Hello (1016)") - - # Wiggle Hips again - robot.webrtc_req(api_id=1033) # WiggleHips - print("Queued: WiggleHips (1033)") - - # Front Pounce - robot.webrtc_req(api_id=1032) # FrontPounce - print("Queued: FrontPounce (1032)") - - # Dance 1 again - robot.webrtc_req(api_id=1022) # Dance1 - print("Queued: Dance1 (1022)") - - # Stretch again - robot.webrtc_req(api_id=1017) # Stretch - print("Queued: Stretch (1017)") - - # Front Jump - robot.webrtc_req(api_id=1031) # FrontJump - print("Queued: FrontJump (1031)") - - # Finger Heart again - robot.webrtc_req(api_id=1036) # FingerHeart - print("Queued: FingerHeart (1036)") - - # Scrape again - robot.webrtc_req(api_id=1029) # Scrape - print("Queued: Scrape (1029)") - - # Hello one more time - robot.webrtc_req(api_id=1016) # Hello - print("Queued: Hello (1016)") - - # Dance 2 again - robot.webrtc_req(api_id=1023) # Dance2 - print("Queued: Dance2 (1023)") - - # Finish with recovery stand - robot.webrtc_req(api_id=1006) # RecoveryStand - print("Queued: RecoveryStand (1006)") - - print("\nAll 20 commands queued successfully! Watch the robot perform them in sequence.") - print("The WebRTC queue manager will process them one by one when the robot is ready.") - print("Press Ctrl+C to stop the program when you've seen enough.\n") - - try: - # Keep the program running so the queue can be processed - while True: - time.sleep(1) - except KeyboardInterrupt: - print("\nStopping the test...") - finally: - # Cleanup - print("Cleaning up resources...") - robot.cleanup() - print("Test completed.") - - -if __name__ == "__main__": - main() diff --git a/tests/test_websocketvis.py b/tests/test_websocketvis.py deleted file mode 100644 index 262555ce50..0000000000 --- a/tests/test_websocketvis.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import math -import os -import pickle -import threading -import time - -from reactivex import operators as ops - -from dimos.robot.global_planner.planner import AstarPlanner -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl -from dimos.types.costmap import Costmap -from dimos.types.vector import Vector -from dimos.web.robot_web_interface import RobotWebInterface -from dimos.web.websocket_vis.helpers import vector_stream -from dimos.web.websocket_vis.server import WebsocketVis - - -def parse_args(): - parser = argparse.ArgumentParser(description="Simple test for vis.") - parser.add_argument( - "--live", - action="store_true", - ) - parser.add_argument( - "--port", type=int, default=5555, help="Port for web visualization interface" - ) - return parser.parse_args() - - -def setup_web_interface(robot, port=5555): - """Set up web interface with robot video and local planner visualization""" - print(f"Setting up web interface on port {port}") - - # Get video stream from robot - video_stream = robot.video_stream_ros.pipe( - ops.share(), - ops.map(lambda frame: frame), - ops.filter(lambda frame: frame is not None), - ) - - # Get local planner visualization stream - local_planner_stream = robot.local_planner_viz_stream.pipe( - ops.share(), - ops.map(lambda frame: frame), - ops.filter(lambda frame: frame is not None), - ) - - # Create web interface with streams - web_interface = RobotWebInterface( - port=port, robot_video=video_stream, local_planner=local_planner_stream - ) - - return web_interface - - -def main(): - args = parse_args() - - websocket_vis = WebsocketVis() - websocket_vis.start() - - web_interface = None - - if args.live: - ros_control = UnitreeROSControl(node_name="web_nav_test", mock_connection=False) - robot = UnitreeGo2(ros_control=ros_control, ip=os.getenv("ROBOT_IP")) - planner = robot.global_planner - - websocket_vis.connect( - vector_stream("robot", lambda: robot.ros_control.transform_euler_pos("base_link")) - ) - websocket_vis.connect( - robot.ros_control.topic("map", Costmap).pipe(ops.map(lambda x: ["costmap", x])) - ) - - # Also set up the web interface with both streams - if hasattr(robot, "video_stream_ros") and hasattr(robot, "local_planner_viz_stream"): - web_interface = setup_web_interface(robot, port=args.port) - - # Start web interface in a separate thread - viz_thread = threading.Thread(target=web_interface.run, daemon=True) - viz_thread.start() - print(f"Web interface available at http://localhost:{args.port}") - - else: - pickle_path = f"{__file__.rsplit('/', 1)[0]}/mockdata/vegas.pickle" - print(f"Loading costmap from {pickle_path}") - planner = AstarPlanner( - get_costmap=lambda: pickle.load(open(pickle_path, "rb")), - get_robot_pos=lambda: Vector(5.0, 5.0), - set_local_nav=lambda x: time.sleep(1) and True, - ) - - def msg_handler(msgtype, data): - if msgtype == "click": - target = Vector(data["position"]) - try: - planner.set_goal(target) - except Exception as e: - print(f"Error setting goal: {e}") - return - - def threaded_msg_handler(msgtype, data): - thread = threading.Thread(target=msg_handler, args=(msgtype, data)) - thread.daemon = True - thread.start() - - websocket_vis.connect(planner.vis_stream()) - websocket_vis.msg_handler = threaded_msg_handler - - print(f"WebSocket server started on port {websocket_vis.port}") - print(planner.get_costmap()) - - planner.plan(Vector(-4.8, -1.0)) # plan a path to the origin - - def fakepos(): - # Simulate a fake vector position change (to test realtime rendering) - vec = Vector(math.sin(time.time()) * 2, math.cos(time.time()) * 2, 0) - print(vec) - return vec - - # if not args.live: - # websocket_vis.connect(rx.interval(0.05).pipe(ops.map(lambda _: ["fakepos", fakepos()]))) - - try: - # Keep the server running - while True: - time.sleep(0.1) - pass - except KeyboardInterrupt: - print("Stopping WebSocket server...") - websocket_vis.stop() - print("WebSocket server stopped") - - -if __name__ == "__main__": - main() diff --git a/tests/test_zed_module.py b/tests/test_zed_module.py deleted file mode 100644 index 03a21ac65d..0000000000 --- a/tests/test_zed_module.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test script for ZED Module with LCM visualization.""" - -import asyncio -import threading -import time - -import cv2 -from dimos_lcm.geometry_msgs import PoseStamped - -# Import LCM message types -from dimos_lcm.sensor_msgs import CameraInfo, Image as LCMImage -import numpy as np - -from dimos import core -from dimos.hardware.zed_camera import ZEDModule -from dimos.perception.common.utils import colorize_depth -from dimos.protocol import pubsub -from dimos.protocol.pubsub.lcmpubsub import LCM, Topic -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("test_zed_module") - - -class ZEDVisualizationNode: - """Node that subscribes to ZED topics and visualizes the data.""" - - def __init__(self): - self.lcm = LCM() - self.latest_color = None - self.latest_depth = None - self.latest_pose = None - self.camera_info = None - self._running = False - - # Subscribe to topics - self.color_topic = Topic("/zed/color_image", LCMImage) - self.depth_topic = Topic("/zed/depth_image", LCMImage) - self.camera_info_topic = Topic("/zed/camera_info", CameraInfo) - self.pose_topic = Topic("/zed/pose", PoseStamped) - - def start(self): - """Start the visualization node.""" - self._running = True - self.lcm.start() - - # Subscribe to topics - self.lcm.subscribe(self.color_topic, self._on_color_image) - self.lcm.subscribe(self.depth_topic, self._on_depth_image) - self.lcm.subscribe(self.camera_info_topic, self._on_camera_info) - self.lcm.subscribe(self.pose_topic, self._on_pose) - - logger.info("Visualization node started, subscribed to ZED topics") - - def stop(self): - """Stop the visualization node.""" - self._running = False - cv2.destroyAllWindows() - - def _on_color_image(self, msg: LCMImage, topic: str): - """Handle color image messages.""" - try: - # Convert LCM message to numpy array - data = np.frombuffer(msg.data, dtype=np.uint8) - - if msg.encoding == "rgb8": - image = data.reshape((msg.height, msg.width, 3)) - # Convert RGB to BGR for OpenCV - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - elif msg.encoding == "mono8": - image = data.reshape((msg.height, msg.width)) - else: - logger.warning(f"Unsupported encoding: {msg.encoding}") - return - - self.latest_color = image - logger.debug(f"Received color image: {msg.width}x{msg.height}") - - except Exception as e: - logger.error(f"Error processing color image: {e}") - - def _on_depth_image(self, msg: LCMImage, topic: str): - """Handle depth image messages.""" - try: - # Convert LCM message to numpy array - if msg.encoding == "32FC1": - data = np.frombuffer(msg.data, dtype=np.float32) - depth = data.reshape((msg.height, msg.width)) - else: - logger.warning(f"Unsupported depth encoding: {msg.encoding}") - return - - self.latest_depth = depth - logger.debug(f"Received depth image: {msg.width}x{msg.height}") - - except Exception as e: - logger.error(f"Error processing depth image: {e}") - - def _on_camera_info(self, msg: CameraInfo, topic: str): - """Handle camera info messages.""" - self.camera_info = msg - logger.info( - f"Received camera info: {msg.width}x{msg.height}, distortion model: {msg.distortion_model}" - ) - - def _on_pose(self, msg: PoseStamped, topic: str): - """Handle pose messages.""" - self.latest_pose = msg - pos = msg.pose.position - ori = msg.pose.orientation - logger.debug( - f"Pose: pos=({pos.x:.2f}, {pos.y:.2f}, {pos.z:.2f}), " - + f"ori=({ori.x:.2f}, {ori.y:.2f}, {ori.z:.2f}, {ori.w:.2f})" - ) - - def visualize(self): - """Run visualization loop.""" - while self._running: - # Create visualization - vis_images = [] - - # Color image - if self.latest_color is not None: - color_vis = self.latest_color.copy() - - # Add pose text if available - if self.latest_pose is not None: - pos = self.latest_pose.pose.position - text = f"Pose: ({pos.x:.2f}, {pos.y:.2f}, {pos.z:.2f})" - cv2.putText( - color_vis, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2 - ) - - vis_images.append(("ZED Color", color_vis)) - - # Depth image - if self.latest_depth is not None: - depth_colorized = colorize_depth(self.latest_depth, max_depth=5.0) - if depth_colorized is not None: - # Convert RGB to BGR for OpenCV - depth_colorized = cv2.cvtColor(depth_colorized, cv2.COLOR_RGB2BGR) - - # Add depth stats - valid_mask = np.isfinite(self.latest_depth) & (self.latest_depth > 0) - if np.any(valid_mask): - min_depth = np.min(self.latest_depth[valid_mask]) - max_depth = np.max(self.latest_depth[valid_mask]) - mean_depth = np.mean(self.latest_depth[valid_mask]) - - text = f"Depth: min={min_depth:.2f}m, max={max_depth:.2f}m, mean={mean_depth:.2f}m" - cv2.putText( - depth_colorized, - text, - (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (255, 255, 255), - 1, - ) - - vis_images.append(("ZED Depth", depth_colorized)) - - # Show windows - for name, image in vis_images: - cv2.imshow(name, image) - - # Handle key press - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - logger.info("Quit requested") - self._running = False - break - elif key == ord("s"): - # Save images - if self.latest_color is not None: - cv2.imwrite("zed_color.png", self.latest_color) - logger.info("Saved color image to zed_color.png") - if self.latest_depth is not None: - np.save("zed_depth.npy", self.latest_depth) - logger.info("Saved depth data to zed_depth.npy") - - time.sleep(0.03) # ~30 FPS - - -async def test_zed_module(): - """Test the ZED Module with visualization.""" - logger.info("Starting ZED Module test") - - # Start Dask - dimos = core.start(1) - - # Enable LCM auto-configuration - pubsub.lcm.autoconf() - - try: - # Deploy ZED module - logger.info("Deploying ZED module...") - zed = dimos.deploy( - ZEDModule, - camera_id=0, - resolution="HD720", - depth_mode="NEURAL", - fps=30, - enable_tracking=True, - publish_rate=10.0, # 10 Hz for testing - frame_id="zed_camera", - ) - - # Configure LCM transports - zed.color_image.transport = core.LCMTransport("/zed/color_image", LCMImage) - zed.depth_image.transport = core.LCMTransport("/zed/depth_image", LCMImage) - zed.camera_info.transport = core.LCMTransport("/zed/camera_info", CameraInfo) - zed.pose.transport = core.LCMTransport("/zed/pose", PoseStamped) - - # Print module info - logger.info("ZED Module configured:") - - # Start ZED module - logger.info("Starting ZED module...") - zed.start() - - # Give module time to initialize - await asyncio.sleep(2) - - # Create and start visualization node - viz_node = ZEDVisualizationNode() - viz_node.start() - - # Run visualization in separate thread - viz_thread = threading.Thread(target=viz_node.visualize, daemon=True) - viz_thread.start() - - logger.info("ZED Module running. Press 'q' in image window to quit, 's' to save images.") - - # Keep running until visualization stops - while viz_node._running: - await asyncio.sleep(0.1) - - # Stop ZED module - logger.info("Stopping ZED module...") - zed.stop() - - # Stop visualization - viz_node.stop() - - except Exception as e: - logger.error(f"Error in test: {e}") - import traceback - - traceback.print_exc() - - finally: - # Clean up - dimos.close() - logger.info("Test completed") - - -if __name__ == "__main__": - # Run the test - asyncio.run(test_zed_module()) diff --git a/tests/test_zed_setup.py b/tests/test_zed_setup.py deleted file mode 100755 index 33aefb65eb..0000000000 --- a/tests/test_zed_setup.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Simple test script to verify ZED camera setup and basic functionality. -""" - -from pathlib import Path -import sys - - -def test_imports(): - """Test that all required modules can be imported.""" - print("Testing imports...") - - try: - import numpy as np - - print("āœ“ NumPy imported successfully") - except ImportError as e: - print(f"āœ— NumPy import failed: {e}") - return False - - try: - import cv2 - - print("āœ“ OpenCV imported successfully") - except ImportError as e: - print(f"āœ— OpenCV import failed: {e}") - return False - - try: - from PIL import Image, ImageDraw, ImageFont - - print("āœ“ PIL imported successfully") - except ImportError as e: - print(f"āœ— PIL import failed: {e}") - return False - - try: - import pyzed.sl as sl - - print("āœ“ ZED SDK (pyzed) imported successfully") - # Note: SDK version method varies between versions - except ImportError as e: - print(f"āœ— ZED SDK import failed: {e}") - print(" Please install ZED SDK and pyzed package") - return False - - try: - from dimos.hardware.zed_camera import ZEDCamera - - print("āœ“ ZEDCamera class imported successfully") - except ImportError as e: - print(f"āœ— ZEDCamera import failed: {e}") - return False - - try: - from dimos.perception.zed_visualizer import ZEDVisualizer - - print("āœ“ ZEDVisualizer class imported successfully") - except ImportError as e: - print(f"āœ— ZEDVisualizer import failed: {e}") - return False - - return True - - -def test_camera_detection(): - """Test if ZED cameras are detected.""" - print("\nTesting camera detection...") - - try: - import pyzed.sl as sl - - # List available cameras - cameras = sl.Camera.get_device_list() - print(f"Found {len(cameras)} ZED camera(s):") - - for i, camera_info in enumerate(cameras): - print(f" Camera {i}:") - print(f" Model: {camera_info.camera_model}") - print(f" Serial: {camera_info.serial_number}") - print(f" State: {camera_info.camera_state}") - - return len(cameras) > 0 - - except Exception as e: - print(f"Error detecting cameras: {e}") - return False - - -def test_basic_functionality(): - """Test basic ZED camera functionality without actually opening the camera.""" - print("\nTesting basic functionality...") - - try: - import pyzed.sl as sl - - from dimos.hardware.zed_camera import ZEDCamera - from dimos.perception.zed_visualizer import ZEDVisualizer - - # Test camera initialization (without opening) - ZEDCamera( - camera_id=0, - resolution=sl.RESOLUTION.HD720, - depth_mode=sl.DEPTH_MODE.NEURAL, - ) - print("āœ“ ZEDCamera instance created successfully") - - # Test visualizer initialization - visualizer = ZEDVisualizer(max_depth=10.0) - print("āœ“ ZEDVisualizer instance created successfully") - - # Test creating a dummy visualization - dummy_rgb = np.zeros((480, 640, 3), dtype=np.uint8) - dummy_depth = np.ones((480, 640), dtype=np.float32) * 2.0 - - visualizer.create_side_by_side_image(dummy_rgb, dummy_depth) - print("āœ“ Dummy visualization created successfully") - - return True - - except Exception as e: - print(f"āœ— Basic functionality test failed: {e}") - return False - - -def main(): - """Run all tests.""" - print("ZED Camera Setup Test") - print("=" * 50) - - # Test imports - if not test_imports(): - print("\nāŒ Import tests failed. Please install missing dependencies.") - return False - - # Test camera detection - cameras_found = test_camera_detection() - if not cameras_found: - print( - "\nāš ļø No ZED cameras detected. Please connect a ZED camera to test capture functionality." - ) - - # Test basic functionality - if not test_basic_functionality(): - print("\nāŒ Basic functionality tests failed.") - return False - - print("\n" + "=" * 50) - if cameras_found: - print("āœ… All tests passed! You can now run the ZED demo:") - print(" python examples/zed_neural_depth_demo.py --display-time 10") - else: - print("āœ… Setup is ready, but no camera detected.") - print(" Connect a ZED camera and run:") - print(" python examples/zed_neural_depth_demo.py --display-time 10") - - return True - - -if __name__ == "__main__": - # Add the project root to Python path - sys.path.append(str(Path(__file__).parent)) - - # Import numpy after path setup - import numpy as np - - success = main() - sys.exit(0 if success else 1) diff --git a/tests/visualization_script.py b/tests/visualization_script.py deleted file mode 100644 index a42b4bf06c..0000000000 --- a/tests/visualization_script.py +++ /dev/null @@ -1,1006 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Visualize pickled manipulation pipeline results.""" - -import os -import pickle -import sys - -import matplotlib -import numpy as np - -# Try to use TkAgg backend for live display, fallback to Agg if not available -try: - matplotlib.use("TkAgg") -except: - try: - matplotlib.use("Qt5Agg") - except: - matplotlib.use("Agg") # Fallback to non-interactive -import matplotlib.pyplot as plt -import open3d as o3d - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import atexit -from contextlib import contextmanager -from datetime import datetime -import time - -import lcm_msgs -from pydrake.all import ( - AddMultibodyPlantSceneGraph, - DiagramBuilder, - JointIndex, - MeshcatVisualizer, - MeshcatVisualizerParams, - Parser, - RigidTransform, - RollPitchYaw, - RotationMatrix, - StartMeshcat, -) -from pydrake.common import MemoryFile -from pydrake.geometry import ( - Box, - CollisionFilterDeclaration, - InMemoryMesh, - Mesh, - ProximityProperties, -) -from pydrake.math import RigidTransform as DrakeRigidTransform -import tf_lcm_py -import trimesh - -from dimos.perception.pointcloud.utils import ( - visualize_clustered_point_clouds, - visualize_pcd, - visualize_voxel_grid, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("visualization_script") - - -def create_point_cloud(color_img, depth_img, intrinsics): - """Create Open3D point cloud from RGB and depth images.""" - fx, fy, cx, cy = intrinsics - height, width = depth_img.shape - - o3d_intrinsics = o3d.camera.PinholeCameraIntrinsic(width, height, fx, fy, cx, cy) - color_o3d = o3d.geometry.Image(color_img) - depth_o3d = o3d.geometry.Image((depth_img * 1000).astype(np.uint16)) - - rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth( - color_o3d, depth_o3d, depth_scale=1000.0, convert_rgb_to_intensity=False - ) - - return o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, o3d_intrinsics) - - -def deserialize_point_cloud(data): - """Reconstruct Open3D PointCloud from serialized data.""" - if data is None: - return None - - pcd = o3d.geometry.PointCloud() - if data.get("points"): - pcd.points = o3d.utility.Vector3dVector(np.array(data["points"])) - if data.get("colors"): - pcd.colors = o3d.utility.Vector3dVector(np.array(data["colors"])) - return pcd - - -def deserialize_voxel_grid(data): - """Reconstruct Open3D VoxelGrid from serialized data.""" - if data is None: - return None - - # Create a point cloud to convert to voxel grid - pcd = o3d.geometry.PointCloud() - voxel_size = data["voxel_size"] - origin = np.array(data["origin"]) - - # Create points from voxel indices - points = [] - colors = [] - for voxel in data["voxels"]: - # Each voxel is (i, j, k, r, g, b) - i, j, k, r, g, b = voxel - # Convert voxel grid index to 3D point - point = origin + np.array([i, j, k]) * voxel_size - points.append(point) - colors.append([r, g, b]) - - if points: - pcd.points = o3d.utility.Vector3dVector(np.array(points)) - pcd.colors = o3d.utility.Vector3dVector(np.array(colors)) - - # Convert to voxel grid - voxel_grid = o3d.geometry.VoxelGrid.create_from_point_cloud(pcd, voxel_size) - return voxel_grid - - -def visualize_results(pickle_path="manipulation_results.pkl"): - """Load pickled results and visualize them.""" - print(f"Loading results from {pickle_path}...") - try: - with open(pickle_path, "rb") as f: - data = pickle.load(f) - - results = data["results"] - data["color_img"] - data["depth_img"] - data["intrinsics"] - - print(f"Loaded results with keys: {list(results.keys())}") - - except FileNotFoundError: - print(f"Error: Pickle file {pickle_path} not found.") - print("Make sure to run test_manipulation_pipeline_single_frame_lcm.py first.") - return - except Exception as e: - print(f"Error loading pickle file: {e}") - return - - # Determine number of subplots based on what results we have - num_plots = 0 - plot_configs = [] - - if "detection_viz" in results and results["detection_viz"] is not None: - plot_configs.append(("detection_viz", "Object Detection")) - num_plots += 1 - - if "segmentation_viz" in results and results["segmentation_viz"] is not None: - plot_configs.append(("segmentation_viz", "Semantic Segmentation")) - num_plots += 1 - - if "pointcloud_viz" in results and results["pointcloud_viz"] is not None: - plot_configs.append(("pointcloud_viz", "All Objects Point Cloud")) - num_plots += 1 - - if "detected_pointcloud_viz" in results and results["detected_pointcloud_viz"] is not None: - plot_configs.append(("detected_pointcloud_viz", "Detection Objects Point Cloud")) - num_plots += 1 - - if "misc_pointcloud_viz" in results and results["misc_pointcloud_viz"] is not None: - plot_configs.append(("misc_pointcloud_viz", "Misc/Background Points")) - num_plots += 1 - - if "grasp_overlay" in results and results["grasp_overlay"] is not None: - plot_configs.append(("grasp_overlay", "Grasp Overlay")) - num_plots += 1 - - if num_plots == 0: - print("No visualization results to display") - return - - # Create subplot layout - if num_plots <= 3: - fig, axes = plt.subplots(1, num_plots, figsize=(6 * num_plots, 5)) - else: - rows = 2 - cols = (num_plots + 1) // 2 - _fig, axes = plt.subplots(rows, cols, figsize=(6 * cols, 5 * rows)) - - # Ensure axes is always a list for consistent indexing - if num_plots == 1: - axes = [axes] - elif num_plots > 2: - axes = axes.flatten() - - # Plot each result - for i, (key, title) in enumerate(plot_configs): - axes[i].imshow(results[key]) - axes[i].set_title(title) - axes[i].axis("off") - - # Hide unused subplots if any - if num_plots > 3: - for i in range(num_plots, len(axes)): - axes[i].axis("off") - - plt.tight_layout() - - # Save and show the plot - output_path = "visualization_results.png" - plt.savefig(output_path, dpi=150, bbox_inches="tight") - print(f"Results visualization saved to: {output_path}") - - # Show plot live as well - plt.show(block=True) - plt.close() - - # Deserialize and reconstruct 3D objects from the pickle file - print("\nReconstructing 3D visualization objects from serialized data...") - - # Reconstruct full point cloud if available - full_pcd = None - if "full_pointcloud" in results and results["full_pointcloud"] is not None: - full_pcd = deserialize_point_cloud(results["full_pointcloud"]) - print(f"Reconstructed full point cloud with {len(np.asarray(full_pcd.points))} points") - - # Visualize reconstructed full point cloud - try: - visualize_pcd( - full_pcd, - window_name="Reconstructed Full Scene Point Cloud", - point_size=2.0, - show_coordinate_frame=True, - ) - except (KeyboardInterrupt, EOFError): - print("\nSkipping full point cloud visualization") - except Exception as e: - print(f"Error in point cloud visualization: {e}") - else: - print("No full point cloud available for visualization") - - # Reconstruct misc clusters if available - if results.get("misc_clusters"): - misc_clusters = [deserialize_point_cloud(cluster) for cluster in results["misc_clusters"]] - cluster_count = len(misc_clusters) - total_misc_points = sum(len(np.asarray(cluster.points)) for cluster in misc_clusters) - print(f"Reconstructed {cluster_count} misc clusters with {total_misc_points} total points") - - # Visualize reconstructed misc clusters - try: - visualize_clustered_point_clouds( - misc_clusters, - window_name="Reconstructed Misc/Background Clusters (DBSCAN)", - point_size=3.0, - show_coordinate_frame=True, - ) - except (KeyboardInterrupt, EOFError): - print("\nSkipping misc clusters visualization") - except Exception as e: - print(f"Error in misc clusters visualization: {e}") - else: - print("No misc clusters available for visualization") - - # Reconstruct voxel grid if available - if "misc_voxel_grid" in results and results["misc_voxel_grid"] is not None: - misc_voxel_grid = deserialize_voxel_grid(results["misc_voxel_grid"]) - if misc_voxel_grid: - voxel_count = len(misc_voxel_grid.get_voxels()) - print(f"Reconstructed voxel grid with {voxel_count} voxels") - - # Visualize reconstructed voxel grid - try: - visualize_voxel_grid( - misc_voxel_grid, - window_name="Reconstructed Misc/Background Voxel Grid", - show_coordinate_frame=True, - ) - except (KeyboardInterrupt, EOFError): - print("\nSkipping voxel grid visualization") - except Exception as e: - print(f"Error in voxel grid visualization: {e}") - else: - print("Failed to reconstruct voxel grid") - else: - print("No voxel grid available for visualization") - - -class DrakeKinematicsEnv: - def __init__( - self, - urdf_path: str, - kinematic_chain_joints: list[str], - links_to_ignore: list[str] | None = None, - ): - self._resources_to_cleanup = [] - - # Register cleanup at exit - atexit.register(self.cleanup_resources) - - # Initialize tf resources once and reuse them - self.buffer = tf_lcm_py.Buffer(30.0) - self._resources_to_cleanup.append(self.buffer) - with self.safe_lcm_instance() as lcm_instance: - self.tf_lcm_instance = lcm_instance - self._resources_to_cleanup.append(self.tf_lcm_instance) - # Create TransformListener with our LCM instance and buffer - self.listener = tf_lcm_py.TransformListener(self.tf_lcm_instance, self.buffer) - self._resources_to_cleanup.append(self.listener) - - # Check if URDF file exists - if not os.path.exists(urdf_path): - raise FileNotFoundError(f"URDF file not found: {urdf_path}") - - # Drake utils initialization - self.meshcat = StartMeshcat() - print(f"Meshcat started at: {self.meshcat.web_url()}") - - self.urdf_path = urdf_path - self.builder = DiagramBuilder() - - self.plant, self.scene_graph = AddMultibodyPlantSceneGraph(self.builder, time_step=0.01) - self.parser = Parser(self.plant) - - # Load the robot URDF - print(f"Loading URDF from: {self.urdf_path}") - self.model_instances = self.parser.AddModelsFromUrl(f"file://{self.urdf_path}") - self.kinematic_chain_joints = kinematic_chain_joints - self.model_instance = self.model_instances[0] if self.model_instances else None - - if not self.model_instances: - raise RuntimeError("Failed to load any model instances from URDF") - - print(f"Loaded {len(self.model_instances)} model instances") - - # Set up collision filtering - if links_to_ignore: - bodies = [] - for link_name in links_to_ignore: - try: - body = self.plant.GetBodyByName(link_name) - if body is not None: - bodies.extend(self.plant.GetBodiesWeldedTo(body)) - except RuntimeError: - print(f"Warning: Link '{link_name}' not found in URDF") - - if bodies: - arm_geoms = self.plant.CollectRegisteredGeometries(bodies) - decl = CollisionFilterDeclaration().ExcludeWithin(arm_geoms) - manager = self.scene_graph.collision_filter_manager() - manager.Apply(decl) - - # Load and process point cloud data - self._load_and_process_point_clouds() - - # Finalize the plant before adding visualizer - self.plant.Finalize() - - # Print some debug info about the plant - print(f"Plant has {self.plant.num_bodies()} bodies") - print(f"Plant has {self.plant.num_joints()} joints") - for i in range(self.plant.num_joints()): - joint = self.plant.get_joint(JointIndex(i)) - print(f" Joint {i}: {joint.name()} (type: {joint.type_name()})") - - # Add visualizer - self.visualizer = MeshcatVisualizer.AddToBuilder( - self.builder, self.scene_graph, self.meshcat, params=MeshcatVisualizerParams() - ) - - # Build the diagram - self.diagram = self.builder.Build() - self.diagram_context = self.diagram.CreateDefaultContext() - self.plant_context = self.plant.GetMyContextFromRoot(self.diagram_context) - - # Set up joint indices - self.joint_indices = [] - for joint_name in self.kinematic_chain_joints: - try: - joint = self.plant.GetJointByName(joint_name) - if joint.num_positions() > 0: - start_index = joint.position_start() - for i in range(joint.num_positions()): - self.joint_indices.append(start_index + i) - print( - f"Added joint '{joint_name}' at indices {start_index} to {start_index + joint.num_positions() - 1}" - ) - except RuntimeError: - print(f"Warning: Joint '{joint_name}' not found in URDF.") - - # Get important frames/bodies - try: - self.end_effector_link = self.plant.GetBodyByName("link6") - self.end_effector_frame = self.plant.GetFrameByName("link6") - print("Found end effector link6") - except RuntimeError: - print("Warning: link6 not found") - self.end_effector_link = None - self.end_effector_frame = None - - try: - self.camera_link = self.plant.GetBodyByName("camera_center_link") - print("Found camera_center_link") - except RuntimeError: - print("Warning: camera_center_link not found") - self.camera_link = None - - # Set robot to a reasonable initial configuration - self._set_initial_configuration() - - # Force initial visualization update - self._update_visualization() - - print("Drake environment initialization complete!") - print(f"Visit {self.meshcat.web_url()} to see the visualization") - - def _load_and_process_point_clouds(self): - """Load point cloud data from pickle file and add to scene""" - pickle_path = "manipulation_results.pkl" - try: - with open(pickle_path, "rb") as f: - data = pickle.load(f) - - results = data["results"] - print(f"Loaded results with keys: {list(results.keys())}") - - except FileNotFoundError: - print(f"Warning: Pickle file {pickle_path} not found.") - print("Skipping point cloud loading.") - return - except Exception as e: - print(f"Warning: Error loading pickle file: {e}") - return - - full_detected_pcd = o3d.geometry.PointCloud() - for obj in results["detected_objects"]: - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(obj["point_cloud_numpy"]) - full_detected_pcd += pcd - - self.process_and_add_object_class("all_objects", results) - self.process_and_add_object_class("misc_clusters", results) - misc_clusters = results["misc_clusters"] - print(type(misc_clusters[0]["points"])) - print(np.asarray(misc_clusters[0]["points"]).shape) - - def process_and_add_object_class(self, object_key: str, results: dict): - # Process detected objects - if object_key in results: - detected_objects = results[object_key] - if detected_objects: - print(f"Processing {len(detected_objects)} {object_key}") - all_decomposed_meshes = [] - - transform = self.get_transform("world", "camera_center_link") - for i in range(len(detected_objects)): - try: - if object_key == "misc_clusters": - points = np.asarray(detected_objects[i]["points"]) - elif "point_cloud_numpy" in detected_objects[i]: - points = detected_objects[i]["point_cloud_numpy"] - elif ( - "point_cloud" in detected_objects[i] - and detected_objects[i]["point_cloud"] - ): - # Handle serialized point cloud - points = np.array(detected_objects[i]["point_cloud"]["points"]) - else: - print(f"Warning: No point cloud data found for object {i}") - continue - - if len(points) < 10: # Need more points for mesh reconstruction - print( - f"Warning: Object {i} has too few points ({len(points)}) for mesh reconstruction" - ) - continue - - # Swap y-z axes since this is a common problem - points = np.column_stack((points[:, 0], points[:, 2], -points[:, 1])) - # Transform points to world frame - points = self.transform_point_cloud_with_open3d(points, transform) - - # Use fast DBSCAN clustering + convex hulls approach - clustered_hulls = self._create_clustered_convex_hulls(points, i) - all_decomposed_meshes.extend(clustered_hulls) - - print( - f"Created {len(clustered_hulls)} clustered convex hulls for object {i}" - ) - - except Exception as e: - print(f"Warning: Failed to process object {i}: {e}") - - if all_decomposed_meshes: - self.register_convex_hulls_as_collision(all_decomposed_meshes, object_key) - print(f"Registered {len(all_decomposed_meshes)} total clustered convex hulls") - else: - print("Warning: No valid clustered convex hulls created from detected objects") - else: - print("No detected objects found") - - def _create_clustered_convex_hulls( - self, points: np.ndarray, object_id: int - ) -> list[o3d.geometry.TriangleMesh]: - """ - Create convex hulls from DBSCAN clusters of point cloud data. - Fast approach: cluster points, then convex hull each cluster. - - Args: - points: Nx3 numpy array of 3D points - object_id: ID for debugging/logging - - Returns: - List of Open3D triangle meshes (convex hulls of clusters) - """ - try: - # Create Open3D point cloud - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(points) - - # Quick outlier removal (optional, can skip for speed) - if len(points) > 50: # Only for larger point clouds - pcd, _ = pcd.remove_statistical_outlier(nb_neighbors=10, std_ratio=2.0) - points = np.asarray(pcd.points) - - if len(points) < 4: - print(f"Warning: Too few points after filtering for object {object_id}") - return [] - - # Try multiple DBSCAN parameter combinations to find clusters - clusters = [] - labels = None - - # Calculate some basic statistics for parameter estimation - if len(points) > 10: - # Compute nearest neighbor distances for better eps estimation - distances = pcd.compute_nearest_neighbor_distance() - avg_nn_distance = np.mean(distances) - np.std(distances) - - print( - f"Object {object_id}: {len(points)} points, avg_nn_dist={avg_nn_distance:.4f}" - ) - - for i in range(20): - try: - eps = avg_nn_distance * (2.0 + (i * 0.1)) - min_samples = 20 - labels = np.array(pcd.cluster_dbscan(eps=eps, min_points=min_samples)) - unique_labels = np.unique(labels) - clusters = unique_labels[unique_labels >= 0] # Remove noise label (-1) - - noise_points = np.sum(labels == -1) - clustered_points = len(points) - noise_points - - print( - f" Try {i + 1}: eps={eps:.4f}, min_samples={min_samples} → {len(clusters)} clusters, {clustered_points}/{len(points)} points clustered" - ) - - # Accept if we found clusters and most points are clustered - if ( - len(clusters) > 0 and clustered_points >= len(points) * 0.95 - ): # At least 30% of points clustered - print(f" āœ“ Accepted parameter set {i + 1}") - break - - except Exception as e: - print( - f" Try {i + 1}: Failed with eps={eps:.4f}, min_samples={min_samples}: {e}" - ) - continue - - if len(clusters) == 0 or labels is None: - print( - f"No clusters found for object {object_id} after all attempts, using entire point cloud" - ) - # Fallback: use entire point cloud as single convex hull - hull_mesh, _ = pcd.compute_convex_hull() - hull_mesh.compute_vertex_normals() - return [hull_mesh] - - print( - f"Found {len(clusters)} clusters for object {object_id} (eps={eps:.3f}, min_samples={min_samples})" - ) - - # Create convex hull for each cluster - convex_hulls = [] - for cluster_id in clusters: - try: - # Get points for this cluster - cluster_mask = labels == cluster_id - cluster_points = points[cluster_mask] - - if len(cluster_points) < 4: - print( - f"Skipping cluster {cluster_id} with only {len(cluster_points)} points" - ) - continue - - # Create point cloud for this cluster - cluster_pcd = o3d.geometry.PointCloud() - cluster_pcd.points = o3d.utility.Vector3dVector(cluster_points) - - # Compute convex hull - hull_mesh, _ = cluster_pcd.compute_convex_hull() - hull_mesh.compute_vertex_normals() - - # Validate hull - if ( - len(np.asarray(hull_mesh.vertices)) >= 4 - and len(np.asarray(hull_mesh.triangles)) >= 4 - ): - convex_hulls.append(hull_mesh) - print( - f" Cluster {cluster_id}: {len(cluster_points)} points → convex hull with {len(np.asarray(hull_mesh.vertices))} vertices" - ) - else: - print(f" Skipping degenerate hull for cluster {cluster_id}") - - except Exception as e: - print(f"Error processing cluster {cluster_id} for object {object_id}: {e}") - - if not convex_hulls: - print( - f"No valid convex hulls created for object {object_id}, using entire point cloud" - ) - # Fallback: use entire point cloud as single convex hull - hull_mesh, _ = pcd.compute_convex_hull() - hull_mesh.compute_vertex_normals() - return [hull_mesh] - - return convex_hulls - - except Exception as e: - print(f"Error in DBSCAN clustering for object {object_id}: {e}") - # Final fallback: single convex hull - try: - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(points) - hull_mesh, _ = pcd.compute_convex_hull() - hull_mesh.compute_vertex_normals() - return [hull_mesh] - except: - return [] - - def _set_initial_configuration(self): - """Set the robot to a reasonable initial joint configuration""" - # Set all joints to zero initially - if self.joint_indices: - q = np.zeros(len(self.joint_indices)) - - # You can customize these values for a better initial pose - # For example, if you know good default joint angles: - if len(q) >= 6: # Assuming at least 6 DOF arm - q[1] = 0.0 # joint1 - q[2] = 0.0 # joint2 - q[3] = 0.0 # joint3 - q[4] = 0.0 # joint4 - q[5] = 0.0 # joint5 - q[6] = 0.0 # joint6 - - # Set the joint positions in the plant context - positions = self.plant.GetPositions(self.plant_context) - for i, joint_idx in enumerate(self.joint_indices): - if joint_idx < len(positions): - positions[joint_idx] = q[i] - - self.plant.SetPositions(self.plant_context, positions) - print(f"Set initial joint configuration: {q}") - else: - print("Warning: No joint indices found, using default configuration") - - def _update_visualization(self): - """Force update the visualization""" - try: - # Get the visualizer's context from the diagram context - visualizer_context = self.visualizer.GetMyContextFromRoot(self.diagram_context) - self.visualizer.ForcedPublish(visualizer_context) - print("Visualization updated successfully") - except Exception as e: - print(f"Error updating visualization: {e}") - - def set_joint_positions(self, joint_positions): - """Set specific joint positions and update visualization""" - if len(joint_positions) != len(self.joint_indices): - raise ValueError( - f"Expected {len(self.joint_indices)} joint positions, got {len(joint_positions)}" - ) - - positions = self.plant.GetPositions(self.plant_context) - for i, joint_idx in enumerate(self.joint_indices): - if joint_idx < len(positions): - positions[joint_idx] = joint_positions[i] - - self.plant.SetPositions(self.plant_context, positions) - self._update_visualization() - print(f"Updated joint positions: {joint_positions}") - - def register_convex_hulls_as_collision( - self, meshes: list[o3d.geometry.TriangleMesh], hull_type: str - ): - """Register convex hulls as collision and visual geometry""" - if not meshes: - print("No meshes to register") - return - - world = self.plant.world_body() - proximity = ProximityProperties() - - for i, mesh in enumerate(meshes): - try: - # Convert Open3D → numpy arrays → trimesh.Trimesh - vertices = np.asarray(mesh.vertices) - faces = np.asarray(mesh.triangles) - - if len(vertices) == 0 or len(faces) == 0: - print(f"Warning: Mesh {i} is empty, skipping") - continue - - tmesh = trimesh.Trimesh(vertices=vertices, faces=faces) - - # Export to OBJ in memory - tmesh_obj_blob = tmesh.export(file_type="obj") - mem_file = MemoryFile( - contents=tmesh_obj_blob, extension=".obj", filename_hint=f"convex_hull_{i}.obj" - ) - in_memory_mesh = InMemoryMesh() - in_memory_mesh.mesh_file = mem_file - drake_mesh = Mesh(in_memory_mesh, scale=1.0) - - pos = np.array([0.0, 0.0, 0.0]) - rpy = RollPitchYaw(0.0, 0.0, 0.0) - X_WG = DrakeRigidTransform(RotationMatrix(rpy), pos) - - # Register collision and visual geometry - self.plant.RegisterCollisionGeometry( - body=world, - X_BG=X_WG, - shape=drake_mesh, - name=f"convex_hull_collision_{i}_{hull_type}", - properties=proximity, - ) - self.plant.RegisterVisualGeometry( - body=world, - X_BG=X_WG, - shape=drake_mesh, - name=f"convex_hull_visual_{i}_{hull_type}", - diffuse_color=np.array([0.7, 0.5, 0.3, 0.8]), # Orange-ish color - ) - - print( - f"Registered convex hull {i} with {len(vertices)} vertices and {len(faces)} faces" - ) - - except Exception as e: - print(f"Warning: Failed to register mesh {i}: {e}") - - # Add a simple table for reference - try: - table_shape = Box(1.0, 1.0, 0.1) # Thinner table - table_pose = RigidTransform(p=[0.5, 0.0, -0.05]) # In front of robot - self.plant.RegisterCollisionGeometry( - world, table_pose, table_shape, "table_collision", proximity - ) - self.plant.RegisterVisualGeometry( - world, table_pose, table_shape, "table_visual", [0.8, 0.6, 0.4, 1.0] - ) - print("Added reference table") - except Exception as e: - print(f"Warning: Failed to add table: {e}") - - def get_seeded_random_rgba(self, id: int): - np.random.seed(id) - return np.random.rand(4) - - @contextmanager - def safe_lcm_instance(self): - """Context manager for safely managing LCM instance lifecycle""" - lcm_instance = tf_lcm_py.LCM() - try: - yield lcm_instance - finally: - pass - - def cleanup_resources(self): - """Clean up resources before exiting""" - # Only clean up once when exiting - print("Cleaning up resources...") - # Force cleanup of resources in reverse order (last created first) - for resource in reversed(self._resources_to_cleanup): - try: - # For objects like TransformListener that might have a close or shutdown method - if hasattr(resource, "close"): - resource.close() - elif hasattr(resource, "shutdown"): - resource.shutdown() - - # Explicitly delete the resource - del resource - except Exception as e: - print(f"Error during cleanup: {e}") - - # Clear the resources list - self._resources_to_cleanup = [] - - def get_transform(self, target_frame, source_frame): - print("Getting transform from", source_frame, "to", target_frame) - attempts = 0 - max_attempts = 20 # Reduced from 120 to avoid long blocking - - while attempts < max_attempts: - try: - # Process LCM messages with error handling - if not self.tf_lcm_instance.handle_timeout(100): # 100ms timeout - # If handle_timeout returns false, we might need to re-check if LCM is still good - if not self.tf_lcm_instance.good(): - print("WARNING: LCM instance is no longer in a good state") - - # Get the most recent timestamp from the buffer instead of using current time - try: - timestamp = self.buffer.get_most_recent_timestamp() - if attempts % 10 == 0: - print(f"Using timestamp from buffer: {timestamp}") - except Exception: - # Fall back to current time if get_most_recent_timestamp fails - timestamp = datetime.now() - if not hasattr(timestamp, "timestamp"): - timestamp.timestamp = ( - lambda: time.mktime(timestamp.timetuple()) + timestamp.microsecond / 1e6 - ) - if attempts % 10 == 0: - print(f"Falling back to current time: {timestamp}") - - # Check if we can find the transform - if self.buffer.can_transform(target_frame, source_frame, timestamp): - # print(f"Found transform between '{target_frame}' and '{source_frame}'!") - - # Look up the transform with the timestamp from the buffer - transform = self.buffer.lookup_transform( - target_frame, - source_frame, - timestamp, - timeout=10.0, - time_tolerance=0.1, - lcm_module=lcm_msgs, - ) - - return transform - - # Increment counter and report status every 10 attempts - attempts += 1 - if attempts % 10 == 0: - print(f"Still waiting... (attempt {attempts}/{max_attempts})") - frames = self.buffer.get_all_frame_names() - if frames: - print(f"Frames received so far ({len(frames)} total):") - for frame in sorted(frames): - print(f" {frame}") - else: - print("No frames received yet") - - # Brief pause - time.sleep(0.5) - - except Exception as e: - print(f"Error during transform lookup: {e}") - attempts += 1 - time.sleep(1) # Longer pause after an error - - print(f"\nERROR: No transform found after {max_attempts} attempts") - return None - - def transform_point_cloud_with_open3d(self, points_np: np.ndarray, transform) -> np.ndarray: - """ - Transforms a point cloud using Open3D given a transform. - - Args: - points_np (np.ndarray): Nx3 array of 3D points. - transform: Transform from tf_lcm_py. - - Returns: - np.ndarray: Nx3 array of transformed 3D points. - """ - if points_np.shape[1] != 3: - print("Input point cloud must have shape Nx3.") - return points_np - - # Convert transform to 4x4 numpy matrix - tf_matrix = np.eye(4) - - # Extract rotation quaternion components - qw = transform.transform.rotation.w - qx = transform.transform.rotation.x - qy = transform.transform.rotation.y - qz = transform.transform.rotation.z - - # Convert quaternion to rotation matrix - # Formula from: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix - tf_matrix[0, 0] = 1 - 2 * qy * qy - 2 * qz * qz - tf_matrix[0, 1] = 2 * qx * qy - 2 * qz * qw - tf_matrix[0, 2] = 2 * qx * qz + 2 * qy * qw - - tf_matrix[1, 0] = 2 * qx * qy + 2 * qz * qw - tf_matrix[1, 1] = 1 - 2 * qx * qx - 2 * qz * qz - tf_matrix[1, 2] = 2 * qy * qz - 2 * qx * qw - - tf_matrix[2, 0] = 2 * qx * qz - 2 * qy * qw - tf_matrix[2, 1] = 2 * qy * qz + 2 * qx * qw - tf_matrix[2, 2] = 1 - 2 * qx * qx - 2 * qy * qy - - # Set translation - tf_matrix[0, 3] = transform.transform.translation.x - tf_matrix[1, 3] = transform.transform.translation.y - tf_matrix[2, 3] = transform.transform.translation.z - - # Create Open3D point cloud - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(points_np) - - # Apply transformation - pcd.transform(tf_matrix) - - # Return as NumPy array - return np.asarray(pcd.points) - - -# Updated main function -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="Visualize manipulation results") - parser.add_argument("--visualize-only", action="store_true", help="Only visualize results") - args = parser.parse_args() - - if args.visualize_only: - visualize_results() - exit(0) - - try: - # Then set up Drake environment - kinematic_chain_joints = [ - "pillar_platform_joint", - "joint1", - "joint2", - "joint3", - "joint4", - "joint5", - "joint6", - ] - - links_to_ignore = [ - "devkit_base_link", - "pillar_platform", - "piper_angled_mount", - "pan_tilt_base", - "pan_tilt_head", - "pan_tilt_pan", - "base_link", - "link1", - "link2", - "link3", - "link4", - "link5", - "link6", - ] - - urdf_path = "./assets/devkit_base_descr.urdf" - urdf_path = os.path.abspath(urdf_path) - - print(f"Attempting to load URDF from: {urdf_path}") - - env = DrakeKinematicsEnv(urdf_path, kinematic_chain_joints, links_to_ignore) - env.set_joint_positions([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - transform = env.get_transform("world", "camera_center_link") - print( - transform.transform.translation.x, - transform.transform.translation.y, - transform.transform.translation.z, - ) - print( - transform.transform.rotation.w, - transform.transform.rotation.x, - transform.transform.rotation.y, - transform.transform.rotation.z, - ) - - # Keep the visualization alive - print("\nVisualization is running. Press Ctrl+C to exit.") - while True: - time.sleep(1) - - except KeyboardInterrupt: - print("\nExiting...") - except Exception as e: - print(f"Error: {e}") - import traceback - - traceback.print_exc() diff --git a/tests/zed_neural_depth_demo.py b/tests/zed_neural_depth_demo.py deleted file mode 100755 index 86daf4107d..0000000000 --- a/tests/zed_neural_depth_demo.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -ZED Camera Neural Depth Demo - OpenCV Live Visualization with Data Saving - -This script demonstrates live visualization of ZED camera RGB and depth data using OpenCV. -Press SPACE to save RGB and depth images to rgbd_data2 folder. -Press ESC or 'q' to quit. -""" - -import argparse -from datetime import datetime -import logging -from pathlib import Path -import sys -import time - -import cv2 -import numpy as np -import open3d as o3d -import yaml - -# Add the project root to Python path -sys.path.append(str(Path(__file__).parent.parent)) - -try: - import pyzed.sl as sl -except ImportError: - print("ERROR: ZED SDK not found. Please install the ZED SDK and pyzed Python package.") - print("Download from: https://www.stereolabs.com/developers/release/") - sys.exit(1) - -from dimos.hardware.zed_camera import ZEDCamera -from dimos.perception.pointcloud.utils import visualize_pcd - -# Configure logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger(__name__) - - -class ZEDLiveVisualizer: - """Live OpenCV visualization for ZED camera data with saving functionality.""" - - def __init__(self, camera, max_depth=10.0, output_dir="assets/rgbd_data2"): - self.camera = camera - self.max_depth = max_depth - self.output_dir = Path(output_dir) - self.save_counter = 0 - - # Store captured pointclouds for later visualization - self.captured_pointclouds = [] - - # Display settings for 480p - self.display_width = 640 - self.display_height = 480 - - # Create output directory structure - self.setup_output_directory() - - # Get camera info for saving - self.camera_info = camera.get_camera_info() - - # Save camera info files once - self.save_camera_info() - - # OpenCV window name (single window) - self.window_name = "ZED Camera - RGB + Depth" - - # Create window - cv2.namedWindow(self.window_name, cv2.WINDOW_AUTOSIZE) - - def setup_output_directory(self): - """Create the output directory structure.""" - self.output_dir.mkdir(exist_ok=True) - (self.output_dir / "color").mkdir(exist_ok=True) - (self.output_dir / "depth").mkdir(exist_ok=True) - (self.output_dir / "pointclouds").mkdir(exist_ok=True) - logger.info(f"Created output directory: {self.output_dir}") - - def save_camera_info(self): - """Save camera info YAML files with ZED camera parameters.""" - # Get current timestamp - now = datetime.now() - timestamp_sec = int(now.timestamp()) - timestamp_nanosec = int((now.timestamp() % 1) * 1e9) - - # Get camera resolution - resolution = self.camera_info.get("resolution", {}) - width = int(resolution.get("width", 1280)) - height = int(resolution.get("height", 720)) - - # Extract left camera parameters (for RGB) from already available camera_info - left_cam = self.camera_info.get("left_cam", {}) - # Convert numpy values to Python floats - fx = float(left_cam.get("fx", 749.341552734375)) - fy = float(left_cam.get("fy", 748.5587768554688)) - cx = float(left_cam.get("cx", 639.4312744140625)) - cy = float(left_cam.get("cy", 357.2478942871094)) - - # Build distortion coefficients from ZED format - # ZED provides k1, k2, p1, p2, k3 - convert to rational_polynomial format - k1 = float(left_cam.get("k1", 0.0)) - k2 = float(left_cam.get("k2", 0.0)) - p1 = float(left_cam.get("p1", 0.0)) - p2 = float(left_cam.get("p2", 0.0)) - k3 = float(left_cam.get("k3", 0.0)) - distortion = [k1, k2, p1, p2, k3, 0.0, 0.0, 0.0] - - # Create camera info structure with plain Python types - camera_info = { - "D": distortion, - "K": [fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0], - "P": [fx, 0.0, cx, 0.0, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0], - "R": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], - "binning_x": 0, - "binning_y": 0, - "distortion_model": "rational_polynomial", - "header": { - "frame_id": "camera_color_optical_frame", - "stamp": {"nanosec": timestamp_nanosec, "sec": timestamp_sec}, - }, - "height": height, - "roi": {"do_rectify": False, "height": 0, "width": 0, "x_offset": 0, "y_offset": 0}, - "width": width, - } - - # Save color camera info - color_info_path = self.output_dir / "color_camera_info.yaml" - with open(color_info_path, "w") as f: - yaml.dump(camera_info, f, default_flow_style=False) - - # Save depth camera info (same as color for ZED) - depth_info_path = self.output_dir / "depth_camera_info.yaml" - with open(depth_info_path, "w") as f: - yaml.dump(camera_info, f, default_flow_style=False) - - logger.info(f"Saved camera info files to {self.output_dir}") - - def normalize_depth_for_display(self, depth_map): - """Normalize depth map for OpenCV visualization.""" - # Handle invalid values - valid_mask = (depth_map > 0) & np.isfinite(depth_map) - - if not np.any(valid_mask): - return np.zeros_like(depth_map, dtype=np.uint8) - - # Normalize to 0-255 for display - depth_norm = np.zeros_like(depth_map, dtype=np.float32) - depth_clipped = np.clip(depth_map[valid_mask], 0, self.max_depth) - depth_norm[valid_mask] = depth_clipped / self.max_depth - - # Convert to 8-bit and apply colormap - depth_8bit = (depth_norm * 255).astype(np.uint8) - depth_colored = cv2.applyColorMap(depth_8bit, cv2.COLORMAP_JET) - - return depth_colored - - def save_frame(self, rgb_img, depth_map): - """Save RGB, depth images, and pointcloud with proper naming convention.""" - # Generate filename with 5-digit zero-padding - filename = f"{self.save_counter:05d}.png" - pcd_filename = f"{self.save_counter:05d}.ply" - - # Save RGB image - rgb_path = self.output_dir / "color" / filename - cv2.imwrite(str(rgb_path), rgb_img) - - # Save depth image (convert to 16-bit for proper depth storage) - depth_path = self.output_dir / "depth" / filename - # Convert meters to millimeters and save as 16-bit - depth_mm = (depth_map * 1000).astype(np.uint16) - cv2.imwrite(str(depth_path), depth_mm) - - # Capture and save pointcloud - pcd = self.camera.capture_pointcloud() - if pcd is not None and len(np.asarray(pcd.points)) > 0: - pcd_path = self.output_dir / "pointclouds" / pcd_filename - o3d.io.write_point_cloud(str(pcd_path), pcd) - - # Store pointcloud for later visualization - self.captured_pointclouds.append(pcd) - - logger.info( - f"Saved frame {self.save_counter}: {rgb_path}, {depth_path}, and {pcd_path}" - ) - else: - logger.warning(f"Failed to capture pointcloud for frame {self.save_counter}") - logger.info(f"Saved frame {self.save_counter}: {rgb_path} and {depth_path}") - - self.save_counter += 1 - - def visualize_captured_pointclouds(self): - """Visualize all captured pointclouds using Open3D, one by one.""" - if not self.captured_pointclouds: - logger.info("No pointclouds captured to visualize") - return - - logger.info( - f"Visualizing {len(self.captured_pointclouds)} captured pointclouds one by one..." - ) - logger.info("Close each pointcloud window to proceed to the next one") - - for i, pcd in enumerate(self.captured_pointclouds): - if len(np.asarray(pcd.points)) > 0: - logger.info(f"Displaying pointcloud {i + 1}/{len(self.captured_pointclouds)}") - visualize_pcd(pcd, window_name=f"ZED Pointcloud {i + 1:05d}", point_size=2.0) - else: - logger.warning(f"Pointcloud {i + 1} is empty, skipping...") - - logger.info("Finished displaying all pointclouds") - - def update_display(self): - """Update the live display with new frames.""" - # Capture frame - left_img, _right_img, depth_map = self.camera.capture_frame() - - if left_img is None or depth_map is None: - return False, None, None - - # Resize RGB to 480p - rgb_resized = cv2.resize(left_img, (self.display_width, self.display_height)) - - # Create depth visualization - depth_colored = self.normalize_depth_for_display(depth_map) - - # Resize depth to 480p - depth_resized = cv2.resize(depth_colored, (self.display_width, self.display_height)) - - # Add text overlays - text_color = (255, 255, 255) - font = cv2.FONT_HERSHEY_SIMPLEX - font_scale = 0.6 - thickness = 2 - - # Add title and instructions to RGB - cv2.putText( - rgb_resized, "RGB Camera Feed", (10, 25), font, font_scale, text_color, thickness - ) - cv2.putText( - rgb_resized, - "SPACE: Save | ESC/Q: Quit", - (10, 50), - font, - font_scale - 0.1, - text_color, - thickness, - ) - - # Add title and stats to depth - cv2.putText( - depth_resized, - f"Depth Map (0-{self.max_depth}m)", - (10, 25), - font, - font_scale, - text_color, - thickness, - ) - cv2.putText( - depth_resized, - f"Saved: {self.save_counter} frames", - (10, 50), - font, - font_scale - 0.1, - text_color, - thickness, - ) - - # Stack images horizontally - combined_display = np.hstack((rgb_resized, depth_resized)) - - # Display combined image - cv2.imshow(self.window_name, combined_display) - - return True, left_img, depth_map - - def handle_key_events(self, rgb_img, depth_map): - """Handle keyboard input.""" - key = cv2.waitKey(1) & 0xFF - - if key == ord(" "): # Space key - save frame - if rgb_img is not None and depth_map is not None: - self.save_frame(rgb_img, depth_map) - return "save" - elif key == 27 or key == ord("q"): # ESC or 'q' - quit - return "quit" - - return "continue" - - def cleanup(self): - """Clean up OpenCV windows.""" - cv2.destroyAllWindows() - - -def main(): - parser = argparse.ArgumentParser( - description="ZED Camera Neural Depth Demo - OpenCV with Data Saving" - ) - parser.add_argument("--camera-id", type=int, default=0, help="ZED camera ID (default: 0)") - parser.add_argument( - "--resolution", - type=str, - default="HD1080", - choices=["HD2K", "HD1080", "HD720", "VGA"], - help="Camera resolution (default: HD1080)", - ) - parser.add_argument( - "--max-depth", - type=float, - default=10.0, - help="Maximum depth for visualization in meters (default: 10.0)", - ) - parser.add_argument( - "--camera-fps", type=int, default=15, help="Camera capture FPS (default: 30)" - ) - parser.add_argument( - "--depth-mode", - type=str, - default="NEURAL", - choices=["NEURAL", "NEURAL_PLUS"], - help="Depth mode (NEURAL=faster, NEURAL_PLUS=more accurate)", - ) - parser.add_argument( - "--output-dir", - type=str, - default="assets/rgbd_data2", - help="Output directory for saved data (default: rgbd_data2)", - ) - - args = parser.parse_args() - - # Map resolution string to ZED enum - resolution_map = { - "HD2K": sl.RESOLUTION.HD2K, - "HD1080": sl.RESOLUTION.HD1080, - "HD720": sl.RESOLUTION.HD720, - "VGA": sl.RESOLUTION.VGA, - } - - depth_mode_map = {"NEURAL": sl.DEPTH_MODE.NEURAL, "NEURAL_PLUS": sl.DEPTH_MODE.NEURAL_PLUS} - - try: - # Initialize ZED camera with neural depth - logger.info( - f"Initializing ZED camera with {args.depth_mode} depth processing at {args.camera_fps} FPS..." - ) - camera = ZEDCamera( - camera_id=args.camera_id, - resolution=resolution_map[args.resolution], - depth_mode=depth_mode_map[args.depth_mode], - fps=args.camera_fps, - ) - - # Open camera - with camera: - # Get camera information - info = camera.get_camera_info() - logger.info(f"Camera Model: {info.get('model', 'Unknown')}") - logger.info(f"Serial Number: {info.get('serial_number', 'Unknown')}") - logger.info(f"Firmware: {info.get('firmware', 'Unknown')}") - logger.info(f"Resolution: {info.get('resolution', {})}") - logger.info(f"Baseline: {info.get('baseline', 0):.3f}m") - - # Initialize visualizer - visualizer = ZEDLiveVisualizer( - camera, max_depth=args.max_depth, output_dir=args.output_dir - ) - - logger.info("Starting live visualization...") - logger.info("Controls:") - logger.info(" SPACE - Save current RGB and depth frame") - logger.info(" ESC/Q - Quit") - - frame_count = 0 - start_time = time.time() - - try: - while True: - loop_start = time.time() - - # Update display - success, rgb_img, depth_map = visualizer.update_display() - - if success: - frame_count += 1 - - # Handle keyboard events - action = visualizer.handle_key_events(rgb_img, depth_map) - - if action == "quit": - break - elif action == "save": - # Frame was saved, no additional action needed - pass - - # Print performance stats every 60 frames - if frame_count % 60 == 0: - elapsed = time.time() - start_time - fps = frame_count / elapsed - logger.info( - f"Frame {frame_count} | FPS: {fps:.1f} | Saved: {visualizer.save_counter}" - ) - - # Small delay to prevent CPU overload - elapsed = time.time() - loop_start - min_frame_time = 1.0 / 60.0 # Cap at 60 FPS - if elapsed < min_frame_time: - time.sleep(min_frame_time - elapsed) - - except KeyboardInterrupt: - logger.info("Stopped by user") - - # Final stats - total_time = time.time() - start_time - if total_time > 0: - avg_fps = frame_count / total_time - logger.info( - f"Final stats: {frame_count} frames in {total_time:.1f}s (avg {avg_fps:.1f} FPS)" - ) - logger.info(f"Total saved frames: {visualizer.save_counter}") - - # Visualize captured pointclouds - visualizer.visualize_captured_pointclouds() - - except Exception as e: - logger.error(f"Error during execution: {e}") - raise - finally: - if "visualizer" in locals(): - visualizer.cleanup() - logger.info("Demo completed") - - -if __name__ == "__main__": - main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..de203d7e4a --- /dev/null +++ b/uv.lock @@ -0,0 +1,10030 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] + +[[package]] +name = "absl-py" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, +] + +[[package]] +name = "accelerate" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, +] + +[[package]] +name = "addict" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "aioice" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "ifaddr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/04/df7286233f468e19e9bedff023b6b246182f0b2ccb04ceeb69b2994021c6/aioice-0.10.2.tar.gz", hash = "sha256:bf236c6829ee33c8e540535d31cd5a066b531cb56de2be94c46be76d68b1a806", size = 44307, upload-time = "2025-11-28T15:56:48.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e3/0d23b1f930c17d371ce1ec36ee529f22fd19ebc2a07fe3418e3d1d884ce2/aioice-0.10.2-py3-none-any.whl", hash = "sha256:14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf", size = 24875, upload-time = "2025-11-28T15:56:47.847Z" }, +] + +[[package]] +name = "aiortc" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aioice" }, + { name = "av" }, + { name = "cryptography" }, + { name = "google-crc32c" }, + { name = "pyee" }, + { name = "pylibsrtp" }, + { name = "pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/9c/4e027bfe0195de0442da301e2389329496745d40ae44d2d7c4571c4290ce/aiortc-1.14.0.tar.gz", hash = "sha256:adc8a67ace10a085721e588e06a00358ed8eaf5f6b62f0a95358ff45628dd762", size = 1180864, upload-time = "2025-10-13T21:40:37.905Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/ab/31646a49209568cde3b97eeade0d28bb78b400e6645c56422c101df68932/aiortc-1.14.0-py3-none-any.whl", hash = "sha256:4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e", size = 93183, upload-time = "2025-10-13T21:40:36.59Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "asyncio" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411, upload-time = "2015-03-10T14:11:26.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767, upload-time = "2015-03-10T14:05:10.959Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "av" +version = "16.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/c3/fd72a0315bc6c943ced1105aaac6e0ec1be57c70d8a616bd05acaa21ffee/av-16.0.1.tar.gz", hash = "sha256:dd2ce779fa0b5f5889a6d9e00fbbbc39f58e247e52d31044272648fe16ff1dbf", size = 3904030, upload-time = "2025-10-13T12:28:51.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3c/eefa29b7d0f5afdf7af9197bbecad8ec2ad06bcb5ac7e909c05a624b00a6/av-16.0.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:8b141aaa29a3afc96a1d467d106790782c1914628b57309eaadb8c10c299c9c0", size = 27206679, upload-time = "2025-10-13T12:24:41.145Z" }, + { url = "https://files.pythonhosted.org/packages/ac/89/a474feb07d5b94aa5af3771b0fe328056e2e0a840039b329f4fa2a1fd13a/av-16.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:4b8a08a59a5be0082af063d3f4b216e3950340121c6ea95b505a3f5f5cc8f21d", size = 21774556, upload-time = "2025-10-13T12:24:44.332Z" }, + { url = "https://files.pythonhosted.org/packages/be/e5/4361010dcac398bc224823e4b2a47803845e159af9f95164662c523770dc/av-16.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:792e7fc3c08eae005ff36486983966476e553cbb55aaeb0ec99adc4909377320", size = 38176763, upload-time = "2025-10-13T12:24:46.98Z" }, + { url = "https://files.pythonhosted.org/packages/d4/db/b27bdd20c9dc80de5b8792dae16dd6f4edf16408c0c7b28070c6228a8057/av-16.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:4e8ef5df76d8d0ee56139789f80bb90ad1a82a7e6df6e080e2e95c06fa22aea7", size = 39696277, upload-time = "2025-10-13T12:24:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c8/dd48e6a3ac1e922c141475a0dc30e2b6dfdef9751b3274829889a9281cce/av-16.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4f7a6985784a7464f078e419c71f5528c3e550ee5d605e7149b4a37a111eb136", size = 39576660, upload-time = "2025-10-13T12:24:55.773Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/223d047e2e60672a2fb5e51e28913de8d52195199f3e949cbfda1e6cd64b/av-16.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3f45c8d7b803b6faa2a25a26de5964a0a897de68298d9c9672c7af9d65d8b48a", size = 40752775, upload-time = "2025-10-13T12:25:00.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/73/73acad21c9203bc63d806e8baf42fe705eb5d36dafd1996b71ab5861a933/av-16.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:58e6faf1d9328d8cc6be14c5aadacb7d2965ed6d6ae1af32696993096543ff00", size = 32302328, upload-time = "2025-10-13T12:25:06.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/f2a483c5273fccd556dfa1fce14fab3b5d6d213b46e28e54e254465a2255/av-16.0.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:e310d1fb42879df9bad2152a8db6d2ff8bf332c8c36349a09d62cc122f5070fb", size = 27191982, upload-time = "2025-10-13T12:25:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/e0/39/dff28bd252131b3befd09d8587992fe18c09d5125eaefc83a6434d5f56ff/av-16.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:2f4b357e5615457a84e6b6290916b22864b76b43d5079e1a73bc27581a5b9bac", size = 21760305, upload-time = "2025-10-13T12:25:14.882Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4d/2312d50a09c84a9b4269f7fea5de84f05dd2b7c7113dd961d31fad6c64c4/av-16.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:286665c77034c3a98080169b8b5586d5568a15da81fbcdaf8099252f2d232d7c", size = 38691616, upload-time = "2025-10-13T12:25:20.063Z" }, + { url = "https://files.pythonhosted.org/packages/15/9a/3d2d30b56252f998e53fced13720e2ce809c4db477110f944034e0fa4c9f/av-16.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f88de8e5b8ea29e41af4d8d61df108323d050ccfbc90f15b13ec1f99ce0e841e", size = 40216464, upload-time = "2025-10-13T12:25:24.848Z" }, + { url = "https://files.pythonhosted.org/packages/98/cb/3860054794a47715b4be0006105158c7119a57be58d9e8882b72e4d4e1dd/av-16.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0cdb71ebe4d1b241cf700f8f0c44a7d2a6602b921e16547dd68c0842113736e1", size = 40094077, upload-time = "2025-10-13T12:25:30.238Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/79830fb8af0a89c015250f7864bbd427dff09c70575c97847055f8a302f7/av-16.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:28c27a65d40e8cf82b6db2543f8feeb8b56d36c1938f50773494cd3b073c7223", size = 41279948, upload-time = "2025-10-13T12:25:35.24Z" }, + { url = "https://files.pythonhosted.org/packages/83/79/6e1463b04382f379f857113b851cf5f9d580a2f7bd794211cd75352f4e04/av-16.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ffea39ac7574f234f5168f9b9602e8d4ecdd81853238ec4d661001f03a6d3f64", size = 32297586, upload-time = "2025-10-13T12:25:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/44/78/12a11d7a44fdd8b26a65e2efa1d8a5826733c8887a989a78306ec4785956/av-16.0.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:e41a8fef85dfb2c717349f9ff74f92f9560122a9f1a94b1c6c9a8a9c9462ba71", size = 27206375, upload-time = "2025-10-13T12:25:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/27/19/3a4d3882852a0ee136121979ce46f6d2867b974eb217a2c9a070939f55ad/av-16.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6352a64b25c9f985d4f279c2902db9a92424e6f2c972161e67119616f0796cb9", size = 21752603, upload-time = "2025-10-13T12:25:49.122Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/f7abefba6e008e2f69bebb9a17ba38ce1df240c79b36a5b5fcacf8c8fcfd/av-16.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5201f7b4b5ed2128118cb90c2a6d64feedb0586ca7c783176896c78ffb4bbd5c", size = 38931978, upload-time = "2025-10-13T12:25:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7a/1305243ab47f724fdd99ddef7309a594e669af7f0e655e11bdd2c325dfae/av-16.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:daecc2072b82b6a942acbdaa9a2e00c05234c61fef976b22713983c020b07992", size = 40549383, upload-time = "2025-10-13T12:26:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/32/b2/357cc063185043eb757b4a48782bff780826103bcad1eb40c3ddfc050b7e/av-16.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6573da96e8bebc3536860a7def108d7dbe1875c86517072431ced702447e6aea", size = 40241993, upload-time = "2025-10-13T12:26:06.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/bb/ced42a4588ba168bf0ef1e9d016982e3ba09fde6992f1dda586fd20dcf71/av-16.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4bc064e48a8de6c087b97dd27cf4ef8c13073f0793108fbce3ecd721201b2502", size = 41532235, upload-time = "2025-10-13T12:26:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c7811eca0f318d5fd3212f7e8c3d8335f75a54907c97a89213dc580b8056/av-16.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0c669b6b6668c8ae74451c15ec6d6d8a36e4c3803dc5d9910f607a174dd18f17", size = 32296912, upload-time = "2025-10-13T12:26:19.187Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/972f199ccc4f8c9e51f59e0f8962a09407396b3f6d11355e2c697ba555f9/av-16.0.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:4c61c6c120f5c5d95c711caf54e2c4a9fb2f1e613ac0a9c273d895f6b2602e44", size = 27170433, upload-time = "2025-10-13T12:26:24.673Z" }, + { url = "https://files.pythonhosted.org/packages/53/9d/0514cbc185fb20353ab25da54197fbd169a233e39efcbb26533c36a9dbb9/av-16.0.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ecc2e41320c69095f44aff93470a0d32c30892b2dbad0a08040441c81efa379", size = 21717654, upload-time = "2025-10-13T12:26:29.12Z" }, + { url = "https://files.pythonhosted.org/packages/32/8c/881409dd124b4e07d909d2b70568acb21126fc747656390840a2238651c9/av-16.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:036f0554d6faef3f4a94acaeb0cedd388e3ab96eb0eb5a14ec27c17369c466c9", size = 38651601, upload-time = "2025-10-13T12:26:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/35/fd/867ba4cc3ab504442dc89b0c117e6a994fc62782eb634c8f31304586f93e/av-16.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:876415470a62e4a3550cc38db2fc0094c25e64eea34d7293b7454125d5958190", size = 40278604, upload-time = "2025-10-13T12:26:39.2Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/63cde866c0af09a1fa9727b4f40b34d71b0535785f5665c27894306f1fbc/av-16.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:56902a06bd0828d13f13352874c370670882048267191ff5829534b611ba3956", size = 39984854, upload-time = "2025-10-13T12:26:44.581Z" }, + { url = "https://files.pythonhosted.org/packages/71/3b/8f40a708bff0e6b0f957836e2ef1f4d4429041cf8d99a415a77ead8ac8a3/av-16.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe988c2bf0fc2d952858f791f18377ea4ae4e19ba3504793799cd6c2a2562edf", size = 41270352, upload-time = "2025-10-13T12:26:50.817Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b5/c114292cb58a7269405ae13b7ba48c7d7bfeebbb2e4e66c8073c065a4430/av-16.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:708a66c248848029bf518f0482b81c5803846f1b597ef8013b19c014470b620f", size = 32273242, upload-time = "2025-10-13T12:26:55.788Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e9/a5b714bc078fdcca8b46c8a0b38484ae5c24cd81d9c1703d3e8ae2b57259/av-16.0.1-cp313-cp313t-macosx_11_0_x86_64.whl", hash = "sha256:79a77ee452537030c21a0b41139bedaf16629636bf764b634e93b99c9d5f4558", size = 27248984, upload-time = "2025-10-13T12:27:00.564Z" }, + { url = "https://files.pythonhosted.org/packages/06/ef/ff777aaf1f88e3f6ce94aca4c5806a0c360e68d48f9d9f0214e42650f740/av-16.0.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:080823a6ff712f81e7089ae9756fb1512ca1742a138556a852ce50f58e457213", size = 21828098, upload-time = "2025-10-13T12:27:05.433Z" }, + { url = "https://files.pythonhosted.org/packages/34/d7/a484358d24a42bedde97f61f5d6ee568a7dd866d9df6e33731378db92d9e/av-16.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:04e00124afa8b46a850ed48951ddda61de874407fb8307d6a875bba659d5727e", size = 40051697, upload-time = "2025-10-13T12:27:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/73/87/6772d6080837da5d5c810a98a95bde6977e1f5a6e2e759e8c9292af9ec69/av-16.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:bc098c1c6dc4e7080629a7e9560e67bd4b5654951e17e5ddfd2b1515cfcd37db", size = 41352596, upload-time = "2025-10-13T12:27:16.217Z" }, + { url = "https://files.pythonhosted.org/packages/bd/58/fe448c60cf7f85640a0ed8936f16bac874846aa35e1baa521028949c1ea3/av-16.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ffd3559a72c46a76aa622630751a821499ba5a780b0047ecc75105d43a6b61", size = 41183156, upload-time = "2025-10-13T12:27:21.574Z" }, + { url = "https://files.pythonhosted.org/packages/85/c6/a039a0979d0c278e1bed6758d5a6186416c3ccb8081970df893fdf9a0d99/av-16.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7a3f1a36b550adadd7513f4f5ee956f9e06b01a88e59f3150ef5fec6879d6f79", size = 42302331, upload-time = "2025-10-13T12:27:26.953Z" }, + { url = "https://files.pythonhosted.org/packages/18/7b/2ca4a9e3609ff155436dac384e360f530919cb1e328491f7df294be0f0dc/av-16.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c6de794abe52b8c0be55d8bb09ade05905efa74b1a5ab4860b4b9c2bfb6578bf", size = 32462194, upload-time = "2025-10-13T12:27:32.942Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d17e379906cf53a7a44dfac9cf7e4b2e7df2082ba2dbf07126055effcc1/av-16.0.1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:4b55ba69a943ae592ad7900da67129422954789de9dc384685d6b529925f542e", size = 27167101, upload-time = "2025-10-13T12:27:38.886Z" }, + { url = "https://files.pythonhosted.org/packages/6c/34/891816cd82d5646cb5a51d201d20be0a578232536d083b7d939734258067/av-16.0.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d4a0c47b6c9bbadad8909b82847f5fe64a608ad392f0b01704e427349bcd9a47", size = 21722708, upload-time = "2025-10-13T12:27:43.29Z" }, + { url = "https://files.pythonhosted.org/packages/1d/20/c24ad34038423ab8c9728cef3301e0861727c188442dcfd70a4a10834c63/av-16.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:8bba52f3035708456f6b1994d10b0371b45cfd8f917b5e84ff81aef4ec2f08bf", size = 38638842, upload-time = "2025-10-13T12:27:49.776Z" }, + { url = "https://files.pythonhosted.org/packages/d7/32/034412309572ba3ad713079d07a3ffc13739263321aece54a3055d7a4f1f/av-16.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:08e34c7e7b5e55e29931180bbe21095e1874ac120992bf6b8615d39574487617", size = 40197789, upload-time = "2025-10-13T12:27:55.688Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/40496298c32f9094e7df28641c5c58aa6fb07554dc232a9ac98a9894376f/av-16.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0d6250ab9db80c641b299987027c987f14935ea837ea4c02c5f5182f6b69d9e5", size = 39980829, upload-time = "2025-10-13T12:28:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7e/5c38268ac1d424f309b13b2de4597ad28daea6039ee5af061e62918b12a8/av-16.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7b621f28d8bcbb07cdcd7b18943ddc040739ad304545715ae733873b6e1b739d", size = 41205928, upload-time = "2025-10-13T12:28:08.431Z" }, + { url = "https://files.pythonhosted.org/packages/e3/07/3176e02692d8753a6c4606021c60e4031341afb56292178eee633b6760a4/av-16.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:92101f49082392580c9dba4ba2fe5b931b3bb0fb75a1a848bfb9a11ded68be91", size = 32272836, upload-time = "2025-10-13T12:28:13.405Z" }, + { url = "https://files.pythonhosted.org/packages/8a/47/10e03b88de097385d1550cbb6d8de96159131705c13adb92bd9b7e677425/av-16.0.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:07c464bf2bc362a154eccc82e235ef64fd3aaf8d76fc8ed63d0ae520943c6d3f", size = 27248864, upload-time = "2025-10-13T12:28:17.467Z" }, + { url = "https://files.pythonhosted.org/packages/b1/60/7447f206bec3e55e81371f1989098baa2fe9adb7b46c149e6937b7e7c1ca/av-16.0.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:750da0673864b669c95882c7b25768cd93ece0e47010d74ebcc29dbb14d611f8", size = 21828185, upload-time = "2025-10-13T12:28:21.461Z" }, + { url = "https://files.pythonhosted.org/packages/68/48/ee2680e7a01bc4911bbe902b814346911fa2528697a44f3043ee68e0f07e/av-16.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0b7c0d060863b2e341d07cd26851cb9057b7979814148b028fb7ee5d5eb8772d", size = 40040572, upload-time = "2025-10-13T12:28:26.585Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/2c43d28871721ae07cde432d6e36ae2f7035197cbadb43764cc5bf3d4b33/av-16.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:e67c2eca6023ca7d76b0709c5f392b23a5defba499f4c262411f8155b1482cbd", size = 41344288, upload-time = "2025-10-13T12:28:32.512Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7f/1d801bff43ae1af4758c45eee2eaae64f303bbb460e79f352f08587fd179/av-16.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3243d54d84986e8fbdc1946db634b0c41fe69b6de35a99fa8b763e18503d040", size = 41175142, upload-time = "2025-10-13T12:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/e4/06/bb363138687066bbf8997c1433dbd9c81762bae120955ea431fb72d69d26/av-16.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bcf73efab5379601e6510abd7afe5f397d0f6defe69b1610c2f37a4a17996b", size = 42293932, upload-time = "2025-10-13T12:28:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/92/15/5e713098a085f970ccf88550194d277d244464d7b3a7365ad92acb4b6dc1/av-16.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6368d4ff153d75469d2a3217bc403630dc870a72fe0a014d9135de550d731a86", size = 32460624, upload-time = "2025-10-13T12:28:48.767Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, + { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bidict" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, +] + +[[package]] +name = "bitsandbytes" +version = "0.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "packaging", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "torch", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/4f/9f6d161e9ea68cdd6b85585dee9b383748ca07431e31c4c134111f87489e/bitsandbytes-0.49.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:7e69951b4d207a676986fce967544d9599f23518d0f09d478295996aeff377c2", size = 31065242, upload-time = "2025-12-11T20:50:41.903Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a8/26f7815b376b1d3dae615263471cb6d0d9f9792a472d5dab529502deac67/bitsandbytes-0.49.0-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:0c46cdef50b3174463b6bdf13715c9f1f00b360be3626e3c5d2f8d226af2cf3f", size = 59053880, upload-time = "2025-12-11T20:50:45.422Z" }, +] + +[[package]] +name = "black" +version = "21.4b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "click" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "regex" }, + { name = "toml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/75/69fabe4a2d578b315a3b90e485b1671b9872d192028944e9af726bb8c452/black-21.4b2.tar.gz", hash = "sha256:fc9bcf3b482b05c1f35f6a882c079dc01b9c7795827532f4cc43c0ec88067bbc", size = 1151620, upload-time = "2021-04-28T15:33:13.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/c3/848edbd902fa908e941eaf72dc142b4a5c86e903c1e0129cf7cd098a485b/black-21.4b2-py3-none-any.whl", hash = "sha256:bff7067d8bc25eb21dcfdbc8c72f2baafd9ec6de4663241a52fb904b304d391f", size = 130959, upload-time = "2021-04-28T15:33:10.622Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "bokeh" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jinja2" }, + { name = "narwhals" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "tornado", marker = "sys_platform != 'emscripten'" }, + { name = "xyzservices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/7ee0c4dfd0255631b0624ce01be178704f91f763f02a1879368eb109befd/bokeh-3.8.2.tar.gz", hash = "sha256:8e7dcacc21d53905581b54328ad2705954f72f2997f99fc332c1de8da53aa3cc", size = 6529251, upload-time = "2026-01-06T00:20:06.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/a8/877f306720bc114c612579c5af36bcb359026b83d051226945499b306b1a/bokeh-3.8.2-py3-none-any.whl", hash = "sha256:5e2c0d84f75acb25d60efb9e4d2f434a791c4639b47d685534194c4e07bd0111", size = 7207131, upload-time = "2026-01-06T00:20:04.917Z" }, +] + +[[package]] +name = "brax" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "etils" }, + { name = "flask" }, + { name = "flask-cors" }, + { name = "flax", version = "0.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "flax", version = "0.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jax", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jaxlib", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jaxopt" }, + { name = "jinja2" }, + { name = "ml-collections" }, + { name = "mujoco" }, + { name = "mujoco-mjx" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "optax" }, + { name = "orbax-checkpoint" }, + { name = "pillow" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tensorboardx" }, + { name = "trimesh" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/2d/2ea28b8c445730452a019118e667416309f217e130fe004e378e6575a15b/brax-0.14.0.tar.gz", hash = "sha256:1102e890040493263e21163f962dd5b850e199726dfd62dc9075657c7d3371b3", size = 205787, upload-time = "2025-12-16T21:04:19.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/6e/831f7903b21c2ffa61dc15e5703bd148084651e4aa2c354b140a3ae44dab/brax-0.14.0-py3-none-any.whl", hash = "sha256:4306b41d7f2f70726657426754c43367e572dca01199a7f1a96d115c13f4352f", size = 350172, upload-time = "2025-12-16T21:04:18.477Z" }, +] + +[[package]] +name = "build" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" }, +] + +[[package]] +name = "catkin-pkg" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "packaging" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/7a/dcd7ba56dc82d88b3059a6770828388fc2e136ca4c5d79003f9febf33087/catkin_pkg-1.1.0.tar.gz", hash = "sha256:df1cb6879a3a772e770a100a6613ce8fc508b4855e5b2790106ddad4a8beb43c", size = 65547, upload-time = "2025-09-10T17:34:36.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/1b/50316bd6f95c50686b35799abebb6168d90ee18b7c03e3065f587f010f7c/catkin_pkg-1.1.0-py3-none-any.whl", hash = "sha256:7f5486b4f5681b5f043316ce10fc638c8d0ba8127146e797c85f4024e4356027", size = 76369, upload-time = "2025-09-10T17:34:35.639Z" }, +] + +[[package]] +name = "cerebras-cloud-sdk" +version = "1.64.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/98/96fa7704e101b5b33c7030a61d081f07c6c117f96d742852d7e06da56345/cerebras_cloud_sdk-1.64.1.tar.gz", hash = "sha256:7200b0cbafaff32e4e6bcb88d5342fc19295435a7a7681fa10156018d79ae67a", size = 131448, upload-time = "2025-12-31T17:54:55.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/96/138ed6ecf48f3b17f68ca06bb5c2b506d34c84d73502c9e3a620da7d44a9/cerebras_cloud_sdk-1.64.1-py3-none-any.whl", hash = "sha256:c9c049586e89dcc4e4a8d726d4725bdba28acb9e8b0f2a9640138b24b92444a0", size = 97798, upload-time = "2025-12-31T17:54:54.072Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "chex" +version = "0.1.90" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version < '3.11'" }, + { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "toolz", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/70/53c7d404ce9e2a94009aea7f77ef6e392f6740e071c62683a506647c520f/chex-0.1.90.tar.gz", hash = "sha256:d3c375aeb6154b08f1cccd2bee4ed83659ee2198a6acf1160d2fe2e4a6c87b5c", size = 92363, upload-time = "2025-07-23T19:50:47.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/3d/46bb04776c465cea2dd8aa2d4b61ab610b707f798f47838ef7e6105b025c/chex-0.1.90-py3-none-any.whl", hash = "sha256:fce3de82588f72d4796e545e574a433aa29229cbdcf792555e41bead24b704ae", size = 101047, upload-time = "2025-07-23T19:50:46.603Z" }, +] + +[[package]] +name = "chex" +version = "0.1.91" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version >= '3.11'" }, + { name = "jax", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jaxlib", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "toolz", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/7d/812f01e7b2ddf28a0caa8dde56bd951a2c8f691c9bbfce38d469458d1502/chex-0.1.91.tar.gz", hash = "sha256:65367a521415ada905b8c0222b0a41a68337fcadf79a1fb6fc992dbd95dd9f76", size = 90302, upload-time = "2025-09-01T21:49:32.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/0c/96102c01dd02ae740d4afc3644d5c7d7fc51d3feefd67300a2aa1ddbf7cb/chex-0.1.91-py3-none-any.whl", hash = "sha256:6fc4cbfc22301c08d4a7ef706045668410100962eba8ba6af03fa07f4e5dcf9b", size = 100965, upload-time = "2025-09-01T21:49:31.141Z" }, +] + +[[package]] +name = "choreographer" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "logistro" }, + { name = "simplejson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/47/64a035c6f764450ea9f902cbeba14c8c70316c2641125510066d8f912bfa/choreographer-1.2.1.tar.gz", hash = "sha256:022afd72b1e9b0bcb950420b134e70055a294c791b6f36cfb47d89745b701b5f", size = 43399, upload-time = "2025-11-09T23:04:44.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/9f/d73dfb85d7a5b1a56a99adc50f2074029468168c970ff5daeade4ad819e4/choreographer-1.2.1-py3-none-any.whl", hash = "sha256:9af5385effa3c204dbc337abf7ac74fd8908ced326a15645dc31dde75718c77e", size = 49338, upload-time = "2025-11-09T23:04:43.154Z" }, +] + +[[package]] +name = "chromadb" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "jsonschema" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "posthog" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/54/2bc73eac5d8fd7ffc41f8e6e4dd13ad0fd916f8973f85b1411011ba1e05b/chromadb-1.4.0.tar.gz", hash = "sha256:5b4e6d1ede4faaaf12ec772c3c603ea19f39b255ef0795855b40dd79f00a4183", size = 2001752, upload-time = "2025-12-24T02:58:18.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/d5/7ce34021304bdf1a5eefaaf434d2be078828dd71aa3871d89eeeecedfb19/chromadb-1.4.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ab4ad96c21d0038f6d8d84b9cac2010ce1f448926e9a2ee35251552f2e85da07", size = 20882057, upload-time = "2025-12-24T02:58:15.916Z" }, + { url = "https://files.pythonhosted.org/packages/76/6d/9fbf794f3672bfaf227b0e8642b1af6e1ef7d5f5b20f7505ac684ff0b155/chromadb-1.4.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:4d3c8abd762f092f73482e3eb1dae560a8a1c2674575d11eaac0dddf35e9cc6d", size = 20148106, upload-time = "2025-12-24T02:58:12.915Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cc/d33e24258027c6a14a49a5abf94c75dd6f82e5ab5ed44fe622c0de303420/chromadb-1.4.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29fe47563c460a6cadbdc481b503c520ab4e424730c97d6a85d488a13009b6ce", size = 20759866, upload-time = "2025-12-24T02:58:06.987Z" }, + { url = "https://files.pythonhosted.org/packages/96/da/048ea86c7cb04a873aaab912be62d90b403a8b15a98ae7781ea777371373/chromadb-1.4.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1942e1ee074c7d1e421ea04391a1fccfd18a4b3b94a8e61e853d88dc6924abfa", size = 21666411, upload-time = "2025-12-24T02:58:10.044Z" }, + { url = "https://files.pythonhosted.org/packages/a0/49/933091cf12ee4ce4527a8e99b778f768f63df67e7d3ed9c20eecc0385169/chromadb-1.4.0-cp39-abi3-win_amd64.whl", hash = "sha256:2ec0485e715357a41078c20ebed65d5d5b941bf2fff418c6f1c64176dc36f837", size = 21930010, upload-time = "2025-12-24T02:58:20.138Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "clip" +version = "1.0" +source = { git = "https://github.com/openai/CLIP.git#dcba3cb2e2827b402d2701e7e1c7d9fed8a20ef1" } +dependencies = [ + { name = "ftfy" }, + { name = "packaging" }, + { name = "regex" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + +[[package]] +name = "colorlog" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "configargparse" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, +] + +[[package]] +name = "contact-graspnet-pytorch" +version = "0.0.0" +source = { git = "https://github.com/dimensionalOS/contact_graspnet_pytorch.git#9f9c7d5df5e8bbf3757fe9c786c67a921809336b" } + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "ctransformers" +version = "0.2.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "py-cpuinfo" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/5e/6ed7eaf8f54b5b078e2a609e90369c6999e67f915b9c1927c0d686c494f9/ctransformers-0.2.27.tar.gz", hash = "sha256:25653d4be8a5ed4e2d3756544c1e9881bf95404be5371c3ed506a256c28663d5", size = 376065, upload-time = "2023-09-10T15:19:14.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/50/0b608e2abee4fc695b4e7ff5f569f5d32faf84a49e322034716fa157d1cf/ctransformers-0.2.27-py3-none-any.whl", hash = "sha256:6a3ba47556471850d95fdbc59299a82ab91c9dc8b40201c5e7e82d71360772d9", size = 9853506, upload-time = "2023-09-10T15:18:58.741Z" }, +] + +[package.optional-dependencies] +cuda = [ + { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12", version = "12.9.1.4", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "nvidia-cuda-runtime-cu12", version = "12.8.90", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cuda-runtime-cu12", version = "12.9.79", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, +] + +[[package]] +name = "cupy-cuda12x" +version = "13.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastrlock" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/2e/db22c5148884e4e384f6ebbc7971fa3710f3ba67ca492798890a0fdebc45/cupy_cuda12x-13.6.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:9e37f60f27ff9625dfdccc4688a09852707ec613e32ea9404f425dd22a386d14", size = 126341714, upload-time = "2025-08-18T08:24:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/53/2b/8064d94a6ab6b5c4e643d8535ab6af6cabe5455765540931f0ef60a0bc3b/cupy_cuda12x-13.6.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:e78409ea72f5ac7d6b6f3d33d99426a94005254fa57e10617f430f9fd7c3a0a1", size = 112238589, upload-time = "2025-08-18T08:24:15.541Z" }, + { url = "https://files.pythonhosted.org/packages/de/7b/bac3ca73e164d2b51c6298620261637c7286e06d373f597b036fc45f5563/cupy_cuda12x-13.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f33c9c975782ef7a42c79b6b4fb3d5b043498f9b947126d792592372b432d393", size = 89874119, upload-time = "2025-08-18T08:24:20.628Z" }, + { url = "https://files.pythonhosted.org/packages/54/64/71c6e08f76c06639e5112f69ee3bc1129be00054ad5f906d7fd3138af579/cupy_cuda12x-13.6.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:c790d012fd4d86872b9c89af9f5f15d91c30b8e3a4aa4dd04c2610f45f06ac44", size = 128016458, upload-time = "2025-08-18T08:24:26.394Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d9/5c5077243cd92368c3eccecdbf91d76db15db338169042ffd1647533c6b1/cupy_cuda12x-13.6.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:77ba6745a130d880c962e687e4e146ebbb9014f290b0a80dbc4e4634eb5c3b48", size = 113039337, upload-time = "2025-08-18T08:24:31.814Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/02bea5cdf108e2a66f98e7d107b4c9a6709e5dbfedf663340e5c11719d83/cupy_cuda12x-13.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:a20b7acdc583643a623c8d8e3efbe0db616fbcf5916e9c99eedf73859b6133af", size = 89885526, upload-time = "2025-08-18T08:24:37.258Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/7e7fc4816d0de0154e5d9053242c3a08a0ca8b43ee656a6f7b3b95055a7b/cupy_cuda12x-13.6.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a6970ceefe40f9acbede41d7fe17416bd277b1bd2093adcde457b23b578c5a59", size = 127334633, upload-time = "2025-08-18T08:24:43.065Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/d7e1295141e7d530674a3cc567e13ed0eb6b81524cb122d797ed996b5bea/cupy_cuda12x-13.6.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:79b0cacb5e8b190ef409f9e03f06ac8de1b021b0c0dda47674d446f5557e0eb1", size = 112886268, upload-time = "2025-08-18T08:24:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/14555b63fd78cfac7b88af0094cea0a3cb845d243661ec7da69f7b3ea0de/cupy_cuda12x-13.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca06fede7b8b83ca9ad80062544ef2e5bb8d4762d1c4fc3ac8349376de9c8a5e", size = 89785108, upload-time = "2025-08-18T08:24:54.527Z" }, + { url = "https://files.pythonhosted.org/packages/19/ec/f62cb991f11fb41291c4c15b6936d7b67ffa71ddb344ad6e8894e06ce58d/cupy_cuda12x-13.6.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e5426ae3b1b9cf59927481e457a89e3f0b50a35b114a8034ec9110e7a833434c", size = 126904601, upload-time = "2025-08-18T08:24:59.951Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b8/30127bcdac53a25f94ee201bf4802fcd8d012145567d77c54174d6d01c01/cupy_cuda12x-13.6.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:52d9e7f83d920da7d81ec2e791c2c2c747fdaa1d7b811971b34865ce6371e98a", size = 112654824, upload-time = "2025-08-18T08:25:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/72/36/c9e24acb19f039f814faea880b3704a3661edaa6739456b73b27540663e3/cupy_cuda12x-13.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:297b4268f839de67ef7865c2202d3f5a0fb8d20bd43360bc51b6e60cb4406447", size = 89750580, upload-time = "2025-08-18T08:25:10.972Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cython" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/10/720e0fb84eab4c927c4dd6b61eb7993f7732dd83d29ba6d73083874eade9/cython-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb0cc0f23b9874ad262d7d2b9560aed9c7e2df07b49b920bda6f2cc9cb505e", size = 2960836, upload-time = "2026-01-04T14:14:51.103Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3d/b26f29092c71c36e0462752885bdfb18c23c176af4de953fdae2772a8941/cython-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f136f379a4a54246facd0eb6f1ee15c3837cb314ce87b677582ec014db4c6845", size = 3370134, upload-time = "2026-01-04T14:14:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/539fb0d09e4f5251b5b14f8daf77e71fee021527f1013791038234618b6b/cython-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ab0632186057406ec729374c737c37051d2eacad9d515d94e5a3b3e58a9b02", size = 3537552, upload-time = "2026-01-04T14:14:56.852Z" }, + { url = "https://files.pythonhosted.org/packages/10/c6/82d19a451c050d1be0f05b1a3302267463d391db548f013ee88b5348a8e9/cython-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ca2399dc75796b785f74fb85c938254fa10c80272004d573c455f9123eceed86", size = 2766191, upload-time = "2026-01-04T14:14:58.709Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/8f06145ec3efa121c8b1b67f06a640386ddacd77ee3e574da582a21b14ee/cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed", size = 2953769, upload-time = "2026-01-04T14:15:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/706cf830eddd831666208af1b3058c2e0758ae157590909c1f634b53bed9/cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3", size = 3243841, upload-time = "2026-01-04T14:15:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/ac/25/58893afd4ef45f79e3d4db82742fa4ff874b936d67a83c92939053920ccd/cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4", size = 3378083, upload-time = "2026-01-04T14:15:04.248Z" }, + { url = "https://files.pythonhosted.org/packages/32/e4/424a004d7c0d8a4050c81846ebbd22272ececfa9a498cb340aa44fccbec2/cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382", size = 2769990, upload-time = "2026-01-04T14:15:06.53Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/03/1c/46e34b08bea19a1cdd1e938a4c123e6299241074642db9d81983cef95e9f/cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891", size = 3226757, upload-time = "2026-01-04T14:15:10.812Z" }, + { url = "https://files.pythonhosted.org/packages/12/33/3298a44d201c45bcf0d769659725ae70e9c6c42adf8032f6d89c8241098d/cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7", size = 3388969, upload-time = "2026-01-04T14:15:12.45Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/4275cd3ea0a4cf4606f9b92e7f8766478192010b95a7f516d1b7cf22cb10/cython-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:767b143704bdd08a563153448955935844e53b852e54afdc552b43902ed1e235", size = 2756457, upload-time = "2026-01-04T14:15:14.67Z" }, + { url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" }, + { url = "https://files.pythonhosted.org/packages/71/bb/8f28c39c342621047fea349a82fac712a5e2b37546d2f737bbde48d5143d/cython-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03893c88299a2c868bb741ba6513357acd104e7c42265809fd58dce1456a36fc", size = 3213148, upload-time = "2026-01-04T14:15:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d2/16fa02f129ed2b627e88d9d9ebd5ade3eeb66392ae5ba85b259d2d52b047/cython-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f81eda419b5ada7b197bbc3c5f4494090e3884521ffd75a3876c93fbf66c9ca8", size = 3375764, upload-time = "2026-01-04T14:15:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/91/3f/deb8f023a5c10c0649eb81332a58c180fad27c7533bb4aae138b5bc34d92/cython-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:83266c356c13c68ffe658b4905279c993d8a5337bb0160fa90c8a3e297ea9a2e", size = 2754238, upload-time = "2026-01-04T14:15:23.001Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/1021ffc80b9c4720b7ba869aea8422c82c84245ef117ebe47a556bdc00c3/cython-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3b5ac54e95f034bc7fb07313996d27cbf71abc17b229b186c1540942d2dc28e", size = 3256146, upload-time = "2026-01-04T14:15:26.741Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/ca221ec7e94b3c5dc4138dcdcbd41178df1729c1e88c5dfb25f9d30ba3da/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f43be4eaa6afd58ce20d970bb1657a3627c44e1760630b82aa256ba74b4acb", size = 3383458, upload-time = "2026-01-04T14:15:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/1388fc0243240cd54994bb74f26aaaf3b2e22f89d3a2cf8da06d75d46ca2/cython-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:983f9d2bb8a896e16fa68f2b37866ded35fa980195eefe62f764ddc5f9f5ef8e", size = 2791241, upload-time = "2026-01-04T14:15:30.448Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/48530d9b9d64ec11dbe0dd3178a5fe1e0b27977c1054ecffb82be81e9b6a/cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581", size = 3210669, upload-time = "2026-01-04T14:15:41.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/91/4865fbfef1f6bb4f21d79c46104a53d1a3fa4348286237e15eafb26e0828/cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06", size = 2856835, upload-time = "2026-01-04T14:15:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/fa/39/60317957dbef179572398253f29d28f75f94ab82d6d39ea3237fb6c89268/cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8", size = 2994408, upload-time = "2026-01-04T14:15:45.422Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/7c24d9292650db4abebce98abc9b49c820d40fa7c87921c0a84c32f4efe7/cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103", size = 2891478, upload-time = "2026-01-04T14:15:47.394Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/03dc3c962cde9da37a93cca8360e576f904d5f9beecfc9d70b1f820d2e5f/cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf", size = 3225663, upload-time = "2026-01-04T14:15:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/b1/97/10b50c38313c37b1300325e2e53f48ea9a2c078a85c0c9572057135e31d5/cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d", size = 3115628, upload-time = "2026-01-04T14:15:51.323Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/d6a353c9b147848122a0db370863601fdf56de2d983b5c4a6a11e6ee3cd7/cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290", size = 2437463, upload-time = "2026-01-04T14:15:53.787Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d8/319a1263b9c33b71343adfd407e5daffd453daef47ebc7b642820a8b68ed/cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a", size = 2442754, upload-time = "2026-01-04T14:15:55.382Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, +] + +[[package]] +name = "dash" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "importlib-metadata" }, + { name = "nest-asyncio" }, + { name = "plotly" }, + { name = "requests" }, + { name = "retrying" }, + { name = "setuptools" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/f9/516671861cf190bda37f6afa696d8a6a6ac593f23d8cf198e16faca044f5/dash-3.3.0.tar.gz", hash = "sha256:eaaa7a671540b5e1db8066f4966d0277d21edc2c7acdaec2fd6d198366a8b0df", size = 7579436, upload-time = "2025-11-12T15:51:54.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/cf/a4853e5b2b2bea55ae909095a8720b3ed50d07bdd40cbeafcedb5a6c47da/dash-3.3.0-py3-none-any.whl", hash = "sha256:8f52415977f7490492dd8a3872279160be8ff253ca9f4d49a4e3ba747fa4bd91", size = 7919707, upload-time = "2025-11-12T15:51:47.432Z" }, +] + +[[package]] +name = "dask" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/29/05feb8e2531c46d763547c66b7f5deb39b53d99b3be1b4ddddbd1cec6567/dask-2025.5.1.tar.gz", hash = "sha256:979d9536549de0e463f4cab8a8c66c3a2ef55791cd740d07d9bf58fab1d1076a", size = 10969324, upload-time = "2025-05-20T19:54:30.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl", hash = "sha256:3b85fdaa5f6f989dde49da6008415b1ae996985ebdfb1e40de2c997d9010371d", size = 1474226, upload-time = "2025-05-20T19:54:20.309Z" }, +] + +[package.optional-dependencies] +complete = [ + { name = "bokeh" }, + { name = "distributed" }, + { name = "jinja2" }, + { name = "lz4" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, + { name = "pyarrow" }, +] + +[[package]] +name = "dataclasses" +version = "0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/12/7919c5d8b9c497f9180db15ea8ead6499812ea8264a6ae18766d93c59fe5/dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97", size = 36581, upload-time = "2020-11-13T14:40:30.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ca/75fac5856ab5cfa51bbbcefa250182e50441074fdc3f803f6e76451fab43/dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", size = 19041, upload-time = "2020-11-13T14:40:29.194Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/75/9e12d4d42349b817cd545b89247696c67917aab907012ae5b64bbfea3199/debugpy-1.8.19.tar.gz", hash = "sha256:eea7e5987445ab0b5ed258093722d5ecb8bb72217c5c9b1e21f64efe23ddebdb", size = 1644590, upload-time = "2025-12-15T21:53:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/98/d57054371887f37d3c959a7a8dc3c76b763acb65f5e78d849d7db7cadc5b/debugpy-1.8.19-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fce6da15d73be5935b4438435c53adb512326a3e11e4f90793ea87cd9f018254", size = 2098493, upload-time = "2025-12-15T21:53:30.149Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dd/c517b9aa3500157a30e4f4c4f5149f880026bd039d2b940acd2383a85d8e/debugpy-1.8.19-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:e24b1652a1df1ab04d81e7ead446a91c226de704ff5dde6bd0a0dbaab07aa3f2", size = 3087875, upload-time = "2025-12-15T21:53:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/d8/57/3d5a5b0da9b63445253107ead151eff29190c6ad7440c68d1a59d56613aa/debugpy-1.8.19-cp310-cp310-win32.whl", hash = "sha256:327cb28c3ad9e17bc925efc7f7018195fd4787c2fe4b7af1eec11f1d19bdec62", size = 5239378, upload-time = "2025-12-15T21:53:32.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/36/7f9053c4c549160c87ae7e43800138f2695578c8b65947114c97250983b6/debugpy-1.8.19-cp310-cp310-win_amd64.whl", hash = "sha256:b7dd275cf2c99e53adb9654f5ae015f70415bbe2bacbe24cfee30d54b6aa03c5", size = 5271129, upload-time = "2025-12-15T21:53:35.085Z" }, + { url = "https://files.pythonhosted.org/packages/80/e2/48531a609b5a2aa94c6b6853afdfec8da05630ab9aaa96f1349e772119e9/debugpy-1.8.19-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:c5dcfa21de1f735a4f7ced4556339a109aa0f618d366ede9da0a3600f2516d8b", size = 2207620, upload-time = "2025-12-15T21:53:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d4/97775c01d56071969f57d93928899e5616a4cfbbf4c8cc75390d3a51c4a4/debugpy-1.8.19-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:806d6800246244004625d5222d7765874ab2d22f3ba5f615416cf1342d61c488", size = 3170796, upload-time = "2025-12-15T21:53:38.513Z" }, + { url = "https://files.pythonhosted.org/packages/8d/7e/8c7681bdb05be9ec972bbb1245eb7c4c7b0679bb6a9e6408d808bc876d3d/debugpy-1.8.19-cp311-cp311-win32.whl", hash = "sha256:783a519e6dfb1f3cd773a9bda592f4887a65040cb0c7bd38dde410f4e53c40d4", size = 5164287, upload-time = "2025-12-15T21:53:40.857Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a8/aaac7ff12ddf5d68a39e13a423a8490426f5f661384f5ad8d9062761bd8e/debugpy-1.8.19-cp311-cp311-win_amd64.whl", hash = "sha256:14035cbdbb1fe4b642babcdcb5935c2da3b1067ac211c5c5a8fdc0bb31adbcaa", size = 5188269, upload-time = "2025-12-15T21:53:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/4a/15/d762e5263d9e25b763b78be72dc084c7a32113a0bac119e2f7acae7700ed/debugpy-1.8.19-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:bccb1540a49cde77edc7ce7d9d075c1dbeb2414751bc0048c7a11e1b597a4c2e", size = 2549995, upload-time = "2025-12-15T21:53:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/a7/88/f7d25c68b18873b7c53d7c156ca7a7ffd8e77073aa0eac170a9b679cf786/debugpy-1.8.19-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:e9c68d9a382ec754dc05ed1d1b4ed5bd824b9f7c1a8cd1083adb84b3c93501de", size = 4309891, upload-time = "2025-12-15T21:53:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4f/a65e973aba3865794da65f71971dca01ae66666132c7b2647182d5be0c5f/debugpy-1.8.19-cp312-cp312-win32.whl", hash = "sha256:6599cab8a783d1496ae9984c52cb13b7c4a3bd06a8e6c33446832a5d97ce0bee", size = 5286355, upload-time = "2025-12-15T21:53:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3a/d3d8b48fec96e3d824e404bf428276fb8419dfa766f78f10b08da1cb2986/debugpy-1.8.19-cp312-cp312-win_amd64.whl", hash = "sha256:66e3d2fd8f2035a8f111eb127fa508469dfa40928a89b460b41fd988684dc83d", size = 5328239, upload-time = "2025-12-15T21:53:48.868Z" }, + { url = "https://files.pythonhosted.org/packages/71/3d/388035a31a59c26f1ecc8d86af607d0c42e20ef80074147cd07b180c4349/debugpy-1.8.19-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:91e35db2672a0abaf325f4868fcac9c1674a0d9ad9bb8a8c849c03a5ebba3e6d", size = 2538859, upload-time = "2025-12-15T21:53:50.478Z" }, + { url = "https://files.pythonhosted.org/packages/4a/19/c93a0772d0962294f083dbdb113af1a7427bb632d36e5314297068f55db7/debugpy-1.8.19-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:85016a73ab84dea1c1f1dcd88ec692993bcbe4532d1b49ecb5f3c688ae50c606", size = 4292575, upload-time = "2025-12-15T21:53:51.821Z" }, + { url = "https://files.pythonhosted.org/packages/5c/56/09e48ab796b0a77e3d7dc250f95251832b8bf6838c9632f6100c98bdf426/debugpy-1.8.19-cp313-cp313-win32.whl", hash = "sha256:b605f17e89ba0ecee994391194285fada89cee111cfcd29d6f2ee11cbdc40976", size = 5286209, upload-time = "2025-12-15T21:53:53.602Z" }, + { url = "https://files.pythonhosted.org/packages/fb/4e/931480b9552c7d0feebe40c73725dd7703dcc578ba9efc14fe0e6d31cfd1/debugpy-1.8.19-cp313-cp313-win_amd64.whl", hash = "sha256:c30639998a9f9cd9699b4b621942c0179a6527f083c72351f95c6ab1728d5b73", size = 5328206, upload-time = "2025-12-15T21:53:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b9/cbec520c3a00508327476c7fce26fbafef98f412707e511eb9d19a2ef467/debugpy-1.8.19-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:1e8c4d1bd230067bf1bbcdbd6032e5a57068638eb28b9153d008ecde288152af", size = 2537372, upload-time = "2025-12-15T21:53:57.318Z" }, + { url = "https://files.pythonhosted.org/packages/88/5e/cf4e4dc712a141e10d58405c58c8268554aec3c35c09cdcda7535ff13f76/debugpy-1.8.19-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d40c016c1f538dbf1762936e3aeb43a89b965069d9f60f9e39d35d9d25e6b809", size = 4268729, upload-time = "2025-12-15T21:53:58.712Z" }, + { url = "https://files.pythonhosted.org/packages/82/a3/c91a087ab21f1047db328c1d3eb5d1ff0e52de9e74f9f6f6fa14cdd93d58/debugpy-1.8.19-cp314-cp314-win32.whl", hash = "sha256:0601708223fe1cd0e27c6cce67a899d92c7d68e73690211e6788a4b0e1903f5b", size = 5286388, upload-time = "2025-12-15T21:54:00.687Z" }, + { url = "https://files.pythonhosted.org/packages/17/b8/bfdc30b6e94f1eff09f2dc9cc1f9cd1c6cde3d996bcbd36ce2d9a4956e99/debugpy-1.8.19-cp314-cp314-win_amd64.whl", hash = "sha256:8e19a725f5d486f20e53a1dde2ab8bb2c9607c40c00a42ab646def962b41125f", size = 5327741, upload-time = "2025-12-15T21:54:02.148Z" }, + { url = "https://files.pythonhosted.org/packages/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.py3-none-any.whl", hash = "sha256:360ffd231a780abbc414ba0f005dad409e71c78637efe8f2bd75837132a41d38", size = 5292321, upload-time = "2025-12-15T21:54:16.024Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "detectron2" +version = "0.6" +source = { git = "https://github.com/facebookresearch/detectron2.git?tag=v0.6#d1e04565d3bec8719335b88be9e9b961bf3ec464" } +dependencies = [ + { name = "black" }, + { name = "cloudpickle" }, + { name = "future" }, + { name = "fvcore" }, + { name = "hydra-core" }, + { name = "iopath" }, + { name = "matplotlib" }, + { name = "omegaconf" }, + { name = "pillow" }, + { name = "pycocotools" }, + { name = "pydot" }, + { name = "tabulate" }, + { name = "tensorboard" }, + { name = "termcolor" }, + { name = "tqdm" }, + { name = "yacs" }, +] + +[[package]] +name = "dimos" +version = "0.0.4" +source = { editable = "." } +dependencies = [ + { name = "asyncio" }, + { name = "colorlog" }, + { name = "dask", extra = ["complete"] }, + { name = "dimos-lcm" }, + { name = "llvmlite" }, + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "open3d" }, + { name = "opencv-python" }, + { name = "plotext" }, + { name = "plum-dispatch" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "pyturbojpeg" }, + { name = "reactivex" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sortedcontainers" }, + { name = "structlog" }, + { name = "terminaltexteffects" }, + { name = "textual" }, + { name = "typer" }, +] + +[package.optional-dependencies] +agents = [ + { name = "anthropic" }, + { name = "bitsandbytes", marker = "sys_platform == 'linux'" }, + { name = "langchain" }, + { name = "langchain-chroma" }, + { name = "langchain-core" }, + { name = "langchain-huggingface" }, + { name = "langchain-ollama" }, + { name = "langchain-openai" }, + { name = "langchain-text-splitters" }, + { name = "mcp" }, + { name = "ollama" }, + { name = "openai" }, + { name = "openai-whisper" }, + { name = "sounddevice" }, +] +cpu = [ + { name = "ctransformers" }, + { name = "onnxruntime" }, +] +cuda = [ + { name = "clip" }, + { name = "ctransformers", extra = ["cuda"] }, + { name = "cupy-cuda12x" }, + { name = "dataclasses" }, + { name = "detectron2" }, + { name = "fasttext" }, + { name = "ftfy" }, + { name = "lvis" }, + { name = "mmcv" }, + { name = "mmengine" }, + { name = "mss" }, + { name = "nltk" }, + { name = "nvidia-nvimgcodec-cu12", extra = ["all"] }, + { name = "onnxruntime-gpu" }, + { name = "regex" }, + { name = "xformers" }, +] +dev = [ + { name = "coverage" }, + { name = "lxml-stubs" }, + { name = "mypy" }, + { name = "pandas-stubs" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-env" }, + { name = "pytest-mock" }, + { name = "pytest-timeout" }, + { name = "requests-mock" }, + { name = "ruff" }, + { name = "terminaltexteffects" }, + { name = "types-colorama" }, + { name = "types-defusedxml" }, + { name = "types-gevent" }, + { name = "types-greenlet" }, + { name = "types-jmespath" }, + { name = "types-jsonschema" }, + { name = "types-networkx" }, + { name = "types-protobuf" }, + { name = "types-psutil" }, + { name = "types-pysocks" }, + { name = "types-pytz" }, + { name = "types-pyyaml" }, + { name = "types-simplejson" }, + { name = "types-tabulate" }, + { name = "types-tensorflow" }, + { name = "types-tqdm" }, + { name = "watchdog" }, +] +drone = [ + { name = "pymavlink" }, +] +manipulation = [ + { name = "contact-graspnet-pytorch" }, + { name = "h5py" }, + { name = "kaleido" }, + { name = "matplotlib" }, + { name = "pandas" }, + { name = "piper-sdk" }, + { name = "plotly" }, + { name = "pyquaternion" }, + { name = "pyrender" }, + { name = "python-fcl" }, + { name = "pyyaml" }, + { name = "rtree" }, + { name = "tqdm" }, + { name = "trimesh" }, +] +misc = [ + { name = "catkin-pkg" }, + { name = "cerebras-cloud-sdk" }, + { name = "clip" }, + { name = "einops" }, + { name = "empy" }, + { name = "gdown" }, + { name = "googlemaps" }, + { name = "ipykernel" }, + { name = "lark" }, + { name = "onnx" }, + { name = "open-clip-torch" }, + { name = "opencv-contrib-python" }, + { name = "pygame" }, + { name = "python-multipart" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sentence-transformers" }, + { name = "tensorboard" }, + { name = "tensorzero" }, + { name = "tiktoken" }, + { name = "timm" }, + { name = "torchreid" }, + { name = "typeguard" }, + { name = "xarm-python-sdk" }, + { name = "yapf" }, +] +perception = [ + { name = "filterpy" }, + { name = "lap" }, + { name = "moondream" }, + { name = "pillow" }, + { name = "transformers", extra = ["torch"] }, + { name = "ultralytics" }, +] +sim = [ + { name = "mujoco" }, + { name = "playground" }, +] +unitree = [ + { name = "anthropic" }, + { name = "bitsandbytes", marker = "sys_platform == 'linux'" }, + { name = "fastapi" }, + { name = "ffmpeg-python" }, + { name = "filterpy" }, + { name = "langchain" }, + { name = "langchain-chroma" }, + { name = "langchain-core" }, + { name = "langchain-huggingface" }, + { name = "langchain-ollama" }, + { name = "langchain-openai" }, + { name = "langchain-text-splitters" }, + { name = "lap" }, + { name = "mcp" }, + { name = "moondream" }, + { name = "ollama" }, + { name = "openai" }, + { name = "openai-whisper" }, + { name = "pillow" }, + { name = "rerun-sdk" }, + { name = "sounddevice" }, + { name = "soundfile" }, + { name = "sse-starlette" }, + { name = "transformers", extra = ["torch"] }, + { name = "ultralytics" }, + { name = "unitree-webrtc-connect-leshy" }, + { name = "uvicorn" }, +] +visualization = [ + { name = "rerun-sdk" }, +] +web = [ + { name = "fastapi" }, + { name = "ffmpeg-python" }, + { name = "soundfile" }, + { name = "sse-starlette" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "anthropic", marker = "extra == 'agents'", specifier = ">=0.19.0" }, + { name = "asyncio", specifier = "==3.4.3" }, + { name = "bitsandbytes", marker = "sys_platform == 'linux' and extra == 'agents'", specifier = ">=0.48.2,<1.0" }, + { name = "catkin-pkg", marker = "extra == 'misc'" }, + { name = "cerebras-cloud-sdk", marker = "extra == 'misc'" }, + { name = "clip", marker = "extra == 'cuda'", git = "https://github.com/openai/CLIP.git" }, + { name = "clip", marker = "extra == 'misc'", git = "https://github.com/openai/CLIP.git" }, + { name = "colorlog", specifier = "==6.9.0" }, + { name = "contact-graspnet-pytorch", marker = "extra == 'manipulation'", git = "https://github.com/dimensionalOS/contact_graspnet_pytorch.git" }, + { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.0" }, + { name = "ctransformers", marker = "extra == 'cpu'", specifier = "==0.2.27" }, + { name = "ctransformers", extras = ["cuda"], marker = "extra == 'cuda'", specifier = "==0.2.27" }, + { name = "cupy-cuda12x", marker = "extra == 'cuda'", specifier = "==13.6.0" }, + { name = "dask", extras = ["complete"], specifier = "==2025.5.1" }, + { name = "dataclasses", marker = "extra == 'cuda'" }, + { name = "detectron2", marker = "extra == 'cuda'", git = "https://github.com/facebookresearch/detectron2.git?tag=v0.6" }, + { name = "dimos", extras = ["agents", "web", "perception", "visualization"], marker = "extra == 'unitree'" }, + { name = "dimos-lcm" }, + { name = "einops", marker = "extra == 'misc'", specifier = "==0.8.1" }, + { name = "empy", marker = "extra == 'misc'", specifier = "==3.3.4" }, + { name = "fastapi", marker = "extra == 'web'", specifier = ">=0.115.6" }, + { name = "fasttext", marker = "extra == 'cuda'" }, + { name = "ffmpeg-python", marker = "extra == 'web'" }, + { name = "filterpy", marker = "extra == 'perception'", specifier = ">=1.4.5" }, + { name = "ftfy", marker = "extra == 'cuda'" }, + { name = "gdown", marker = "extra == 'misc'", specifier = "==5.2.0" }, + { name = "googlemaps", marker = "extra == 'misc'", specifier = ">=4.10.0" }, + { name = "h5py", marker = "extra == 'manipulation'", specifier = ">=3.7.0" }, + { name = "ipykernel", marker = "extra == 'misc'" }, + { name = "kaleido", marker = "extra == 'manipulation'", specifier = ">=0.2.1" }, + { name = "langchain", marker = "extra == 'agents'", specifier = ">=1,<2" }, + { name = "langchain-chroma", marker = "extra == 'agents'", specifier = ">=1,<2" }, + { name = "langchain-core", marker = "extra == 'agents'", specifier = ">=1,<2" }, + { name = "langchain-huggingface", marker = "extra == 'agents'", specifier = ">=1,<2" }, + { name = "langchain-ollama", marker = "extra == 'agents'", specifier = ">=1,<2" }, + { name = "langchain-openai", marker = "extra == 'agents'", specifier = ">=1,<2" }, + { name = "langchain-text-splitters", marker = "extra == 'agents'", specifier = ">=1,<2" }, + { name = "lap", marker = "extra == 'perception'", specifier = ">=0.5.12" }, + { name = "lark", marker = "extra == 'misc'" }, + { name = "llvmlite", specifier = ">=0.42.0" }, + { name = "lvis", marker = "extra == 'cuda'" }, + { name = "lxml-stubs", marker = "extra == 'dev'", specifier = ">=0.5.1,<1" }, + { name = "matplotlib", marker = "extra == 'manipulation'", specifier = ">=3.7.1" }, + { name = "mcp", marker = "extra == 'agents'", specifier = ">=1.0.0" }, + { name = "mmcv", marker = "extra == 'cuda'", specifier = ">=2.1.0" }, + { name = "mmengine", marker = "extra == 'cuda'", specifier = ">=0.10.3" }, + { name = "moondream", marker = "extra == 'perception'" }, + { name = "mss", marker = "extra == 'cuda'" }, + { name = "mujoco", marker = "extra == 'sim'", specifier = ">=3.3.4" }, + { name = "mypy", marker = "extra == 'dev'", specifier = "==1.19.0" }, + { name = "nltk", marker = "extra == 'cuda'" }, + { name = "numba", specifier = ">=0.60.0" }, + { name = "numpy", specifier = ">=1.26.4" }, + { name = "nvidia-nvimgcodec-cu12", extras = ["all"], marker = "extra == 'cuda'" }, + { name = "ollama", marker = "extra == 'agents'", specifier = ">=0.6.0" }, + { name = "onnx", marker = "extra == 'misc'" }, + { name = "onnxruntime", marker = "extra == 'cpu'" }, + { name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.1" }, + { name = "open-clip-torch", marker = "extra == 'misc'", specifier = "==3.2.0" }, + { name = "open3d" }, + { name = "openai", marker = "extra == 'agents'" }, + { name = "openai-whisper", marker = "extra == 'agents'" }, + { name = "opencv-contrib-python", marker = "extra == 'misc'", specifier = "==4.10.0.84" }, + { name = "opencv-python" }, + { name = "pandas", marker = "extra == 'manipulation'", specifier = ">=1.5.2" }, + { name = "pandas-stubs", marker = "extra == 'dev'", specifier = ">=2.3.2.250926,<3" }, + { name = "pillow", marker = "extra == 'perception'" }, + { name = "piper-sdk", marker = "extra == 'manipulation'" }, + { name = "playground", marker = "extra == 'sim'", specifier = ">=0.0.5" }, + { name = "plotext", specifier = "==5.3.2" }, + { name = "plotly", marker = "extra == 'manipulation'", specifier = ">=5.9.0" }, + { name = "plum-dispatch", specifier = "==2.5.7" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.2.0" }, + { name = "pydantic" }, + { name = "pydantic-settings", specifier = ">=2.11.0,<3" }, + { name = "pygame", marker = "extra == 'misc'", specifier = ">=2.6.1" }, + { name = "pymavlink", marker = "extra == 'drone'" }, + { name = "pyquaternion", marker = "extra == 'manipulation'", specifier = ">=0.9.9" }, + { name = "pyrender", marker = "extra == 'manipulation'", specifier = ">=0.1.45" }, + { name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.5" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==0.26.0" }, + { name = "pytest-env", marker = "extra == 'dev'", specifier = "==1.1.5" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = "==3.15.0" }, + { name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" }, + { name = "python-dotenv" }, + { name = "python-fcl", marker = "extra == 'manipulation'", specifier = ">=0.7.0.4" }, + { name = "python-multipart", marker = "extra == 'misc'", specifier = "==0.0.20" }, + { name = "pyturbojpeg", specifier = "==1.8.2" }, + { name = "pyyaml", marker = "extra == 'manipulation'", specifier = ">=6.0" }, + { name = "reactivex" }, + { name = "regex", marker = "extra == 'cuda'" }, + { name = "requests-mock", marker = "extra == 'dev'", specifier = "==1.12.1" }, + { name = "rerun-sdk", marker = "extra == 'visualization'", specifier = ">=0.20.0" }, + { name = "rtree", marker = "extra == 'manipulation'" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.3" }, + { name = "scikit-learn", marker = "extra == 'misc'" }, + { name = "scipy", specifier = ">=1.15.1" }, + { name = "sentence-transformers", marker = "extra == 'misc'" }, + { name = "sortedcontainers", specifier = "==2.4.0" }, + { name = "sounddevice", marker = "extra == 'agents'" }, + { name = "soundfile", marker = "extra == 'web'" }, + { name = "sse-starlette", marker = "extra == 'web'", specifier = ">=2.2.1" }, + { name = "structlog", specifier = ">=25.5.0,<26" }, + { name = "tensorboard", marker = "extra == 'misc'", specifier = "==2.20.0" }, + { name = "tensorzero", marker = "extra == 'misc'", specifier = "==2025.7.5" }, + { name = "terminaltexteffects", specifier = "==0.12.2" }, + { name = "terminaltexteffects", marker = "extra == 'dev'", specifier = "==0.12.2" }, + { name = "textual", specifier = "==3.7.1" }, + { name = "tiktoken", marker = "extra == 'misc'", specifier = ">=0.8.0" }, + { name = "timm", marker = "extra == 'misc'", specifier = ">=1.0.15" }, + { name = "torchreid", marker = "extra == 'misc'", specifier = "==0.2.5" }, + { name = "tqdm", marker = "extra == 'manipulation'", specifier = ">=4.65.0" }, + { name = "transformers", extras = ["torch"], marker = "extra == 'perception'", specifier = "==4.49.0" }, + { name = "trimesh", marker = "extra == 'manipulation'", specifier = ">=3.22.0" }, + { name = "typeguard", marker = "extra == 'misc'" }, + { name = "typer", specifier = ">=0.19.2,<1" }, + { name = "types-colorama", marker = "extra == 'dev'", specifier = ">=0.4.15.20250801,<1" }, + { name = "types-defusedxml", marker = "extra == 'dev'", specifier = ">=0.7.0.20250822,<1" }, + { name = "types-gevent", marker = "extra == 'dev'", specifier = ">=25.4.0.20250915,<26" }, + { name = "types-greenlet", marker = "extra == 'dev'", specifier = ">=3.2.0.20250915,<4" }, + { name = "types-jmespath", marker = "extra == 'dev'", specifier = ">=1.0.2.20250809,<2" }, + { name = "types-jsonschema", marker = "extra == 'dev'", specifier = ">=4.25.1.20251009,<5" }, + { name = "types-networkx", marker = "extra == 'dev'", specifier = ">=3.5.0.20251001,<4" }, + { name = "types-protobuf", marker = "extra == 'dev'", specifier = ">=6.32.1.20250918,<7" }, + { name = "types-psutil", marker = "extra == 'dev'", specifier = ">=7.0.0.20251001,<8" }, + { name = "types-pysocks", marker = "extra == 'dev'", specifier = ">=1.7.1.20251001,<2" }, + { name = "types-pytz", marker = "extra == 'dev'", specifier = ">=2025.2.0.20250809,<2026" }, + { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20250915,<7" }, + { name = "types-simplejson", marker = "extra == 'dev'", specifier = ">=3.20.0.20250822,<4" }, + { name = "types-tabulate", marker = "extra == 'dev'", specifier = ">=0.9.0.20241207,<1" }, + { name = "types-tensorflow", marker = "extra == 'dev'", specifier = ">=2.18.0.20251008,<3" }, + { name = "types-tqdm", marker = "extra == 'dev'", specifier = ">=4.67.0.20250809,<5" }, + { name = "ultralytics", marker = "extra == 'perception'", specifier = ">=8.3.70" }, + { name = "unitree-webrtc-connect-leshy", marker = "extra == 'unitree'", specifier = ">=2.0.7" }, + { name = "uvicorn", marker = "extra == 'web'", specifier = ">=0.34.0" }, + { name = "watchdog", marker = "extra == 'dev'", specifier = ">=3.0.0" }, + { name = "xarm-python-sdk", marker = "extra == 'misc'", specifier = ">=1.17.0" }, + { name = "xformers", marker = "extra == 'cuda'", specifier = ">=0.0.20" }, + { name = "yapf", marker = "extra == 'misc'", specifier = "==0.40.2" }, +] +provides-extras = ["misc", "visualization", "agents", "web", "perception", "unitree", "manipulation", "cpu", "cuda", "dev", "sim", "drone"] + +[[package]] +name = "dimos-lcm" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "foxglove-websocket" }, + { name = "lcm" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/2f/c24d06fc33f0042b3caa4cb2177493a13661f587c5e26788f42c25aed530/dimos_lcm-0.1.1.tar.gz", hash = "sha256:7ef035c3b0bae8a422dc3b38669757982626a7efbd366907be4f8b47700d8289", size = 83474, upload-time = "2026-01-03T13:20:05.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/01/00065b713b1b9c23371b67292383e1f4ca83766fb258b9efb612a330188b/dimos_lcm-0.1.1-py3-none-any.whl", hash = "sha256:4e0906fa98ce57be6015b26f3e5e1e7a701219f65805540e4f6ff1edcb527453", size = 497467, upload-time = "2026-01-03T13:20:03.865Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distributed" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "dask" }, + { name = "jinja2" }, + { name = "locket" }, + { name = "msgpack" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "sortedcontainers" }, + { name = "tblib" }, + { name = "toolz" }, + { name = "tornado" }, + { name = "urllib3" }, + { name = "zict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/ba/45950f405d023a520a4d10753ef40209a465b86c8fdc131236ec29bcb15c/distributed-2025.5.1.tar.gz", hash = "sha256:cf1d62a2c17a0a9fc1544bd10bb7afd39f22f24aaa9e3df3209c44d2cfb16703", size = 1107874, upload-time = "2025-05-20T19:54:26.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/65/89601dcc7383f0e5109e59eab90677daa9abb260d821570cd6089c8894bf/distributed-2025.5.1-py3-none-any.whl", hash = "sha256:74782b965ddb24ce59c6441fa777e944b5962d82325cc41f228537b59bb7fbbe", size = 1014789, upload-time = "2025-05-20T19:54:21.935Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "einops" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805, upload-time = "2025-02-09T03:17:00.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359, upload-time = "2025-02-09T03:17:01.998Z" }, +] + +[[package]] +name = "empy" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/95/88ed47cb7da88569a78b7d6fb9420298df7e99997810c844a924d96d3c08/empy-3.3.4.tar.gz", hash = "sha256:73ac49785b601479df4ea18a7c79bc1304a8a7c34c02b9472cf1206ae88f01b3", size = 62857, upload-time = "2019-03-21T20:22:03.951Z" } + +[[package]] +name = "etils" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/a0/522bbff0f3cdd37968f90dd7f26c7aa801ed87f5ba335f156de7f2b88a48/etils-1.13.0.tar.gz", hash = "sha256:a5b60c71f95bcd2d43d4e9fb3dc3879120c1f60472bb5ce19f7a860b1d44f607", size = 106368, upload-time = "2025-07-15T10:29:10.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl", hash = "sha256:d9cd4f40fbe77ad6613b7348a18132cc511237b6c076dbb89105c0b520a4c6bb", size = 170603, upload-time = "2025-07-15T10:29:09.076Z" }, +] + +[package.optional-dependencies] +epath = [ + { name = "fsspec" }, + { name = "importlib-resources" }, + { name = "typing-extensions" }, + { name = "zipp" }, +] +epy = [ + { name = "typing-extensions" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[[package]] +name = "fastcrc" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/79/0afaff8ff928ce9990ca883998c4ea7a7f07f2dfea3ebd6d65ba2aadfd4e/fastcrc-0.3.5.tar.gz", hash = "sha256:3705cbad6b3f283a04256f97ae899404794395090ff5966eac79fe303c13e93e", size = 11979, upload-time = "2025-12-31T18:23:09.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/6f/4777a0687161bd73a0be8efc6d000f30687f82aa8860f0259a0bad4a29b5/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45a04142b34d1a54891b16766955cc38fc85a2323094457e9a03f8d918a389df", size = 283015, upload-time = "2025-12-31T18:19:46.237Z" }, + { url = "https://files.pythonhosted.org/packages/57/88/66c38ffc73dd3f2e935b0d0b21a33edc79990ee7c991a76aeefb2a105628/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e44a765e411a2bda54c186d689af383686c0d78010e10944c954e6e9bfcd09f7", size = 290648, upload-time = "2025-12-31T18:20:03.301Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e3/63ceaf3792bb4d5e746510194522c4f5c028bd706f2fb04f9879592bc2b5/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebab06b7b90606e31e572392ba1ed37211f7b270db339ceca8f93762fc5c2d54", size = 408788, upload-time = "2025-12-31T18:20:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/86/69/576d04272abbf2e9b86101f46701e556355c1abe0bb3de9e109710bc4b22/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f30b692f574b8b79b42f1fbd6858633e864f5ccf8c0e64cb37d3ba62c51e0d8", size = 307550, upload-time = "2025-12-31T18:21:00.849Z" }, + { url = "https://files.pythonhosted.org/packages/70/e9/89898caa3000fc1252effb8fea1b5623ae50eca18af573c5a1f5ac156174/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e93da6affe340258f1c34b13dcabc67d947b5dc4f7a26da3df86bb910baa21a0", size = 287814, upload-time = "2025-12-31T18:21:29.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/73/8aeaf53c0e7f4aa77356b9f02fcb36124b71a58510733b4871838396954e/fastcrc-0.3.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:214e23ddd648aa83d2a55b22d3045ec5effc5dd3e4119957fb724f5aa6b1613d", size = 291599, upload-time = "2025-12-31T18:20:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/93/51/0e153482e481a955bdbabbb5b0acf07004f09943f0e3895f0c48c8b0cfc8/fastcrc-0.3.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7f804305286f5ad547dace645c1f47aa2993c056ba81bfeb5879a07aeafac56", size = 300587, upload-time = "2025-12-31T18:21:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8c/b8064404ef151e23112d6b1a602d6c92ef1c0920fca67f8cfd2b34d37513/fastcrc-0.3.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8392ee32adc4e278ba24f7eb87f7768ea0bccaccf6fd6f66dba6465001f05842", size = 465311, upload-time = "2025-12-31T18:21:52.704Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/3ea91a6901f486c549ad6cbb89d2ce29d0eb5b919d3ee44c9f0a6b322a55/fastcrc-0.3.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ff21e981ceacf9edebfebdca481b14e8662128a86e811d6d18237274a445cc94", size = 560907, upload-time = "2025-12-31T18:22:12.391Z" }, + { url = "https://files.pythonhosted.org/packages/90/39/acc1e9f012bc71a6b5d04b7f20279f11ab9d2814d7c65bb0e294d9808cb1/fastcrc-0.3.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85123fa2895f1608967b31e5aa675a1022fe44aecd6994d1e26221e1bcdc208d", size = 520223, upload-time = "2025-12-31T18:22:30.909Z" }, + { url = "https://files.pythonhosted.org/packages/05/0d/9a51369da7de00afef5d7a746e88ca632f3c9c5ba623492305a5edb6dacb/fastcrc-0.3.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d027f5edcb8de021f2efd34507031978b3ea046a52681294b2eb0478bfc902a6", size = 490849, upload-time = "2025-12-31T18:22:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5b/d50b6e8e04ead6cbd2c0593e8775343c55631cf4ffded8ef0ae7348b295c/fastcrc-0.3.5-cp310-cp310-win_amd64.whl", hash = "sha256:5bf02d21694aded7e1293b4b0dbad4c9ba6c21f8a64a4e391230b56a3d341570", size = 147556, upload-time = "2025-12-31T18:23:10.838Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/5442f5ed1c5bcb030863612b7e4c7ead6e6f7158c5271dc7df31b6b31e50/fastcrc-0.3.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b43219f6b52a6aad73f33e397b504bf41e9e82c4db33483d7116624d78f79d2b", size = 258410, upload-time = "2025-12-31T18:21:46.246Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/868ecef894d2e233e6389567a59d4997ddfcac86d09fcfa887c84820bf37/fastcrc-0.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce9fb7d960c3bd167924dfca0fdc5dc1c031d0c69e54f67b9a3f3f80e416222c", size = 254154, upload-time = "2025-12-31T18:21:40.975Z" }, + { url = "https://files.pythonhosted.org/packages/85/df/9b749669136c5e813b85d02b1525afd4d1b1f100df94bc83de43645940dd/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09f9e741545a2ddff2510e11d7a81493054c281304896f82ef49e74a13f730f1", size = 282905, upload-time = "2025-12-31T18:19:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ea/751d815b5c0a1f455eba6202ffe68ba8d3d145ae06367ef17aeb95a92675/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd2f00e7a497d8bc55909ad8e4ded8b1e66e365f2334aba7e6afd6040da941b8", size = 290535, upload-time = "2025-12-31T18:20:05.084Z" }, + { url = "https://files.pythonhosted.org/packages/47/11/67625802a5fa68ed5ca237b42320a9a60098886eb3f9875ceb7035ebfbe0/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41e48f8ffe70f8c4efd6e1a8e662dc9fa30ae27b35ddd270debd095286234713", size = 408362, upload-time = "2025-12-31T18:20:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/cb/59/f9289da84866f8f9e345b96f92a55437db0d8575a706fa31dc22beff198e/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7f2d18026a1248ab004719ded24c6cb101d53c611690c4d807605036bf235e8", size = 307457, upload-time = "2025-12-31T18:21:02.07Z" }, + { url = "https://files.pythonhosted.org/packages/3b/66/80171dc72309ab97224ab55f3402029a8a0dbf28fbb44da7402cb12fda9a/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e969315376ac484b40e7a962788251c3e1dcd0d86f8e770a5a92f3d7d43af9", size = 287591, upload-time = "2025-12-31T18:21:30.974Z" }, + { url = "https://files.pythonhosted.org/packages/4e/50/f89493bd44cf682eac0ec68f378ac098d786d2aa45f2f8e8c7292130e21d/fastcrc-0.3.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8bdc486207e8d3278746a3c91adbe367f5439a4898acc758be22c13aead4241a", size = 291394, upload-time = "2025-12-31T18:20:43.412Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9a/e1472e9b45e4f5774b40644f1e92870960add0832dc45d832ee9dd7a4343/fastcrc-0.3.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a488e2e74621fdd198e3fbc43e53c58c98ce4c52c9a903caf0422e219858b1a5", size = 300273, upload-time = "2025-12-31T18:21:20.035Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ba/b6f5b750a77bcd712e3a6a05e8c6b07b584738af9b264936254579ef19b0/fastcrc-0.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:94b0809c0041269501739535ff405f413fc7753145b5ab42e1ba9149656aacf6", size = 465298, upload-time = "2025-12-31T18:21:54.028Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f5/711f41226ba6a8fe788f93f1123581481b34eb083a0319d26fc72deb3e45/fastcrc-0.3.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e20086dff65974ff6f2800e752bf34eb79ef4ab1ed9a9171c754ffad61e4da34", size = 560732, upload-time = "2025-12-31T18:22:13.8Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/ba53b7f1f0b4a4c28f75d1603bd5e535255e2c5727af206c9664f151e04a/fastcrc-0.3.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1c7273ba4d85d1502230a4f9afdcc618c7e87c4334006f553d3023120259c4", size = 519984, upload-time = "2025-12-31T18:22:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/75/7f234ed34a1cc405a0fc839d6cd01cf1744ace0a12ec8462069d35f9d9d9/fastcrc-0.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:241722fe3a6c1f8ff184f74f4fb368d194484df51eb5ee35c121492a1d758a70", size = 490533, upload-time = "2025-12-31T18:22:51.49Z" }, + { url = "https://files.pythonhosted.org/packages/29/ee/920bc7471e58dc85e6914f8d5f143da61de6158226fadcf244f0ce0d15b1/fastcrc-0.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:35a5ee5ceff7fe05bce8e5e937f80ce852506efbe345af3fc552bd7e9eed86cf", size = 147383, upload-time = "2025-12-31T18:23:12.849Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6e/d57d65fb5a36fcbf6d35f759172ebf18646c2abdc3ce5300d4b1c010337a/fastcrc-0.3.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6d0a131010c608f46ad2ab1aea2b4ec2a01be90f18af8ff94b58ded7043d123e", size = 255680, upload-time = "2025-12-31T18:21:47.841Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/56a568a74f35bbd0b6b4b983b39de06ba7a21bc0502545a63d9eca8004a3/fastcrc-0.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3302c42d6356da9b0cad35e9cebff262c4542a5cdace98857a16bf7203166ed", size = 251091, upload-time = "2025-12-31T18:21:42.197Z" }, + { url = "https://files.pythonhosted.org/packages/27/bf/a7412fef1676e98ba07cb64b8a7b5b721a5b63f8fc6cccad215e52b4ac67/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8883e1ad3e530f9c97f41fbee35ae555876186475918fd42da5f78e6da674322", size = 282008, upload-time = "2025-12-31T18:19:48.897Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e1/a70192cccd292a977998ff44150cf12680dc82b9f706df89f757903d275f/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15ada82abfc4d6e775b61d4591b42ce2c95994ab9139bc31ba2085ba05b32135", size = 290131, upload-time = "2025-12-31T18:20:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/e2/16/5d1ac72c26494a7eb9ced6034bbdde1efbbbfbb1275c70e70748188e622b/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:402c4663ecb5eb00d2ddb58407796cfb731f72e9e348f152809d6292c5229ba7", size = 407328, upload-time = "2025-12-31T18:20:26.344Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/c54608230fede639d3d443cd6fd08cf53457fe347f13649112816d94fd66/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4270ff2603b6d5bdac43c92e317605cb921f605a05d383917222ada402ed0b8e", size = 307459, upload-time = "2025-12-31T18:21:03.396Z" }, + { url = "https://files.pythonhosted.org/packages/c3/37/379fae277f2b73d0703996c5b78e5ebdde1a346d8c4d5bb9a6fb2fa4df6b/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9088dc6f0ff21535fd29ff4639ce5e7b5cb4666fe30040fbfe29614c46ef6c7", size = 286617, upload-time = "2025-12-31T18:21:32.338Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/e389d565cac63d620e9e501ee3b297b01144165eaef9fdd616fbfa1fdbd0/fastcrc-0.3.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:4b5860aaf5e1114731b63e924be35179b570016ac3fcdd051265c5665c34efa9", size = 290784, upload-time = "2025-12-31T18:20:44.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/f8/2857b9e0076d4d5838f9393e5d47624fe28b9c6f154697c8e420249f3c4e/fastcrc-0.3.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a69b53da8ffbfe60099555a9f38ebb05519ba76233caf0f866ac5943efd1df3", size = 299395, upload-time = "2025-12-31T18:21:21.254Z" }, + { url = "https://files.pythonhosted.org/packages/fb/60/e69d182d150f41ca8db07c0ba5d77d371ee52ebce13537d1d487a45980aa/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a9d2b2afcfab28dbc9fa4aa516a4570524cb74d0aa734f0cf612bc9070c360d", size = 464295, upload-time = "2025-12-31T18:21:55.355Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/60f3379f11f55e2bda0f46e660b1ae772f3031e751649474c9ba7ad5e52d/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c96dbf431f7214d22329650121a5f0c41377722833f255f2d926d7df0d4b1143", size = 560169, upload-time = "2025-12-31T18:22:15.081Z" }, + { url = "https://files.pythonhosted.org/packages/76/5c/8bd19ba855624aea12a6c2da5cef2cf3d12119c55acd048349583c218a7d/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:186e3f5fdfa912b43cd9700dc6be5c5c126fe8e159eb65f0757a912e0db671d4", size = 518877, upload-time = "2025-12-31T18:22:33.91Z" }, + { url = "https://files.pythonhosted.org/packages/12/54/50b211283dc54f5af3517dee0b94797f04848c844a3765fd56a5aa899a0c/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4680abf2b254599d6d21bb58c1140e4a8142d951d94240c91cc30362c26c54c5", size = 489218, upload-time = "2025-12-31T18:22:52.785Z" }, + { url = "https://files.pythonhosted.org/packages/dd/af/b77460dbe826793fc65a39b4959efa677d12be6d6680cab6b24035b82da2/fastcrc-0.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:aa8614b0340be45b280c2c4f0330a546c71a2167c4414625096254969750b17b", size = 146970, upload-time = "2025-12-31T18:23:14.477Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/f13eb8e644f18c621d1003d1e34209759ed28ace2eb698809558f45f831e/fastcrc-0.3.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:846aca923cc96965f41a9ebb5c2a4172d948d67285b2e6f2af495fda55d2d624", size = 255919, upload-time = "2025-12-31T18:21:49.158Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ab/72a8a7881f1ac1fd5ab8679fe29a57dbf0523d9c5ee9538da744d5e10d95/fastcrc-0.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fe5d56b33a68bc647242345484281e9df7818adb7c6f690393e318a413597872", size = 251366, upload-time = "2025-12-31T18:21:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3a/605dda593c0e089b9eaf8a6b614fd3da174ecd746c7ea1212033f1ff897a/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:953093ed390ad1601288d349a0f08a48562b6b547ee184c8396b88dff50a6a5f", size = 282465, upload-time = "2025-12-31T18:19:50.36Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cb/f0bb9501c96471ec415319b342995d823a2c9482bcebff705fead1e23224/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50679ec92e0c668b76bb1b9f5d48f7341fecc593419a60cce245f05e3631be10", size = 290304, upload-time = "2025-12-31T18:20:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6e/a59baef04c2e991e9a07494b585906aa23017d2262580c453cca29448270/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d377bb515176d08167daa5784f37f0372280042bde0974d6cdcf0874ce33fdc", size = 409004, upload-time = "2025-12-31T18:20:27.895Z" }, + { url = "https://files.pythonhosted.org/packages/86/97/9931fbc57226a83a76ee31fd197e5fb39979cb02cd975a598ab9c9a4e13d/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89e6f4df265d4443c45329857ddee4b445546c39d839e107dc471ba9398cde1d", size = 307402, upload-time = "2025-12-31T18:21:05.107Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2b/aea9bd3694fa323737cf6db7ac63a3fe21cc8987fe6d2b9f97531801688b/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65d6210030a84ed3aaec0736e18908b2e499784c51f6ffd0d8f7d96de90eea1", size = 286802, upload-time = "2025-12-31T18:21:34.406Z" }, + { url = "https://files.pythonhosted.org/packages/15/03/6667737b2a24bd48a15bea1924bed3d7cd322813bc61f5ee3ea7632417fa/fastcrc-0.3.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a1be96b4090518cd6718c24a0f012f7f385fabbd255ce6a7b0d8ec025c9fb377", size = 291114, upload-time = "2025-12-31T18:20:45.99Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/983934a2a56078a022e6f82f3fd6baf40d7e85871658590b57274179dc85/fastcrc-0.3.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63cf91059d8ab4fdb8875296cff961f7e30af81d1263ba11de4feabea980f932", size = 299618, upload-time = "2025-12-31T18:21:22.499Z" }, + { url = "https://files.pythonhosted.org/packages/e4/22/59cfae39201db940a1f97bb0fd8f5508c7065394a19a77cd6c5d6cbf2f6b/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5ad026ab9afe90effe55d2237d0357cc0edcfbb1b7cd7fa6c9c12b8649d30a9", size = 464682, upload-time = "2025-12-31T18:21:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4c/b129f316ddcbf4bf1c0745b209a45a5f2bf5bfd4ccd528893d3d25ce53f6/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2e4e0f053f9c683d11dea4282f9e0a9c5f0299883a4dd83e36eb0da83716f4f9", size = 560357, upload-time = "2025-12-31T18:22:16.694Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/c7342e8a966fb3bff7672b5727e08c54e509a470e4f96623cc5c6ff8679c/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:80fb2879c0e0bb1d20ea599e4033f48a50d3565550a484a4f3f020847d929569", size = 519044, upload-time = "2025-12-31T18:22:35.399Z" }, + { url = "https://files.pythonhosted.org/packages/02/b6/e65ba338709e49d205a612c69996b66af1bfd78e9c9278fffc0bea8613c3/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03ff342e97ff48f9f3c8aa12c48d87ed90b50b753d9cf65d2fecdb8a67cef20d", size = 489513, upload-time = "2025-12-31T18:22:54.496Z" }, + { url = "https://files.pythonhosted.org/packages/58/dd/a0338c32d8e404f32b26b6948d0b60cedc07e1fa76661c331931473ead71/fastcrc-0.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:6d59f686f253478626b9befa4dfff723d7ae5509d2aa507a1bf26cfd4ec05ae4", size = 147181, upload-time = "2025-12-31T18:23:16.014Z" }, + { url = "https://files.pythonhosted.org/packages/59/39/1a656464fca9c32722b36065cbf3ae989da8783e0d012102ade5e968b385/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2b9f060e14b291e35783372afb87227c7f1dc29f876b76f06c39552103b6545", size = 282367, upload-time = "2025-12-31T18:19:51.761Z" }, + { url = "https://files.pythonhosted.org/packages/fa/35/d9efe66a001f989e84e3d1d196f9cc8764dcead87f43e9f22b3f1ea6d1e1/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93e2761f46567334690c4becc8adcd33c3c8bd40c0f05b1a2b151265d57aff73", size = 289402, upload-time = "2025-12-31T18:20:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ca/c10d7fc598528052463d892c4e71c01c00985cfdb86500da3550fb0b6e75/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f07abfb775b6b7c4a55819847f9f9ddd6b710ebc5e822989860771f77a08bf9c", size = 408177, upload-time = "2025-12-31T18:20:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/c5/72/59bbfe8c6bdb17eb86a43a958268da3fefe3d0da67496e3112dfb84f276a/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5c57c4f70c59f6adafa8b3ae8ac1f60da42a87479d6e0eea04dbaa3c00aca6e", size = 307543, upload-time = "2025-12-31T18:21:06.85Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/904760e09f5768ecbf25da8fddf57fb4fb1599b92780294f7e6172e2a398/fastcrc-0.3.5-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:03f50409fbcb118e66739012a7debcfd55dd409d6d87c32986080efdf0a70144", size = 290841, upload-time = "2025-12-31T18:20:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/340e3d9525d442189883fce613134a5acf167d7f3e48d95036f1e0c9a2dc/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89e6247432e481c420daeb670751a496680484c0c6f69e2198522d6f0f6a5b3a", size = 464580, upload-time = "2025-12-31T18:21:58.673Z" }, + { url = "https://files.pythonhosted.org/packages/4c/97/e2a90908069c89ea41b1cf7ae956fb77219d293ebe62cca32d6c2a077c16/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:9d40dcef226068888898c30ef4005c22e697360685d7e40945365bee63e15d26", size = 559567, upload-time = "2025-12-31T18:22:18.133Z" }, + { url = "https://files.pythonhosted.org/packages/91/93/b8652fabe301faf050cd19d2f6ae655a61f743687fb8dfc8e614fbf9c493/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:edabc8ee13f7581e3fb42344999a35a6a253cb65ac019969dc38aa45dac32ee8", size = 518538, upload-time = "2025-12-31T18:22:36.82Z" }, + { url = "https://files.pythonhosted.org/packages/98/c9/585216c6a93b398b3c911653eacaf05c5dc731db39b55f99415025af90bc/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:430ae0104f9eafe893f232e81834144ba31041bcc63a3eb379d22d0978c6e926", size = 489898, upload-time = "2025-12-31T18:22:55.903Z" }, + { url = "https://files.pythonhosted.org/packages/57/aa/845d6257bac319b9c1fe16648f2e3d0efa1bbbaf8a5b7b4977851d8102ae/fastcrc-0.3.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b6507652630bc1076f57fe56f265634df21f811d6882a2723be854c58664318c", size = 255565, upload-time = "2025-12-31T18:21:50.502Z" }, + { url = "https://files.pythonhosted.org/packages/f7/66/f67a5e6bf89fa7de858fd055b5a69f00029c16cabf5dcf9bc750db120257/fastcrc-0.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a4c39b9c4a8a37f2fb0a255e3b63b640a5426c0daf3d790399ea85aaabad7f6", size = 251112, upload-time = "2025-12-31T18:21:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/74/a1/c7568f21ad284e68faed0093ccb68bb5d5b248bd08f6241dedfe69ff000b/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d0621722fc4c17bdd7e772fb3fb5384d7c985bb1c8f66256a1dba374e2d12a5", size = 282301, upload-time = "2025-12-31T18:19:53.016Z" }, + { url = "https://files.pythonhosted.org/packages/35/57/da0342f2702e6b50b4d4e5befb2fcd127e82762fe30925b9160eed2184a1/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:648d63f41da1216ef1143573fef35ad2eb34913496ccec58138c2619b10ea469", size = 289811, upload-time = "2025-12-31T18:20:11.584Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d6/94e24eb87bb02546b2b809a8c05034e1e90df3179a44260451e640533a9c/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2be6196f5c4a40b7fca04ef0cc176aa30ac2e19206f362b953fe0db00ea76080", size = 406010, upload-time = "2025-12-31T18:20:30.67Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/c4d71a07bcba4761db0c8ab70b4cb2d1fbd803f72d97e8cde188bd1cb658/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c70e985afa6ec16eebaeb0f3d6bfacb46826636f59853f1169e2eb2a336a2c5", size = 307190, upload-time = "2025-12-31T18:21:08.204Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/bba64d12b0c22d839ddb8cfafae49bb332ae925e839fff2b7752bb20d8dc/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562820b556d9d2070948a89cb76c34d6ec777bbcd3f70bdb89638a16b6072964", size = 286376, upload-time = "2025-12-31T18:21:35.986Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/4fab9f16bb9e6eb6d0c74f913691c25c6f332761c255edd5f3e22a57bd65/fastcrc-0.3.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:99a333fa78aa32f2a024528828cfad71aa39533f210d121ec3962e70542d857b", size = 290450, upload-time = "2025-12-31T18:20:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/41/2c/d4e4072c39f40c8a8550499722ab2539d1de1f30feb97f637d48d33325c7/fastcrc-0.3.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d752dc2285dc446fdf8e3d6e349a3428f46f7b8f059842bdabbb989332d1f3e", size = 299070, upload-time = "2025-12-31T18:21:23.761Z" }, + { url = "https://files.pythonhosted.org/packages/0d/37/12fe830bdfe3b39367645d5b2f8fb2359dc46e67346e91fdd7b9253ad501/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c996c9273c4d3b4c2902852a51b7142bd14a6c750f84bec47778225f7f8952c3", size = 464591, upload-time = "2025-12-31T18:22:00.931Z" }, + { url = "https://files.pythonhosted.org/packages/c3/95/edda45991f71b1ec6022c1649397c1b3d82d45cf3c6323684f9a97a4a9ce/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d595809fd266b037c22a22c2d72375c3f572d89200827ecaaa7c002b015b8a2e", size = 559904, upload-time = "2025-12-31T18:22:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d8/aaeb37ebc079ad945dd3aca3fae0f358d5c05af760305e6b6fef63d1c4c7/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6ee18383827caee641d22f60be89e0a0220c1d5a00c5e8cbb744aac1d5bc876", size = 518525, upload-time = "2025-12-31T18:22:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b5/7479aadffc83bb152884f65c8d543e61d2e95592d4ed1e902019fe5b80f2/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:276591e6828cd4dba70cdfc16450607d742c78119d46060a6cf90ef006a13c71", size = 489362, upload-time = "2025-12-31T18:22:57.322Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/4c3af4d4410aff8eef2077425406ddb20ccd3eb8b0fb5d6b6bd5fd2510a3/fastcrc-0.3.5-cp314-cp314-win32.whl", hash = "sha256:08ade4c88109a21ad098b99a9dc51bb3b689f9079828b5e390373414d0868828", size = 139114, upload-time = "2025-12-31T18:23:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/c2564781aeb0c7ae8a6554329b2f05b8a84a2376d2c0ba170ed44ddcc78c/fastcrc-0.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:01565a368ce5fe8a8578992601575313027fb16f55cf3e62c339ae89ccd6ccd2", size = 147063, upload-time = "2025-12-31T18:23:17.152Z" }, + { url = "https://files.pythonhosted.org/packages/33/79/277500a3ac37b3adf2b1c7ab59ddb72587104b7edb5d1a44f9b0a5af4704/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d169f76f8f6330ef4741eadda4cba8f0254c3ec472ed80ebb98d35fc50227d7c", size = 281969, upload-time = "2025-12-31T18:19:54.201Z" }, + { url = "https://files.pythonhosted.org/packages/08/d3/fe850eaf2e4b9be41fa4ae76c4d02bdf809a382d0d7b63cf71d84b47ecca/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d45e1940d5439f2f6fa1f8f1e4202861fb77335c7432f3fc197960af0c6f335d", size = 289745, upload-time = "2025-12-31T18:20:13.831Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/ec4f384a15a95bbad15e359d9d63294c4842eee08f5c576ee22ff3c45035/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8527fded11884789b0ecb9b7d93d9093e38dbfc47d4daefa948447e2397d10b", size = 407620, upload-time = "2025-12-31T18:20:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/98/b8/feb49bf3a2539df193481915653da52a442395c35ffeaf1deb0a649bae87/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6530a28d45ca0754bbeca3a820ae0cce3ded7f3428ed67b587d3ac8ea45fc4aa", size = 307050, upload-time = "2025-12-31T18:21:09.566Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/93fe977525ccb06a491490f53042b3682f15e626263917e3e94cd66d877a/fastcrc-0.3.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4f1229f339b32987e4ad35ae500564a782ce4e62f260150928c0233f32bb6e83", size = 290287, upload-time = "2025-12-31T18:20:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e5/0b6ae9ca6cc8ae599ca298e8a42d96febbc69b52d285464bb994f0a682ff/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7d7a56aa52c40d4293230d2751f136346d6a2b392fa2a38fe342754a6d9c238e", size = 464193, upload-time = "2025-12-31T18:22:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/4a/97/7d4ed6342b244c30b81daabaa6eac591426e216455205e5c85b8566fcd19/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5816694e6aac595cca7a4331c503ed00a22889e0f97a0fa82779ed27316c90ee", size = 559728, upload-time = "2025-12-31T18:22:21.143Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4e/b9cac563d0692345bc9e6dfdc7db92695dd1b3b431ac8fe61ec1dbd6d637/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b598bb000c4290e1eb847ae20a1e07f4ad064d364c2471864fa4c8ccca0f22f6", size = 518422, upload-time = "2025-12-31T18:22:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/a3/58/ef3751447b173ae84d046f90a7dac869b9ff4e11639694f60d8c04f784ea/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:56d564c7ec3dc8116c291963834f0925b4a32df1ea53d239fd470fcdde6b80e4", size = 488965, upload-time = "2025-12-31T18:22:59.006Z" }, + { url = "https://files.pythonhosted.org/packages/f7/35/77b6bc7a7e0396481b2fa73a8d72972225358e27464d2e244aa484767aa4/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:405011e48990c92b9276b449084cedab5c0d64d1a5f72f088b8a4d47de0fbbae", size = 283892, upload-time = "2025-12-31T18:19:59.267Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1b/0b69cfc6fa1a4cb47a9c9a5b85018232e69f516d766bbc5bb923c175dafa/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1f4165fdc701385fa9a4bd431d9152ea0ec7943de9b48e7758ed38dc0acb4c48", size = 290946, upload-time = "2025-12-31T18:20:19.773Z" }, + { url = "https://files.pythonhosted.org/packages/17/56/6ffbb62317f053dd35901ab493e04fc52e579283d25a7741e6e0c399bd85/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b0def4dbe9587b6a4a592c52f1f66fa0e40a3d1aa9575707ec2fd959224a37", size = 408841, upload-time = "2025-12-31T18:20:37.593Z" }, + { url = "https://files.pythonhosted.org/packages/83/67/6dbd9039a0e827edbc11f5f25770a24fea979e7a0211bd880a42175170e2/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24dc361d014a98220866995f9b82019a5e9bacc4ba5802a07c3ceddcd5c4de9e", size = 308712, upload-time = "2025-12-31T18:21:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/33/d5/cdc732e11d65f039c97e5bdbe06b2a52ea9b300798be86e0893b5422f224/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:20bef9df7276df191a3a9d500f1ea1508fc3b38c0a8982577d14977dabc928ad", size = 292413, upload-time = "2025-12-31T18:20:56.138Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/dda5bfb5bd11362d860c1fbe9211bd1bef1fe303c1636164bb1bd2980bf3/fastcrc-0.3.5-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e6332b6f685f9067e6fe89756246a9bb292b02ebb07127417ed470985ce99e4d", size = 466003, upload-time = "2025-12-31T18:22:07.705Z" }, + { url = "https://files.pythonhosted.org/packages/a0/35/1106f93d26e333d677c14989e9007dab348ab9d05edf36d088d0e4fb2c2b/fastcrc-0.3.5-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:e2338c08961021b74599b8ad4e2d573f1b32e9f21cdf4025cbe9835f1acec5ad", size = 561365, upload-time = "2025-12-31T18:22:26.775Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3c/1c386d858c398eb5f2b8e64d81075205bda2c60638a97a2f8186d640bbd3/fastcrc-0.3.5-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:7f1b6160e2fb9f06fb9cb9425dcbc0056fefefc42ec448d0b05a68ae37ccadca", size = 520807, upload-time = "2025-12-31T18:22:45.296Z" }, + { url = "https://files.pythonhosted.org/packages/76/0c/4ea0247b3783462206b1e9fd90f5fa43b2faee078a3b4a2ab457c4a8174e/fastcrc-0.3.5-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:33b0fdacf8a158140319bbd3a8f2aeeff7e26d8d48802ea32d5957355fd57526", size = 491332, upload-time = "2025-12-31T18:23:05.768Z" }, + { url = "https://files.pythonhosted.org/packages/80/76/587ffb201ff1ae0d891f148dab1b9e50fed1ec97d9961791d36ff6d0dc49/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f537661551c6bf334b69def2c593c6d9e09d0189ef79f283797ae7ae906d3737", size = 283824, upload-time = "2025-12-31T18:20:00.905Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/1ff2241e31777a63f92eec9b6aca7e5029b606c62b263bb4b6e71e695cb2/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b706331c2b1631dd8d551aebe1e3d17b3ab911e25b2e20e62c1d33a6193dd3fc", size = 290742, upload-time = "2025-12-31T18:20:20.967Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d0/85ed12802a49c5d136ca9e411432eef11b257330a2897e83967ff686901e/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f43950f77e6fd5d3435a9b4972cb7df73d736883ab30c3aea8db2b109c38c9c2", size = 408729, upload-time = "2025-12-31T18:20:38.831Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/1e7062d7cde651c2e6d3948a0feb826e9cf4f95478b54d2ec187aabb22f4/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d318159bbac3b5437f557e4b68daf2f2f5d0435996046fdd93d5fe8b4d577e1", size = 308351, upload-time = "2025-12-31T18:21:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/16/74/a47a8d955ff9c6c434cd2e3537bb9db60f5d0a1030702e3efa3aa0383d37/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba44180d802de71833170575ef23091bdd0a33ddc44c74bef408f589eddbe910", size = 287944, upload-time = "2025-12-31T18:21:39.647Z" }, + { url = "https://files.pythonhosted.org/packages/bf/67/406749fae0ecdd445e86f33e4994438b85fcbf754c671a72e53ce82bbf4d/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:2b5a320ec3e4cd92a8c49cd21abcaf5c5e317e9f1980fee09a1350371e593272", size = 292193, upload-time = "2025-12-31T18:20:57.676Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/fde844c0809f9ce9c60b17b4b3977659b0a38e9ecb17236e0dc71cb1b38d/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:562258863d349b1bb19f41f4d735fcc36101c332da18e6318a5773c54c73bff0", size = 300914, upload-time = "2025-12-31T18:21:28.105Z" }, + { url = "https://files.pythonhosted.org/packages/05/c3/76a684b9f8deb35e20f40953399d77474ee7c3830bd58b51455de874d2e4/fastcrc-0.3.5-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e503965227516516bfbffc42d3ddb5018b356a119bc07c009f3030ee2e7de90b", size = 465939, upload-time = "2025-12-31T18:22:09.277Z" }, + { url = "https://files.pythonhosted.org/packages/02/64/c3b6d51719d8816b1b4aa94aa745e119b4b9e6d4b02ad6856ddda67220f0/fastcrc-0.3.5-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:90890a523881e6005b1526451a8f050ad5a3302cf48084305044468314afe1ab", size = 561123, upload-time = "2025-12-31T18:22:28.135Z" }, + { url = "https://files.pythonhosted.org/packages/0d/46/296e6a81454b8d585fcbbe73bbf618856981030245704b2672e50d1910ff/fastcrc-0.3.5-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:e4ab063872bede0e88dc3ffd3815e3c4723c26f35ae6ef2cecd3acbaf7c36aff", size = 520641, upload-time = "2025-12-31T18:22:47.111Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/d5417aa573f502b7aa037a46e1279b4906511d2ad6bb93b0a531a454f393/fastcrc-0.3.5-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fa17dbea2c0984f204318d64da0c5109e8afc0f3fa218d836b42a6c4a6f6a27e", size = 491214, upload-time = "2025-12-31T18:23:07.131Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "fastrlock" +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/b1/1c3d635d955f2b4bf34d45abf8f35492e04dbd7804e94ce65d9f928ef3ec/fastrlock-0.8.3.tar.gz", hash = "sha256:4af6734d92eaa3ab4373e6c9a1dd0d5ad1304e172b1521733c6c3b3d73c8fa5d", size = 79327, upload-time = "2024-12-17T11:03:39.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/02/3f771177380d8690812d5b2b7736dc6b6c8cd1c317e4572e65f823eede08/fastrlock-0.8.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cc5fa9166e05409f64a804d5b6d01af670979cdb12cd2594f555cb33cdc155bd", size = 55094, upload-time = "2024-12-17T11:01:49.721Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/aae7ed94b8122c325d89eb91336084596cebc505dc629b795fcc9629606d/fastrlock-0.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7a77ebb0a24535ef4f167da2c5ee35d9be1e96ae192137e9dc3ff75b8dfc08a5", size = 48220, upload-time = "2024-12-17T11:01:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/96/87/9807af47617fdd65c68b0fcd1e714542c1d4d3a1f1381f591f1aa7383a53/fastrlock-0.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:d51f7fb0db8dab341b7f03a39a3031678cf4a98b18533b176c533c122bfce47d", size = 49551, upload-time = "2024-12-17T11:01:52.316Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/e201634810ac9aee59f93e3953cb39f98157d17c3fc9d44900f1209054e9/fastrlock-0.8.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:767ec79b7f6ed9b9a00eb9ff62f2a51f56fdb221c5092ab2dadec34a9ccbfc6e", size = 49398, upload-time = "2024-12-17T11:01:53.514Z" }, + { url = "https://files.pythonhosted.org/packages/15/a1/439962ed439ff6f00b7dce14927e7830e02618f26f4653424220a646cd1c/fastrlock-0.8.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d6a77b3f396f7d41094ef09606f65ae57feeb713f4285e8e417f4021617ca62", size = 53334, upload-time = "2024-12-17T11:01:55.518Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/1ae90829dd40559ab104e97ebe74217d9da794c4bb43016da8367ca7a596/fastrlock-0.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:92577ff82ef4a94c5667d6d2841f017820932bc59f31ffd83e4a2c56c1738f90", size = 52495, upload-time = "2024-12-17T11:01:57.76Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/5e746ee6f3d7afbfbb0d794c16c71bfd5259a4e3fb1dda48baf31e46956c/fastrlock-0.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3df8514086e16bb7c66169156a8066dc152f3be892c7817e85bf09a27fa2ada2", size = 51972, upload-time = "2024-12-17T11:02:01.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/a7/8b91068f00400931da950f143fa0f9018bd447f8ed4e34bed3fe65ed55d2/fastrlock-0.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:001fd86bcac78c79658bac496e8a17472d64d558cd2227fdc768aa77f877fe40", size = 30946, upload-time = "2024-12-17T11:02:03.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/9e/647951c579ef74b6541493d5ca786d21a0b2d330c9514ba2c39f0b0b0046/fastrlock-0.8.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:f68c551cf8a34b6460a3a0eba44bd7897ebfc820854e19970c52a76bf064a59f", size = 55233, upload-time = "2024-12-17T11:02:04.795Z" }, + { url = "https://files.pythonhosted.org/packages/be/91/5f3afba7d14b8b7d60ac651375f50fff9220d6ccc3bef233d2bd74b73ec7/fastrlock-0.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:55d42f6286b9d867370af4c27bc70d04ce2d342fe450c4a4fcce14440514e695", size = 48911, upload-time = "2024-12-17T11:02:06.173Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/e37bd72d7d70a8a551b3b4610d028bd73ff5d6253201d5d3cf6296468bee/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:bbc3bf96dcbd68392366c477f78c9d5c47e5d9290cb115feea19f20a43ef6d05", size = 50357, upload-time = "2024-12-17T11:02:07.418Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ef/a13b8bab8266840bf38831d7bf5970518c02603d00a548a678763322d5bf/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:77ab8a98417a1f467dafcd2226718f7ca0cf18d4b64732f838b8c2b3e4b55cb5", size = 50222, upload-time = "2024-12-17T11:02:08.745Z" }, + { url = "https://files.pythonhosted.org/packages/01/e2/5e5515562b2e9a56d84659377176aef7345da2c3c22909a1897fe27e14dd/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04bb5eef8f460d13b8c0084ea5a9d3aab2c0573991c880c0a34a56bb14951d30", size = 54553, upload-time = "2024-12-17T11:02:10.925Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8f/65907405a8cdb2fc8beaf7d09a9a07bb58deff478ff391ca95be4f130b70/fastrlock-0.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c9d459ce344c21ff03268212a1845aa37feab634d242131bc16c2a2355d5f65", size = 53362, upload-time = "2024-12-17T11:02:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b9/ae6511e52738ba4e3a6adb7c6a20158573fbc98aab448992ece25abb0b07/fastrlock-0.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33e6fa4af4f3af3e9c747ec72d1eadc0b7ba2035456c2afb51c24d9e8a56f8fd", size = 52836, upload-time = "2024-12-17T11:02:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/3e/c26f8192c93e8e43b426787cec04bb46ac36e72b1033b7fe5a9267155fdf/fastrlock-0.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:5e5f1665d8e70f4c5b4a67f2db202f354abc80a321ce5a26ac1493f055e3ae2c", size = 31046, upload-time = "2024-12-17T11:02:15.033Z" }, + { url = "https://files.pythonhosted.org/packages/00/df/56270f2e10c1428855c990e7a7e5baafa9e1262b8e789200bd1d047eb501/fastrlock-0.8.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8cb2cf04352ea8575d496f31b3b88c42c7976e8e58cdd7d1550dfba80ca039da", size = 55727, upload-time = "2024-12-17T11:02:17.26Z" }, + { url = "https://files.pythonhosted.org/packages/57/21/ea1511b0ef0d5457efca3bf1823effb9c5cad4fc9dca86ce08e4d65330ce/fastrlock-0.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85a49a1f1e020097d087e1963e42cea6f307897d5ebe2cb6daf4af47ffdd3eed", size = 52201, upload-time = "2024-12-17T11:02:19.512Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/cdecb7aa976f34328372f1c4efd6c9dc1b039b3cc8d3f38787d640009a25/fastrlock-0.8.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f13ec08f1adb1aa916c384b05ecb7dbebb8df9ea81abd045f60941c6283a670", size = 53924, upload-time = "2024-12-17T11:02:20.85Z" }, + { url = "https://files.pythonhosted.org/packages/88/6d/59c497f8db9a125066dd3a7442fab6aecbe90d6fec344c54645eaf311666/fastrlock-0.8.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0ea4e53a04980d646def0f5e4b5e8bd8c7884288464acab0b37ca0c65c482bfe", size = 52140, upload-time = "2024-12-17T11:02:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/62/04/9138943c2ee803d62a48a3c17b69de2f6fa27677a6896c300369e839a550/fastrlock-0.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:38340f6635bd4ee2a4fb02a3a725759fe921f2ca846cb9ca44531ba739cc17b4", size = 53261, upload-time = "2024-12-17T11:02:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4b/db35a52589764c7745a613b6943bbd018f128d42177ab92ee7dde88444f6/fastrlock-0.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:da06d43e1625e2ffddd303edcd6d2cd068e1c486f5fd0102b3f079c44eb13e2c", size = 31235, upload-time = "2024-12-17T11:02:25.708Z" }, + { url = "https://files.pythonhosted.org/packages/92/74/7b13d836c3f221cff69d6f418f46c2a30c4b1fe09a8ce7db02eecb593185/fastrlock-0.8.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5264088185ca8e6bc83181dff521eee94d078c269c7d557cc8d9ed5952b7be45", size = 54157, upload-time = "2024-12-17T11:02:29.196Z" }, + { url = "https://files.pythonhosted.org/packages/06/77/f06a907f9a07d26d0cca24a4385944cfe70d549a2c9f1c3e3217332f4f12/fastrlock-0.8.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a98ba46b3e14927550c4baa36b752d0d2f7387b8534864a8767f83cce75c160", size = 50954, upload-time = "2024-12-17T11:02:32.12Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4e/94480fb3fd93991dd6f4e658b77698edc343f57caa2870d77b38c89c2e3b/fastrlock-0.8.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbdea6deeccea1917c6017d353987231c4e46c93d5338ca3e66d6cd88fbce259", size = 52535, upload-time = "2024-12-17T11:02:33.402Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a7/ee82bb55b6c0ca30286dac1e19ee9417a17d2d1de3b13bb0f20cefb86086/fastrlock-0.8.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6e5bfecbc0d72ff07e43fed81671747914d6794e0926700677ed26d894d4f4f", size = 50942, upload-time = "2024-12-17T11:02:34.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/1d/d4b7782ef59e57dd9dde69468cc245adafc3674281905e42fa98aac30a79/fastrlock-0.8.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:2a83d558470c520ed21462d304e77a12639859b205759221c8144dd2896b958a", size = 52044, upload-time = "2024-12-17T11:02:36.613Z" }, + { url = "https://files.pythonhosted.org/packages/28/a3/2ad0a0a69662fd4cf556ab8074f0de978ee9b56bff6ddb4e656df4aa9e8e/fastrlock-0.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8", size = 30472, upload-time = "2024-12-17T11:02:37.983Z" }, +] + +[[package]] +name = "fasttext" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pybind11" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/3b/9a10b95eaf565358339162848863197c3f0a29b540ca22b2951df2d66a48/fasttext-0.9.3.tar.gz", hash = "sha256:eb03f2ef6340c6ac9e4398a30026f05471da99381b307aafe2f56e4cd26baaef", size = 73439, upload-time = "2024-06-12T09:44:42.544Z" } + +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "future" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/e0/a75dbe4bca1e7d41307323dad5ea2efdd95408f74ab2de8bd7dba9b51a1a/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510, upload-time = "2026-01-02T15:33:32.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697, upload-time = "2026-01-02T15:33:31.133Z" }, +] + +[[package]] +name = "filterpy" +version = "1.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/1d/ac8914360460fafa1990890259b7fa5ef7ba4cd59014e782e4ab3ab144d8/filterpy-1.4.5.zip", hash = "sha256:4f2a4d39e4ea601b9ab42b2db08b5918a9538c168cff1c6895ae26646f3d73b1", size = 177985, upload-time = "2018-10-10T22:38:24.63Z" } + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "flask-cors" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, +] + +[[package]] +name = "flask-socketio" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "python-socketio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/28/deac60f5c6faf9c3e0aed07aa3a92b0741c6709841aa3eba12417bbc8303/flask_socketio-5.6.0.tar.gz", hash = "sha256:42a7bc552013633875ad320e39462323b4f7334594f1658d72b6ffed99940d4c", size = 37667, upload-time = "2025-12-25T19:30:26.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/f9/6a743926417124d5c6dcbc056d569b8bde7be73596404d35881a3ff1496e/flask_socketio-5.6.0-py3-none-any.whl", hash = "sha256:894ad031d9440ca3fad388dd301ca33d13b301a2563933ca608d30979ef0a7c1", size = 18397, upload-time = "2025-12-25T19:30:24.928Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "flax" +version = "0.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "msgpack", marker = "python_full_version < '3.11'" }, + { name = "optax", marker = "python_full_version < '3.11'" }, + { name = "orbax-checkpoint", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, + { name = "rich", marker = "python_full_version < '3.11'" }, + { name = "tensorstore", version = "0.1.78", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "treescope", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/76/4ea55a60a47e98fcff591238ee26ed4624cb4fdc4893aa3ebf78d0d021f4/flax-0.10.7.tar.gz", hash = "sha256:2930d6671e23076f6db3b96afacf45c5060898f5c189ecab6dda7e05d26c2085", size = 5136099, upload-time = "2025-07-02T06:10:07.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/f6/560d338687d40182c8429cf35c64cc022e0d57ba3e52191c4a78ed239b4e/flax-0.10.7-py3-none-any.whl", hash = "sha256:4033223a9a9969ba0b252e085e9714d0a1e9124ac300aaf48e92c40769c420f6", size = 456944, upload-time = "2025-07-02T06:10:05.807Z" }, +] + +[[package]] +name = "flax" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "jax", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "msgpack", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "optax", marker = "python_full_version >= '3.11'" }, + { name = "orbax-checkpoint", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "rich", marker = "python_full_version >= '3.11'" }, + { name = "tensorstore", version = "0.1.80", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "treescope", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/7e/c4c66ab9b41149cf7a1961907d9a844832af1e76b121b35235a618c92825/flax-0.12.2.tar.gz", hash = "sha256:e9723b0881e571abe61885bb8770f53fdb3c383b6b3f5a923dcf6f1e9a687905", size = 5008370, upload-time = "2025-12-18T22:36:19.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/6b/7b75508251f4220df8f68e7718b476ee3d614a2a51f9eace97393ee91b46/flax-0.12.2-py3-none-any.whl", hash = "sha256:912fdd8a7c623ec8b2694b28d2827608e7fc82a3a6f8fff17ec5038f2bca66f4", size = 488031, upload-time = "2025-12-18T22:36:18.01Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, + { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, + { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "foxglove-websocket" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/b5/df32ac550eb0df9000ed78d872eb19738edecfd88f47fe08588d5066f317/foxglove_websocket-0.1.4.tar.gz", hash = "sha256:2ec8936982e478d103dd90268a572599fc0cce45a4ab95490d5bc31f7c8a8af8", size = 16616, upload-time = "2025-07-14T20:26:28.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/73/3a3e6cb864ddf98800a9236ad497d32e5b50eb1682ac659f7d669d92faec/foxglove_websocket-0.1.4-py3-none-any.whl", hash = "sha256:772e24e2c98bdfc704df53f7177c8ff5bab0abc4dac59a91463aca16debdd83a", size = 14392, upload-time = "2025-07-14T20:26:26.899Z" }, +] + +[[package]] +name = "freetype-py" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/9c/61ba17f846b922c2d6d101cc886b0e8fb597c109cedfcb39b8c5d2304b54/freetype-py-2.5.1.zip", hash = "sha256:cfe2686a174d0dd3d71a9d8ee9bf6a2c23f5872385cf8ce9f24af83d076e2fbd", size = 851738, upload-time = "2024-08-29T18:32:26.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/a8/258dd138ebe60c79cd8cfaa6d021599208a33f0175a5e29b01f60c9ab2c7/freetype_py-2.5.1-py3-none-macosx_10_9_universal2.whl", hash = "sha256:d01ded2557694f06aa0413f3400c0c0b2b5ebcaabeef7aaf3d756be44f51e90b", size = 1747885, upload-time = "2024-08-29T18:32:17.604Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/280ad06dc944e40789b0a641492321a2792db82edda485369cbc59d14366/freetype_py-2.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2f6b3d68496797da23204b3b9c4e77e67559c80390fc0dc8b3f454ae1cd819", size = 1051055, upload-time = "2024-08-29T18:32:19.153Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/853cad240ec63e21a37a512ee19c896b655ce1772d803a3dd80fccfe63fe/freetype_py-2.5.1-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:289b443547e03a4f85302e3ac91376838e0d11636050166662a4f75e3087ed0b", size = 1043856, upload-time = "2024-08-29T18:32:20.565Z" }, + { url = "https://files.pythonhosted.org/packages/93/6f/fcc1789e42b8c6617c3112196d68e87bfe7d957d80812d3c24d639782dcb/freetype_py-2.5.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cd3bfdbb7e1a84818cfbc8025fca3096f4f2afcd5d4641184bf0a3a2e6f97bbf", size = 1108180, upload-time = "2024-08-29T18:32:21.871Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1b/161d3a6244b8a820aef188e4397a750d4a8196316809576d015f26594296/freetype_py-2.5.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3c1aefc4f0d5b7425f014daccc5fdc7c6f914fb7d6a695cc684f1c09cd8c1660", size = 1106792, upload-time = "2024-08-29T18:32:23.134Z" }, + { url = "https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl", hash = "sha256:0b7f8e0342779f65ca13ef8bc103938366fecade23e6bb37cb671c2b8ad7f124", size = 814608, upload-time = "2024-08-29T18:32:24.648Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, +] + +[[package]] +name = "ftfy" +version = "6.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" }, +] + +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, +] + +[[package]] +name = "fvcore" +version = "0.1.5.post20221221" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iopath" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "tabulate" }, + { name = "termcolor" }, + { name = "tqdm" }, + { name = "yacs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/93/d056a9c4efc6c79ba7b5159cc66bb436db93d2cc46dca18ed65c59cc8e4e/fvcore-0.1.5.post20221221.tar.gz", hash = "sha256:f2fb0bb90572ae651c11c78e20493ed19b2240550a7e4bbb2d6de87bdd037860", size = 50217, upload-time = "2022-12-21T08:10:53.563Z" } + +[[package]] +name = "gdown" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "filelock" }, + { name = "requests", extra = ["socks"] }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/6a/37e6b70c5bda3161e40265861e63b64a86bfc6ca6a8f1c35328a675c84fd/gdown-5.2.0.tar.gz", hash = "sha256:2145165062d85520a3cd98b356c9ed522c5e7984d408535409fd46f94defc787", size = 284647, upload-time = "2024-05-12T06:45:12.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/70/e07c381e6488a77094f04c85c9caf1c8008cdc30778f7019bc52e5285ef0/gdown-5.2.0-py3-none-any.whl", hash = "sha256:33083832d82b1101bdd0e9df3edd0fbc0e1c5f14c9d8c38d2a35bf1683b526d6", size = 18235, upload-time = "2024-05-12T06:45:10.017Z" }, +] + +[[package]] +name = "glfw" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/72/642d4f12f61816ac96777f7360d413e3977a7dd08237d196f02da681b186/glfw-2.10.0.tar.gz", hash = "sha256:801e55d8581b34df9aa2cfea43feb06ff617576e2a8cc5dac23ee75b26d10abe", size = 31475, upload-time = "2025-09-12T08:54:38.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/1f/a9ce08b1173b0ab625ee92f0c47a5278b3e76fd367699880d8ee7d56c338/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl", hash = "sha256:5f365a8c94bcea71ec91327e7c16e7cf739128479a18b8c1241b004b40acc412", size = 105329, upload-time = "2025-09-12T08:54:27.938Z" }, + { url = "https://files.pythonhosted.org/packages/7c/96/5a2220abcbd027eebcf8bedd28207a2de168899e51be13ba01ebdd4147a1/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl", hash = "sha256:5328db1a92d07abd988730517ec02aa8390d3e6ef7ce98c8b57ecba2f43a39ba", size = 102179, upload-time = "2025-09-12T08:54:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9d/41/a5bd1d9e1808f400102bd7d328c4ac17b65fb2fc8014014ec6f23d02f662/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_aarch64.whl", hash = "sha256:312c4c1dd5509613ed6bc1e95a8dbb75a36b6dcc4120f50dc3892b40172e9053", size = 230039, upload-time = "2025-09-12T08:54:30.201Z" }, + { url = "https://files.pythonhosted.org/packages/80/aa/3b503c448609dee6cb4e7138b4109338f0e65b97be107ab85562269d378d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_x86_64.whl", hash = "sha256:59c53387dc08c62e8bed86bbe3a8d53ab1b27161281ffa0e7f27b64284e2627c", size = 241984, upload-time = "2025-09-12T08:54:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2d/bfe39a42cad8e80b02bf5f7cae19ba67832c1810bbd3624a8e83153d74a4/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_aarch64.whl", hash = "sha256:c6f292fdaf3f9a99e598ede6582d21c523a6f51f8f5e66213849101a6bcdc699", size = 231052, upload-time = "2025-09-12T08:54:32.859Z" }, + { url = "https://files.pythonhosted.org/packages/f7/02/6e639e90f181dc9127046e00d0528f9f7ad12d428972e3a5378b9aefdb0b/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl", hash = "sha256:7916034efa867927892635733a3b6af8cd95ceb10566fd7f1e0d2763c2ee8b12", size = 243525, upload-time = "2025-09-12T08:54:34.006Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/cb588ca65561defe0fc48d1df4c2ac12569b81231ae4f2b52ab37007d0bd/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win32.whl", hash = "sha256:6c9549da71b93e367b4d71438798daae1da2592039fd14204a80a1a2348ae127", size = 552685, upload-time = "2025-09-12T08:54:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/86/27/00c9c96af18ac0a5eac2ff61cbe306551a2d770d7173f396d0792ee1a59e/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl", hash = "sha256:6292d5d6634d668cd23d337e6089491d3945a9aa4ac6e1667b0003520d7caa51", size = 559466, upload-time = "2025-09-12T08:54:37.661Z" }, +] + +[[package]] +name = "google-auth" +version = "2.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719, upload-time = "2026-01-06T21:55:31.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867, upload-time = "2026-01-06T21:55:28.6Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, + { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, + { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "googlemaps" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/26/bca4d737a9acea25e94c19940a780bbf0be64a691f7caf3a68467d3a5838/googlemaps-4.10.0.tar.gz", hash = "sha256:3055fcbb1aa262a9159b589b5e6af762b10e80634ae11c59495bd44867e47d88", size = 33056, upload-time = "2023-01-26T16:45:02.501Z" } + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, + { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h5py" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/30/8fa61698b438dd751fa46a359792e801191dadab560d0a5f1c709443ef8e/h5py-3.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67e59f6c2f19a32973a40f43d9a088ae324fe228c8366e25ebc57ceebf093a6b", size = 3414477, upload-time = "2025-10-16T10:33:24.201Z" }, + { url = "https://files.pythonhosted.org/packages/16/16/db2f63302937337c4e9e51d97a5984b769bdb7488e3d37632a6ac297f8ef/h5py-3.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e2f471688402c3404fa4e13466e373e622fd4b74b47b56cfdff7cc688209422", size = 2850298, upload-time = "2025-10-16T10:33:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2e/f1bb7de9b05112bfd14d5206090f0f92f1e75bbb412fbec5d4653c3d44dd/h5py-3.15.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c45802bcb711e128a6839cb6c01e9ac648dc55df045c9542a675c771f15c8d5", size = 4523605, upload-time = "2025-10-16T10:33:31.168Z" }, + { url = "https://files.pythonhosted.org/packages/05/8a/63f4b08f3628171ce8da1a04681a65ee7ac338fde3cb3e9e3c9f7818e4da/h5py-3.15.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64ce3f6470adb87c06e3a8dd1b90e973699f1759ad79bfa70c230939bff356c9", size = 4735346, upload-time = "2025-10-16T10:33:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/74/48/f16d12d9de22277605bcc11c0dcab5e35f06a54be4798faa2636b5d44b3c/h5py-3.15.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4411c1867b9899a25e983fff56d820a66f52ac326bbe10c7cdf7d832c9dcd883", size = 4175305, upload-time = "2025-10-16T10:33:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/47cdbff65b2ce53c27458c6df63a232d7bb1644b97df37b2342442342c84/h5py-3.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2cbc4104d3d4aca9d6db8c0c694555e255805bfeacf9eb1349bda871e26cacbe", size = 4653602, upload-time = "2025-10-16T10:33:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/dc08de359c2f43a67baa529cb70d7f9599848750031975eed92d6ae78e1d/h5py-3.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:01f55111ca516f5568ae7a7fc8247dfce607de331b4467ee8a9a6ed14e5422c7", size = 2873601, upload-time = "2025-10-16T10:33:45.323Z" }, + { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, + { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, + { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, + { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, +] + +[[package]] +name = "hydra-core" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "iopath" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "portalocker" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/20/65dd9bd25a1eb7fa35b5ae38d289126af065f8a0c1f6a90564f4bff0f89d/iopath-0.1.9-py3-none-any.whl", hash = "sha256:9058ac24f0328decdf8dbe209b33074b8702a3c4d9ba2f7801d46cb61a880b6e", size = 27367, upload-time = "2021-06-25T07:10:15.198Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, +] + +[[package]] +name = "ipython" +version = "8.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, +] + +[[package]] +name = "ipython" +version = "9.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl", hash = "sha256:b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b", size = 621431, upload-time = "2026-01-05T12:36:44.669Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jax" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ml-dtypes", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "opt-einsum", marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/1e/267f59c8fb7f143c3f778c76cb7ef1389db3fd7e4540f04b9f42ca90764d/jax-0.6.2.tar.gz", hash = "sha256:a437d29038cbc8300334119692744704ca7941490867b9665406b7f90665cd96", size = 2334091, upload-time = "2025-06-17T23:10:27.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/a8/97ef0cbb7a17143ace2643d600a7b80d6705b2266fc31078229e406bdef2/jax-0.6.2-py3-none-any.whl", hash = "sha256:bb24a82dc60ccf704dcaf6dbd07d04957f68a6c686db19630dd75260d1fb788c", size = 2722396, upload-time = "2025-06-17T23:10:25.293Z" }, +] + +[[package]] +name = "jax" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "jaxlib", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ml-dtypes", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opt-einsum", marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/25/5efb46e5492076622d9150ed394da97ef9aad393aa52f7dd7e980f836e1f/jax-0.8.2.tar.gz", hash = "sha256:1a685ded06a8223a7b52e45e668e406049dbbead02873f2b5a4d881ba7b421ae", size = 2505776, upload-time = "2025-12-18T18:41:59.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/f7/ae4ecf183d9693cd5fcce7ee063c5e54f173b66dc80a8a79951861e1b557/jax-0.8.2-py3-none-any.whl", hash = "sha256:d0478c5dc74406441efcd25731166a65ee782f13c352fa72dc7d734351909355", size = 2925344, upload-time = "2025-12-18T18:39:38.645Z" }, +] + +[[package]] +name = "jaxlib" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "ml-dtypes", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/c5/41598634c99cbebba46e6777286fb76abc449d33d50aeae5d36128ca8803/jaxlib-0.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4601b2b5dc8c23d6afb293eacfb9aec4e1d1871cb2f29c5a151d103e73b0f8", size = 54298019, upload-time = "2025-06-17T23:10:36.916Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/db07d746cd5867d5967528e7811da53374e94f64e80a890d6a5a4b95b130/jaxlib-0.6.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:4205d098ce8efb5f7fe2fe5098bae6036094dc8d8829f5e0e0d7a9b155326336", size = 79440052, upload-time = "2025-06-17T23:10:41.282Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d8/b7ae9e819c62c1854dbc2c70540a5c041173fbc8bec5e78ab7fd615a4aee/jaxlib-0.6.2-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:c087a0eb6fb7f6f8f54d56f4730328dfde5040dd3b5ddfa810e7c28ea7102b42", size = 89917034, upload-time = "2025-06-17T23:10:45.897Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/87e91bc70569ac5c3e3449eefcaf47986e892f10cfe1d5e5720dceae3068/jaxlib-0.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:153eaa51f778b60851720729d4f461a91edd9ba3932f6f3bc598d4413870038b", size = 57896337, upload-time = "2025-06-17T23:10:50.179Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/6899b0aed36a4acc51319465ddd83c7c300a062a9e236cceee00984ffe0b/jaxlib-0.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a208ff61c58128d306bb4e5ad0858bd2b0960f2c1c10ad42c548f74a60c0020e", size = 54300346, upload-time = "2025-06-17T23:10:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/e6/03/34bb6b346609079a71942cfbf507892e3c877a06a430a0df8429c455cebc/jaxlib-0.6.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:11eae7e05bc5a79875da36324afb9eddd4baeaef2a0386caf6d4f3720b9aef28", size = 79438425, upload-time = "2025-06-17T23:10:58.356Z" }, + { url = "https://files.pythonhosted.org/packages/80/02/49b05cbab519ffd3cb79586336451fbbf8b6523f67128a794acc9f179000/jaxlib-0.6.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:335d7e3515ce78b52a410136f46aa4a7ea14d0e7d640f34e1e137409554ad0ac", size = 89920354, upload-time = "2025-06-17T23:11:03.086Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7a/93b28d9452b46c15fc28dd65405672fc8a158b35d46beabaa0fe9631afb0/jaxlib-0.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6815509997d6b05e5c9daa7994b9ad473ce3e8c8a17bdbbcacc3c744f76f7a0", size = 57895707, upload-time = "2025-06-17T23:11:07.074Z" }, + { url = "https://files.pythonhosted.org/packages/ac/db/05e702d2534e87abf606b1067b46a273b120e6adc7d459696e3ce7399317/jaxlib-0.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d8a684a8be949dd87dd4acc97101b4106a0dc9ad151ec891da072319a57b99", size = 54301644, upload-time = "2025-06-17T23:11:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/0d/8a/b0a96887b97a25d45ae2c30e4acecd2f95acd074c18ec737dda8c5cc7016/jaxlib-0.6.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:87ec2dc9c3ed9ab936eec8535160c5fbd2c849948559f1c5daa75f63fabe5942", size = 79439161, upload-time = "2025-06-17T23:11:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e8/71c2555431edb5dd115cf86a7b599aa7e1be26728d89ae59aa11251d299c/jaxlib-0.6.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:f1dd09b481a93c1d4c750013f467f74194493ba7bd29fcd4d1cec16e3a214f65", size = 89942952, upload-time = "2025-06-17T23:11:19.181Z" }, + { url = "https://files.pythonhosted.org/packages/de/3a/06849113c844b86d20174df54735c84202ccf82cbd36d805f478c834418b/jaxlib-0.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:921dbd4db214eba19a29ba9f2450d880e08b2b2c7b968f28cc89da3e62366af4", size = 57919603, upload-time = "2025-06-17T23:11:23.207Z" }, + { url = "https://files.pythonhosted.org/packages/af/38/bed4279c2a3407820ed8bcd72dbad43c330ada35f88fafe9952b35abf785/jaxlib-0.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bff67b188133ce1f0111c7b163ac321fd646b59ed221ea489063e2e0f85cb967", size = 54300638, upload-time = "2025-06-17T23:11:26.372Z" }, + { url = "https://files.pythonhosted.org/packages/52/dc/9e35a1dc089ddf3d6be53ef2e6ba4718c5b6c0f90bccc535a20edac0c895/jaxlib-0.6.2-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:70498837caf538bd458ff6858c8bfd404db82015aba8f663670197fa9900ff02", size = 79439983, upload-time = "2025-06-17T23:11:30.016Z" }, + { url = "https://files.pythonhosted.org/packages/34/16/e93f0184b80a4e1ad38c6998aa3a2f7569c0b0152cbae39f7572393eda04/jaxlib-0.6.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:f94163f14c8fd3ba93ae14b631abacf14cb031bba0b59138869984b4d10375f8", size = 89941720, upload-time = "2025-06-17T23:11:34.62Z" }, + { url = "https://files.pythonhosted.org/packages/06/b9/ea50792ee0333dba764e06c305fe098bce1cb938dcb66fbe2fc47ef5dd02/jaxlib-0.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:b977604cd36c74b174d25ed685017379468138eb747d865f75e466cb273c801d", size = 57919073, upload-time = "2025-06-17T23:11:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/9596391c104a0547fcaf6a8c72078bbae79dbc8e7f0843dc8318f6606328/jaxlib-0.6.2-cp313-cp313t-manylinux2014_aarch64.whl", hash = "sha256:39cf9555f85ae1ce2e2c1a59fc71f2eca4f9867a7cb934fef881ba56b11371d1", size = 79579638, upload-time = "2025-06-17T23:11:43.054Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/f6e80f7f4cacfc9f03e64ac57ecb856b140de7c2f939b25f8dcf1aff63f9/jaxlib-0.6.2-cp313-cp313t-manylinux2014_x86_64.whl", hash = "sha256:3abd536e44b05fb1657507e3ff1fc3691f99613bae3921ecab9e82f27255f784", size = 90066675, upload-time = "2025-06-17T23:11:47.454Z" }, +] + +[[package]] +name = "jaxlib" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "ml-dtypes", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/87/0a44b1a5c558e6d8e4fd796d4f9efe5c8cac2b3013ab7349968c65931fa4/jaxlib-0.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:490bf0cb029c73c65c9431124b86cdc95082dbc1fb76fc549d24d75da33e5454", size = 55929353, upload-time = "2025-12-18T18:40:35.844Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d2/b37c86ee35d9ea7ee67c81e9166b31e18aa3784e1b96e8a60f52bbb8c9c0/jaxlib-0.8.2-cp311-cp311-manylinux_2_27_aarch64.whl", hash = "sha256:bb89be452b1b808d3f88fc01c415b364a260be4cc7ac120c038009f6150a32dc", size = 74548611, upload-time = "2025-12-18T18:40:39.67Z" }, + { url = "https://files.pythonhosted.org/packages/65/7d/9bb1cd620d8093098203b17d227a902939afec00da1c63cb719a9fe89525/jaxlib-0.8.2-cp311-cp311-manylinux_2_27_x86_64.whl", hash = "sha256:ccf77da917a20935247c990691decfcbdd06c25ef0ac94d914a04aadb22f714c", size = 80127195, upload-time = "2025-12-18T18:40:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/56d830c7fcf1736cbfb11d8cf79c1932f826f319d2467becb02933df3ba9/jaxlib-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:dffc22b5b732b9556d92c918b251c61bcc046617c4dbb51e1f7a656587fddffb", size = 60338464, upload-time = "2025-12-18T18:40:47.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/77/18ac0ac08c76bf12ed47b0c2d7d35f3fc3d065bd105b36937901eab1455c/jaxlib-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:023de6f3f56da2af7037970996500586331fdb50b530ecbb54b9666da633bd00", size = 55938204, upload-time = "2025-12-18T18:40:50.859Z" }, + { url = "https://files.pythonhosted.org/packages/33/c5/fa809591cbddc0d7bbef9c95962a0b521ae4a168b0ff375cadf37840b97d/jaxlib-0.8.2-cp312-cp312-manylinux_2_27_aarch64.whl", hash = "sha256:3b16e50c5b730c9dd0a49e55f1acfaa722b00b1af0522a591558dcc0464252f2", size = 74550881, upload-time = "2025-12-18T18:40:54.491Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/e386c4bbfda3fb326a01594cc46c8ac90cdeeeacee4c553d9e3848f75893/jaxlib-0.8.2-cp312-cp312-manylinux_2_27_x86_64.whl", hash = "sha256:2b9789bd08f8b0cc5a5c12ae896fe432d5942e32e417091b8b5a96a9a6fd5cf1", size = 80135127, upload-time = "2025-12-18T18:40:58.808Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4c/0c90b1e2b47fdf34cd352a01c42c2628d115a6f015d4a3230060bb0d97af/jaxlib-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:f472cc72e3058e50b5f0230b236d5a1183bf6c3d5423d2a52eff07bcf34908de", size = 60361039, upload-time = "2025-12-18T18:41:02.367Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/c0ec75e43a13b2457d78d509f49b49a57fa302ffced4f4a2778e428cb0a6/jaxlib-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d006db96be020c8165212a1216372f8acac4ff4f8fb067743d694ef2b301ace", size = 55939058, upload-time = "2025-12-18T18:41:06.199Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e2/2d3eff7a49ca37ef6929bf67b8ab4c933ab53a115060e60c239702028568/jaxlib-0.8.2-cp313-cp313-manylinux_2_27_aarch64.whl", hash = "sha256:7c304f3a016965b9d1f5239a8a0399a73925f5604fe914c5ca66ecf734bf6422", size = 74550207, upload-time = "2025-12-18T18:41:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e0/91e5762a7ddb6351b07c742ca407cd28e26043d6945d6228b6c1b0881a45/jaxlib-0.8.2-cp313-cp313-manylinux_2_27_x86_64.whl", hash = "sha256:1bfbcf6c3de221784fa4cdb6765a09d71cb4298b15626b3d0409b3dfcd8a8667", size = 80133534, upload-time = "2025-12-18T18:41:14.193Z" }, + { url = "https://files.pythonhosted.org/packages/85/68/25b38673b07a808616ce7b6efb3eed491f983f3373a09cbbd03f67178563/jaxlib-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:f205e91c3a152a2a76c0bc59a6a2de03e87ec261b91e8812922777185e7b08f5", size = 60358239, upload-time = "2025-12-18T18:41:17.661Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/753c4b16297576e33cb41bf605d27fefd016867d365861c43c505afd1579/jaxlib-0.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f28edac8c226fc07fa3e8af6f9defede8ac2c307429e3291edce8739d39becc9", size = 56035453, upload-time = "2025-12-18T18:41:21.004Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/891f967b01a60de1dbcb8c40b6fee28cc39c670c27c919756c41d8c89ebe/jaxlib-0.8.2-cp313-cp313t-manylinux_2_27_aarch64.whl", hash = "sha256:7da8127557c786264049ae55460d1b8d04cc3cdf0403a087f2fc1e6d313ec722", size = 74661142, upload-time = "2025-12-18T18:41:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5c/3f1476cd6cbc0e2aa661cb750489739aeda500473d91dc79837b5bc9247f/jaxlib-0.8.2-cp313-cp313t-manylinux_2_27_x86_64.whl", hash = "sha256:28eec1a4e0639a0d8702cea3cb70dd3663053dbfa344452994ea48dc6ceadaa5", size = 80238500, upload-time = "2025-12-18T18:41:28.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9d/dca93d916bf8664d7a2bb73ea3d219028dabbe382c31774348963287356a/jaxlib-0.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:beffb004e7eeb5c9afb24439e2b2cf45a4ee3e3e8adf45e355edf2af62acf8b8", size = 55943240, upload-time = "2025-12-18T18:41:32.095Z" }, + { url = "https://files.pythonhosted.org/packages/f0/47/7407d010db7f5ec1c25a8b8d379defc0c8b4daaaa829c88355e03c0ad314/jaxlib-0.8.2-cp314-cp314-manylinux_2_27_aarch64.whl", hash = "sha256:68108dff0de74adc468016be9a19f80efe48c660c0d5a122287094b44b092afc", size = 74560018, upload-time = "2025-12-18T18:41:36.154Z" }, + { url = "https://files.pythonhosted.org/packages/5e/27/2e6032727e41ce74914277478021140947af59127d68aa9e6f3776b428fd/jaxlib-0.8.2-cp314-cp314-manylinux_2_27_x86_64.whl", hash = "sha256:e6a97dfb0232eed9a2bb6e3828e4f682dbac1a7fea840bfda574cae2dbf5faf9", size = 80156235, upload-time = "2025-12-18T18:41:40.227Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8c/af5a00b07a446414edf6b84a7397eab02cf01ba44b6ae1fce7798ce4c127/jaxlib-0.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:05b958f497e49824c432e734bb059723b7dfe69e2ad696a9f9c8ad82fff7c3f8", size = 62673493, upload-time = "2025-12-18T18:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/4d/eb/ad70fe97fda465d536625bef39ee381a7f8fed1f1bf0bc296510bac32ec5/jaxlib-0.8.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964626f581beab31ee6826b228fcc2ec5181b05cecf94a528dff97921c145dbc", size = 56037334, upload-time = "2025-12-18T18:41:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/34/97/0741440c66a49ec3702f6c28a5608c7543243b1728c3f465505ed5bfe7d2/jaxlib-0.8.2-cp314-cp314t-manylinux_2_27_aarch64.whl", hash = "sha256:a397ea7dcb37d689ce79173eeb99b2f1347637a36be9a27f20ae6848bfc58bfc", size = 74661591, upload-time = "2025-12-18T18:41:51.285Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c4/388797324c201830ac414562eb6697fa38837f40852bdc4d0f464d65889c/jaxlib-0.8.2-cp314-cp314t-manylinux_2_27_x86_64.whl", hash = "sha256:aa8701b6356f098e8452c3cec762fb5f706fcb8f67ffd65964f63982479aa23b", size = 80236629, upload-time = "2025-12-18T18:41:56.05Z" }, +] + +[[package]] +name = "jaxopt" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jax", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jaxlib", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/da/ff7d7fbd13b8ed5e8458e80308d075fc649062b9f8676d3fc56f2dc99a82/jaxopt-0.8.5.tar.gz", hash = "sha256:2790bd68ef132b216c083a8bc7a2704eceb35a92c0fc0a1e652e79dfb1e9e9ab", size = 121709, upload-time = "2025-04-14T17:59:01.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/d8/55e0901103c93d57bab3b932294c216f0cbd49054187ce29f8f13808d530/jaxopt-0.8.5-py3-none-any.whl", hash = "sha256:ff221d1a86908ec759eb1e219ee1d12bf208a70707e961bf7401076fe7cf4d5e", size = 172434, upload-time = "2025-04-14T17:59:00.342Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, + { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/27/d10de45e8ad4ce872372c4a3a37b7b35b6b064f6f023a5c14ffcced4d59d/jupyter_client-8.7.0.tar.gz", hash = "sha256:3357212d9cbe01209e59190f67a3a7e1f387a4f4e88d1e0433ad84d7b262531d", size = 344691, upload-time = "2025-12-09T18:37:01.953Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl", hash = "sha256:3671a94fd25e62f5f2f554f5e95389c2294d89822378a5f2dd24353e1494a9e0", size = 106215, upload-time = "2025-12-09T18:37:00.024Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "kaleido" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "choreographer" }, + { name = "logistro" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pytest-timeout" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/76eec859b71eda803a88ea50ed3f270281254656bb23d19eb0a39aa706a0/kaleido-1.2.0.tar.gz", hash = "sha256:fa621a14423e8effa2895a2526be00af0cf21655be1b74b7e382c171d12e71ef", size = 64160, upload-time = "2025-11-04T21:24:23.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/97/f6de8d4af54d6401d6581a686cce3e3e2371a79ba459a449104e026c08bc/kaleido-1.2.0-py3-none-any.whl", hash = "sha256:c27ed82b51df6b923d0e656feac221343a0dbcd2fb9bc7e6b1db97f61e9a1513", size = 68997, upload-time = "2025-11-04T21:24:21.704Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, + { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, + { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, + { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "kubernetes" +version = "33.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "google-auth" }, + { name = "oauthlib" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/52/19ebe8004c243fdfa78268a96727c71e08f00ff6fe69a301d0b7fcbce3c2/kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993", size = 1036779, upload-time = "2025-06-09T21:57:58.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/43/d9bebfc3db7dea6ec80df5cb2aad8d274dd18ec2edd6c4f21f32c237cbbb/kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5", size = 1941335, upload-time = "2025-06-09T21:57:56.327Z" }, +] + +[[package]] +name = "langchain" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/6e/fbde54bd2974cf2775bca289a2d9e8b3d7594f29389614d23f40227380fe/langchain-1.2.1.tar.gz", hash = "sha256:cdd7ede31e30cb181a09de8a9d4dc4d23147870b9348db5a82bd166e237da808", size = 544729, upload-time = "2026-01-07T00:12:46.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/b0/64b97723cf7f5bdc8e6c6963b76db01a1c1dcc89e5fd8aa9709c196ff3af/langchain-1.2.1-py3-none-any.whl", hash = "sha256:5c499284b7a38a7978a9d8036775538dc3fef1bf35121e6ac509a203deb9ef5a", size = 105035, upload-time = "2026-01-07T00:12:45.667Z" }, +] + +[[package]] +name = "langchain-chroma" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chromadb" }, + { name = "langchain-core" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/0e/54896830b7331c90788cf96b2c37858977c199da9ecdaf85cf11eb6e6bc1/langchain_chroma-1.1.0.tar.gz", hash = "sha256:8069685e7848041e998d16c8a4964256b031fd20551bf59429173415bc2adc12", size = 220382, upload-time = "2025-12-12T16:23:01.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/35/2a6d1191acaad043647e28313b0ecd161d61f09d8be37d1996a90d752c13/langchain_chroma-1.1.0-py3-none-any.whl", hash = "sha256:ff65e4a2ccefb0fb9fde2ff38705022ace402f979d557f018f6e623f7288f0fc", size = 12981, upload-time = "2025-12-12T16:23:00.196Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/ce/ba5ed5ea6df22965b2893c2ed28ebb456204962723d408904c4acfa5e942/langchain_core-1.2.6.tar.gz", hash = "sha256:b4e7841dd7f8690375aa07c54739178dc2c635147d475e0c2955bf82a1afa498", size = 833343, upload-time = "2026-01-02T21:35:44.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/40/0655892c245d8fbe6bca6d673ab5927e5c3ab7be143de40b52289a0663bc/langchain_core-1.2.6-py3-none-any.whl", hash = "sha256:aa6ed954b4b1f4504937fe75fdf674317027e9a91ba7a97558b0de3dc8004e34", size = 489096, upload-time = "2026-01-02T21:35:43.391Z" }, +] + +[[package]] +name = "langchain-huggingface" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "langchain-core" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/2c/4fddeb3387baa05b6a95870ad514f649cafb46e0c0ef9caf949d974e55d2/langchain_huggingface-1.2.0.tar.gz", hash = "sha256:18a2d79955271261fb245b233fea6aa29625576e841f2b4f5bee41e51cc70949", size = 255602, upload-time = "2025-12-12T22:19:51.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/ce/502157ef7390a31cc67e5873ad66e737a25d1d33fcf6936e5c9a0a451409/langchain_huggingface-1.2.0-py3-none-any.whl", hash = "sha256:0ff6a17d3eb36ce2304f446e3285c74b59358703e8f7916c15bfcf9ec7b57bf1", size = 30671, upload-time = "2025-12-12T22:19:50.023Z" }, +] + +[[package]] +name = "langchain-ollama" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ollama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/51/72cd04d74278f3575f921084f34280e2f837211dc008c9671c268c578afe/langchain_ollama-1.0.1.tar.gz", hash = "sha256:e37880c2f41cdb0895e863b1cfd0c2c840a117868b3f32e44fef42569e367443", size = 153850, upload-time = "2025-12-12T21:48:28.68Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/46/f2907da16dc5a5a6c679f83b7de21176178afad8d2ca635a581429580ef6/langchain_ollama-1.0.1-py3-none-any.whl", hash = "sha256:37eb939a4718a0255fe31e19fbb0def044746c717b01b97d397606ebc3e9b440", size = 29207, upload-time = "2025-12-12T21:48:27.832Z" }, +] + +[[package]] +name = "langchain-openai" +version = "1.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/67/228dc28b4498ea16422577013b5bb4ba35a1b99f8be975d6747c7a9f7e6a/langchain_openai-1.1.6.tar.gz", hash = "sha256:e306612654330ae36fb6bbe36db91c98534312afade19e140c3061fe4208dac8", size = 1038310, upload-time = "2025-12-18T17:58:52.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/5b/1f6521df83c1a8e8d3f52351883b59683e179c0aa1bec75d0a77a394c9e7/langchain_openai-1.1.6-py3-none-any.whl", hash = "sha256:c42d04a67a85cee1d994afe400800d2b09ebf714721345f0b651eb06a02c3948", size = 84701, upload-time = "2025-12-18T17:58:51.527Z" }, +] + +[[package]] +name = "langchain-text-splitters" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/42/c178dcdc157b473330eb7cc30883ea69b8ec60078c7b85e2d521054c4831/langchain_text_splitters-1.1.0.tar.gz", hash = "sha256:75e58acb7585dc9508f3cd9d9809cb14751283226c2d6e21fb3a9ae57582ca22", size = 272230, upload-time = "2025-12-14T01:15:38.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/1a/a84ed1c046deecf271356b0179c1b9fba95bfdaa6f934e1849dee26fad7b/langchain_text_splitters-1.1.0-py3-none-any.whl", hash = "sha256:f00341fe883358786104a5f881375ac830a4dd40253ecd42b4c10536c6e4693f", size = 34182, upload-time = "2025-12-14T01:15:37.382Z" }, +] + +[[package]] +name = "langgraph" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/47/28f4d4d33d88f69de26f7a54065961ac0c662cec2479b36a2db081ef5cb6/langgraph-1.0.5.tar.gz", hash = "sha256:7f6ae59622386b60fe9fa0ad4c53f42016b668455ed604329e7dc7904adbf3f8", size = 493969, upload-time = "2025-12-12T23:05:48.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/1b/e318ee76e42d28f515d87356ac5bd7a7acc8bad3b8f54ee377bef62e1cbf/langgraph-1.0.5-py3-none-any.whl", hash = "sha256:b4cfd173dca3c389735b47228ad8b295e6f7b3df779aba3a1e0c23871f81281e", size = 157056, upload-time = "2025-12-12T23:05:46.499Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/07/2b1c042fa87d40cf2db5ca27dc4e8dd86f9a0436a10aa4361a8982718ae7/langgraph_checkpoint-3.0.1.tar.gz", hash = "sha256:59222f875f85186a22c494aedc65c4e985a3df27e696e5016ba0b98a5ed2cee0", size = 137785, upload-time = "2025-11-04T21:55:47.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/e3/616e3a7ff737d98c1bbb5700dd62278914e2a9ded09a79a1fa93cf24ce12/langgraph_checkpoint-3.0.1-py3-none-any.whl", hash = "sha256:9b04a8d0edc0474ce4eaf30c5d731cee38f11ddff50a6177eead95b5c4e4220b", size = 46249, upload-time = "2025-11-04T21:55:46.472Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/f9/54f8891b32159e4542236817aea2ee83de0de18bce28e9bdba08c7f93001/langgraph_prebuilt-1.0.5.tar.gz", hash = "sha256:85802675ad778cc7240fd02d47db1e0b59c0c86d8369447d77ce47623845db2d", size = 144453, upload-time = "2025-11-20T16:47:39.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/5e/aeba4a5b39fe6e874e0dd003a82da71c7153e671312671a8dacc5cb7c1af/langgraph_prebuilt-1.0.5-py3-none-any.whl", hash = "sha256:22369563e1848862ace53fbc11b027c28dd04a9ac39314633bb95f2a7e258496", size = 35072, upload-time = "2025-11-20T16:47:38.187Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d3/b6be0b0aba2a53a8920a2b0b4328a83121ec03eea9952e576d06a4182f6f/langgraph_sdk-0.3.1.tar.gz", hash = "sha256:f6dadfd2444eeff3e01405a9005c95fb3a028d4bd954ebec80ea6150084f92bb", size = 130312, upload-time = "2025-12-18T22:11:47.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/fe/0c1c9c01a154eba62b20b02fabe811fd94a2b810061ae9e4d8462b8cf85a/langgraph_sdk-0.3.1-py3-none-any.whl", hash = "sha256:0b856923bfd20bf3441ce9d03bef488aa333fb610e972618799a9d584436acad", size = 66517, upload-time = "2025-12-18T22:11:46.625Z" }, +] + +[[package]] +name = "langsmith" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/a3/d36a9935fd1215e21d022a5773b243b6eec12ba11fde3eb8ba1f8384b01e/langsmith-0.6.1.tar.gz", hash = "sha256:bf35f9ffa592d602d5b11d23890d51342f321ac7f5e0cb6a22ab48fbdb88853a", size = 884701, upload-time = "2026-01-06T20:15:38.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/01/9a3f0ff60afcb30383ea9775e9f9a233c0127bad7c786d878f78b487bebb/langsmith-0.6.1-py3-none-any.whl", hash = "sha256:cad1f0a5cb8baf01490d2d90b7515d2cecc31648237bf070d2e6c0e7d58a2079", size = 282977, upload-time = "2026-01-06T20:15:36.579Z" }, +] + +[[package]] +name = "lap" +version = "0.5.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/cf/ef745c8977cbb26fba5f8433fd4bfd6bf009a90802c0a1cc7139e11f478b/lap-0.5.12.tar.gz", hash = "sha256:570b414ea7ae6c04bd49d0ec8cdac1dc5634737755784d44e37f9f668bab44fd", size = 1520169, upload-time = "2024-11-30T14:27:56.096Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a7/d66e91ea92628f1e1572db6eb5cd0baa549ef523308f1ce469ea2b380b37/lap-0.5.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c3a38070b24531949e30d7ebc83ca533fcbef6b1d6562f035cae3b44dfbd5ec", size = 1481332, upload-time = "2024-11-30T01:20:54.008Z" }, + { url = "https://files.pythonhosted.org/packages/30/8a/a0e54a284828edc049a1d005fad835e7c8b2d2a563641ec0d3c6fb5ee6d4/lap-0.5.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a301dc9b8a30e41e4121635a0e3d0f6374a08bb9509f618d900e18d209b815c4", size = 1478472, upload-time = "2024-11-30T01:21:10.314Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d6/679d73d2552d0e36c5a2751b6509a62f1fa69d6a2976dac07568498eefde/lap-0.5.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0c1b9ab32c9ba9a94e3f139a0c30141a15fb9e71d69570a6851bbae254c299", size = 1697145, upload-time = "2024-11-30T01:21:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/fa/93/dcfdcd73848c72a0aec5ff587840812764844cdb0b58dd9394e689b8bc09/lap-0.5.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f702e9fbbe3aa265708817ba9d4efb44d52f7013b792c9795f7501ecf269311a", size = 1700582, upload-time = "2024-11-30T01:22:09.43Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1d/66f32e54bbf005fe8483065b3afec4b427f2583df6ae53a2dd540c0f7227/lap-0.5.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9836f034c25b1dfeabd812b7359816911ed05fe55f53e70c30ef849adf07df02", size = 1688038, upload-time = "2024-11-30T01:22:11.863Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1c/faf992abd15b643bd7d70aabcf13ef7544f11ac1167436049a3a0090ce17/lap-0.5.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0416780dbdca2769231a53fb5491bce52775299b014041296a8b5be2d00689df", size = 1697169, upload-time = "2024-11-30T01:22:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a2/9af5372d383310174f1a9e429da024ae2eaa762e6ee3fc59bdc936a1f6db/lap-0.5.12-cp310-cp310-win_amd64.whl", hash = "sha256:2d6e137e1beb779fcd6a42968feb6a122fdddf72e5b58d865191c31a01ba6804", size = 1477867, upload-time = "2024-11-30T01:22:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ad/9bb92211ea5b5b43d98f5a57b3e98ccff125ea9bc397f185d5eff1a04260/lap-0.5.12-cp310-cp310-win_arm64.whl", hash = "sha256:a40d52c5511421497ae3f82a5ca85a5442d8776ba2991c6fca146afceea7608f", size = 1467318, upload-time = "2024-11-30T01:22:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/62/ef/bc8bbc34585bcbed2b277d734008480d9ed08a6e3f2de3842ad482484e9c/lap-0.5.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d928652e77bec5a71dc4eb4fb8e15d455253b2a391ca8478ceab7d171cbaec2e", size = 1481210, upload-time = "2024-11-30T01:22:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/ab/81/0d3b31d18bbdcdaab678b461d99688ec3e6a2d2cda2aa9af2ae8ed6910e1/lap-0.5.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4a0ea039fcb2fd388b5e7c1be3402c483d32d3ef8c70261c69ab969ec25cd83", size = 1478370, upload-time = "2024-11-30T01:23:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/3d/90/bd6cff1b6a0c30594a7a2bf94c5f184105e8eb26fa250ce22efdeef58a3a/lap-0.5.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87c0e736c31af0a827dc642132d09c5d4f77d30f5b3f0743b9cd31ef12adb96c", size = 1718144, upload-time = "2024-11-30T01:23:03.345Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d6/97564ef3571cc2a60a6e3ee2f452514b2e549637247cb7de7004e0769864/lap-0.5.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5270141f97027776ced4b6540d51899ff151d8833b5f93f2428de36c2270a9ed", size = 1720027, upload-time = "2024-11-30T01:23:32.025Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7d/73a51aeec1e22257589dad46c724d4d736aa56fdf4c0eff29c06102e21ae/lap-0.5.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:04dc4b44c633051a9942ad60c9ad3da28d7c5f09de93d6054b763c57cbc4ac90", size = 1711923, upload-time = "2024-11-30T01:23:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/c1be3d9ebe479beff3d6ee4453908a343c7a388386de28037ff2767debf9/lap-0.5.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:560ec8b9100f78d6111b0acd9ff8805e4315372f23c2dcad2f5f9f8d9c681261", size = 1720922, upload-time = "2024-11-30T01:24:14.228Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/18c0c4edadbf9744a02131901c8a856303a901367881e44796a94190b560/lap-0.5.12-cp311-cp311-win_amd64.whl", hash = "sha256:851b9bcc898fa763d6e7c307d681dde199ca969ab00e8292fc13cff34107ea38", size = 1478202, upload-time = "2024-11-30T01:24:29.681Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d2/dcde0db492eb7a2c228e8839e831c6c5fc68f85bea586206405abd2eb44e/lap-0.5.12-cp311-cp311-win_arm64.whl", hash = "sha256:49e14fdbf4d55e7eda6dfd3aba433a91b00d87c7be4dd25059952b871b1e3399", size = 1467411, upload-time = "2024-11-30T01:24:31.92Z" }, + { url = "https://files.pythonhosted.org/packages/24/29/50a77fa27ed19b75b7599defedafd5f4a64a66bdb6255f733fdb8c9fafcb/lap-0.5.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1211fca9d16c0b1383c7a93be2045096ca5e4c306e794fcf777ac52b30f98829", size = 1481435, upload-time = "2024-11-30T01:24:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2b/41acf93603d3db57e512c77c98f4f71545602efa0574ca685608078cc0f5/lap-0.5.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8dcafbf8363308fb289d7cd3ae9df375ad090dbc2b70f5d7d038832e87d2b1a1", size = 1478195, upload-time = "2024-11-30T01:25:16.925Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6e/d7644b2b2675e2c29cc473c3dde136f02f4ed30ecbc8ef89b51cbb4f7ad1/lap-0.5.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f721ed3fd2b4f6f614870d12aec48bc44c089587930512c3187c51583c811b1c", size = 1725693, upload-time = "2024-11-30T01:25:19.404Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3c/8d3f80135022a2db3eb7212fa9c735b7111dcb149d53deb62357ff2386f0/lap-0.5.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:797d9e14e517ac06337b6dca875bdf9f0d88ec4c3214ebb6d0676fed197dc13f", size = 1726953, upload-time = "2024-11-30T01:25:44.067Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e1/badf139f34ff7c7c07ba55e6f39de9ea443d9b75fd97cc4ed0ce67eeb36b/lap-0.5.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a2424daf7c7afec9b93ed02af921813ab4330826948ce780a25d94ca42df605", size = 1712981, upload-time = "2024-11-30T01:25:58.948Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4a/e2d0925e5ead474709eb89c6bbb9cd188396c9e3384a1f5d2491a38aeab6/lap-0.5.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1c34c3d8aefbf7d0cb709801ccf78c6ac31f4b1dc26c169ed1496ed3cb6f4556", size = 1728876, upload-time = "2024-11-30T01:26:25.744Z" }, + { url = "https://files.pythonhosted.org/packages/46/89/73bad73b005e7f681f8cfa2c8748e9d766b91da781d07f300f86a9eb4f03/lap-0.5.12-cp312-cp312-win_amd64.whl", hash = "sha256:753ef9bd12805adbf0d09d916e6f0d271aebe3d2284a1f639bd3401329e436e5", size = 1476975, upload-time = "2024-11-30T01:26:40.341Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/00df0c44b728119fe770e0526f850b0a9201f23bf4276568aef5b372982e/lap-0.5.12-cp312-cp312-win_arm64.whl", hash = "sha256:83e507f6def40244da3e03c71f1b1f54ceab3978cde72a84b84caadd8728977e", size = 1466243, upload-time = "2024-11-30T01:26:43.202Z" }, + { url = "https://files.pythonhosted.org/packages/e1/07/85a389eb4c6a9bf342f79811dd868ed3b6e56402f1dfa71474cec3c5ac30/lap-0.5.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c4fdbd8d94ad5da913ade49635bad3fc4352ee5621a9f785494c11df5412d6d", size = 1479752, upload-time = "2024-11-30T01:27:06.417Z" }, + { url = "https://files.pythonhosted.org/packages/b1/01/46ba9ab4b9d95b43058591094e49ef21bd7e6fe2eb5202ece0b23240b2dc/lap-0.5.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2d01113eec42174e051ee5cebb5d33ec95d37bd2c422b7a3c09bbebaf30b635", size = 1477146, upload-time = "2024-11-30T01:27:26.769Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c3/9f6829a20e18c6ca3a3e97fcab815f0d888b552e3e37b892d908334d0f22/lap-0.5.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6e8ed53cb4d85fa0875092bc17436d7eeab2c7fb3574e551c611c352fea8c8", size = 1717458, upload-time = "2024-11-30T01:27:29.936Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bb/0f3a44d7220bd48f9a313a64f4c228a02cbb0fb1f55fd449de7a0659a5e2/lap-0.5.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dd54bf8bb48c87f6276555e8014d4ea27742d84ddbb0e7b68be575f4ca438d7", size = 1720277, upload-time = "2024-11-30T01:28:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/3e/48/5dcfd7f97a5ac696ad1fe750528784694c374ee64312bfbf96d14284f74a/lap-0.5.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9db0e048cfb561f21671a3603dc2761f108b3111da66a7b7d2f035974dcf966e", size = 1712562, upload-time = "2024-11-30T01:28:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/77/60/ac8702518e4d7c7a284b40b1aae7b4e264a029a8476cb674067a26c17f3c/lap-0.5.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:517b8bd02e56b8466244fc4c0988aece04e6f8b11f43406ae195b4ce308733fb", size = 1724195, upload-time = "2024-11-30T01:28:46.411Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3b/62181a81af89a6e7cefca2390d1f0822f7f6b73b40393ea04000c1ac0435/lap-0.5.12-cp313-cp313-win_amd64.whl", hash = "sha256:59dba008db14f640a20f4385916def4b343fa59efb4e82066df81db5a9444d5e", size = 1476213, upload-time = "2024-11-30T01:29:03.832Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4b/2db5ddb766cda2bdbf4012771d067d2b1c91e0e2d2c5ca0573efcd7ad321/lap-0.5.12-cp313-cp313-win_arm64.whl", hash = "sha256:30309f6aff8e4d616856ec8c6eec7ad5b48d2687887b931302b5c8e6dfac347a", size = 1465708, upload-time = "2024-11-30T01:29:34.141Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "lcm" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/c8/a5a6c1b0d55bf3dec92daf5db9719923e56e0fdfd9e299a4997632d54a6f/lcm-1.5.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:46021ad6e2c63d8a2ee2e22d9ccc193e8f95b8ee1084567722c6428e1e92d615", size = 3494809, upload-time = "2025-10-23T20:33:34.358Z" }, + { url = "https://files.pythonhosted.org/packages/25/35/9a7e6c619332b9a71ec2a6f53a5a83fd231f3b243789745419a64e778805/lcm-1.5.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:79decf56efedc81e3fe0ae5757cabec908ec17a23b792586304550e97d86be46", size = 2861313, upload-time = "2025-10-23T20:33:37.574Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/dfc70f70eaffde8e63c2227c7199ef7fdad1d411612d3082cddad2fdbab4/lcm-1.5.2-cp310-cp310-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ad0c911e67c023fb455a730f7bf94ddbc2e128c87743a603778f01033eb8a92", size = 2853116, upload-time = "2025-10-23T20:33:40.655Z" }, + { url = "https://files.pythonhosted.org/packages/d4/33/067d66c0acd06d9d00fd657e33718e2daf16a82159df8e3ff51e5273d9b3/lcm-1.5.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a04a7b1a623086c3a665356bc0e232185e4d247c8be07d70cbc41b0c1d9a506d", size = 2881542, upload-time = "2025-10-23T20:33:44.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/be8bcffbcdc7fcc04d8ce5fc16be976c65f9d767afe96ab13886ad274be4/lcm-1.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebb81f718088ed7ad6ec73f3dc429f9cf36cc40784a5afc15648b1bf341a1b38", size = 1497534, upload-time = "2025-10-23T20:33:46.316Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c3/68e47479ff2cd2b430541b6305ccc9b0147b36b4df046a85ca71473de048/lcm-1.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:11aed9a77dffef96b3fc973737ab073d18e0389666aaf3b945ed51566c7427c7", size = 1341790, upload-time = "2025-10-23T20:33:48.006Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/71a687347a9f80dbd01b6be4d8c26edf64b8aac2fc6644452f52d3573eb2/lcm-1.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:33a51c4617479350ecacc7b30ccf41918fadc3144f456d79248aaaa589fb1739", size = 1542532, upload-time = "2025-10-23T20:33:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c8/c6f3fd72383f847b150cd7a98d74bd0eb4ac38a18afa3bb7b5e771b12a57/lcm-1.5.2-cp310-cp310-win32.whl", hash = "sha256:c76e9c8de6243f9c05e4024459be1e3e497bc324a52edc8e826513082f288716", size = 4192732, upload-time = "2025-10-23T20:33:53.722Z" }, + { url = "https://files.pythonhosted.org/packages/52/ec/d2c3fcae355714994ea9ea4ae74fa242eb41f435d3bd340b4271c8e4584f/lcm-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa31cd789075cfb1799bd9d14841e7d82988b634533d73c8e1fabf551cdbd08e", size = 4408972, upload-time = "2025-10-23T20:33:59.654Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/9bab9e481d7ff83cd8bdc7fb5f96ec98d56f5aa4c65cf0b6266cbf787e6d/lcm-1.5.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:6014e57b4c8f08d9514c4860d6b700f69b3df5352b92271e5ea8f2cee06e8cc4", size = 3561741, upload-time = "2025-10-23T20:34:02.985Z" }, + { url = "https://files.pythonhosted.org/packages/91/4e/a123ddfe36831b64079c50b0957da12eed7dc961190b43c7ddf1f3fb39cf/lcm-1.5.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:24bf2e2895daf045101507c0fc55cb6a055430c89a6038528abb04d83db4838f", size = 3494889, upload-time = "2025-10-23T20:34:06.416Z" }, + { url = "https://files.pythonhosted.org/packages/85/a0/0d5ce44ea370f46d94bc15d51ee4889821c4e1d8664f206b33ee768d6a38/lcm-1.5.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e79945a1d78e9fadacd05913c3edd7cb036159a9394bb0124dfdc2fe0f74708f", size = 2861375, upload-time = "2025-10-23T20:34:09.513Z" }, + { url = "https://files.pythonhosted.org/packages/8f/56/fc2c7aeb084150688474cd503919d157b95dc9c597160562f38bac02427b/lcm-1.5.2-cp311-cp311-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cf485e679f0dc94c08c90b91221d12d14699f44c64ee967835625068ecb2a22e", size = 2853092, upload-time = "2025-10-23T20:34:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/35/71/d3ac3d849a26bd5d3487a05ca0f867a3638593984b1fac41fb6d9ca04fbd/lcm-1.5.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:288b1f1a90420bc3c21aaaa8375d72c80ff16b35848acdd207070fdb8924b152", size = 2881455, upload-time = "2025-10-23T20:34:15.407Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2f/4bc34fbd695ad30f58b6999552f28744997773cd68bc59a3d85917dec6a4/lcm-1.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:46823e31741ef2f0fdbab184edbc653c6440c039ad704467577750807b74bcf4", size = 1497533, upload-time = "2025-10-23T20:34:17.506Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b1/406af095fb7cd08c89cffcadfdc935c22605ceb69ab5f30dcaec98c5d6fb/lcm-1.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c25758e876e8233d5ab14fe29bba8d2d9d4baf488db01c56414417cb3f77b9c1", size = 1341790, upload-time = "2025-10-23T20:34:19.129Z" }, + { url = "https://files.pythonhosted.org/packages/7f/40/75066b7d5f2b07e58c0418813d796a9df917cfce0407dfe6ac333e0a9167/lcm-1.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4b822ab01a461364a7cf7f44ecfcd5879730e3280678ce7fd6139740c8ec9d04", size = 1542533, upload-time = "2025-10-23T20:34:21.301Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a0/ccb1ef4e3f6e20433fc5738f6f1ac011cdf79efb11cd13e68a7a67372149/lcm-1.5.2-cp311-cp311-win32.whl", hash = "sha256:0d047399e629c1f436c012373505135038918b8d340ca792acd36a21f4b65c1e", size = 4192663, upload-time = "2025-10-23T20:34:25.015Z" }, + { url = "https://files.pythonhosted.org/packages/2a/37/9d5a138375dda75afcff852fbefaf0446f16d809977ff1fce67f15c087d2/lcm-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:ee0b121f4e44d050e0a2247a467e2ad3a46f8ef51f3ca97a67963bc193d49311", size = 4408922, upload-time = "2025-10-23T20:34:29.097Z" }, + { url = "https://files.pythonhosted.org/packages/9a/54/035da62f6d66be55af5ff54a74b281eaf477c68bbcdcd1abd7af7d1b24f7/lcm-1.5.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:a97858daaee197c86d4b8e07be8776f9e1a0d534fdc12652843109bf4136f494", size = 3561813, upload-time = "2025-10-23T20:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9a/be81983818b96c6ef8b908d981d31714a927b0946c11029227e7abd9b395/lcm-1.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:29f1789cca094defbbf212384e7dae5c74db00fc0099b423b4dbb95c4bd42eef", size = 3494781, upload-time = "2025-10-23T20:34:35.598Z" }, + { url = "https://files.pythonhosted.org/packages/28/9b/dcafbbb3b9e1053642ee99271f1878c7a89a63a1bca4043f3e41276ee2db/lcm-1.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d7c36fbdd4dad337db70f89cb6235f8b75f20d4a28e683f332d23dbaa4ac1a05", size = 2861393, upload-time = "2025-10-23T20:34:41.31Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/d6704a6e95684d5567c558509b84d0badca886e7f245fa89323fb203a563/lcm-1.5.2-cp312-cp312-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ef22397ae59c20bbafd5e152f1db00830d69d5d3e3904ca0b8fc658c415e32a", size = 2852933, upload-time = "2025-10-23T20:34:45.436Z" }, + { url = "https://files.pythonhosted.org/packages/66/f6/d92a3d3bee9a7d016084da33aa558683918e5a8a1d4207fb188f901a7684/lcm-1.5.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6386c60190fb180ec40ae5a6820103b1db96d1d1996834461c6a95d116219aaa", size = 2881476, upload-time = "2025-10-23T20:34:48.571Z" }, + { url = "https://files.pythonhosted.org/packages/b3/15/38d577361788c8b0a90e53bf3a108f13d3234f1e9fcdda67191f2119c57b/lcm-1.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec234ed44e9c0f090014f65f3b82ba8b390485769073994ed7d77c7a75b06187", size = 1497521, upload-time = "2025-10-23T20:38:25.569Z" }, + { url = "https://files.pythonhosted.org/packages/35/2f/80628f6a5e1984c97755bf332cf8d0551ddf73f379e10e6b22e44aead561/lcm-1.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1f22b9beb77df7ef19725881abeb9acad9648c2b8874ed6cb9f0587442db503d", size = 1341802, upload-time = "2025-10-23T20:38:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/2c2f3fc73fca45dd0204ae0c794d98b58f001cd1032bfa93e2fc0846c7e4/lcm-1.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6b41b7933d0c4ad89db4e1f6b05ec7bcb99a9a65d5f961f8c1eab5819c6b50a", size = 1542621, upload-time = "2025-10-23T20:38:29.243Z" }, + { url = "https://files.pythonhosted.org/packages/26/ba/46d35b86bc23431d81826e11768e2c445e954478dc0e2f59a7f9fb84352e/lcm-1.5.2-cp312-cp312-win32.whl", hash = "sha256:8e1603ba8e1bf80d62644a1762a8785fe31211bf9c672b57f2bb7978271a3edb", size = 4192867, upload-time = "2025-10-23T20:38:32.937Z" }, + { url = "https://files.pythonhosted.org/packages/30/fa/f4d73ae40dfae33fba25a6ab70b7c70c3c49c699bf908399b0dd15e5f136/lcm-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d034c3623b878a0cb24362298fec4d908ca916d00d15133e31954eaf2d6072f4", size = 4409073, upload-time = "2025-10-23T20:38:37.067Z" }, + { url = "https://files.pythonhosted.org/packages/34/d2/bf7992a1573079329c4eb5cf34f5109046e2432b966da70eb7e6a22defe4/lcm-1.5.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:253522c62073d8b60d12a2f9efe8744c17a4d4f298de4585018320db7b2bf528", size = 3561913, upload-time = "2025-10-23T20:38:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/f26552f6f0a48dc4931d7f9cf00adbe0d972d1710a17b7c49599115b48da/lcm-1.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6f623d92091894f1e1829c5a2b973da89af27c07ea4047003b56aac2e1ad0888", size = 3494776, upload-time = "2025-10-23T20:38:43.87Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e8/673ecec01037b977db6ca34a657e04ef487f6c6b13c2c71dc16c1c3b3e0a/lcm-1.5.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0aba4fddc0bf8b9703af3f7a1dd7571a96ba40c5c40c04cf273a67755848bfdc", size = 2861402, upload-time = "2025-10-23T20:38:46.642Z" }, + { url = "https://files.pythonhosted.org/packages/ef/11/a20a879f4be8ba545ae748f2e41c53eac7ef16bb313339fb39ac97ae54ac/lcm-1.5.2-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3c97429fc741cd1e3b08775649af71cc6716d35a34160a6be094db89b582b179", size = 2853161, upload-time = "2025-10-23T20:38:50.244Z" }, + { url = "https://files.pythonhosted.org/packages/b9/97/d7594a34ae05618786e39c8156ab23abd10e3009d17d19c6068c66491e38/lcm-1.5.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e314e125a4a818f9a2d8e8a2dd8c39d3fe080278391490dfa808123f515cfe4b", size = 2881504, upload-time = "2025-10-23T20:38:53.035Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c8/f999d66df34b0e9db866e544ad01a2842607b6debed7ac201eecb3cebce0/lcm-1.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:55c7fb7eea47e101e4419a9c35ee386da3c3a874c84834694feb6016cffc23f9", size = 1497529, upload-time = "2025-10-23T20:38:54.9Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/51e79f9ed886eb0b24a9b840eab07a729d4e696cbb6ac070052966e542e4/lcm-1.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4587f97e2d2623551c092e7a9ee363cc3df65c2f6fb09bcd4d79b07b60515fde", size = 1341805, upload-time = "2025-10-23T20:38:56.555Z" }, + { url = "https://files.pythonhosted.org/packages/3e/78/9b47aa19d416bd671ce538223fcc044a5b8a5edc88f0de97e27081517f07/lcm-1.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f41574cc3da5af6d27e16ceb55cb27e4486403d3755bc3cd1cd9366ae758f9e", size = 1542632, upload-time = "2025-10-23T20:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/e8/36/0843752b05625b899063202f715f541eb4cdbeb8da86c2ee44b235865f22/lcm-1.5.2-cp313-cp313-win32.whl", hash = "sha256:b81f995c7104168d0079a24df26adf2174ff88ac99964c3cd04e0eb3d92c41fe", size = 4192684, upload-time = "2025-10-23T20:39:02.073Z" }, + { url = "https://files.pythonhosted.org/packages/f6/3e/eaf0282e8bc604c8630c99bf1cdcbffd7b7e14b4d37e6f5291e6ec0832dd/lcm-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:8c2a9646fdb446edda2a8d80d155fc6c29aa9618e8837267781f610d0da88fe5", size = 4408991, upload-time = "2025-10-23T20:39:06.066Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d7/c760e59a707c0925004f97a7282f05aee9cfe9c786a0ab82d936189f0f3e/lcm-1.5.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:d73506022bec844b4600fd0a2cddd0aa7ffbce87d16f115bcbbcf9a1d811175d", size = 3562016, upload-time = "2025-10-23T20:39:09.6Z" }, + { url = "https://files.pythonhosted.org/packages/17/a5/02c4d75bd644742677b11c45c9cb0eb45244c2b2c3b5b47dde0767f3fe41/lcm-1.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3d580d496a9b4ba8b8746b474d094876b0c7152893b4b97620ad3c44906ebe4b", size = 3494802, upload-time = "2025-10-23T20:39:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/20/a9/3ec71158e6aa12b0f45daa0b456e7fd7495453a74f202259e6cdbc2a2953/lcm-1.5.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a65bf315781e3252708887a9f142d0dedd82fa446483860169f432a33d8ed0f4", size = 2861486, upload-time = "2025-10-23T20:39:15.915Z" }, + { url = "https://files.pythonhosted.org/packages/49/e3/088dd2ba297697f7566518c8027838d4b8ed32974745a8ba899bdeb7fd64/lcm-1.5.2-cp314-cp314-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23e6bf3c7b1572c3a6fb505fba4f1179dc6c0fba04ff5e117dd0c33199569300", size = 2852991, upload-time = "2025-10-23T20:39:18.729Z" }, + { url = "https://files.pythonhosted.org/packages/bd/bf/965f3552cff59b8de0fdc2f5ed6195946cdd4a0861bdf6f82736f28c37af/lcm-1.5.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49fd13d8d5e967cfe7ddcd5a8b490743b02e95238a2ab1dbf7cea701fbb3f37c", size = 2881653, upload-time = "2025-10-23T20:39:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/5b93f4435625d8519fb48114bf60980cadbdecf67e036478fcb25e23d60b/lcm-1.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:599e3b400d2aff260d5f8c5386d6ae9fe7d414cc6ec869dafbf5b399ef040a3a", size = 1497598, upload-time = "2025-10-23T20:39:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/87/54/0431bb0ee7b223a70c35129e9af71c152b184d637543c1120e6cb14246c6/lcm-1.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:85257675c0b4c0035d7a11f0623537df5121411923d3d354c78547864e08aa10", size = 1341793, upload-time = "2025-10-23T20:39:25.628Z" }, + { url = "https://files.pythonhosted.org/packages/eb/42/045dd17d86e77de1547bee632dbb8d03bd6720880a232f995c0bf846e539/lcm-1.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:df99360130df5ce89b54034fb9b6b4db0a752bb6e5ab826c1fa99b4bc78d8022", size = 1542610, upload-time = "2025-10-23T20:39:27.462Z" }, + { url = "https://files.pythonhosted.org/packages/34/fd/07951d7252baa327d88e5154cefbfc3ceef17cdbacbbaf7a1d4cb4a1ec98/lcm-1.5.2-cp314-cp314-win32.whl", hash = "sha256:19617cdfa1e3f757798d3f03789dbc76148bd03aa4b07a7fa456bc3af8a74c86", size = 4237953, upload-time = "2025-10-23T20:39:31.269Z" }, + { url = "https://files.pythonhosted.org/packages/80/73/623eb9f29fe54ef2109cc9ed6d49dbdd1845c625463390c067fb2ea9c7a8/lcm-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:6ba84f4e97f61ea55bb09e8201b0bd47380332118e7199674ec9f85cb1175de3", size = 4467113, upload-time = "2025-10-23T20:39:35.195Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/84/2cfb1f3b9b60bab52e16a220c931223fc8e963d0d7bb9132bef012aafc3f/librt-0.7.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0", size = 54709, upload-time = "2026-01-01T23:50:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/19/a1/3127b277e9d3784a8040a54e8396d9ae5c64d6684dc6db4b4089b0eedcfb/librt-0.7.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8", size = 56658, upload-time = "2026-01-01T23:50:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e9/b91b093a5c42eb218120445f3fef82e0b977fa2225f4d6fc133d25cdf86a/librt-0.7.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06", size = 161026, upload-time = "2026-01-01T23:50:50.853Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cb/1ded77d5976a79d7057af4a010d577ce4f473ff280984e68f4974a3281e5/librt-0.7.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495", size = 169529, upload-time = "2026-01-01T23:50:52.24Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/6ca5bdaa701e15f05000ac1a4c5d1475c422d3484bd3d1ca9e8c2f5be167/librt-0.7.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0", size = 183271, upload-time = "2026-01-01T23:50:55.287Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2d/55c0e38073997b4bbb5ddff25b6d1bbba8c2f76f50afe5bb9c844b702f34/librt-0.7.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e", size = 179039, upload-time = "2026-01-01T23:50:56.807Z" }, + { url = "https://files.pythonhosted.org/packages/33/4e/3662a41ae8bb81b226f3968426293517b271d34d4e9fd4b59fc511f1ae40/librt-0.7.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c", size = 173505, upload-time = "2026-01-01T23:50:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5d/cf768deb8bdcbac5f8c21fcb32dd483d038d88c529fd351bbe50590b945d/librt-0.7.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8", size = 193570, upload-time = "2026-01-01T23:50:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/ee70effd13f1d651976d83a2812391f6203971740705e3c0900db75d4bce/librt-0.7.7-cp310-cp310-win32.whl", hash = "sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900", size = 42600, upload-time = "2026-01-01T23:51:00.694Z" }, + { url = "https://files.pythonhosted.org/packages/f0/eb/dc098730f281cba76c279b71783f5de2edcba3b880c1ab84a093ef826062/librt-0.7.7-cp310-cp310-win_amd64.whl", hash = "sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58", size = 48977, upload-time = "2026-01-01T23:51:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/f0/56/30b5c342518005546df78841cb0820ae85a17e7d07d521c10ef367306d0d/librt-0.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee", size = 54709, upload-time = "2026-01-01T23:51:02.774Z" }, + { url = "https://files.pythonhosted.org/packages/72/78/9f120e3920b22504d4f3835e28b55acc2cc47c9586d2e1b6ba04c3c1bf01/librt-0.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8", size = 56663, upload-time = "2026-01-01T23:51:03.838Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/7d7a1ee7dfc1151836028eba25629afcf45b56bbc721293e41aa2e9b8934/librt-0.7.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85", size = 161705, upload-time = "2026-01-01T23:51:04.917Z" }, + { url = "https://files.pythonhosted.org/packages/45/a5/952bc840ac8917fbcefd6bc5f51ad02b89721729814f3e2bfcc1337a76d6/librt-0.7.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99", size = 171029, upload-time = "2026-01-01T23:51:06.09Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bf/c017ff7da82dc9192cf40d5e802a48a25d00e7639b6465cfdcee5893a22c/librt-0.7.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb", size = 184704, upload-time = "2026-01-01T23:51:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/77/ec/72f3dd39d2cdfd6402ab10836dc9cbf854d145226062a185b419c4f1624a/librt-0.7.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74", size = 180719, upload-time = "2026-01-01T23:51:09.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/86/06e7a1a81b246f3313bf515dd9613a1c81583e6fd7843a9f4d625c4e926d/librt-0.7.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75", size = 174537, upload-time = "2026-01-01T23:51:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/83/08/f9fb2edc9c7a76e95b2924ce81d545673f5b034e8c5dd92159d1c7dae0c6/librt-0.7.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28", size = 195238, upload-time = "2026-01-01T23:51:11.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/56/ea2d2489d3ea1f47b301120e03a099e22de7b32c93df9a211e6ff4f9bf38/librt-0.7.7-cp311-cp311-win32.whl", hash = "sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151", size = 42939, upload-time = "2026-01-01T23:51:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/c288f417e42ba2a037f1c0753219e277b33090ed4f72f292fb6fe175db4c/librt-0.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a", size = 49240, upload-time = "2026-01-01T23:51:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/738eb33a6c1516fdb2dfd2a35db6e5300f7616679b573585be0409bc6890/librt-0.7.7-cp311-cp311-win_arm64.whl", hash = "sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890", size = 42613, upload-time = "2026-01-01T23:51:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" }, + { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/af0ffb724814cc2ea64445acad05f71cff5f799bb7efb22e47ee99340dbc/llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547", size = 37232768, upload-time = "2025-12-08T18:15:25.055Z" }, + { url = "https://files.pythonhosted.org/packages/c9/19/5018e5352019be753b7b07f7759cdabb69ca5779fea2494be8839270df4c/llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d", size = 56275173, upload-time = "2025-12-08T18:15:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c9/d57877759d707e84c082163c543853245f91b70c804115a5010532890f18/llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269", size = 55128628, upload-time = "2025-12-08T18:15:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" }, +] + +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, +] + +[[package]] +name = "logistro" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/90/bfd7a6fab22bdfafe48ed3c4831713cb77b4779d18ade5e248d5dbc0ca22/logistro-2.0.1.tar.gz", hash = "sha256:8446affc82bab2577eb02bfcbcae196ae03129287557287b6a070f70c1985047", size = 8398, upload-time = "2025-11-01T02:41:18.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/6aa79ba3570bddd1bf7e951c6123f806751e58e8cce736bad77b2cf348d7/logistro-2.0.1-py3-none-any.whl", hash = "sha256:06ffa127b9fb4ac8b1972ae6b2a9d7fde57598bf5939cd708f43ec5bba2d31eb", size = 8555, upload-time = "2025-11-01T02:41:17.587Z" }, +] + +[[package]] +name = "lvis" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cycler" }, + { name = "cython" }, + { name = "kiwisolver" }, + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opencv-python" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/fe/c18531099e7538bd6a53de8b2f8e900a5cf6a82d0c603325031a4122da5a/lvis-0.5.3.tar.gz", hash = "sha256:55aeeb84174abea2ed0d6985a8e93aa9bdbb60c61c6db130c8269a275ef61a6e", size = 12084, upload-time = "2020-06-18T01:34:01.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/b6/1992240ab48310b5360bfdd1d53163f43bb97d90dc5dc723c67d41c38e78/lvis-0.5.3-py3-none-any.whl", hash = "sha256:4f07153330df342b3161fafb46641ce7c02864113a8ddf0d6ffab6b02407bef0", size = 14024, upload-time = "2020-06-18T01:34:00.332Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "lxml-stubs" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778, upload-time = "2024-01-10T09:37:46.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584, upload-time = "2024-01-10T09:37:44.931Z" }, +] + +[[package]] +name = "lz4" +version = "4.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/45/2466d73d79e3940cad4b26761f356f19fd33f4409c96f100e01a5c566909/lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d", size = 207396, upload-time = "2025-11-03T13:01:24.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/7da96077a7e8918a5a57a25f1254edaf76aefb457666fcc1066deeecd609/lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f", size = 207154, upload-time = "2025-11-03T13:01:26.922Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/0fb54f84fd1890d4af5bc0a3c1fa69678451c1a6bd40de26ec0561bb4ec5/lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3", size = 1291053, upload-time = "2025-11-03T13:01:28.396Z" }, + { url = "https://files.pythonhosted.org/packages/15/45/8ce01cc2715a19c9e72b0e423262072c17d581a8da56e0bd4550f3d76a79/lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758", size = 1278586, upload-time = "2025-11-03T13:01:29.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/34/7be9b09015e18510a09b8d76c304d505a7cbc66b775ec0b8f61442316818/lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1", size = 1367315, upload-time = "2025-11-03T13:01:31.054Z" }, + { url = "https://files.pythonhosted.org/packages/2a/94/52cc3ec0d41e8d68c985ec3b2d33631f281d8b748fb44955bc0384c2627b/lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc", size = 88173, upload-time = "2025-11-03T13:01:32.643Z" }, + { url = "https://files.pythonhosted.org/packages/ca/35/c3c0bdc409f551404355aeeabc8da343577d0e53592368062e371a3620e1/lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd", size = 99492, upload-time = "2025-11-03T13:01:33.813Z" }, + { url = "https://files.pythonhosted.org/packages/1d/02/4d88de2f1e97f9d05fd3d278fe412b08969bc94ff34942f5a3f09318144a/lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397", size = 91280, upload-time = "2025-11-03T13:01:35.081Z" }, + { url = "https://files.pythonhosted.org/packages/93/5b/6edcd23319d9e28b1bedf32768c3d1fd56eed8223960a2c47dacd2cec2af/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4", size = 207391, upload-time = "2025-11-03T13:01:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/5f9b772e85b3d5769367a79973b8030afad0d6b724444083bad09becd66f/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43", size = 207146, upload-time = "2025-11-03T13:01:37.928Z" }, + { url = "https://files.pythonhosted.org/packages/04/f4/f66da5647c0d72592081a37c8775feacc3d14d2625bbdaabd6307c274565/lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7", size = 1292623, upload-time = "2025-11-03T13:01:39.341Z" }, + { url = "https://files.pythonhosted.org/packages/85/fc/5df0f17467cdda0cad464a9197a447027879197761b55faad7ca29c29a04/lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb", size = 1279982, upload-time = "2025-11-03T13:01:40.816Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/b55cb577aa148ed4e383e9700c36f70b651cd434e1c07568f0a86c9d5fbb/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989", size = 1368674, upload-time = "2025-11-03T13:01:42.118Z" }, + { url = "https://files.pythonhosted.org/packages/fb/31/e97e8c74c59ea479598e5c55cbe0b1334f03ee74ca97726e872944ed42df/lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d", size = 88168, upload-time = "2025-11-03T13:01:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/18/47/715865a6c7071f417bef9b57c8644f29cb7a55b77742bd5d93a609274e7e/lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004", size = 99491, upload-time = "2025-11-03T13:01:44.167Z" }, + { url = "https://files.pythonhosted.org/packages/14/e7/ac120c2ca8caec5c945e6356ada2aa5cfabd83a01e3170f264a5c42c8231/lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b", size = 91271, upload-time = "2025-11-03T13:01:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/016e4f6de37d806f7cc8f13add0a46c9a7cfc41a5ddc2bc831d7954cf1ce/lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e", size = 207163, upload-time = "2025-11-03T13:01:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/8d/df/0fadac6e5bd31b6f34a1a8dbd4db6a7606e70715387c27368586455b7fc9/lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a", size = 207150, upload-time = "2025-11-03T13:01:47.205Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/34e36cc49bb16ca73fb57fbd4c5eaa61760c6b64bce91fcb4e0f4a97f852/lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5", size = 1292045, upload-time = "2025-11-03T13:01:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/b1d8e3741e9fc89ed3b5f7ef5f22586c07ed6bb04e8343c2e98f0fa7ff04/lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e", size = 1279546, upload-time = "2025-11-03T13:01:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/e3867222474f6c1b76e89f3bd914595af69f55bf2c1866e984c548afdc15/lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", size = 1368249, upload-time = "2025-11-03T13:01:51.273Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e7/d667d337367686311c38b580d1ca3d5a23a6617e129f26becd4f5dc458df/lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50", size = 88189, upload-time = "2025-11-03T13:01:52.605Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0b/a54cd7406995ab097fceb907c7eb13a6ddd49e0b231e448f1a81a50af65c/lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33", size = 99497, upload-time = "2025-11-03T13:01:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7e/dc28a952e4bfa32ca16fa2eb026e7a6ce5d1411fcd5986cd08c74ec187b9/lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301", size = 91279, upload-time = "2025-11-03T13:01:54.419Z" }, + { url = "https://files.pythonhosted.org/packages/2f/46/08fd8ef19b782f301d56a9ccfd7dafec5fd4fc1a9f017cf22a1accb585d7/lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c", size = 207171, upload-time = "2025-11-03T13:01:56.595Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3f/ea3334e59de30871d773963997ecdba96c4584c5f8007fd83cfc8f1ee935/lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a", size = 207163, upload-time = "2025-11-03T13:01:57.721Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/7b3a2a0feb998969f4793c650bb16eff5b06e80d1f7bff867feb332f2af2/lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d", size = 1292136, upload-time = "2025-11-03T13:02:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/89/d1/f1d259352227bb1c185288dd694121ea303e43404aa77560b879c90e7073/lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c", size = 1279639, upload-time = "2025-11-03T13:02:01.649Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fb/ba9256c48266a09012ed1d9b0253b9aa4fe9cdff094f8febf5b26a4aa2a2/lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64", size = 1368257, upload-time = "2025-11-03T13:02:03.35Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6d/dee32a9430c8b0e01bbb4537573cabd00555827f1a0a42d4e24ca803935c/lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832", size = 88191, upload-time = "2025-11-03T13:02:04.406Z" }, + { url = "https://files.pythonhosted.org/packages/18/e0/f06028aea741bbecb2a7e9648f4643235279a770c7ffaf70bd4860c73661/lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22", size = 99502, upload-time = "2025-11-03T13:02:05.886Z" }, + { url = "https://files.pythonhosted.org/packages/61/72/5bef44afb303e56078676b9f2486f13173a3c1e7f17eaac1793538174817/lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9", size = 91285, upload-time = "2025-11-03T13:02:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/6a5c2952971af73f15ed4ebfdd69774b454bd0dc905b289082ca8664fba1/lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f", size = 207348, upload-time = "2025-11-03T13:02:08.117Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d7/fd62cbdbdccc35341e83aabdb3f6d5c19be2687d0a4eaf6457ddf53bba64/lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba", size = 207340, upload-time = "2025-11-03T13:02:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/225ffadaacb4b0e0eb5fd263541edd938f16cd21fe1eae3cd6d5b6a259dc/lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d", size = 1293398, upload-time = "2025-11-03T13:02:10.272Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9e/2ce59ba4a21ea5dc43460cba6f34584e187328019abc0e66698f2b66c881/lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67", size = 1281209, upload-time = "2025-11-03T13:02:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/4d946bd1624ec229b386a3bc8e7a85fa9a963d67d0a62043f0af0978d3da/lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d", size = 1369406, upload-time = "2025-11-03T13:02:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/d429ba4720a9064722698b4b754fb93e42e625f1318b8fe834086c7c783b/lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901", size = 88325, upload-time = "2025-11-03T13:02:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/7ba10c9b97c06af6c8f7032ec942ff127558863df52d866019ce9d2425cf/lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb", size = 99643, upload-time = "2025-11-03T13:02:15.978Z" }, + { url = "https://files.pythonhosted.org/packages/77/4d/a175459fb29f909e13e57c8f475181ad8085d8d7869bd8ad99033e3ee5fa/lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd", size = 91504, upload-time = "2025-11-03T13:02:17.313Z" }, + { url = "https://files.pythonhosted.org/packages/63/9c/70bdbdb9f54053a308b200b4678afd13efd0eafb6ddcbb7f00077213c2e5/lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f", size = 207586, upload-time = "2025-11-03T13:02:18.263Z" }, + { url = "https://files.pythonhosted.org/packages/b6/cb/bfead8f437741ce51e14b3c7d404e3a1f6b409c440bad9b8f3945d4c40a7/lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6", size = 207161, upload-time = "2025-11-03T13:02:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/e7/18/b192b2ce465dfbeabc4fc957ece7a1d34aded0d95a588862f1c8a86ac448/lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9", size = 1292415, upload-time = "2025-11-03T13:02:20.829Z" }, + { url = "https://files.pythonhosted.org/packages/67/79/a4e91872ab60f5e89bfad3e996ea7dc74a30f27253faf95865771225ccba/lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668", size = 1279920, upload-time = "2025-11-03T13:02:22.013Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/d52c7b11eaa286d49dae619c0eec4aabc0bf3cda7a7467eb77c62c4471f3/lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f", size = 1368661, upload-time = "2025-11-03T13:02:23.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/137ddeea14c2cb86864838277b2607d09f8253f152156a07f84e11768a28/lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67", size = 90139, upload-time = "2025-11-03T13:02:24.301Z" }, + { url = "https://files.pythonhosted.org/packages/18/2c/8332080fd293f8337779a440b3a143f85e374311705d243439a3349b81ad/lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be", size = 101497, upload-time = "2025-11-03T13:02:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/2635a8141c9a4f4bc23f5135a92bbcf48d928d8ca094088c962df1879d64/lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7", size = 93812, upload-time = "2025-11-03T13:02:26.133Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] +plugins = [ + { name = "mdit-py-plugins" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mcp" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "ml-collections" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/f8/1a9ae6696dbb6bc9c44ddf5c5e84710d77fe9a35a57e8a06722e1836a4a6/ml_collections-1.1.0.tar.gz", hash = "sha256:0ac1ac6511b9f1566863e0bb0afad0c64e906ea278ad3f4d2144a55322671f6f", size = 61356, upload-time = "2025-04-17T08:25:02.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/8a/18d4ff2c7bd83f30d6924bd4ad97abf418488c3f908dea228d6f0961ad68/ml_collections-1.1.0-py3-none-any.whl", hash = "sha256:23b6fa4772aac1ae745a96044b925a5746145a70734f087eaca6626e92c05cbc", size = 76707, upload-time = "2025-04-17T08:24:59.038Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + +[[package]] +name = "mmcv" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "addict" }, + { name = "mmengine" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opencv-python" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "regex", marker = "sys_platform == 'win32'" }, + { name = "yapf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a2/57a733e7e84985a8a0e3101dfb8170fc9db92435c16afad253069ae3f9df/mmcv-2.2.0.tar.gz", hash = "sha256:ac479247e808d8802f89eadf04d4118de86bdfe81361ec5aed0cc1bf731c67c9", size = 479121, upload-time = "2024-04-24T14:24:28.064Z" } + +[[package]] +name = "mmengine" +version = "0.10.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "addict" }, + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opencv-python" }, + { name = "pyyaml" }, + { name = "regex", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "termcolor" }, + { name = "yapf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/14/959360bbd8374e23fc1b720906999add16a3ac071a501636db12c5861ff5/mmengine-0.10.7.tar.gz", hash = "sha256:d20ffcc31127567e53dceff132612a87f0081de06cbb7ab2bdb7439125a69225", size = 378090, upload-time = "2025-03-04T12:23:09.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/8e/f98332248aad102511bea4ae19c0ddacd2f0a994f3ca4c82b7a369e0af8b/mmengine-0.10.7-py3-none-any.whl", hash = "sha256:262ac976a925562f78cd5fd14dd1bc9b680ed0aa81f0d85b723ef782f99c54ee", size = 452720, upload-time = "2025-03-04T12:23:06.339Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582, upload-time = "2025-07-29T07:43:48.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/2b/870f0ff5ecf312c58500f45950751f214b7068665e66e9bfd8bc2595587c/mmh3-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc", size = 56119, upload-time = "2025-07-29T07:41:39.117Z" }, + { url = "https://files.pythonhosted.org/packages/3b/88/eb9a55b3f3cf43a74d6bfa8db0e2e209f966007777a1dc897c52c008314c/mmh3-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328", size = 40634, upload-time = "2025-07-29T07:41:40.626Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4c/8e4b3878bf8435c697d7ce99940a3784eb864521768069feaccaff884a17/mmh3-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d", size = 40080, upload-time = "2025-07-29T07:41:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/0a254402c8c5ca424a0a9ebfe870f5665922f932830f0a11a517b6390a09/mmh3-5.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e", size = 95321, upload-time = "2025-07-29T07:41:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/29306d5eca6dfda4b899d22c95b5420db4e0ffb7e0b6389b17379654ece5/mmh3-5.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515", size = 101220, upload-time = "2025-07-29T07:41:43.572Z" }, + { url = "https://files.pythonhosted.org/packages/49/f7/0dd1368e531e52a17b5b8dd2f379cce813bff2d0978a7748a506f1231152/mmh3-5.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3", size = 103991, upload-time = "2025-07-29T07:41:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/35/06/abc7122c40f4abbfcef01d2dac6ec0b77ede9757e5be8b8a40a6265b1274/mmh3-5.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e", size = 110894, upload-time = "2025-07-29T07:41:45.849Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/837885759afa4baccb8e40456e1cf76a4f3eac835b878c727ae1286c5f82/mmh3-5.2.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d", size = 118327, upload-time = "2025-07-29T07:41:47.224Z" }, + { url = "https://files.pythonhosted.org/packages/40/cc/5683ba20a21bcfb3f1605b1c474f46d30354f728a7412201f59f453d405a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772", size = 101701, upload-time = "2025-07-29T07:41:48.259Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/99ab3fb940150aec8a26dbdfc39b200b5592f6aeb293ec268df93e054c30/mmh3-5.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89", size = 96712, upload-time = "2025-07-29T07:41:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/61/04/d7c4cb18f1f001ede2e8aed0f9dbbfad03d161c9eea4fffb03f14f4523e5/mmh3-5.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2", size = 110302, upload-time = "2025-07-29T07:41:50.387Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bf/4dac37580cfda74425a4547500c36fa13ef581c8a756727c37af45e11e9a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc", size = 111929, upload-time = "2025-07-29T07:41:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b1/49f0a582c7a942fb71ddd1ec52b7d21d2544b37d2b2d994551346a15b4f6/mmh3-5.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106", size = 100111, upload-time = "2025-07-29T07:41:53.139Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/ccec09f438caeb2506f4c63bb3b99aa08a9e09880f8fc047295154756210/mmh3-5.2.0-cp310-cp310-win32.whl", hash = "sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d", size = 40783, upload-time = "2025-07-29T07:41:54.463Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f4/8d39a32c8203c1cdae88fdb04d1ea4aa178c20f159df97f4c5a2eaec702c/mmh3-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb", size = 41549, upload-time = "2025-07-29T07:41:55.295Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/30efb1cd945e193f62574144dd92a0c9ee6463435e4e8ffce9b9e9f032f0/mmh3-5.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8", size = 39335, upload-time = "2025-07-29T07:41:56.194Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/399567b3796e134352e11a8b973cd470c06b2ecfad5468fe580833be442b/mmh3-5.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1", size = 56107, upload-time = "2025-07-29T07:41:57.07Z" }, + { url = "https://files.pythonhosted.org/packages/c3/09/830af30adf8678955b247d97d3d9543dd2fd95684f3cd41c0cd9d291da9f/mmh3-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051", size = 40635, upload-time = "2025-07-29T07:41:57.903Z" }, + { url = "https://files.pythonhosted.org/packages/07/14/eaba79eef55b40d653321765ac5e8f6c9ac38780b8a7c2a2f8df8ee0fb72/mmh3-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10", size = 40078, upload-time = "2025-07-29T07:41:58.772Z" }, + { url = "https://files.pythonhosted.org/packages/bb/26/83a0f852e763f81b2265d446b13ed6d49ee49e1fc0c47b9655977e6f3d81/mmh3-5.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c", size = 97262, upload-time = "2025-07-29T07:41:59.678Z" }, + { url = "https://files.pythonhosted.org/packages/00/7d/b7133b10d12239aeaebf6878d7eaf0bf7d3738c44b4aba3c564588f6d802/mmh3-5.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762", size = 103118, upload-time = "2025-07-29T07:42:01.197Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3e/62f0b5dce2e22fd5b7d092aba285abd7959ea2b17148641e029f2eab1ffa/mmh3-5.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4", size = 106072, upload-time = "2025-07-29T07:42:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/66/84/ea88bb816edfe65052c757a1c3408d65c4201ddbd769d4a287b0f1a628b2/mmh3-5.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363", size = 112925, upload-time = "2025-07-29T07:42:03.632Z" }, + { url = "https://files.pythonhosted.org/packages/2e/13/c9b1c022807db575fe4db806f442d5b5784547e2e82cff36133e58ea31c7/mmh3-5.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8", size = 120583, upload-time = "2025-07-29T07:42:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/0e2dfe1a38f6a78788b7eb2b23432cee24623aeabbc907fed07fc17d6935/mmh3-5.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed", size = 99127, upload-time = "2025-07-29T07:42:05.929Z" }, + { url = "https://files.pythonhosted.org/packages/77/27/aefb7d663b67e6a0c4d61a513c83e39ba2237e8e4557fa7122a742a23de5/mmh3-5.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646", size = 98544, upload-time = "2025-07-29T07:42:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/ab/97/a21cc9b1a7c6e92205a1b5fa030cdf62277d177570c06a239eca7bd6dd32/mmh3-5.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b", size = 106262, upload-time = "2025-07-29T07:42:07.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/18/db19ae82ea63c8922a880e1498a75342311f8aa0c581c4dd07711473b5f7/mmh3-5.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779", size = 109824, upload-time = "2025-07-29T07:42:08.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f5/41dcf0d1969125fc6f61d8618b107c79130b5af50b18a4651210ea52ab40/mmh3-5.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2", size = 97255, upload-time = "2025-07-29T07:42:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/cce9eaa0efac1f0e735bb178ef9d1d2887b4927fe0ec16609d5acd492dda/mmh3-5.2.0-cp311-cp311-win32.whl", hash = "sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28", size = 40779, upload-time = "2025-07-29T07:42:10.546Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/3fa0290122e6d5a7041b50ae500b8a9f4932478a51e48f209a3879fe0b9b/mmh3-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee", size = 41549, upload-time = "2025-07-29T07:42:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/3a/54/c277475b4102588e6f06b2e9095ee758dfe31a149312cdbf62d39a9f5c30/mmh3-5.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9", size = 39336, upload-time = "2025-07-29T07:42:12.209Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6a/d5aa7edb5c08e0bd24286c7d08341a0446f9a2fbbb97d96a8a6dd81935ee/mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be", size = 56141, upload-time = "2025-07-29T07:42:13.456Z" }, + { url = "https://files.pythonhosted.org/packages/08/49/131d0fae6447bc4a7299ebdb1a6fb9d08c9f8dcf97d75ea93e8152ddf7ab/mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd", size = 40681, upload-time = "2025-07-29T07:42:14.306Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6f/9221445a6bcc962b7f5ff3ba18ad55bba624bacdc7aa3fc0a518db7da8ec/mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96", size = 40062, upload-time = "2025-07-29T07:42:15.08Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d4/6bb2d0fef81401e0bb4c297d1eb568b767de4ce6fc00890bc14d7b51ecc4/mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094", size = 97333, upload-time = "2025-07-29T07:42:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/44/e0/ccf0daff8134efbb4fbc10a945ab53302e358c4b016ada9bf97a6bdd50c1/mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037", size = 103310, upload-time = "2025-07-29T07:42:17.796Z" }, + { url = "https://files.pythonhosted.org/packages/02/63/1965cb08a46533faca0e420e06aff8bbaf9690a6f0ac6ae6e5b2e4544687/mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773", size = 106178, upload-time = "2025-07-29T07:42:19.281Z" }, + { url = "https://files.pythonhosted.org/packages/c2/41/c883ad8e2c234013f27f92061200afc11554ea55edd1bcf5e1accd803a85/mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5", size = 113035, upload-time = "2025-07-29T07:42:20.356Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/1ccade8b1fa625d634a18bab7bf08a87457e09d5ec8cf83ca07cbea9d400/mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50", size = 120784, upload-time = "2025-07-29T07:42:21.377Z" }, + { url = "https://files.pythonhosted.org/packages/77/1c/919d9171fcbdcdab242e06394464ccf546f7d0f3b31e0d1e3a630398782e/mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765", size = 99137, upload-time = "2025-07-29T07:42:22.344Z" }, + { url = "https://files.pythonhosted.org/packages/66/8a/1eebef5bd6633d36281d9fc83cf2e9ba1ba0e1a77dff92aacab83001cee4/mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43", size = 98664, upload-time = "2025-07-29T07:42:23.269Z" }, + { url = "https://files.pythonhosted.org/packages/13/41/a5d981563e2ee682b21fb65e29cc0f517a6734a02b581359edd67f9d0360/mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4", size = 106459, upload-time = "2025-07-29T07:42:24.238Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/342494cd6ab792d81e083680875a2c50fa0c5df475ebf0b67784f13e4647/mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3", size = 110038, upload-time = "2025-07-29T07:42:25.629Z" }, + { url = "https://files.pythonhosted.org/packages/28/44/efda282170a46bb4f19c3e2b90536513b1d821c414c28469a227ca5a1789/mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c", size = 97545, upload-time = "2025-07-29T07:42:27.04Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/534ae319c6e05d714f437e7206f78c17e66daca88164dff70286b0e8ea0c/mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49", size = 40805, upload-time = "2025-07-29T07:42:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f6/f6abdcfefcedab3c964868048cfe472764ed358c2bf6819a70dd4ed4ed3a/mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3", size = 41597, upload-time = "2025-07-29T07:42:28.894Z" }, + { url = "https://files.pythonhosted.org/packages/15/fd/f7420e8cbce45c259c770cac5718badf907b302d3a99ec587ba5ce030237/mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0", size = 39350, upload-time = "2025-07-29T07:42:29.794Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874, upload-time = "2025-07-29T07:42:30.662Z" }, + { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012, upload-time = "2025-07-29T07:42:31.539Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197, upload-time = "2025-07-29T07:42:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840, upload-time = "2025-07-29T07:42:33.227Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644, upload-time = "2025-07-29T07:42:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153, upload-time = "2025-07-29T07:42:35.031Z" }, + { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684, upload-time = "2025-07-29T07:42:35.914Z" }, + { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057, upload-time = "2025-07-29T07:42:36.755Z" }, + { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344, upload-time = "2025-07-29T07:42:37.675Z" }, + { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325, upload-time = "2025-07-29T07:42:38.591Z" }, + { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240, upload-time = "2025-07-29T07:42:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060, upload-time = "2025-07-29T07:42:40.585Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781, upload-time = "2025-07-29T07:42:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174, upload-time = "2025-07-29T07:42:42.587Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734, upload-time = "2025-07-29T07:42:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493, upload-time = "2025-07-29T07:42:45.07Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089, upload-time = "2025-07-29T07:42:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571, upload-time = "2025-07-29T07:42:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806, upload-time = "2025-07-29T07:42:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600, upload-time = "2025-07-29T07:42:49.371Z" }, + { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349, upload-time = "2025-07-29T07:42:50.268Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/a20db059a8a47048aaf550da14a145b56e9c7386fb8280d3ce2962dcebf7/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078", size = 39209, upload-time = "2025-07-29T07:42:51.559Z" }, + { url = "https://files.pythonhosted.org/packages/98/dd/e5094799d55c7482d814b979a0fd608027d0af1b274bfb4c3ea3e950bfd5/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501", size = 39843, upload-time = "2025-07-29T07:42:52.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/7844d7f832c85400e7cc89a1348e4e1fdd38c5a38415bb5726bbb8fcdb6c/mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b", size = 40648, upload-time = "2025-07-29T07:42:53.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bf/71f791f48a21ff3190ba5225807cbe4f7223360e96862c376e6e3fb7efa7/mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770", size = 56164, upload-time = "2025-07-29T07:42:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/70/1f/f87e3d34d83032b4f3f0f528c6d95a98290fcacf019da61343a49dccfd51/mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110", size = 40692, upload-time = "2025-07-29T07:42:55.234Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e2/db849eaed07117086f3452feca8c839d30d38b830ac59fe1ce65af8be5ad/mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647", size = 40068, upload-time = "2025-07-29T07:42:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/209af927207af77425b044e32f77f49105a0b05d82ff88af6971d8da4e19/mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63", size = 97367, upload-time = "2025-07-29T07:42:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/78adf4104c425606a9ce33fb351f790c76a6c2314969c4a517d1ffc92196/mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12", size = 103306, upload-time = "2025-07-29T07:42:58.522Z" }, + { url = "https://files.pythonhosted.org/packages/a3/79/c2b89f91b962658b890104745b1b6c9ce38d50a889f000b469b91eeb1b9e/mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22", size = 106312, upload-time = "2025-07-29T07:42:59.552Z" }, + { url = "https://files.pythonhosted.org/packages/4b/14/659d4095528b1a209be90934778c5ffe312177d51e365ddcbca2cac2ec7c/mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5", size = 113135, upload-time = "2025-07-29T07:43:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/cd7734a779389a8a467b5c89a48ff476d6f2576e78216a37551a97e9e42a/mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07", size = 120775, upload-time = "2025-07-29T07:43:02.124Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ca/8256e3b96944408940de3f9291d7e38a283b5761fe9614d4808fcf27bd62/mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935", size = 99178, upload-time = "2025-07-29T07:43:03.182Z" }, + { url = "https://files.pythonhosted.org/packages/8a/32/39e2b3cf06b6e2eb042c984dab8680841ac2a0d3ca6e0bea30db1f27b565/mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7", size = 98738, upload-time = "2025-07-29T07:43:04.207Z" }, + { url = "https://files.pythonhosted.org/packages/61/d3/7bbc8e0e8cf65ebbe1b893ffa0467b7ecd1bd07c3bbf6c9db4308ada22ec/mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5", size = 106510, upload-time = "2025-07-29T07:43:05.656Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/b97e53724b52374e2f3859046f0eb2425192da356cb19784d64bc17bb1cf/mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384", size = 110053, upload-time = "2025-07-29T07:43:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/ac/62/3688c7d975ed195155671df68788c83fed6f7909b6ec4951724c6860cb97/mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e", size = 97546, upload-time = "2025-07-29T07:43:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ca/3b/c6153250f03f71a8b7634cded82939546cdfba02e32f124ff51d52c6f991/mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0", size = 41422, upload-time = "2025-07-29T07:43:09.216Z" }, + { url = "https://files.pythonhosted.org/packages/74/01/a27d98bab083a435c4c07e9d1d720d4c8a578bf4c270bae373760b1022be/mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b", size = 42135, upload-time = "2025-07-29T07:43:10.183Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c9/dbba5507e95429b8b380e2ba091eff5c20a70a59560934dff0ad8392b8c8/mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115", size = 39879, upload-time = "2025-07-29T07:43:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/c8c0ef839c17258b9de41b84f663574fabcf8ac2007b7416575e0f65ff6e/mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932", size = 57696, upload-time = "2025-07-29T07:43:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/95e2b9ff201e89f9fe37036037ab61a6c941942b25cdb7b6a9df9b931993/mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c", size = 41421, upload-time = "2025-07-29T07:43:13.269Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/9be23ad0b7001a4b22752e7693be232428ecc0a35068a4ff5c2f14ef8b20/mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be", size = 40853, upload-time = "2025-07-29T07:43:14.888Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1b/96b32058eda1c1dee8264900c37c359a7325c1f11f5ff14fd2be8e24eff9/mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb", size = 109694, upload-time = "2025-07-29T07:43:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/a2ae44cd7dad697b6dea48390cbc977b1e5ca58fda09628cbcb2275af064/mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65", size = 117438, upload-time = "2025-07-29T07:43:16.865Z" }, + { url = "https://files.pythonhosted.org/packages/a0/08/bfb75451c83f05224a28afeaf3950c7b793c0b71440d571f8e819cfb149a/mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991", size = 120409, upload-time = "2025-07-29T07:43:18.207Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/8b118b69b2ff8df568f742387d1a159bc654a0f78741b31437dd047ea28e/mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645", size = 125909, upload-time = "2025-07-29T07:43:19.39Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/168cc0b6a30650032e351a3b89b8a47382da541993a03af91e1ba2501234/mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3", size = 135331, upload-time = "2025-07-29T07:43:20.435Z" }, + { url = "https://files.pythonhosted.org/packages/31/05/e3a9849b1c18a7934c64e831492c99e67daebe84a8c2f2c39a7096a830e3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279", size = 110085, upload-time = "2025-07-29T07:43:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/a96bcc306e3404601418b2a9a370baec92af84204528ba659fdfe34c242f/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513", size = 111195, upload-time = "2025-07-29T07:43:23.066Z" }, + { url = "https://files.pythonhosted.org/packages/af/29/0fd49801fec5bff37198684e0849b58e0dab3a2a68382a357cfffb0fafc3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db", size = 116919, upload-time = "2025-07-29T07:43:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/2d/04/4f3c32b0a2ed762edca45d8b46568fc3668e34f00fb1e0a3b5451ec1281c/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667", size = 123160, upload-time = "2025-07-29T07:43:25.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/76/3d29eaa38821730633d6a240d36fa8ad2807e9dfd432c12e1a472ed211eb/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5", size = 110206, upload-time = "2025-07-29T07:43:26.699Z" }, + { url = "https://files.pythonhosted.org/packages/44/1c/ccf35892684d3a408202e296e56843743e0b4fb1629e59432ea88cdb3909/mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7", size = 41970, upload-time = "2025-07-29T07:43:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/b9e4f1e5adb5e21eb104588fcee2cd1eaa8308255173481427d5ecc4284e/mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d", size = 43063, upload-time = "2025-07-29T07:43:28.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455, upload-time = "2025-07-29T07:43:29.563Z" }, +] + +[[package]] +name = "moondream" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/d7/85e4d020c4d00f4842b35773e4442fe5cea310e4ebc6a1856e55d3e1a658/moondream-0.2.0.tar.gz", hash = "sha256:402655cc23b94490512caa1cf9f250fc34d133dfdbac201f78b32cbdeabdae0d", size = 97837, upload-time = "2025-11-25T18:22:04.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/cf/369278487161c8d8eadd1a6cee8b0bd629936a1b263bbeccf71342b24dc8/moondream-0.2.0-py3-none-any.whl", hash = "sha256:ca722763bddcce7c13faf87fa3e6b834f86f7bea22bc8794fc1fe15f2d826d93", size = 96169, upload-time = "2025-11-25T18:22:03.465Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, + { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + +[[package]] +name = "mss" +version = "10.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/ca/49b67437a8c46d9732c9c274d7b1fc0c181cfe290d699a0c5e94701dfe79/mss-10.1.0.tar.gz", hash = "sha256:7182baf7ee16ca569e2804028b6ab9bcbf6be5c46fc2880840f33b513b9cb4f8", size = 84200, upload-time = "2025-08-16T12:11:00.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/28/1e3e5cd1d677cca68b26166f704f72e35b1e8b6d5076d8ebeebc4e40a649/mss-10.1.0-py3-none-any.whl", hash = "sha256:9179c110cadfef5dc6dc4a041a0cd161c74c379218648e6640b48c6b5cfe8918", size = 24525, upload-time = "2025-08-16T12:10:59.111Z" }, +] + +[[package]] +name = "mujoco" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "etils", extra = ["epath"] }, + { name = "glfw" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyopengl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/3b/f688fbe34eb609ffdc9dc0f53f7acd3327588f970752780d05a0762d3511/mujoco-3.4.0.tar.gz", hash = "sha256:5a6dc6b7db41eb0ab8724cd477bd0316ba4b53debfc2d80a2d6f444a116fb8d2", size = 826806, upload-time = "2025-12-05T23:13:46.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/a0/ff8c20b923675ee803580bb8a33a2781e48c007a2845607f15184cf7fc32/mujoco-3.4.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:b7ae8a534ecf6afab3abab3dc0718ea47f89a2e2f096905870cbf5faf23076c3", size = 6905902, upload-time = "2025-12-05T23:12:59.76Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/2875a57cdb423d98bdcb359f34af5eb8a24e48a903f4bb7110d75e478dac/mujoco-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7960cf47b4ed274955280200a812e6d780f03707d5258b42c3afb249051216ee", size = 6861873, upload-time = "2025-12-05T23:13:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/67/05/736c180caf0b051ec5ba26ab024e609fb18545b212dd0ee96ec84458f184/mujoco-3.4.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0918ff57a92ba00a95538bf6f0c67973044953bb090b9fd811f9ee6cda4ffcb8", size = 6487647, upload-time = "2025-12-05T23:13:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/71/95/ba02262c7a7a786a64b8d77315e7e4d3c77598ff63d8cd605ba2b96ec349/mujoco-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b2b8a75ea191ae577bfa2385ca6ecd6328f37f6f46bc3cfb41835b2653f716a", size = 6911042, upload-time = "2025-12-05T23:13:07.727Z" }, + { url = "https://files.pythonhosted.org/packages/22/d5/f94edc884c11b63f0d7ba9322ec4ca98bc7cad57c5c034c2e3332287d9ad/mujoco-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ffe79a1476767806318b7dfbbb642e428b873385fb2d2f06e69d461459d01ed1", size = 5399174, upload-time = "2025-12-05T23:13:10.271Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5f92234b0e2f2b8c5b392fd71be3cfb5363bdccded4cac0b5889d07da6cb/mujoco-3.4.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:b456e1d6c3ca7010480d52f7645e49f4b564952063c5a52af1524e226ea72920", size = 6919759, upload-time = "2025-12-05T23:13:12.444Z" }, + { url = "https://files.pythonhosted.org/packages/09/d4/99acbb782cc2c2ab5e5dacdf48b5d2850b641e34a91715d262d14103b764/mujoco-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98d3ab7b02d99ac2866bb111807797549853d6ddf485ddc072cec3c1d33dfad8", size = 6874776, upload-time = "2025-12-05T23:13:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/57/92/1e10be7922508a307017e2a62852555ab4f61148d9791a7bcdf03d902a9a/mujoco-3.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00088b3879cff675b0bad2ddaf27f1088d7d35f020903c25088c43360adcc311", size = 6501363, upload-time = "2025-12-05T23:13:17.313Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c1/37c07061be2b410de33624093fc06fc49ec13888252f1a6c30fcd40633bd/mujoco-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b3dce70abdd0ac6475bb8040d7d97a4e3ccf5f6a00cc06086b7a169e246522", size = 6925780, upload-time = "2025-12-05T23:13:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/04/0a/c1055f2329761c87edcdc18a480ab43ba942ed10f156888b4744d69c1deb/mujoco-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e0ab4a450826ca04db11608325b37adf77dee4cde2a9dd9d43e6fa46c44545a9", size = 5422000, upload-time = "2025-12-05T23:13:21.679Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c3/858d2e6fd3bf986a64bb5f0f157b601c2b9604d2b43bbc0469fe1b44d61b/mujoco-3.4.0-cp312-cp312-macosx_10_16_x86_64.whl", hash = "sha256:96bccc995fc561078b5cac1e53f8ba2ba8619348bc0c6cff15bbf6f9a441d220", size = 6922335, upload-time = "2025-12-05T23:13:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/10dbaef500af18866a41d842f2f95cf113aacf0f1eb91677c4817cff3495/mujoco-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:339800c695166c8041cdef95ea5384fc607ad1b86c19528c785a17ed742c3a5e", size = 6793849, upload-time = "2025-12-05T23:13:25.714Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6b/522696a7413f33596b8d18bca52dee2f7c0ecf23a5f08097f346e5a5b656/mujoco-3.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1efdab2b146cdbcef4560606b6cbc74abe80f2b94a8593a5cdc469172085a2b3", size = 6520021, upload-time = "2025-12-05T23:13:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/c6/10/fa6b8762efbb02bd349503a39fb9dbbcf9e12041b0c5b29d484cafc09355/mujoco-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e5a36e61495be2a855f61194813e1277ab4b330cc180e50c8e3c7a459dc40b6", size = 6995663, upload-time = "2025-12-05T23:13:30.436Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/5336e8e86e429a7f301dac63cae6ae75ff9e91bfb01502a55f60d7305eca/mujoco-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:10314213c395aedeaf2778596e78dd9ae01d74dc92d4f75c696707781d59826a", size = 5497926, upload-time = "2025-12-05T23:13:32.382Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ba/9fa63c63728d9ac0982b77af650229f14b1aa53e3331dba7ef6829fdda56/mujoco-3.4.0-cp313-cp313-macosx_10_16_x86_64.whl", hash = "sha256:345fb5adb4e9c1c108aab2ff8418280edf61cec5b705c483eae680c4cf350898", size = 6922502, upload-time = "2025-12-05T23:13:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/51/c3e5c3b199b1b74c85f0cb02dc0ef80363bf91ea245ee8a932804768d5e8/mujoco-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87a68d063d06d261e83755093e79371901b6a8171b0b8b88dcb020f966d4e463", size = 6794329, upload-time = "2025-12-05T23:13:37.557Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2f/b2f531ae6e8fbbf095dfbb614b7d1130d3d6791920a9b075861a13f5a97b/mujoco-3.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06cd3366b9548b251c3170c9b073e41a9f4a621b4f7e59ceb5fee8c46f6165ca", size = 6520414, upload-time = "2025-12-05T23:13:39.778Z" }, + { url = "https://files.pythonhosted.org/packages/df/72/0c47350ec39611ff8defe9e8af10c23c9ad0235974f1999a523fcd1c3e68/mujoco-3.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7edaffb223cf1343ed980030c466170eb8f9d624cf69c9a99925cbce371f22db", size = 6996132, upload-time = "2025-12-05T23:13:41.996Z" }, + { url = "https://files.pythonhosted.org/packages/dc/25/bbf8c01758d619c86bcbe58db6a7d61ca423e7c76791f323b8ea2e92c2bd/mujoco-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:c01a842a17c0229dce2fb65a051a8e6ee5f5307c50c825c850e3702dda4344e6", size = 5497055, upload-time = "2025-12-05T23:13:44.795Z" }, +] + +[[package]] +name = "mujoco-mjx" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "etils", extra = ["epath"] }, + { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jax", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jaxlib", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mujoco" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "trimesh" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/57/2cebcde17bdad9c575c71a301ea1524eb9dba76a974e24f07abf714050be/mujoco_mjx-3.4.0.tar.gz", hash = "sha256:10fa51a92c22affd27c9205c5fb965c14c256729ab58fd2021dc9e4df9bedec6", size = 6872370, upload-time = "2025-12-05T23:14:13.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/2f/8c2f734a0c4762416895ae3a7a9f46b10a3a8f3d72c0457d72b31d329c34/mujoco_mjx-3.4.0-py3-none-any.whl", hash = "sha256:b64e0e33e367027e912893701cce905efd1397e2234bca171c73990b9f088171", size = 6953534, upload-time = "2025-12-05T23:14:11.191Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, + { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, + { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, + { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, + { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, + { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "narwhals" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/6d/b57c64e5038a8cf071bce391bb11551657a74558877ac961e7fa905ece27/narwhals-2.15.0.tar.gz", hash = "sha256:a9585975b99d95084268445a1fdd881311fa26ef1caa18020d959d5b2ff9a965", size = 603479, upload-time = "2026-01-06T08:10:13.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl", hash = "sha256:cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6", size = 432856, upload-time = "2026-01-06T08:10:11.511Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nltk" +version = "3.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629, upload-time = "2025-10-01T07:19:23.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404, upload-time = "2025-10-01T07:19:21.648Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numba" +version = "0.63.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/ce/5283d4ffa568f795bb0fd61ee1f0efc0c6094b94209259167fc8d4276bde/numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779", size = 2680810, upload-time = "2025-12-10T02:56:55.269Z" }, + { url = "https://files.pythonhosted.org/packages/0f/72/a8bda517e26d912633b32626333339b7c769ea73a5c688365ea5f88fd07e/numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4", size = 3739735, upload-time = "2025-12-10T02:56:57.922Z" }, + { url = "https://files.pythonhosted.org/packages/ca/17/1913b7c1173b2db30fb7a9696892a7c4c59aeee777a9af6859e9e01bac51/numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e", size = 3446707, upload-time = "2025-12-10T02:56:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/703db56c3061e9fdad5e79c91452947fdeb2ec0bdfe4affe9b144e7025e0/numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d", size = 2747374, upload-time = "2025-12-10T02:57:07.908Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501, upload-time = "2025-12-10T02:57:09.797Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945, upload-time = "2025-12-10T02:57:11.697Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827, upload-time = "2025-12-10T02:57:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/18/3f/a9b106e93c5bd7434e65f044bae0d204e20aa7f7f85d72ceb872c7c04216/numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87", size = 2747262, upload-time = "2025-12-10T02:57:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, + { url = "https://files.pythonhosted.org/packages/57/f7/e19e6eff445bec52dde5bed1ebb162925a8e6f988164f1ae4b3475a73680/numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc", size = 2680954, upload-time = "2025-12-10T02:57:24.145Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/1e222edba1e20e6b113912caa9b1665b5809433cbcb042dfd133c6f1fd38/numba-0.63.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53de693abe4be3bd4dee38e1c55f01c55ff644a6a3696a3670589e6e4c39cde2", size = 3809736, upload-time = "2025-12-10T02:57:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/590bad11a8b3feeac30a24d01198d46bdb76ad15c70d3a530691ce3cae58/numba-0.63.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81227821a72a763c3d4ac290abbb4371d855b59fdf85d5af22a47c0e86bf8c7e", size = 3508854, upload-time = "2025-12-10T02:57:27.438Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f5/3800384a24eed1e4d524669cdbc0b9b8a628800bb1e90d7bd676e5f22581/numba-0.63.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb227b07c2ac37b09432a9bda5142047a2d1055646e089d4a240a2643e508102", size = 2750228, upload-time = "2025-12-10T02:57:30.36Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/53be2aa8a55ee2608ebe1231789cbb217f6ece7f5e1c685d2f0752e95a5b/numba-0.63.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f180883e5508940cc83de8a8bea37fc6dd20fbe4e5558d4659b8b9bef5ff4731", size = 2681153, upload-time = "2025-12-10T02:57:32.016Z" }, + { url = "https://files.pythonhosted.org/packages/13/91/53e59c86759a0648282368d42ba732c29524a745fd555ed1fb1df83febbe/numba-0.63.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0938764afa82a47c0e895637a6c55547a42c9e1d35cac42285b1fa60a8b02bb", size = 3778718, upload-time = "2025-12-10T02:57:33.764Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/2be19eba50b0b7636f6d1f69dfb2825530537708a234ba1ff34afc640138/numba-0.63.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f90a929fa5094e062d4e0368ede1f4497d5e40f800e80aa5222c4734236a2894", size = 3478712, upload-time = "2025-12-10T02:57:35.518Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/4d0c9e756732577a52211f31da13a3d943d185f7fb90723f56d79c696caa/numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f", size = 2752161, upload-time = "2025-12-10T02:57:37.12Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, + { url = "https://files.pythonhosted.org/packages/70/61/7d7b3c70186fb651d0fbd35b01dbfc8e755f69fd58f817f3d0f642df20c3/nvidia_cublas_cu12-12.8.4.1-py3-none-win_amd64.whl", hash = "sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af", size = 567544208, upload-time = "2025-03-07T01:53:30.535Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.9.1.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/6c/90d3f532f608a03a13c1d6c16c266ffa3828e8011b1549d3b61db2ad59f5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:7a950dae01add3b415a5a5cdc4ec818fb5858263e9cca59004bb99fdbbd3a5d6", size = 575006342, upload-time = "2025-06-05T20:04:16.902Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, + { url = "https://files.pythonhosted.org/packages/30/a5/a515b7600ad361ea14bfa13fb4d6687abf500adc270f19e89849c0590492/nvidia_cuda_runtime_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8", size = 944318, upload-time = "2025-03-07T01:51:01.794Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.9.79" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e0/0279bd94539fda525e0c8538db29b72a5a8495b0c12173113471d28bce78/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83469a846206f2a733db0c42e223589ab62fd2fabac4432d2f8802de4bded0a4", size = 3515012, upload-time = "2025-06-05T20:00:35.519Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-libnvcomp-cu12" +version = "5.1.0.21" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/23/b20f2381c7e92c704386428fe79736a13c50f452376453fdc60fcc0ec1b0/nvidia_libnvcomp_cu12-5.1.0.21-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:77dfb3cb8c8995dfa0279ba99b0501e03cbe77e876aab44f4693abdcfac549ce", size = 28802614, upload-time = "2025-12-02T19:05:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/08/ab/844fcbaa46cc1242632b4b94b4ffc210ec3d8d8f30ad8f7f1c27767389a9/nvidia_libnvcomp_cu12-5.1.0.21-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:68de61183edb9a870c9a608273a2b5da97dea18e3552096c61fafd9bb2689db0", size = 28958714, upload-time = "2025-12-02T19:01:40.466Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/c6e92d9587b9ad63c08b1b94c5ae2216319491d0bd4f40f2a9a431d4841f/nvidia_libnvcomp_cu12-5.1.0.21-py3-none-win_amd64.whl", hash = "sha256:1352c7c4264ee5357f8f20e4a8da7f2f91debe21d8968f44576a7f4b51f91533", size = 28490640, upload-time = "2025-12-02T19:07:28.096Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvimgcodec-cu12" +version = "0.7.0.11" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/48/74d33dd126f84a4212480e2cf07504f457b5bae5acd33c0f6bf839ea17d4/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:52d834be8122bb5b8fc3151cc3bedb95368b3e7ac76af0c4561772ab2a847b2b", size = 27409358, upload-time = "2025-12-02T09:28:16.358Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/f06528ebcb82da84f4a96efe7a210c277767cb86ad2f61f8b1a17d17f251/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:32d3457859c5784e4c0f6a2f56b6a9afec8fe646cec1cbe4bb5c320948d92dfe", size = 33735220, upload-time = "2025-12-02T09:30:02.546Z" }, + { url = "https://files.pythonhosted.org/packages/be/79/95b36049a9504d59d79929e9f3bec001b270f29aec8486e5fb9783a9502c/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-win_amd64.whl", hash = "sha256:495e07e071fcb2115f7f1948a04f6c51f96d61b83c614af753f7cc1bf369a46c", size = 18448810, upload-time = "2025-12-02T09:20:33.838Z" }, +] + +[package.optional-dependencies] +all = [ + { name = "nvidia-libnvcomp-cu12" }, + { name = "nvidia-nvjpeg-cu12" }, + { name = "nvidia-nvjpeg2k-cu12" }, + { name = "nvidia-nvtiff-cu12" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvjpeg-cu12" +version = "12.4.0.76" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/48/5c12a3e6afe070ff563375cc72b42e9c7400bd0b44c734591049410be7fd/nvidia_nvjpeg_cu12-12.4.0.76-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f52c5ef7cf56e8bffac8903a59f14494017a52e4fe89d5a1d16c1e88d7bbf194", size = 5273693, upload-time = "2025-06-05T20:10:35.162Z" }, + { url = "https://files.pythonhosted.org/packages/57/68/d3526394584134a23f2500833c62d3352e1feda7547041f4612b1a183aa3/nvidia_nvjpeg_cu12-12.4.0.76-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3888f10b32fbd58e80166c48e01073732d752fa5f167b7cb5b9615f1c6375a20", size = 5313609, upload-time = "2025-06-05T20:10:43.92Z" }, + { url = "https://files.pythonhosted.org/packages/bc/28/e05bb8e6cdb98e79c6822f8bbd7154a26d8102412b3a0bfd5e4c7c52db8c/nvidia_nvjpeg_cu12-12.4.0.76-py3-none-win_amd64.whl", hash = "sha256:21923726db667bd53050d0de88320983ff423322b7f376057dd943e487c40abc", size = 4741398, upload-time = "2025-06-05T20:16:19.152Z" }, +] + +[[package]] +name = "nvidia-nvjpeg2k-cu12" +version = "0.9.1.47" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/421625f754862b893c2f487090b4b6b86337801451f0623cda9d21d111b4/nvidia_nvjpeg2k_cu12-0.9.1.47-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f6787aed8f9d0c839ea4e0ae190af90bcc71a9a6b4e3965d5b67c22a00f58714", size = 7344958, upload-time = "2025-11-13T18:17:15.127Z" }, + { url = "https://files.pythonhosted.org/packages/85/91/41abf44089ceb8b29479cdef2ca952277cc6667d40affedd39c3f1744d7e/nvidia_nvjpeg2k_cu12-0.9.1.47-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6672c85e47ab61ffe3d19da8a41fd597155852e6e219ddc90a133623b54f7818", size = 7402941, upload-time = "2025-11-13T18:13:28.977Z" }, + { url = "https://files.pythonhosted.org/packages/01/b2/ab62e6c008f3080743477de31da22eb83b374c37fe5d387e7435e507914f/nvidia_nvjpeg2k_cu12-0.9.1.47-py3-none-win_amd64.whl", hash = "sha256:ebb5d34d68beb70c2718c769996d9d8e49a2d9acacc79f6235c07649a4045e97", size = 6973975, upload-time = "2025-11-13T18:25:26.611Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.3.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, +] + +[[package]] +name = "nvidia-nvtiff-cu12" +version = "0.6.0.78" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/19/9529fbda1e7a24b45649c9bc86cf6490d5b53f63e6b17d851f1528ff8380/nvidia_nvtiff_cu12-0.6.0.78-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9193a46eaef2d52a92178c34e2404f621b581d651d2c7ab2d83c24fee6fcc136", size = 2478534, upload-time = "2025-11-13T18:26:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/62/4b/24805e9c56936dd57a1830b65b53234853f429cea5edbcbfdf853ceebdcf/nvidia_nvtiff_cu12-0.6.0.78-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b48517578de6f1a6e806e00ef0da6d673036957560efbe9fa2934707d5d18c00", size = 2518414, upload-time = "2025-11-13T18:16:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/1d818455e6c6182354fb5b17a6c9d7dcfb002e64e258554fe3410ea44510/nvidia_nvtiff_cu12-0.6.0.78-py3-none-win_amd64.whl", hash = "sha256:daf9035b5efc315ef904b449564d1d9d9a502f38e115cf5757d98f9c52a284d0", size = 2055719, upload-time = "2025-11-13T18:29:01.023Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "ollama" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "onnx" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/bf/824b13b7ea14c2d374b48a296cfa412442e5559326fbab5441a4fcb68924/onnx-1.20.0.tar.gz", hash = "sha256:1a93ec69996b4556062d552ed1aa0671978cfd3c17a40bf4c89a1ae169c6a4ad", size = 12049527, upload-time = "2025-12-01T18:14:34.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/8fd768f715a990d3b5786c9bffa6f158934cc1935f2774dd15b26c62f99f/onnx-1.20.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:7e706470f8b731af6d0347c4f01b8e0e1810855d0c71c467066a5bd7fa21704b", size = 18341375, upload-time = "2025-12-01T18:13:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/cf/47/9fdb6e8bde5f77f8bdcf7e584ad88ffa7a189338b92658351518c192bde0/onnx-1.20.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e941d0f3edd57e1d63e2562c74aec2803ead5b965e76ccc3d2b2bd4ae0ea054", size = 17899075, upload-time = "2025-12-01T18:13:32.375Z" }, + { url = "https://files.pythonhosted.org/packages/b2/17/7bb16372f95a8a8251c202018952a747ac7f796a9e6d5720ed7b36680834/onnx-1.20.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6930ed7795912c4298ec8642b33c99c51c026a57edf17788b8451fe22d11e674", size = 18118826, upload-time = "2025-12-01T18:13:35.077Z" }, + { url = "https://files.pythonhosted.org/packages/19/d8/19e3f599601195b1d8ff0bf9e9469065ebeefd9b5e5ec090344f031c38cb/onnx-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f8424c95491de38ecc280f7d467b298cb0b7cdeb1cd892eb9b4b9541c00a600e", size = 16364286, upload-time = "2025-12-01T18:13:38.304Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/11d2db50a6c56092bd2e22515fe6998309c7b2389ed67f8ffd27285c33b5/onnx-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:1ecca1f963d69e002c03000f15844f8cac3b6d7b6639a934e73571ee02d59c35", size = 16487791, upload-time = "2025-12-01T18:13:41.062Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9a/125ad5ed919d1782b26b0b4404e51adc44afd029be30d5a81b446dccd9c5/onnx-1.20.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:00dc8ae2c7b283f79623961f450b5515bd2c4b47a7027e7a1374ba49cef27768", size = 18341929, upload-time = "2025-12-01T18:13:43.79Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3c/85280dd05396493f3e1b4feb7a3426715e344b36083229437f31d9788a01/onnx-1.20.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f62978ecfb8f320faba6704abd20253a5a79aacc4e5d39a9c061dd63d3b7574f", size = 17899362, upload-time = "2025-12-01T18:13:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/26/db/e11cf9aaa6ccbcd27ea94d321020fef3207cba388bff96111e6431f97d1a/onnx-1.20.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:71177f8fd5c0dd90697bc281f5035f73707bdac83257a5c54d74403a1100ace9", size = 18119129, upload-time = "2025-12-01T18:13:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0b/1b99e7ba5ccfa8ecb3509ec579c8520098d09b903ccd520026d60faa7c75/onnx-1.20.0-cp311-cp311-win32.whl", hash = "sha256:1d3d0308e2c194f4b782f51e78461b567fac8ce6871c0cf5452ede261683cc8f", size = 16364604, upload-time = "2025-12-01T18:13:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/51/ab/7399817821d0d18ff67292ac183383e41f4f4ddff2047902f1b7b51d2d40/onnx-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a6de7dda77926c323b0e5a830dc9c2866ce350c1901229e193be1003a076c25", size = 16488019, upload-time = "2025-12-01T18:13:55.776Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/23059c11d9c0fb1951acec504a5cc86e1dd03d2eef3a98cf1941839f5322/onnx-1.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:afc4cf83ce5d547ebfbb276dae8eb0ec836254a8698d462b4ba5f51e717fd1ae", size = 16446841, upload-time = "2025-12-01T18:13:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/2caa972a31014a8cb4525f715f2a75d93caef9d4b9da2809cc05d0489e43/onnx-1.20.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:31efe37d7d1d659091f34ddd6a31780334acf7c624176832db9a0a8ececa8fb5", size = 18340913, upload-time = "2025-12-01T18:14:00.477Z" }, + { url = "https://files.pythonhosted.org/packages/78/bb/b98732309f2f6beb4cdcf7b955d7bbfd75a191185370ee21233373db381e/onnx-1.20.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d75da05e743eb9a11ff155a775cae5745e71f1cd0ca26402881b8f20e8d6e449", size = 17896118, upload-time = "2025-12-01T18:14:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/38aa564871d062c11538d65c575af9c7e057be880c09ecbd899dd1abfa83/onnx-1.20.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02e0d72ab09a983fce46686b155a5049898558d9f3bc6e8515120d6c40666318", size = 18115415, upload-time = "2025-12-01T18:14:06.261Z" }, + { url = "https://files.pythonhosted.org/packages/3b/17/a600b62cf4ad72976c66f83ce9e324205af434706ad5ec0e35129e125aef/onnx-1.20.0-cp312-abi3-win32.whl", hash = "sha256:392ca68b34b97e172d33b507e1e7bfdf2eea96603e6e7ff109895b82ff009dc7", size = 16363019, upload-time = "2025-12-01T18:14:09.16Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3b/5146ba0a89f73c026bb468c49612bab8d005aa28155ebf06cf5f2eb8d36c/onnx-1.20.0-cp312-abi3-win_amd64.whl", hash = "sha256:259b05758d41645f5545c09f887187662b350d40db8d707c33c94a4f398e1733", size = 16485934, upload-time = "2025-12-01T18:14:13.046Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bc/d251b97395e721b3034e9578d4d4d9fb33aac4197ae16ce8c7ed79a26dce/onnx-1.20.0-cp312-abi3-win_arm64.whl", hash = "sha256:2d25a9e1fde44bc69988e50e2211f62d6afcd01b0fd6dfd23429fd978a35d32f", size = 16444946, upload-time = "2025-12-01T18:14:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/8d/11/4d47409e257013951a17d08c31988e7c2e8638c91d4d5ce18cc57c6ea9d9/onnx-1.20.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:7646e700c0a53770a86d5a9a582999a625a3173c4323635960aec3cba8441c6a", size = 18348524, upload-time = "2025-12-01T18:14:18.102Z" }, + { url = "https://files.pythonhosted.org/packages/67/60/774d29a0f00f84a4ec624fe35e0c59e1dbd7f424adaab751977a45b60e05/onnx-1.20.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0bdfd22fe92b87bf98424335ec1191ed79b08cd0f57fe396fab558b83b2c868", size = 17900987, upload-time = "2025-12-01T18:14:20.835Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/6bd82b81b85b2680e3de8cf7b6cc49a7380674b121265bb6e1e2ff3bb0aa/onnx-1.20.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1a4e02148b2a7a4b82796d0ecdb6e49ba7abd34bb5a9de22af86aad556fb76", size = 18121332, upload-time = "2025-12-01T18:14:24.558Z" }, + { url = "https://files.pythonhosted.org/packages/d1/42/d2cd00c84def4e17b471e24d82a1d2e3c5be202e2c163420b0353ddf34df/onnx-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2241c85fdaa25a66565fcd1d327c7bcd8f55165420ebaee1e9563c3b9bf961c9", size = 16492660, upload-time = "2025-12-01T18:14:27.456Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/1106de50a17f2a2dfbb4c8bb3cf2f99be2c7ac2e19abbbf9e07ab47b1b35/onnx-1.20.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ee46cdc5abd851a007a4be81ee53e0e303cf9a0e46d74231d5d361333a1c9411", size = 16448588, upload-time = "2025-12-01T18:14:32.277Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/db/db/81bf3d7cecfbfed9092b6b4052e857a769d62ed90561b410014e0aae18db/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", size = 19153079, upload-time = "2025-10-27T23:05:57.686Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/a382452b17cf70a2313153c520ea4c96ab670c996cb3a95cc5d5ac7bfdac/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", size = 15219883, upload-time = "2025-10-22T03:46:21.66Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/179bf90679984c85b417664c26aae4f427cba7514bd2d65c43b181b7b08b/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", size = 17370357, upload-time = "2025-10-22T03:46:57.968Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6d/738e50c47c2fd285b1e6c8083f15dac1a5f6199213378a5f14092497296d/onnxruntime-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", size = 13467651, upload-time = "2025-10-27T23:06:11.904Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" }, + { url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" }, + { url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" }, + { url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" }, + { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" }, + { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" }, +] + +[[package]] +name = "onnxruntime-gpu" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/ae/39283748c68a96be4f5f8a9561e0e3ca92af1eae6c2b1c07fb1da5f65cd1/onnxruntime_gpu-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18de50c6c8eea50acc405ea13d299aec593e46478d7a22cd32cdbbdf7c42899d", size = 300525411, upload-time = "2025-10-22T16:56:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/21/c9/47abd3ec1f34498224d2a8f5cc4d1445eb5cc7dee8e3644b1a972619c0d2/onnxruntime_gpu-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:deba091e15357355aa836fd64c6c4ac97dd0c4609c38b08a69675073ea46b321", size = 244505340, upload-time = "2025-10-27T22:47:43.215Z" }, + { url = "https://files.pythonhosted.org/packages/43/a4/e3d7fbe32b44e814ae24ed642f05fac5d96d120efd82db7a7cac936e85a9/onnxruntime_gpu-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59", size = 300525715, upload-time = "2025-10-22T16:56:19.928Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5c/dba7c009e73dcce02e7f714574345b5e607c5c75510eb8d7bef682b45e5d/onnxruntime_gpu-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4", size = 244506823, upload-time = "2025-10-22T16:55:09.526Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/b7140a4f1615195938c7e358c0804bb84271f0d6886b5cbf105c6cb58aae/onnxruntime_gpu-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2", size = 300509596, upload-time = "2025-10-22T16:56:31.728Z" }, + { url = "https://files.pythonhosted.org/packages/87/da/2685c79e5ea587beddebe083601fead0bdf3620bc2f92d18756e7de8a636/onnxruntime_gpu-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95", size = 244508327, upload-time = "2025-10-22T16:55:19.397Z" }, + { url = "https://files.pythonhosted.org/packages/03/05/40d561636e4114b54aa06d2371bfbca2d03e12cfdf5d4b85814802f18a75/onnxruntime_gpu-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e8f75af5da07329d0c3a5006087f4051d8abd133b4be7c9bae8cdab7bea4c26", size = 300515567, upload-time = "2025-10-22T16:56:43.794Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3b/418300438063d403384c79eaef1cb13c97627042f2247b35a887276a355a/onnxruntime_gpu-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:7f1b3f49e5e126b99e23ec86b4203db41c2a911f6165f7624f2bc8267aaca767", size = 244507535, upload-time = "2025-10-22T16:55:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/b8/dc/80b145e3134d7eba31309b3299a2836e37c76e4c419a261ad9796f8f8d65/onnxruntime_gpu-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20959cd4ae358aab6579ab9123284a7b1498f7d51ec291d429a5edc26511306f", size = 300525759, upload-time = "2025-10-22T16:56:56.925Z" }, +] + +[[package]] +name = "open-clip-torch" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ftfy" }, + { name = "huggingface-hub" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "timm" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/46/fb8be250fa7fcfc56fbeb41583645e18d868268f67fbbbeb8ed62a8ff18a/open_clip_torch-3.2.0.tar.gz", hash = "sha256:62b7743012ccc40fb7c64819fa762fba0a13dd74585ac733babe58c2974c2506", size = 1502853, upload-time = "2025-09-21T17:32:08.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/91/397327cc1597fa317942cc15bef414175eee4b3c2263b34407c57f3521f9/open_clip_torch-3.2.0-py3-none-any.whl", hash = "sha256:e1f5b3ecbadb6d8ea64b1f887db23efee9739e7c0d0075a8a2a3cabae8fed8d1", size = 1546677, upload-time = "2025-09-21T17:32:06.269Z" }, +] + +[[package]] +name = "open3d" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "addict" }, + { name = "configargparse" }, + { name = "dash" }, + { name = "flask" }, + { name = "matplotlib" }, + { name = "nbformat" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pyquaternion" }, + { name = "pyyaml" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tqdm" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/4b/91e8a4100adf0ccd2f7ad21dd24c2e3d8f12925396528d0462cfb1735e5a/open3d-0.19.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:f7128ded206e07987cc29d0917195fb64033dea31e0d60dead3629b33d3c175f", size = 103086005, upload-time = "2025-01-08T07:25:56.755Z" }, + { url = "https://files.pythonhosted.org/packages/c7/45/13bc9414ee9db611cba90b9efa69f66f246560e8ade575f1ee5b7f7b5d31/open3d-0.19.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:5b60234fa6a56a20caf1560cad4e914133c8c198d74d7b839631c90e8592762e", size = 447678387, upload-time = "2025-01-08T07:21:55.27Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1c/0219416429f88ebc94fcb269fb186b153affe5b91dffe8f9062330d7776d/open3d-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:18bb8b86e5fa9e582ed11b9651ff6e4a782e6778c9b8bfc344fc866dc8b5f49c", size = 69150378, upload-time = "2025-01-08T07:27:10.462Z" }, + { url = "https://files.pythonhosted.org/packages/a7/37/8d1746fcb58c37a9bd868fdca9a36c25b3c277bd764b7146419d11d2a58d/open3d-0.19.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:117702467bfb1602e9ae0ee5e2c7bcf573ebcd227b36a26f9f08425b52c89929", size = 103098641, upload-time = "2025-01-08T07:26:12.371Z" }, + { url = "https://files.pythonhosted.org/packages/bc/50/339bae21d0078cc3d3735e8eaf493a353a17dcc95d76bcefaa8edcf723d3/open3d-0.19.0-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:678017392f6cc64a19d83afeb5329ffe8196893de2432f4c258eaaa819421bb5", size = 447683616, upload-time = "2025-01-08T07:22:48.098Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3c/358f1cc5b034dc6a785408b7aa7643e503229d890bcbc830cda9fce778b1/open3d-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:02091c309708f09da1167d2ea475e05d19f5e81dff025145f3afd9373cbba61f", size = 69151111, upload-time = "2025-01-08T07:27:22.662Z" }, + { url = "https://files.pythonhosted.org/packages/37/c5/286c605e087e72ad83eab130451ce13b768caa4374d926dc735edc20da5a/open3d-0.19.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:9e4a8d29443ba4c83010d199d56c96bf553dd970d3351692ab271759cbe2d7ac", size = 103202754, upload-time = "2025-01-08T07:26:27.169Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/3723e5ade77c234a1650db11cbe59fe25c4f5af6c224f8ea22ff088bb36a/open3d-0.19.0-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:01e4590dc2209040292ebe509542fbf2bf869ea60bcd9be7a3fe77b65bad3192", size = 447665185, upload-time = "2025-01-08T07:23:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/35a6e0a35aa72420e75dc28d54b24beaff79bcad150423e47c67d2ad8773/open3d-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:665839837e1d3a62524804c31031462c3b548a2b6ed55214e6deb91522844f97", size = 69169961, upload-time = "2025-01-08T07:27:35.392Z" }, +] + +[[package]] +name = "openai" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" }, +] + +[[package]] +name = "openai-whisper" +version = "20250625" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tiktoken" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "triton", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'linux2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/8e/d36f8880bcf18ec026a55807d02fe4c7357da9f25aebd92f85178000c0dc/openai_whisper-20250625.tar.gz", hash = "sha256:37a91a3921809d9f44748ffc73c0a55c9f366c85a3ef5c2ae0cc09540432eb96", size = 803191, upload-time = "2025-06-26T01:06:13.34Z" } + +[[package]] +name = "opencv-contrib-python" +version = "4.10.0.84" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/33/7b8ec6c4d45e678b26297e4a5e76464a93033a9adcc8c17eac01097065f6/opencv-contrib-python-4.10.0.84.tar.gz", hash = "sha256:4a3eae0ed9cadf1abe9293a6938a25a540e2fd6d7fc308595caa5896c8b36a0c", size = 150433857, upload-time = "2024-06-17T18:30:50.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/64/c1194510eaed272d86b53a08c790ca6ed1c450f06d401c49c8145fc46d40/opencv_contrib_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:ee4b0919026d8c533aeb69b16c6ec4a891a2f6844efaa14121bf68838753209c", size = 63667391, upload-time = "2024-06-18T04:57:54.718Z" }, + { url = "https://files.pythonhosted.org/packages/09/94/d077c4c976c2d7a88812fd55396e92edae0e0c708689dbd8c8f508920e47/opencv_contrib_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:dea80d4db73b8acccf9e16b5744bf3654f47b22745074263f0a6c10de26c5ef5", size = 66278032, upload-time = "2024-06-17T19:34:23.718Z" }, + { url = "https://files.pythonhosted.org/packages/f8/76/f76fe74b864f3cfa737173ca12e8890aad8369e980006fb8a0b6cd14c6c7/opencv_contrib_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:040575b69e4f3aa761676bace4e3d1b8485fbfaf77ef77b266ab6bda5a3b5e9b", size = 47384495, upload-time = "2024-06-17T20:00:39.027Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e0/8f5d065ebb2e5941d289c5f653f944318f9e418bc5167bc6a346ab5e0f6a/opencv_contrib_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a261223db41f6e512d76deaf21c8fcfb4fbbcbc2de62ca7f74a05f2c9ee489ef", size = 68681489, upload-time = "2024-06-17T18:30:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/7041bd7350cb1a26fa80415a7664b6f04f7ccbf0c12b9318d564cdf35932/opencv_contrib_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2a36257ec1375d1bec2a62177ea39828ff9804de6831ee39646bdc875c343cec", size = 34506122, upload-time = "2024-06-17T18:28:29.922Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9e/7110d2c5d543ab03b9581dbb1f8e2429863e44e0c9b4960b766f230c1279/opencv_contrib_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:47ec3160dae75f70e099b286d1a2e086d20dac8b06e759f60eaf867e6bdecba7", size = 45541421, upload-time = "2024-06-17T18:28:46.012Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" }, + { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "opt-einsum" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004, upload-time = "2024-09-26T14:33:24.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932, upload-time = "2024-09-26T14:33:23.039Z" }, +] + +[[package]] +name = "optax" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "chex", version = "0.1.90", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "chex", version = "0.1.91", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jax", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jaxlib", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/3b/90c11f740a3538200b61cd2b7d9346959cb9e31e0bdea3d2f886b7262203/optax-0.2.6.tar.gz", hash = "sha256:ba8d1e12678eba2657484d6feeca4fb281b8066bdfd5efbfc0f41b87663109c0", size = 269660, upload-time = "2025-09-15T22:41:24.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/ec/19c6cc6064c7fc8f0cd6d5b37c4747849e66040c6ca98f86565efc2c227c/optax-0.2.6-py3-none-any.whl", hash = "sha256:f875251a5ab20f179d4be57478354e8e21963373b10f9c3b762b94dcb8c36d91", size = 367782, upload-time = "2025-09-15T22:41:22.825Z" }, +] + +[[package]] +name = "orbax-checkpoint" +version = "0.11.31" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "aiofiles" }, + { name = "etils", extra = ["epath", "epy"] }, + { name = "humanize" }, + { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jax", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "msgpack" }, + { name = "nest-asyncio" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "simplejson" }, + { name = "tensorstore", version = "0.1.78", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "tensorstore", version = "0.1.80", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/1e/c65e35ab5ef9d380f4a4ce7f983a0dd360d229eec22204aacb80d2b91aca/orbax_checkpoint-0.11.31.tar.gz", hash = "sha256:f021193a619782655798bc4a285f40612f6fe647ddeb303d1f49cdbc5645e319", size = 406137, upload-time = "2025-12-11T18:09:17.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/3a/abbc3c5cac2e082e88cfec2161bf837f18fef786caaa3f007594c839fc8c/orbax_checkpoint-0.11.31-py3-none-any.whl", hash = "sha256:b00e39cd61cbd6c7c78b091ccac0ed1bbf3cf7788e761618e7070761195bfcc0", size = 602358, upload-time = "2025-12-11T18:09:15.667Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, + { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" }, + { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" }, + { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" }, + { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" }, + { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, + { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, + { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, + { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, + { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, + { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, + { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, + { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, + { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, + { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, + { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, + { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, + { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/96/34c40d621996c2f377a18decbd3c59f031dde73c3ba47d1e1e8f29a05aaa/ormsgpack-1.12.1.tar.gz", hash = "sha256:a3877fde1e4f27a39f92681a0aab6385af3a41d0c25375d33590ae20410ea2ac", size = 39476, upload-time = "2025-12-14T07:57:43.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/da/caf25cc54d6870089a0b5614c4c5914dd3fae45f9f7f84a32445ad0612e3/ormsgpack-1.12.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:62e3614cab63fa5aa42f5f0ca3cd12899f0bfc5eb8a5a0ebab09d571c89d427d", size = 376182, upload-time = "2025-12-14T07:56:46.094Z" }, + { url = "https://files.pythonhosted.org/packages/fc/02/ccc9170c6bee86f428707f15b5ad68d42c71d43856e1b8e37cdfea50af5b/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86d9fbf85c05c69c33c229d2eba7c8c3500a56596cd8348131c918acd040d6af", size = 202339, upload-time = "2025-12-14T07:56:47.609Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/10309a5a6421adaedab710a72470143d664bb0a043cc095c1311878325a0/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d246e66f09d8e0f96e770829149ee83206e90ed12f5987998bb7be84aec99fe", size = 210720, upload-time = "2025-12-14T07:56:48.66Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b4/92a0f7a00c5f0c71b51dc3112e53b1ca937b9891a08979d06524db11b799/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfc2c830a1ed2d00de713d08c9e62efa699e8fd29beafa626aaebe466f583ebb", size = 211264, upload-time = "2025-12-14T07:56:49.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/5cce85c8e58fcaa048c75fbbe37816a1b3fb58ba4289a7dedc4f4ed9ce82/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc892757d8f9eea5208268a527cf93c98409802f6a9f7c8d71a7b8f9ba5cb944", size = 386076, upload-time = "2025-12-14T07:56:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/d0/f18d258c733eb22eadad748659f7984d0b6a851fb3deefcb33f50e9a947a/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0de1dbcf11ea739ac4a882b43d5c2055e6d99ce64e8d6502e25d6d881700c017", size = 479570, upload-time = "2025-12-14T07:56:52.912Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3a/b362dff090f4740090fe51d512f24b1e320d1f96497ebf9248e2a04ac88f/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5065dfb9ec4db93241c60847624d9aeef4ccb449c26a018c216b55c69be83c0", size = 387859, upload-time = "2025-12-14T07:56:53.968Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/d948965598b2b7872800076da5c02573aa72f716be57a3d4fe60490b2a2a/ormsgpack-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d17103c4726181d7000c61b751c881f1b6f401d146df12da028fc730227df19", size = 115906, upload-time = "2025-12-14T07:56:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/57/e2/f5b89365c8dc8025c27d31316038f1c103758ddbf87dc0fa8e3f78f66907/ormsgpack-1.12.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4038f59ae0e19dac5e5d9aae4ec17ff84a79e046342ee73ccdecf3547ecf0d34", size = 376180, upload-time = "2025-12-14T07:56:56.521Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/3f694e06f5e32c6d65066f53b4a025282a5072b6b336c17560b00e04606d/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16c63b0c5a3eec467e4bb33a14dabba076b7d934dff62898297b5c0b5f7c3cb3", size = 202338, upload-time = "2025-12-14T07:56:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f5/6d95d7b7c11f97a92522082fc7e5d1ab34537929f1e13f4c369f392f19d0/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74fd6a8e037eb310dda865298e8d122540af00fe5658ec18b97a1d34f4012e4d", size = 210720, upload-time = "2025-12-14T07:56:58.968Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/9a49a2686f8b7165dcb2342b8554951263c30c0f0825f1fcc2d56e736a6b/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ad60308e233dd824a1859eabb5fe092e123e885eafa4ad5789322329c80fb5", size = 211264, upload-time = "2025-12-14T07:57:00.099Z" }, + { url = "https://files.pythonhosted.org/packages/02/31/2fdc36eaeca2182900b96fc7b19755f293283fe681750e3d295733d62f0e/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:35127464c941c1219acbe1a220e48d55e7933373d12257202f4042f7044b4c90", size = 386081, upload-time = "2025-12-14T07:57:01.177Z" }, + { url = "https://files.pythonhosted.org/packages/f0/65/0a765432f08ae26b4013c6a9aed97be17a9ef85f1600948a474b518e27dd/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c48d1c50794692d1e6e3f8c3bb65f5c3acfaae9347e506484a65d60b3d91fb50", size = 479572, upload-time = "2025-12-14T07:57:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4f/f2f15ebef786ad71cea420bf8692448fbddf04d1bf3feaa68bd5ee3172e6/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b512b2ad6feaaefdc26e05431ed2843e42483041e354e167c53401afaa83d919", size = 387862, upload-time = "2025-12-14T07:57:03.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/eb/86fbef1d605fa91ecef077f93f9d0e34fc39b23475dfe3ffb92f6c8db28d/ormsgpack-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:93f30db95e101a9616323bfc50807ad00e7f6197cea2216d2d24af42afc77d88", size = 115900, upload-time = "2025-12-14T07:57:05.137Z" }, + { url = "https://files.pythonhosted.org/packages/5b/67/7ba1a46e6a6e263fc42a4fafc24afc1ab21a66116553cad670426f0bd9ef/ormsgpack-1.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:d75b5fa14f6abffce2c392ee03b4731199d8a964c81ee8645c4c79af0e80fd50", size = 109868, upload-time = "2025-12-14T07:57:06.834Z" }, + { url = "https://files.pythonhosted.org/packages/17/fe/ab9167ca037406b5703add24049cf3e18021a3b16133ea20615b1f160ea4/ormsgpack-1.12.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4d7fb0e1b6fbc701d75269f7405a4f79230a6ce0063fb1092e4f6577e312f86d", size = 376725, upload-time = "2025-12-14T07:57:07.894Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ea/2820e65f506894c459b840d1091ae6e327fde3d5a3f3b002a11a1b9bdf7d/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43a9353e2db5b024c91a47d864ef15eaa62d81824cfc7740fed4cef7db738694", size = 202466, upload-time = "2025-12-14T07:57:09.049Z" }, + { url = "https://files.pythonhosted.org/packages/45/8b/def01c13339c5bbec2ee1469ef53e7fadd66c8d775df974ee4def1572515/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc8fe866b7706fc25af0adf1f600bc06ece5b15ca44e34641327198b821e5c3c", size = 210748, upload-time = "2025-12-14T07:57:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d2/bf350c92f7f067dd9484499705f2d8366d8d9008a670e3d1d0add1908f85/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813755b5f598a78242042e05dfd1ada4e769e94b98c9ab82554550f97ff4d641", size = 211510, upload-time = "2025-12-14T07:57:11.165Z" }, + { url = "https://files.pythonhosted.org/packages/74/92/9d689bcb95304a6da26c4d59439c350940c25d1b35f146d402ccc6344c51/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8eea2a13536fae45d78f93f2cc846c9765c7160c85f19cfefecc20873c137cdd", size = 386237, upload-time = "2025-12-14T07:57:12.306Z" }, + { url = "https://files.pythonhosted.org/packages/17/fe/bd3107547f8b6129265dd957f40b9cd547d2445db2292aacb13335a7ea89/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7a02ebda1a863cbc604740e76faca8eee1add322db2dcbe6cf32669fffdff65c", size = 479589, upload-time = "2025-12-14T07:57:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7c/e8e5cc9edb967d44f6f85e9ebdad440b59af3fae00b137a4327dc5aed9bb/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd63897c439931cdf29348e5e6e8c330d529830e848d10767615c0f3d1b82", size = 388077, upload-time = "2025-12-14T07:57:14.551Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/5031797e43b58506f28a8760b26dc23f2620fb4f2200c4c1b3045603e67e/ormsgpack-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:362f2e812f8d7035dc25a009171e09d7cc97cb30d3c9e75a16aeae00ca3c1dcf", size = 116190, upload-time = "2025-12-14T07:57:15.575Z" }, + { url = "https://files.pythonhosted.org/packages/1e/fd/9f43ea6425e383a6b2dbfafebb06fd60e8d68c700ef715adfbcdb499f75d/ormsgpack-1.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:6190281e381db2ed0045052208f47a995ccf61eed48f1215ae3cce3fbccd59c5", size = 109990, upload-time = "2025-12-14T07:57:16.419Z" }, + { url = "https://files.pythonhosted.org/packages/11/42/f110dfe7cf23a52a82e23eb23d9a6a76ae495447d474686dfa758f3d71d6/ormsgpack-1.12.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9663d6b3ecc917c063d61a99169ce196a80f3852e541ae404206836749459279", size = 376746, upload-time = "2025-12-14T07:57:17.699Z" }, + { url = "https://files.pythonhosted.org/packages/11/76/b386e508a8ae207daec240201a81adb26467bf99b163560724e86bd9ff33/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32e85cfbaf01a94a92520e7fe7851cfcfe21a5698299c28ab86194895f9b9233", size = 202489, upload-time = "2025-12-14T07:57:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0e/5db7a63f387149024572daa3d9512fe8fb14bf4efa0722d6d491bed280e7/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabfd2c24b59c7c69870a5ecee480dfae914a42a0c2e7c9d971cf531e2ba471a", size = 210757, upload-time = "2025-12-14T07:57:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/64/79/3a9899e57cb57430bd766fc1b4c9ad410cb2ba6070bc8cf6301e7d385768/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bbf2b64afeded34ccd8e25402e4bca038757913931fa0d693078d75563f6f9", size = 211518, upload-time = "2025-12-14T07:57:20.972Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/4f41710ae9fe50d7fcbe476793b3c487746d0e1cc194cc0fee42ff6d989b/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9959a71dde1bd0ced84af17facc06a8afada495a34e9cb1bad8e9b20d4c59cef", size = 386251, upload-time = "2025-12-14T07:57:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/bf/54/ba0c97d6231b1f01daafaa520c8cce1e1b7fceaae6fdc1c763925874a7de/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:e9be0e3b62d758f21f5b20e0e06b3a240ec546c4a327bf771f5825462aa74714", size = 479607, upload-time = "2025-12-14T07:57:23.525Z" }, + { url = "https://files.pythonhosted.org/packages/18/75/19a9a97a462776d525baf41cfb7072734528775f0a3d5fbfab3aa7756b9b/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a29d49ab7fdd77ea787818e60cb4ef491708105b9c4c9b0f919201625eb036b5", size = 388062, upload-time = "2025-12-14T07:57:24.616Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/ec26e3f44e9632ecd2f43638b7b37b500eaea5d79cab984ad0b94be14f82/ormsgpack-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:c418390b47a1d367e803f6c187f77e4d67c7ae07ba962e3a4a019001f4b0291a", size = 116195, upload-time = "2025-12-14T07:57:25.626Z" }, + { url = "https://files.pythonhosted.org/packages/7d/64/bfa5f4a34d0f15c6aba1b73e73f7441a66d635bd03249d334a4796b7a924/ormsgpack-1.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:cfa22c91cffc10a7fbd43729baff2de7d9c28cef2509085a704168ae31f02568", size = 109986, upload-time = "2025-12-14T07:57:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/87/0e/78e5697164e3223b9b216c13e99f1acbc1ee9833490d68842b13da8ba883/ormsgpack-1.12.1-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b93c91efb1a70751a1902a5b43b27bd8fd38e0ca0365cf2cde2716423c15c3a6", size = 376758, upload-time = "2025-12-14T07:57:27.641Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/3a3cbb64703263d7bbaed7effa3ce78cb9add360a60aa7c544d7df28b641/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf0ea0389167b5fa8d2933dd3f33e887ec4ba68f89c25214d7eec4afd746d22", size = 202487, upload-time = "2025-12-14T07:57:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2c/807ebe2b77995599bbb1dec8c3f450d5d7dddee14ce3e1e71dc60e2e2a74/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4c29af837f35af3375070689e781161e7cf019eb2f7cd641734ae45cd001c0d", size = 210853, upload-time = "2025-12-14T07:57:30.508Z" }, + { url = "https://files.pythonhosted.org/packages/25/57/2cdfc354e3ad8e847628f511f4d238799d90e9e090941e50b9d5ba955ae2/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336fc65aa0fe65896a3dabaae31e332a0a98b4a00ad7b0afde21a7505fd23ff3", size = 211545, upload-time = "2025-12-14T07:57:31.585Z" }, + { url = "https://files.pythonhosted.org/packages/76/1d/c6fda560e4a8ff865b3aec8a86f7c95ab53f4532193a6ae4ab9db35f85aa/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:940f60aabfefe71dd6b82cb33f4ff10b2e7f5fcfa5f103cdb0a23b6aae4c713c", size = 386333, upload-time = "2025-12-14T07:57:32.957Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/715081b36fceb8b497c68b87d384e1cc6d9c9c130ce3b435634d3d785b86/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:596ad9e1b6d4c95595c54aaf49b1392609ca68f562ce06f4f74a5bc4053bcda4", size = 479701, upload-time = "2025-12-14T07:57:34.686Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cf/01ad04def42b3970fc1a302c07f4b46339edf62ef9650247097260471f40/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:575210e8fcbc7b0375026ba040a5eef223e9f66a4453d9623fc23282ae09c3c8", size = 388148, upload-time = "2025-12-14T07:57:35.771Z" }, + { url = "https://files.pythonhosted.org/packages/15/91/1fff2fc2b5943c740028f339154e7103c8f2edf1a881d9fbba2ce11c3b1d/ormsgpack-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:647daa3718572280893456be44c60aea6690b7f2edc54c55648ee66e8f06550f", size = 116201, upload-time = "2025-12-14T07:57:36.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/66/142b542aed3f96002c7d1c33507ca6e1e0d0a42b9253ab27ef7ed5793bd9/ormsgpack-1.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:a8b3ab762a6deaf1b6490ab46dda0c51528cf8037e0246c40875c6fe9e37b699", size = 110029, upload-time = "2025-12-14T07:57:37.703Z" }, + { url = "https://files.pythonhosted.org/packages/38/b3/ef4494438c90359e1547eaed3c5ec46e2c431d59a3de2af4e70ebd594c49/ormsgpack-1.12.1-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:12087214e436c1f6c28491949571abea759a63111908c4f7266586d78144d7a8", size = 376777, upload-time = "2025-12-14T07:57:38.795Z" }, + { url = "https://files.pythonhosted.org/packages/05/a0/1149a7163f8b0dfbc64bf9099b6f16d102ad3b03bcc11afee198d751da2d/ormsgpack-1.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6d54c14cf86ef13f10ccade94d1e7de146aa9b17d371e18b16e95f329393b7", size = 202490, upload-time = "2025-12-14T07:57:40.168Z" }, + { url = "https://files.pythonhosted.org/packages/68/82/f2ec5e758d6a7106645cca9bb7137d98bce5d363789fa94075be6572057c/ormsgpack-1.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f3584d07882b7ea2a1a589f795a3af97fe4c2932b739408e6d1d9d286cad862", size = 211733, upload-time = "2025-12-14T07:57:42.253Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pandas-stubs" +version = "2.3.3.251219" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "types-pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ee/5407e9e63d22a47774f9246ca80b24f82c36f26efd39f9e3c5b584b915aa/pandas_stubs-2.3.3.251219.tar.gz", hash = "sha256:dc2883e6daff49d380d1b5a2e864983ab9be8cd9a661fa861e3dea37559a5af4", size = 106899, upload-time = "2025-12-19T15:49:53.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/20/69f2a39792a653fd64d916cd563ed79ec6e5dcfa6408c4674021d810afcf/pandas_stubs-2.3.3.251219-py3-none-any.whl", hash = "sha256:ccc6337febb51d6d8a08e4c96b479478a0da0ef704b5e08bd212423fe1cb549c", size = 163667, upload-time = "2025-12-19T15:49:52.072Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload-time = "2024-07-01T09:45:22.07Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658, upload-time = "2024-07-01T09:45:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075, upload-time = "2024-07-01T09:45:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808, upload-time = "2024-07-01T09:45:30.305Z" }, + { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290, upload-time = "2024-07-01T09:45:32.868Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163, upload-time = "2024-07-01T09:45:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100, upload-time = "2024-07-01T09:45:37.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880, upload-time = "2024-07-01T09:45:39.89Z" }, + { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218, upload-time = "2024-07-01T09:45:42.771Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487, upload-time = "2024-07-01T09:45:45.176Z" }, + { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219, upload-time = "2024-07-01T09:45:47.274Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload-time = "2024-07-01T09:45:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload-time = "2024-07-01T09:45:52.462Z" }, + { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload-time = "2024-07-01T09:45:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804, upload-time = "2024-07-01T09:45:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126, upload-time = "2024-07-01T09:46:00.713Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541, upload-time = "2024-07-01T09:46:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616, upload-time = "2024-07-01T09:46:05.356Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802, upload-time = "2024-07-01T09:46:08.145Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213, upload-time = "2024-07-01T09:46:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498, upload-time = "2024-07-01T09:46:12.685Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219, upload-time = "2024-07-01T09:46:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" }, + { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" }, + { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" }, + { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload-time = "2024-07-01T09:46:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload-time = "2024-07-01T09:46:47.331Z" }, + { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload-time = "2024-07-01T09:46:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload-time = "2024-07-01T09:46:51.811Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload-time = "2024-07-01T09:46:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload-time = "2024-07-01T09:46:56.664Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload-time = "2024-07-01T09:46:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload-time = "2024-07-01T09:47:01.189Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889, upload-time = "2024-07-01T09:48:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160, upload-time = "2024-07-01T09:48:07.206Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020, upload-time = "2024-07-01T09:48:09.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539, upload-time = "2024-07-01T09:48:12.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125, upload-time = "2024-07-01T09:48:14.891Z" }, + { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373, upload-time = "2024-07-01T09:48:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, +] + +[[package]] +name = "piper-sdk" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-can" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c4/06172af8276170ff0f484e2065f853eef53d7ced3cc822730f55ea110f3b/piper_sdk-0.6.1.tar.gz", hash = "sha256:2a154870992379f5048caf70662fdbb29f11b7cb17846d6a23afc07cd3d57217", size = 161302, upload-time = "2025-10-30T06:38:53.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/0c/4473a7a9aca9c50798abec6a77e8e5e714ad968399db3d2b86162a05177c/piper_sdk-0.6.1-py3-none-any.whl", hash = "sha256:743557e1b8dfe685f2c33d728ab28c3ff510de8860d6494e54ed5d801493d65c", size = 193748, upload-time = "2025-10-30T06:38:51.368Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "playground" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "brax" }, + { name = "etils" }, + { name = "flax", version = "0.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "flax", version = "0.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jax", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "lxml" }, + { name = "ml-collections" }, + { name = "mujoco" }, + { name = "mujoco-mjx" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/f9/a430e60d73211cc43d32c21c23ccefca8540aebf1fce666ade6f0d567760/playground-0.0.5.tar.gz", hash = "sha256:cd8a9a623378175c1c460b0bb87bb6800d78e52eb62111708215b14085004c50", size = 7298640, upload-time = "2025-06-23T18:40:23.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/593497bba948b5bddc1684752047407fb3b6b54e72fd9ba23a073612b02f/playground-0.0.5-py3-none-any.whl", hash = "sha256:73224992b7e3a9ec9f237cfba1769f56e5dbc9f00f00fa3ecfb737d087b890cd", size = 7431365, upload-time = "2025-06-23T18:40:21.2Z" }, +] + +[[package]] +name = "plotext" +version = "5.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/d7/f75f397af966fe252d0d34ffd3cae765317fce2134f925f95e7d6725d1ce/plotext-5.3.2.tar.gz", hash = "sha256:52d1e932e67c177bf357a3f0fe6ce14d1a96f7f7d5679d7b455b929df517068e", size = 61967, upload-time = "2024-09-24T15:13:37.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/1e/12fe7c40cd2099a1f454518754ed229b01beaf3bbb343127f0cc13ce6c22/plotext-5.3.2-py3-none-any.whl", hash = "sha256:394362349c1ddbf319548cfac17ca65e6d5dfc03200c40dfdc0503b3e95a2283", size = 64047, upload-time = "2024-09-24T15:13:36.296Z" }, +] + +[[package]] +name = "plotly" +version = "6.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/05/1199e2a03ce6637960bc1e951ca0f928209a48cfceb57355806a88f214cf/plotly-6.5.0.tar.gz", hash = "sha256:d5d38224883fd38c1409bef7d6a8dc32b74348d39313f3c52ca998b8e447f5c8", size = 7013624, upload-time = "2025-11-17T18:39:24.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl", hash = "sha256:5ac851e100367735250206788a2b1325412aa4a4917a4fe3e6f0bc5aa6f3d90a", size = 9893174, upload-time = "2025-11-17T18:39:20.351Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "plum-dispatch" +version = "2.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/46/ab3928e864b0a88a8ae6987b3da3b7ae32fe0a610264f33272139275dab5/plum_dispatch-2.5.7.tar.gz", hash = "sha256:a7908ad5563b93f387e3817eb0412ad40cfbad04bc61d869cf7a76cd58a3895d", size = 35452, upload-time = "2025-01-17T20:07:31.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/31/21609a9be48e877bc33b089a7f495c853215def5aeb9564a31c210d9d769/plum_dispatch-2.5.7-py3-none-any.whl", hash = "sha256:06471782eea0b3798c1e79dca2af2165bafcfa5eb595540b514ddd81053b1ede", size = 42612, upload-time = "2025-01-17T20:07:26.461Z" }, +] + +[[package]] +name = "polars" +version = "1.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "polars-runtime-32" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/dc/56f2a90c79a2cb13f9e956eab6385effe54216ae7a2068b3a6406bae4345/polars-1.36.1.tar.gz", hash = "sha256:12c7616a2305559144711ab73eaa18814f7aa898c522e7645014b68f1432d54c", size = 711993, upload-time = "2025-12-10T01:14:53.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/c6/36a1b874036b49893ecae0ac44a2f63d1a76e6212631a5b2f50a86e0e8af/polars-1.36.1-py3-none-any.whl", hash = "sha256:853c1bbb237add6a5f6d133c15094a9b727d66dd6a4eb91dbb07cdb056b2b8ef", size = 802429, upload-time = "2025-12-10T01:13:53.838Z" }, +] + +[[package]] +name = "polars-runtime-32" +version = "1.36.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/df/597c0ef5eb8d761a16d72327846599b57c5d40d7f9e74306fc154aba8c37/polars_runtime_32-1.36.1.tar.gz", hash = "sha256:201c2cfd80ceb5d5cd7b63085b5fd08d6ae6554f922bcb941035e39638528a09", size = 2788751, upload-time = "2025-12-10T01:14:54.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/ea/871129a2d296966c0925b078a9a93c6c5e7facb1c5eebfcd3d5811aeddc1/polars_runtime_32-1.36.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:327b621ca82594f277751f7e23d4b939ebd1be18d54b4cdf7a2f8406cecc18b2", size = 43494311, upload-time = "2025-12-10T01:13:56.096Z" }, + { url = "https://files.pythonhosted.org/packages/d8/76/0038210ad1e526ce5bb2933b13760d6b986b3045eccc1338e661bd656f77/polars_runtime_32-1.36.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ab0d1f23084afee2b97de8c37aa3e02ec3569749ae39571bd89e7a8b11ae9e83", size = 39300602, upload-time = "2025-12-10T01:13:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/54/1e/2707bee75a780a953a77a2c59829ee90ef55708f02fc4add761c579bf76e/polars_runtime_32-1.36.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:899b9ad2e47ceb31eb157f27a09dbc2047efbf4969a923a6b1ba7f0412c3e64c", size = 44511780, upload-time = "2025-12-10T01:14:02.285Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/3fede95feee441be64b4bcb32444679a8fbb7a453a10251583053f6efe52/polars_runtime_32-1.36.1-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:d9d077bb9df711bc635a86540df48242bb91975b353e53ef261c6fae6cb0948f", size = 40688448, upload-time = "2025-12-10T01:14:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/05/0f/e629713a72999939b7b4bfdbf030a32794db588b04fdf3dc977dd8ea6c53/polars_runtime_32-1.36.1-cp39-abi3-win_amd64.whl", hash = "sha256:cc17101f28c9a169ff8b5b8d4977a3683cd403621841623825525f440b564cf0", size = 44464898, upload-time = "2025-12-10T01:14:08.296Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d8/a12e6aa14f63784cead437083319ec7cece0d5bb9a5bfe7678cc6578b52a/polars_runtime_32-1.36.1-cp39-abi3-win_arm64.whl", hash = "sha256:809e73857be71250141225ddd5d2b30c97e6340aeaa0d445f930e01bef6888dc", size = 39798896, upload-time = "2025-12-10T01:14:11.568Z" }, +] + +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + +[[package]] +name = "posthog" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/20/60ae67bb9d82f00427946218d49e2e7e80fb41c15dc5019482289ec9ce8d/posthog-5.4.0.tar.gz", hash = "sha256:701669261b8d07cdde0276e5bc096b87f9e200e3b9589c5ebff14df658c5893c", size = 88076, upload-time = "2025-06-20T23:19:23.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364, upload-time = "2025-06-20T23:19:22.001Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" }, + { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pyarrow" +version = "22.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" }, + { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" }, + { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" }, + { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, + { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, + { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pyaudio" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/1d/8878c7752febb0f6716a7e1a52cb92ac98871c5aa522cba181878091607c/PyAudio-0.2.14.tar.gz", hash = "sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87", size = 47066, upload-time = "2023-11-07T07:11:48.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/90/1553487277e6aa25c0b7c2c38709cdd2b49e11c66c0b25c6e8b7b6638c72/PyAudio-0.2.14-cp310-cp310-win32.whl", hash = "sha256:126065b5e82a1c03ba16e7c0404d8f54e17368836e7d2d92427358ad44fefe61", size = 144624, upload-time = "2023-11-07T07:11:33.599Z" }, + { url = "https://files.pythonhosted.org/packages/27/bc/719d140ee63cf4b0725016531d36743a797ffdbab85e8536922902c9349a/PyAudio-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:2a166fc88d435a2779810dd2678354adc33499e9d4d7f937f28b20cc55893e83", size = 164069, upload-time = "2023-11-07T07:11:35.439Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f0/b0eab89eafa70a86b7b566a4df2f94c7880a2d483aa8de1c77d335335b5b/PyAudio-0.2.14-cp311-cp311-win32.whl", hash = "sha256:506b32a595f8693811682ab4b127602d404df7dfc453b499c91a80d0f7bad289", size = 144624, upload-time = "2023-11-07T07:11:36.94Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/f043c854aad450a76e476b0cf9cda1956419e1dacf1062eb9df3c0055abe/PyAudio-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:bbeb01d36a2f472ae5ee5e1451cacc42112986abe622f735bb870a5db77cf903", size = 164070, upload-time = "2023-11-07T07:11:38.579Z" }, + { url = "https://files.pythonhosted.org/packages/8d/45/8d2b76e8f6db783f9326c1305f3f816d4a12c8eda5edc6a2e1d03c097c3b/PyAudio-0.2.14-cp312-cp312-win32.whl", hash = "sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b", size = 144750, upload-time = "2023-11-07T07:11:40.142Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/d25812e5f79f06285767ec607b39149d02aa3b31d50c2269768f48768930/PyAudio-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3", size = 164126, upload-time = "2023-11-07T07:11:41.539Z" }, + { url = "https://files.pythonhosted.org/packages/3a/77/66cd37111a87c1589b63524f3d3c848011d21ca97828422c7fde7665ff0d/PyAudio-0.2.14-cp313-cp313-win32.whl", hash = "sha256:95328285b4dab57ea8c52a4a996cb52be6d629353315be5bfda403d15932a497", size = 150982, upload-time = "2024-11-20T19:12:12.404Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8b/7f9a061c1cc2b230f9ac02a6003fcd14c85ce1828013aecbaf45aa988d20/PyAudio-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:692d8c1446f52ed2662120bcd9ddcb5aa2b71f38bda31e58b19fb4672fffba69", size = 173655, upload-time = "2024-11-20T19:12:13.616Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/47/16d7af6fae7803f4c691856bc0d8d433ccf30e106432e2ef7707ee19a38a/pybase64-1.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f63aa7f29139b8a05ce5f97cdb7fad63d29071e5bdc8a638a343311fe996112a", size = 38241, upload-time = "2025-12-06T13:22:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3e/268beb8d2240ab55396af4d1b45d2494935982212549b92a5f5b57079bd3/pybase64-1.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5943ec1ae87a8b4fe310905bb57205ea4330c75e2c628433a7d9dd52295b588", size = 31672, upload-time = "2025-12-06T13:22:28.854Z" }, + { url = "https://files.pythonhosted.org/packages/80/14/4365fa33222edcc46b6db4973f9e22bda82adfb6ab2a01afff591f1e41c8/pybase64-1.4.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5f2b8aef86f35cd5894c13681faf433a1fffc5b2e76544dcb5416a514a1a8347", size = 65978, upload-time = "2025-12-06T13:22:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/1c/22/e89739d8bc9b96c68ead44b4eec42fe555683d9997e4ba65216d384920fc/pybase64-1.4.3-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6ec7e53dd09b0a8116ccf5c3265c7c7fce13c980747525be76902aef36a514a", size = 68903, upload-time = "2025-12-06T13:22:31.29Z" }, + { url = "https://files.pythonhosted.org/packages/77/e1/7e59a19f8999cdefe9eb0d56bfd701dd38263b0f6fb4a4d29fce165a1b36/pybase64-1.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7528604cd69c538e1dbaafded46e9e4915a2adcd6f2a60fcef6390d87ca922ea", size = 57516, upload-time = "2025-12-06T13:22:32.395Z" }, + { url = "https://files.pythonhosted.org/packages/42/ad/f47dc7e6fe32022b176868b88b671a32dab389718c8ca905cab79280aaaf/pybase64-1.4.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4ec645f32b50593879031e09158f8681a1db9f5df0f72af86b3969a1c5d1fa2b", size = 54533, upload-time = "2025-12-06T13:22:33.457Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/7ab312b5a324833953b00e47b23eb4f83d45bd5c5c854b4b4e51b2a0cf5b/pybase64-1.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:634a000c5b3485ccc18bb9b244e0124f74b6fbc7f43eade815170237a7b34c64", size = 57187, upload-time = "2025-12-06T13:22:34.566Z" }, + { url = "https://files.pythonhosted.org/packages/2c/84/80acab1fcbaaae103e6b862ef5019192c8f2cd8758433595a202179a0d1d/pybase64-1.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:309ea32ad07639a485580af1be0ad447a434deb1924e76adced63ac2319cfe15", size = 57730, upload-time = "2025-12-06T13:22:35.581Z" }, + { url = "https://files.pythonhosted.org/packages/1f/24/84256d472400ea3163d7d69c44bb7e2e1027f0f1d4d20c47629a7dc4578e/pybase64-1.4.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d10d517566b748d3f25f6ac7162af779360c1c6426ad5f962927ee205990d27c", size = 53036, upload-time = "2025-12-06T13:22:36.621Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/33aecbed312ee0431798a73fa25e00dedbffdd91389ee23121fed397c550/pybase64-1.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a74cc0f4d835400857cc5c6d27ec854f7949491e07a04e6d66e2137812831f4c", size = 56321, upload-time = "2025-12-06T13:22:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/a341b050746658cbec8cab3c733aeb3ef52ce8f11e60d0d47adbdf729ebf/pybase64-1.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1b591d774ac09d5eb73c156a03277cb271438fbd8042bae4109ff3a827cd218c", size = 50114, upload-time = "2025-12-06T13:22:38.752Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/f7e6680ae6dc4ddff39112ad66e0fa6b2ec346e73881bafc08498c560bc0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5eb588d35a04302ef6157d17db62354a787ac6f8b1585dd0b90c33d63a97a550", size = 66570, upload-time = "2025-12-06T13:22:40.221Z" }, + { url = "https://files.pythonhosted.org/packages/4c/71/774748eecc7fe23869b7e5df028e3c4c2efa16b506b83ea3fa035ea95dc2/pybase64-1.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df8b122d5be2c96962231cc4831d9c2e1eae6736fb12850cec4356d8b06fe6f8", size = 55700, upload-time = "2025-12-06T13:22:41.289Z" }, + { url = "https://files.pythonhosted.org/packages/b3/91/dd15075bb2fe0086193e1cd4bad80a43652c38d8a572f9218d46ba721802/pybase64-1.4.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:31b7a85c661fc591bbcce82fb8adaebe2941e6a83b08444b0957b77380452a4b", size = 52491, upload-time = "2025-12-06T13:22:42.628Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/f357d63ea3774c937fc47160e040419ed528827aa3d4306d5ec9826259c0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e6d7beaae65979fef250e25e66cf81c68a8f81910bcda1a2f43297ab486a7e4e", size = 53957, upload-time = "2025-12-06T13:22:44.615Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/243693771701a54e67ff5ccbf4c038344f429613f5643169a7befc51f007/pybase64-1.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4a6276bc3a3962d172a2b5aba544d89881c4037ea954517b86b00892c703d007", size = 68422, upload-time = "2025-12-06T13:22:45.641Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/f987081bf6bc1d1eda3012dae1b06ad427732ef9933a632cb8b58f9917f8/pybase64-1.4.3-cp310-cp310-win32.whl", hash = "sha256:4bdd07ef017515204ee6eaab17e1ad05f83c0ccb5af8ae24a0fe6d9cb5bb0b7a", size = 33622, upload-time = "2025-12-06T13:22:47.348Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/c169a769fe90128f16d394aad87b2096dd4bf2f035ae0927108a46b617df/pybase64-1.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:5db0b6bbda15110db2740c61970a8fda3bf9c93c3166a3f57f87c7865ed1125c", size = 35799, upload-time = "2025-12-06T13:22:48.731Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/bdbe6af0bd4f3fe5bc70e77ead7f7d523bb9d3ca3ad50ac42b9adbb9ca14/pybase64-1.4.3-cp310-cp310-win_arm64.whl", hash = "sha256:f96367dfc82598569aa02b1103ebd419298293e59e1151abda2b41728703284b", size = 31158, upload-time = "2025-12-06T13:22:50.021Z" }, + { url = "https://files.pythonhosted.org/packages/2b/63/21e981e9d3f1f123e0b0ee2130112b1956cad9752309f574862c7ae77c08/pybase64-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70b0d4a4d54e216ce42c2655315378b8903933ecfa32fced453989a92b4317b2", size = 38237, upload-time = "2025-12-06T13:22:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/92/fb/3f448e139516404d2a3963915cc10dc9dde7d3a67de4edba2f827adfef17/pybase64-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8127f110cdee7a70e576c5c9c1d4e17e92e76c191869085efbc50419f4ae3c72", size = 31673, upload-time = "2025-12-06T13:22:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/bb06a5b9885e7d853ac1e801c4d8abfdb4c8506deee33e53d55aa6690e67/pybase64-1.4.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f9ef0388878bc15a084bd9bf73ec1b2b4ee513d11009b1506375e10a7aae5032", size = 68331, upload-time = "2025-12-06T13:22:54.197Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/8d60b9ec5e658185fc2ee3333e01a6e30d717cf677b24f47cbb3a859d13c/pybase64-1.4.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95a57cccf106352a72ed8bc8198f6820b16cc7d55aa3867a16dea7011ae7c218", size = 71370, upload-time = "2025-12-06T13:22:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/a3e5c1667cc8c38d025a4636855de0fc117fc62e2afeb033a3c6f12c6a22/pybase64-1.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cd1c47dfceb9c7bd3de210fb4e65904053ed2d7c9dce6d107f041ff6fbd7e21", size = 59834, upload-time = "2025-12-06T13:22:56.682Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/8ffcf9810bd23f3984698be161cf7edba656fd639b818039a7be1d6405d4/pybase64-1.4.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9fe9922698f3e2f72874b26890d53a051c431d942701bb3a37aae94da0b12107", size = 56652, upload-time = "2025-12-06T13:22:57.724Z" }, + { url = "https://files.pythonhosted.org/packages/81/62/379e347797cdea4ab686375945bc77ad8d039c688c0d4d0cfb09d247beb9/pybase64-1.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:af5f4bd29c86b59bb4375e0491d16ec8a67548fa99c54763aaedaf0b4b5a6632", size = 59382, upload-time = "2025-12-06T13:22:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f2/9338ffe2f487086f26a2c8ca175acb3baa86fce0a756ff5670a0822bb877/pybase64-1.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c302f6ca7465262908131411226e02100f488f531bb5e64cb901aa3f439bccd9", size = 59990, upload-time = "2025-12-06T13:23:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/85a6142b65b4df8625b337727aa81dc199642de3d09677804141df6ee312/pybase64-1.4.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2f3f439fa4d7fde164ebbbb41968db7d66b064450ab6017c6c95cef0afa2b349", size = 54923, upload-time = "2025-12-06T13:23:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/ac/00/e40215d25624012bf5b7416ca37f168cb75f6dd15acdb91ea1f2ea4dc4e7/pybase64-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a23c6866551043f8b681a5e1e0d59469148b2920a3b4fc42b1275f25ea4217a", size = 58664, upload-time = "2025-12-06T13:23:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/d7e19a63e795c13837f2356268d95dc79d1180e756f57ced742a1e52fdeb/pybase64-1.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:56e6526f8565642abc5f84338cc131ce298a8ccab696b19bdf76fa6d7dc592ef", size = 52338, upload-time = "2025-12-06T13:23:04.458Z" }, + { url = "https://files.pythonhosted.org/packages/f2/32/3c746d7a310b69bdd9df77ffc85c41b80bce00a774717596f869b0d4a20e/pybase64-1.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6a792a8b9d866ffa413c9687d9b611553203753987a3a582d68cbc51cf23da45", size = 68993, upload-time = "2025-12-06T13:23:05.526Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b3/63cec68f9d6f6e4c0b438d14e5f1ef536a5fe63ce14b70733ac5e31d7ab8/pybase64-1.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62ad29a5026bb22cfcd1ca484ec34b0a5ced56ddba38ceecd9359b2818c9c4f9", size = 58055, upload-time = "2025-12-06T13:23:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cb/7acf7c3c06f9692093c07f109668725dc37fb9a3df0fa912b50add645195/pybase64-1.4.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11b9d1d2d32ec358c02214363b8fc3651f6be7dd84d880ecd597a6206a80e121", size = 54430, upload-time = "2025-12-06T13:23:07.936Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/4eb33ff35d173bfff4002e184ce8907f5d0a42d958d61cd9058ef3570179/pybase64-1.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0aebaa7f238caa0a0d373616016e2040c6c879ebce3ba7ab3c59029920f13640", size = 56272, upload-time = "2025-12-06T13:23:09.253Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/a76d65c375a254e65b730c6f56bf528feca91305da32eceab8bcc08591e6/pybase64-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e504682b20c63c2b0c000e5f98a80ea867f8d97642e042a5a39818e44ba4d599", size = 70904, upload-time = "2025-12-06T13:23:10.336Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/8338b6d3da3c265002839e92af0a80d6db88385c313c73f103dfb800c857/pybase64-1.4.3-cp311-cp311-win32.whl", hash = "sha256:e9a8b81984e3c6fb1db9e1614341b0a2d98c0033d693d90c726677db1ffa3a4c", size = 33639, upload-time = "2025-12-06T13:23:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/39/dc/32efdf2f5927e5449cc341c266a1bbc5fecd5319a8807d9c5405f76e6d02/pybase64-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:a90a8fa16a901fabf20de824d7acce07586e6127dc2333f1de05f73b1f848319", size = 35797, upload-time = "2025-12-06T13:23:13.174Z" }, + { url = "https://files.pythonhosted.org/packages/da/59/eda4f9cb0cbce5a45f0cd06131e710674f8123a4d570772c5b9694f88559/pybase64-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:61d87de5bc94d143622e94390ec3e11b9c1d4644fe9be3a81068ab0f91056f59", size = 31160, upload-time = "2025-12-06T13:23:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158, upload-time = "2025-12-06T13:23:46.872Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672, upload-time = "2025-12-06T13:23:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" }, + { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425, upload-time = "2025-12-06T13:23:52.732Z" }, + { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327, upload-time = "2025-12-06T13:23:53.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294, upload-time = "2025-12-06T13:23:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858, upload-time = "2025-12-06T13:23:56.031Z" }, + { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629, upload-time = "2025-12-06T13:23:57.12Z" }, + { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448, upload-time = "2025-12-06T13:23:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841, upload-time = "2025-12-06T13:23:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910, upload-time = "2025-12-06T13:24:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335, upload-time = "2025-12-06T13:24:02.046Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486, upload-time = "2025-12-06T13:24:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978, upload-time = "2025-12-06T13:24:04.221Z" }, + { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684, upload-time = "2025-12-06T13:24:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832, upload-time = "2025-12-06T13:24:06.35Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175, upload-time = "2025-12-06T13:24:07.419Z" }, + { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497, upload-time = "2025-12-06T13:24:08.873Z" }, + { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076, upload-time = "2025-12-06T13:24:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317, upload-time = "2025-12-06T13:24:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534, upload-time = "2025-12-06T13:24:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399, upload-time = "2025-12-06T13:24:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487, upload-time = "2025-12-06T13:24:15.177Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959, upload-time = "2025-12-06T13:24:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874, upload-time = "2025-12-06T13:24:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572, upload-time = "2025-12-06T13:24:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636, upload-time = "2025-12-06T13:24:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193, upload-time = "2025-12-06T13:24:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655, upload-time = "2025-12-06T13:24:22.673Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471, upload-time = "2025-12-06T13:24:23.8Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119, upload-time = "2025-12-06T13:24:24.994Z" }, + { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791, upload-time = "2025-12-06T13:24:26.046Z" }, + { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701, upload-time = "2025-12-06T13:24:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, + { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" }, + { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" }, + { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" }, + { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/545fd4935a0e1ddd7147f557bf8157c73eecec9cffd523382fa7af2557de/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_10_9_x86_64.whl", hash = "sha256:d27c1dfdb0c59a5e758e7a98bd78eaca5983c22f4a811a36f4f980d245df4611", size = 38393, upload-time = "2025-12-06T13:26:19.535Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/ae7a96be9ddc96030d4e9dffc43635d4e136b12058b387fd47eb8301b60f/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0f1a0c51d6f159511e3431b73c25db31095ee36c394e26a4349e067c62f434e5", size = 32109, upload-time = "2025-12-06T13:26:20.72Z" }, + { url = "https://files.pythonhosted.org/packages/bf/44/d4b7adc7bf4fd5b52d8d099121760c450a52c390223806b873f0b6a2d551/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a492518f3078a4e3faaef310697d21df9c6bc71908cebc8c2f6fbfa16d7d6b1f", size = 43227, upload-time = "2025-12-06T13:26:21.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/86/2ba2d8734ef7939debeb52cf9952e457ba7aa226cae5c0e6dd631f9b851f/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae1a0f47784fd16df90d8acc32011c8d5fcdd9ab392c9ec49543e5f6a9c43a4", size = 35804, upload-time = "2025-12-06T13:26:23.149Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5b/19c725dc3aaa6281f2ce3ea4c1628d154a40dd99657d1381995f8096768b/pybase64-1.4.3-graalpy311-graalpy242_311_native-win_amd64.whl", hash = "sha256:03cea70676ffbd39a1ab7930a2d24c625b416cacc9d401599b1d29415a43ab6a", size = 35880, upload-time = "2025-12-06T13:26:24.663Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/cf/6e712491bd665ea8633efb0b484121893ea838d8e830e06f39f2aae37e58/pybase64-1.4.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94cf50c36bb2f8618982ee5a978c4beed9db97d35944fa96e8586dd953c7994a", size = 38007, upload-time = "2025-12-06T13:26:32.804Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/9272cae1c49176337dcdbd97511e2843faae1aaf5a5fb48569093c6cd4ce/pybase64-1.4.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:01bc3ff5ca1341685c6d2d945b035f442f7b9c3b068a5c6ee8408a41fda5754e", size = 31538, upload-time = "2025-12-06T13:26:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/20/f2/17546f97befe429c73f622bbd869ceebb518c40fdb0dec4c4f98312e80a5/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:03d0aa3761a99034960496280c02aa063f856a3cc9b33771bc4eab0e4e72b5c2", size = 40682, upload-time = "2025-12-06T13:26:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/92/a0/464b36d5dfb61f3da17858afaeaa876a9342d58e9f17803ce7f28b5de9e8/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ca5b1ce768520acd6440280cdab35235b27ad2faacfcec064bc9c3377066ef1", size = 41306, upload-time = "2025-12-06T13:26:36.351Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/a748dfc0969a8d960ecf1e82c8a2a16046ffec22f8e7ece582aa3b1c6cf9/pybase64-1.4.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3caa1e2ddad1c50553ffaaa1c86b74b3f9fbd505bea9970326ab88fc68c4c184", size = 35452, upload-time = "2025-12-06T13:26:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/4d37bd3577d1aa6c732dc099087fe027c48873e223de3784b095e5653f8b/pybase64-1.4.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bd47076f736b27a8b0f9b30d93b6bb4f5af01b0dc8971f883ed3b75934f39a99", size = 36125, upload-time = "2025-12-06T13:26:39.78Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/160dded493c00d3376d4ad0f38a2119c5345de4a6693419ad39c3565959b/pybase64-1.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:277de6e03cc9090fb359365c686a2a3036d23aee6cd20d45d22b8c89d1247f17", size = 37939, upload-time = "2025-12-06T13:26:41.014Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/a0f10be8d648d6f8f26e560d6e6955efa7df0ff1e009155717454d76f601/pybase64-1.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab1dd8b1ed2d1d750260ed58ab40defaa5ba83f76a30e18b9ebd5646f6247ae5", size = 31466, upload-time = "2025-12-06T13:26:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/22/832a2f9e76cdf39b52e01e40d8feeb6a04cf105494f2c3e3126d0149717f/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bd4d2293de9fd212e294c136cec85892460b17d24e8c18a6ba18750928037750", size = 40681, upload-time = "2025-12-06T13:26:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/12/d7/6610f34a8972415fab3bb4704c174a1cc477bffbc3c36e526428d0f3957d/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af6d0d3a691911cc4c9a625f3ddcd3af720738c21be3d5c72de05629139d393", size = 41294, upload-time = "2025-12-06T13:26:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/64/25/ed24400948a6c974ab1374a233cb7e8af0a5373cea0dd8a944627d17c34a/pybase64-1.4.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfc8c49a28322d82242088378f8542ce97459866ba73150b062a7073e82629d", size = 35447, upload-time = "2025-12-06T13:26:46.098Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" }, +] + +[[package]] +name = "pybind11" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/7b/a6d8dcb83c457e24a9df1e4d8fd5fb8034d4bbc62f3c324681e8a9ba57c2/pybind11-3.0.1.tar.gz", hash = "sha256:9c0f40056a016da59bab516efb523089139fcc6f2ba7e4930854c61efb932051", size = 546914, upload-time = "2025-08-22T20:09:27.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/8a/37362fc2b949d5f733a8b0f2ff51ba423914cabefe69f1d1b6aab710f5fe/pybind11-3.0.1-py3-none-any.whl", hash = "sha256:aa8f0aa6e0a94d3b64adfc38f560f33f15e589be2175e103c0a33c6bce55ee89", size = 293611, upload-time = "2025-08-22T20:09:25.235Z" }, +] + +[[package]] +name = "pycocotools" +version = "2.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/df/32354b5dda963ffdfc8f75c9acf8828ef7890723a4ed57bb3ff2dc1d6f7e/pycocotools-2.0.11.tar.gz", hash = "sha256:34254d76da85576fcaf5c1f3aa9aae16b8cb15418334ba4283b800796bd1993d", size = 25381, upload-time = "2025-12-15T22:31:46.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/4b/0c040fcda2c4fa4827b1a64e3185d99d5f954e45cc9463ba7385a1173a77/pycocotools-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:484d33515353186aadba9e2a290d81b107275cdb9565084e31a5568a52a0b120", size = 160351, upload-time = "2025-12-15T22:30:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/fe/861db6515824815eaabce27734653a6b100ddb22364b3345dd862b2c5b65/pycocotools-2.0.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca9f120f719ec405ad0c74ccfdb8402b0c37bd5f88ab5b6482a0de2efd5a36f4", size = 463947, upload-time = "2025-12-15T22:30:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a1/b4b49b85763043372e66baa10dffa42337cf4687d6db22546c27f3a4d732/pycocotools-2.0.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e40a3a898c6e5340b8d70cf7984868b9bff8c3d80187de9a3b661d504d665978", size = 472455, upload-time = "2025-12-15T22:30:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/fac670296e6a2b45eb7434d0480b9af6cb85a8de4f4848b49b01154bc859/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4cdfd2c676f30838aa0b1047441892fb4f97d70bf3df480bcc7a18a64d7d4", size = 457911, upload-time = "2025-12-15T22:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/f5/6158de63354dfcb677c8da34a4d205cc532e3277338ab7e6dea1310ba8de/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08c79789fd79e801ae4ecfcfeec32b31e36254e7a2b4019af28c104975d5e730", size = 476472, upload-time = "2025-12-15T22:30:59.736Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/46d2a782cda19ba1beb7c431f417e1e478f0bf1273fa5fe5d10de7c18d76/pycocotools-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:f78cbb1a32d061fcad4bdba083de70a39a21c1c3d9235a3f77d8f007541ec5ef", size = 80165, upload-time = "2025-12-15T22:31:00.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5c/6bd945781bb04c2148929183d1d67b05ce07996313b0f87bb88c6a805493/pycocotools-2.0.11-cp310-cp310-win_arm64.whl", hash = "sha256:e21311ea71f85591680d8992858e2d44a2a156dc3b2bf1c5c901c4a19348177b", size = 69358, upload-time = "2025-12-15T22:31:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/41ce3fce61b7721158f21b61727eb054805babc0088cfa48506935b80a36/pycocotools-2.0.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81bdceebb4c64e9265213e2d733808a12f9c18dfb14457323cc6b9af07fa0e61", size = 158947, upload-time = "2025-12-15T22:31:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9b/a739705b246445bd1376394bf9d1ec2dd292b16740e92f203461b2bb12ed/pycocotools-2.0.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c05f91ccc658dfe01325267209c4b435da1722c93eeb5749fabc1d087b6882", size = 485174, upload-time = "2025-12-15T22:31:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/7a12752784e57d8034a76c245c618a2f88a9d2463862b990f314aea7e5d6/pycocotools-2.0.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18ba75ff58cedb33a85ce2c18f1452f1fe20c9dd59925eec5300b2bf6205dbe1", size = 493172, upload-time = "2025-12-15T22:31:05.504Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fc/d703599ac728209dba08aea8d4bee884d5adabfcd9041abed1658d863747/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:693417797f0377fd094eb815c0a1e7d1c3c0251b71e3b3779fce3b3cf24793c5", size = 480506, upload-time = "2025-12-15T22:31:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/d9/e1cfc320bbb2cd58c3b4398c3821cbe75d93c16ed3135ac9e774a18a02d3/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6a07071c441d0f5e480a8f287106191582e40289d4e242dfe684e0c8a751088", size = 497595, upload-time = "2025-12-15T22:31:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/d17f6111c2a6ae8631d4fa90202bea05844da715d61431fbc34d276462d5/pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34", size = 80519, upload-time = "2025-12-15T22:31:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/76b00b31a724c3f5ccdab0f85e578afb2ca38d33be0a0e98f1770cafd958/pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec", size = 69304, upload-time = "2025-12-15T22:31:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/87/12/2f2292332456e4e4aba1dec0e3de8f1fc40fb2f4fdb0ca1cb17db9861682/pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4", size = 147795, upload-time = "2025-12-15T22:31:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/31c81e99d596a20c137d8a2e7a25f39a88f88fada5e0b253fce7323ecf0d/pycocotools-2.0.11-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd72b9734e6084b217c1fc3945bfd4ec05bdc75a44e4f0c461a91442bb804973", size = 168931, upload-time = "2025-12-15T22:31:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/5f/63/fdd488e4cd0fdc6f93134f2cd68b1fce441d41566e86236bf6156961ef9b/pycocotools-2.0.11-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eb43b79448476b094240450420b7425d06e297880144b8ea6f01e9b4340e43", size = 484856, upload-time = "2025-12-15T22:31:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fc/c83648a8fb7ea3b8e2ce2e761b469807e6cadb81577bf1af31c4f2ef0d87/pycocotools-2.0.11-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3546b93b39943347c4f5b0694b5824105cbe2174098a416bcad4acd9c21e957", size = 480994, upload-time = "2025-12-15T22:31:22.426Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/35e1122c0d007288aa9545be9549cbc7a4987b2c22f21d75045260a8b5b8/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:efd1694b2075f2f10c5828f10f6e6c4e44368841fd07dae385c3aa015c8e25f9", size = 467956, upload-time = "2025-12-15T22:31:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/30cfe8142470da3e45abe43a9842449ca0180d993320559890e2be19e4a5/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:368244f30eb8d6cae7003aa2c0831fbdf0153664a32859ec7fbceea52bfb6878", size = 474658, upload-time = "2025-12-15T22:31:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/254ca92604106c7a5af3258e589e465e681fe0166f9b10f97d8ca70934d6/pycocotools-2.0.11-cp313-cp313t-win_amd64.whl", hash = "sha256:ac8aa17263e6489aa521f9fa91e959dfe0ea3a5519fde2cbf547312cdce7559e", size = 89681, upload-time = "2025-12-15T22:31:26.025Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/c019314dc122ad5e6281de420adc105abe9b59d00008f72ef3ad32b1e328/pycocotools-2.0.11-cp313-cp313t-win_arm64.whl", hash = "sha256:04480330df5013f6edd94891a0ee8294274185f1b5093d1b0f23d51778f0c0e9", size = 70520, upload-time = "2025-12-15T22:31:26.999Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/58b35c88f2086c043ff1c87bd8e7bf36f94e84f7b01a5e00b6f5fabb92a7/pycocotools-2.0.11-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a6b13baf6bfcf881b6d6ac6e23c776f87a68304cd86e53d1d6b9afa31e363c4e", size = 169883, upload-time = "2025-12-15T22:31:28.233Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/b970eefb78746c8b4f8b3fa1b49d9f3ec4c5429ef3c5d4bbcc55abebe478/pycocotools-2.0.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78bae4a9de9d34c4759754a848dfb3306f9ef1c2fcb12164ffbd3d013d008321", size = 486894, upload-time = "2025-12-15T22:31:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f7/db7436820a1948d96fa9764b6026103e808840979be01246049f2c1e7f94/pycocotools-2.0.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d896f4310379849dfcfa7893afb0ff21f4f3cdb04ab3f61b05dd98953dd0ad", size = 483249, upload-time = "2025-12-15T22:31:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a6/a14a12c9f50c41998fdc0d31fd3755bcbce124bac9abb1d6b99d1853cafd/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eebd723503a2eb2c8b285f56ea3be1d9f3875cd7c40d945358a428db94f14015", size = 469070, upload-time = "2025-12-15T22:31:32.821Z" }, + { url = "https://files.pythonhosted.org/packages/46/de/aa4f65ece3da8e89310a1be00cad0700170fd13f41a3aaae2712291269d5/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bd7a1e19ef56a828a94bace673372071d334a9232cd32ae3cd48845a04d45c4f", size = 475589, upload-time = "2025-12-15T22:31:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/04a30df03ae6236b369b361df0c50531d173d03678978806aa2182e02d1e/pycocotools-2.0.11-cp314-cp314t-win_amd64.whl", hash = "sha256:63026e11a56211058d0e84e8263f74cbccd5e786fac18d83fd221ecb9819fcc7", size = 93863, upload-time = "2025-12-15T22:31:35.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/05/8942b640d6307a21c3ede188e8c56f07bedf246fac0e501437dbda72a350/pycocotools-2.0.11-cp314-cp314t-win_arm64.whl", hash = "sha256:8cedb8ccb97ffe9ed2c8c259234fa69f4f1e8665afe3a02caf93f6ef2952c07f", size = 72038, upload-time = "2025-12-15T22:31:36.768Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pydot" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6", size = 37087, upload-time = "2025-06-17T20:09:55.25Z" }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, +] + +[[package]] +name = "pyee" +version = "13.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, +] + +[[package]] +name = "pygame" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f", size = 14808125, upload-time = "2024-09-29T13:41:34.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/0b/334c7c50a2979e15f2a027a41d1ca78ee730d5b1c7f7f4b26d7cb899839d/pygame-2.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9beeb647e555afb5657111fa83acb74b99ad88761108eaea66472e8b8547b55b", size = 13109297, upload-time = "2024-09-29T14:25:34.709Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/f8b1069788d1bd42e63a960d74d3355242480b750173a42b2749687578ca/pygame-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10e3d2a55f001f6c0a6eb44aa79ea7607091c9352b946692acedb2ac1482f1c9", size = 12375837, upload-time = "2024-09-29T14:25:50.538Z" }, + { url = "https://files.pythonhosted.org/packages/bc/33/a1310386b8913ce1bdb90c33fa536970e299ad57eb35785f1d71ea1e2ad3/pygame-2.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816e85000c5d8b02a42b9834f761a5925ef3377d2924e3a7c4c143d2990ce5b8", size = 13607860, upload-time = "2024-09-29T11:10:44.173Z" }, + { url = "https://files.pythonhosted.org/packages/88/0f/4e37b115056e43714e7550054dd3cd7f4d552da54d7fc58a2fb1407acda5/pygame-2.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a78fd030d98faab4a8e27878536fdff7518d3e062a72761c552f624ebba5a5f", size = 14304696, upload-time = "2024-09-29T11:39:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/11/b3/de6ed93ae483cf3bac8f950a955e83f7ffe59651fd804d100fff65d66d6c/pygame-2.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3ad64d685f84a34ebe5daacb39fff14f1251acb34c098d760d63fee768f50c", size = 13977684, upload-time = "2024-09-29T11:39:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/d86440aa879708c41844bafc6b3eb42c6d8cf54082482499b53139133e2a/pygame-2.6.1-cp310-cp310-win32.whl", hash = "sha256:9dd5c054d4bd875a8caf978b82672f02bec332f52a833a76899220c460bb4b58", size = 10251775, upload-time = "2024-09-29T11:40:34.952Z" }, + { url = "https://files.pythonhosted.org/packages/38/88/8de61324775cf2c844a51d8db14a8a6d2a9092312f27678f6eaa3a460376/pygame-2.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:00827aba089355925902d533f9c41e79a799641f03746c50a374dc5c3362e43d", size = 10618801, upload-time = "2024-09-29T12:13:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ca/8f367cb9fe734c4f6f6400e045593beea2635cd736158f9fabf58ee14e3c/pygame-2.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1", size = 13113753, upload-time = "2024-09-29T14:26:13.751Z" }, + { url = "https://files.pythonhosted.org/packages/83/47/6edf2f890139616b3219be9cfcc8f0cb8f42eb15efd59597927e390538cb/pygame-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21", size = 12378146, upload-time = "2024-09-29T14:26:22.456Z" }, + { url = "https://files.pythonhosted.org/packages/00/9e/0d8aa8cf93db2d2ee38ebaf1c7b61d0df36ded27eb726221719c150c673d/pygame-2.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856", size = 13611760, upload-time = "2024-09-29T11:10:47.317Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9e/d06adaa5cc65876bcd7a24f59f67e07f7e4194e6298130024ed3fb22c456/pygame-2.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1", size = 14298054, upload-time = "2024-09-29T11:39:53.891Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a1/9ae2852ebd3a7cc7d9ae7ff7919ab983e4a5c1b7a14e840732f23b2b48f6/pygame-2.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60", size = 13977107, upload-time = "2024-09-29T11:39:56.831Z" }, + { url = "https://files.pythonhosted.org/packages/31/df/6788fd2e9a864d0496a77670e44a7c012184b7a5382866ab0e60c55c0f28/pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c", size = 10250863, upload-time = "2024-09-29T11:44:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/d2/55/ca3eb851aeef4f6f2e98a360c201f0d00bd1ba2eb98e2c7850d80aabc526/pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299", size = 10622016, upload-time = "2024-09-29T12:17:01.545Z" }, + { url = "https://files.pythonhosted.org/packages/92/16/2c602c332f45ff9526d61f6bd764db5096ff9035433e2172e2d2cadae8db/pygame-2.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e", size = 13118279, upload-time = "2024-09-29T14:26:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/cd/53/77ccbc384b251c6e34bfd2e734c638233922449a7844e3c7a11ef91cee39/pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf", size = 12384524, upload-time = "2024-09-29T14:26:49.996Z" }, + { url = "https://files.pythonhosted.org/packages/06/be/3ed337583f010696c3b3435e89a74fb29d0c74d0931e8f33c0a4246307a9/pygame-2.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116", size = 13587123, upload-time = "2024-09-29T11:10:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/b015586a450db59313535662991b34d24c1f0c0dc149cc5f496573900f4e/pygame-2.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d", size = 14275532, upload-time = "2024-09-29T11:39:59.356Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f2/d31e6ad42d657af07be2ffd779190353f759a07b51232b9e1d724f2cda46/pygame-2.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88", size = 13952653, upload-time = "2024-09-29T11:40:01.781Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/8ea2a6979e6fa971702fece1747e862e2256d4a8558fe0da6364dd946c53/pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e", size = 10252421, upload-time = "2024-09-29T11:14:26.877Z" }, + { url = "https://files.pythonhosted.org/packages/5f/90/7d766d54bb95939725e9a9361f9c06b0cfbe3fe100aa35400f0a461a278a/pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65", size = 10624591, upload-time = "2024-09-29T11:52:54.489Z" }, + { url = "https://files.pythonhosted.org/packages/e1/91/718acf3e2a9d08a6ddcc96bd02a6f63c99ee7ba14afeaff2a51c987df0b9/pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2", size = 13090765, upload-time = "2024-09-29T14:27:02.377Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c6/9cb315de851a7682d9c7568a41ea042ee98d668cb8deadc1dafcab6116f0/pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171", size = 12381704, upload-time = "2024-09-29T14:27:10.228Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8f/617a1196e31ae3b46be6949fbaa95b8c93ce15e0544266198c2266cc1b4d/pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b", size = 13581091, upload-time = "2024-09-29T11:30:27.653Z" }, + { url = "https://files.pythonhosted.org/packages/3b/87/2851a564e40a2dad353f1c6e143465d445dab18a95281f9ea458b94f3608/pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b", size = 14273844, upload-time = "2024-09-29T11:40:04.138Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/aa23aa2e70bcba42c989c02e7228273c30f3b44b9b264abb93eaeff43ad7/pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c", size = 13951197, upload-time = "2024-09-29T11:40:06.785Z" }, + { url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309, upload-time = "2024-09-29T11:10:23.329Z" }, + { url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084, upload-time = "2024-09-29T11:48:51.587Z" }, +] + +[[package]] +name = "pyglet" +version = "2.1.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/6b/84c397a74cd33eb377168c682e9e3d6b90c1c10c661e11ea5b397ac8497c/pyglet-2.1.11.tar.gz", hash = "sha256:8285d0af7d0ab443232a81df4d941e0d5c48c18a23ec770b3e5c59a222f5d56e", size = 6594448, upload-time = "2025-11-07T04:29:52.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/a2/2b09fbff0eedbe44fbf164b321439a38f7c5568d8b754aa197ee45886431/pyglet-2.1.11-py3-none-any.whl", hash = "sha256:fa0f4fdf366cfc5040aeb462416910b0db2fa374b7d620b7a432178ca3fa8af1", size = 1032213, upload-time = "2025-11-07T04:29:46.06Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pylibsrtp" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/a6/6e532bec974aaecbf9fe4e12538489fb1c28456e65088a50f305aeab9f89/pylibsrtp-1.0.0.tar.gz", hash = "sha256:b39dff075b263a8ded5377f2490c60d2af452c9f06c4d061c7a2b640612b34d4", size = 10858, upload-time = "2025-10-13T16:12:31.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/af/89e61a62fa3567f1b7883feb4d19e19564066c2fcd41c37e08d317b51881/pylibsrtp-1.0.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:822c30ea9e759b333dc1f56ceac778707c51546e97eb874de98d7d378c000122", size = 1865017, upload-time = "2025-10-13T16:12:15.62Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0e/8d215484a9877adcf2459a8b28165fc89668b034565277fd55d666edd247/pylibsrtp-1.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aaad74e5c8cbc1c32056c3767fea494c1e62b3aea2c908eda2a1051389fdad76", size = 2182739, upload-time = "2025-10-13T16:12:17.121Z" }, + { url = "https://files.pythonhosted.org/packages/57/3f/76a841978877ae13eac0d4af412c13bbd5d83b3df2c1f5f2175f2e0f68e5/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9209b86e662ebbd17c8a9e8549ba57eca92a3e87fb5ba8c0e27b8c43cd08a767", size = 2732922, upload-time = "2025-10-13T16:12:18.348Z" }, + { url = "https://files.pythonhosted.org/packages/0e/14/cf5d2a98a66fdfe258f6b036cda570f704a644fa861d7883a34bc359501e/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc", size = 2434534, upload-time = "2025-10-13T16:12:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/bd/08/a3f6e86c04562f7dce6717cd2206a0f84ca85c5e38121d998e0e330194c3/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_28_i686.whl", hash = "sha256:81fb8879c2e522021a7cbd3f4bda1b37c192e1af939dfda3ff95b4723b329663", size = 2345818, upload-time = "2025-10-13T16:12:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d5/130c2b5b4b51df5631684069c6f0a6761c59d096a33d21503ac207cf0e47/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4ddb562e443cf2e557ea2dfaeef0d7e6b90e96dd38eb079b4ab2c8e34a79f50b", size = 2774490, upload-time = "2025-10-13T16:12:22.659Z" }, + { url = "https://files.pythonhosted.org/packages/91/e3/715a453bfee3bea92a243888ad359094a7727cc6d393f21281320fe7798c/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f02e616c9dfab2b03b32d8cc7b748f9d91814c0211086f987629a60f05f6e2cc", size = 2372603, upload-time = "2025-10-13T16:12:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/e3/56/52fa74294254e1f53a4ff170ee2006e57886cf4bb3db46a02b4f09e1d99f/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c134fa09e7b80a5b7fed626230c5bc257fd771bd6978e754343e7a61d96bc7e6", size = 2451269, upload-time = "2025-10-13T16:12:25.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/51/2e9b34f484cbdd3bac999bf1f48b696d7389433e900639089e8fc4e0da0d/pylibsrtp-1.0.0-cp310-abi3-win32.whl", hash = "sha256:bae377c3b402b17b9bbfbfe2534c2edba17aa13bea4c64ce440caacbe0858b55", size = 1247503, upload-time = "2025-10-13T16:12:27.39Z" }, + { url = "https://files.pythonhosted.org/packages/c3/70/43db21af194580aba2d9a6d4c7bd8c1a6e887fa52cd810b88f89096ecad2/pylibsrtp-1.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:8d6527c4a78a39a8d397f8862a8b7cdad4701ee866faf9de4ab8c70be61fd34d", size = 1601659, upload-time = "2025-10-13T16:12:29.037Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, +] + +[[package]] +name = "pymavlink" +version = "2.4.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastcrc" }, + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a2/0a4ce323178f60f869e42a2ed3844bead7b685807674bef966a39661606e/pymavlink-2.4.49.tar.gz", hash = "sha256:d7cf10d5592d038a18aa972711177ebb88be2143efcc258df630b0513e9da2c2", size = 6172115, upload-time = "2025-08-01T23:33:10.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/ab/f41e116c3753398fa773acfcd576bccceff4d1da2534ec0bfbcbf0433337/pymavlink-2.4.49-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7c92b066ff05587dbdbc517f20eb7f98f2aa13916223fcd6daed54a8d1b7bc5", size = 6289986, upload-time = "2025-08-01T23:31:28.865Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/d61d3454884048227307f638bc91efd06832c0f273791d3b445e12d3789f/pymavlink-2.4.49-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be3607975eb7392b89a847c6810a4b5e2e3ea6935c7875dfb1289de39ccb4aea", size = 6225493, upload-time = "2025-08-01T23:31:30.773Z" }, + { url = "https://files.pythonhosted.org/packages/66/4a/fbf69e38bdd7e0b4803cb61c7ddfc9c9cc5b501843b5c822527e072192a1/pymavlink-2.4.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29943b071e75c4caa87fb7b1e15cdcde712d31dbd1c1babac6b45264ba1a6c97", size = 6222403, upload-time = "2025-08-01T23:31:32.804Z" }, + { url = "https://files.pythonhosted.org/packages/21/6d/a326e64e59ad7b54ca1e7b26e54ce13da7a2237865f6c446a9d442d9ffcb/pymavlink-2.4.49-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:55f0de705a985dc47b384a3aae1b1425217614c604b6defffac9f247c98c99af", size = 6336320, upload-time = "2025-08-01T23:31:34.348Z" }, + { url = "https://files.pythonhosted.org/packages/b1/26/e73f67f0a21564b68cae454b49b6536e8139bcff5fff629291a656951920/pymavlink-2.4.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e03ae4c4a9d3cd261a09c1dcc3b83e4d0493c9d4a8a438219f7133b1bd6d74a", size = 6348490, upload-time = "2025-08-01T23:31:35.866Z" }, + { url = "https://files.pythonhosted.org/packages/7f/60/74b123aca08a005500f2d7bddd76add7be34d62e15792c104674010cb444/pymavlink-2.4.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86ffb6df2e29cb5365a2ec875ec903e4cbd516523d43eaacec2e9fca97567e0d", size = 6348133, upload-time = "2025-08-01T23:31:37.119Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e1/c6c8554a7e2b384672353345fbff06a066a2eb238e814978d0f414e804ce/pymavlink-2.4.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a74d5fc8a825d2ffe48921f5d10a053408d44608012fb19161747fa28c8b5383", size = 6341961, upload-time = "2025-08-01T23:31:40.139Z" }, + { url = "https://files.pythonhosted.org/packages/04/b9/0634eb528d57892a81a4bed81917f80fc5f60df0a14112be2045b0365a3c/pymavlink-2.4.49-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cdf11ac8ac1158fc9428cd1a2b5128b504549920b28d5dbbb052310179f501d6", size = 6339292, upload-time = "2025-08-01T23:31:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/6d/12/834979e873d65332ec5a25be49245042b3bbd8b0e1ad093fcb90328b23f5/pymavlink-2.4.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b30bfb76ff520508297836b8d9c11787c33897cee2099188e07c54b0678db6d5", size = 6346117, upload-time = "2025-08-01T23:31:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4e/336df7081a8303036bab84cb96f520218fe2f117df77c8deea6c31d7f682/pymavlink-2.4.49-cp310-cp310-win32.whl", hash = "sha256:4269be350ecb674e90df91539aded072b2ff7153c2cf4b9fdadd9e49cd67d040", size = 6230571, upload-time = "2025-08-01T23:31:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/cb/17/873c51e5966318c61ceee6c1e4631be795f3ec70e824569ba1433da67d2f/pymavlink-2.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:dcf50ea5da306025dd5ddd45ed603e5f8a06c292dd22a143c9ff4292627ca338", size = 6241529, upload-time = "2025-08-01T23:31:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/06/6503887e624b09a02d0a1203a4cedb979ab42a8fa42432760c5ba836c622/pymavlink-2.4.49-cp310-cp310-win_arm64.whl", hash = "sha256:5e4a91f6dabe4c7087ad3a6564c00992b38a336be021f093a01910efbbe2efb2", size = 6231870, upload-time = "2025-08-01T23:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/a7/44/2cf9031edf12949b400e3e1db8f29e55f91f04c2ed31e8bf36e0c6be78f7/pymavlink-2.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6d1c16ee2b913cbd9768709bb9d6716320b317f632cd588fa112e45309c62a33", size = 6289780, upload-time = "2025-08-01T23:31:49.072Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ee/2d5843485a072d0b1f6f37461ce6144c739f976e65f972082b0299dc233a/pymavlink-2.4.49-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85155f395a238764eab374edaff901cfe24ecc884c0bc1ed94818d89ab30c6b4", size = 6225400, upload-time = "2025-08-01T23:31:50.559Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f3/01d67e77aa776928ff89d35de5a2a2039489a0b77a0ad8c2b1ccb4dceb9e/pymavlink-2.4.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf1d869a6730f9cce98779258b203ebd084ba905b34c2dc0e0507789571903c0", size = 6222298, upload-time = "2025-08-01T23:31:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/4b/de/e4f25fee7948652ea9cb61500256d800bb7635d44258b4e85a8900ff4228/pymavlink-2.4.49-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a1867635ada32078bca118dd2b521bec71276a7a5d08e6afb0077cab409cd14", size = 6359387, upload-time = "2025-08-01T23:31:53.042Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4e/2b2fbadf4f9e941fcf2141c9499c444cd003b8bb6a1ff0a52a1a4f37929f/pymavlink-2.4.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31b77118c7a3fa6adb9ec3e2401d26c4f2988e9c5a0b37f5a96526a4cecb81aa", size = 6368235, upload-time = "2025-08-01T23:31:54.294Z" }, + { url = "https://files.pythonhosted.org/packages/50/23/c6c9b75009433fcaa3ba40115b7ade2e0c9206826f916d1b15c7bfa7ae17/pymavlink-2.4.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58e954576b0323535336d347a7bb1c96794e1f2a966afd48e2fd304c5d40ab65", size = 6367486, upload-time = "2025-08-01T23:31:55.558Z" }, + { url = "https://files.pythonhosted.org/packages/89/3d/27695922636033890b1f2ff2a5c05d4ba413dbfc4c120de2f4768e9efc40/pymavlink-2.4.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:86d9c9dc261f1f7b785c45b6db002f6a9738ec8923065f83a6065fc0585a116e", size = 6360966, upload-time = "2025-08-01T23:31:56.865Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/c04174858b9ab5534bafab6a1b61046bea7fcd7036afebba74c543083eab/pymavlink-2.4.49-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:813f59d4942d5c635369869436440124e10fed5ee97c85dab146d081acc04763", size = 6362035, upload-time = "2025-08-01T23:31:58.378Z" }, + { url = "https://files.pythonhosted.org/packages/60/60/4f121e717dd627f37554e88e7435fe21edbb79ce17ff4f3c1bc4bbc51ff3/pymavlink-2.4.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a99736ed9e42935f2d9e0cba1c315320d77ed8fb2153c4dbf8778a521101ddf", size = 6364653, upload-time = "2025-08-01T23:31:59.561Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f1/d6a74f1fa88307d0feb8812bf09d193dbb7819d32fca031086dfcbf6bf63/pymavlink-2.4.49-cp311-cp311-win32.whl", hash = "sha256:5e14316b7bc02b93d509aa719ae6600bbc8f8fc4a8b62062d129089e5c07fb62", size = 6230360, upload-time = "2025-08-01T23:32:01.109Z" }, + { url = "https://files.pythonhosted.org/packages/32/72/2e145ed2f76852fe0dbf9db8d9cc0d7c802ed23cb75cbe1fd3a30ae19956/pymavlink-2.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:c5702142ad5727fce926c54233016e076fb4288cd8211954cc9efdc523f9714a", size = 6241353, upload-time = "2025-08-01T23:32:02.346Z" }, + { url = "https://files.pythonhosted.org/packages/66/46/c8eb26b1ef82378fc30ea0c6b128422f0a69e1ec0e8e0feeae30bd28028b/pymavlink-2.4.49-cp311-cp311-win_arm64.whl", hash = "sha256:e69036e0556a688aeb6a4a5acb4737bbf275713090f6839dda36db4cabbb676b", size = 6231600, upload-time = "2025-08-01T23:32:03.647Z" }, + { url = "https://files.pythonhosted.org/packages/e8/81/062427da96311d359ed799af8569b7b3ffa25c333fb4a961478ce5a4735f/pymavlink-2.4.49-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b7925330a4bb30bcc732971cfeb1aa54515efd28f4588d7abc942967d7a2298b", size = 6291309, upload-time = "2025-08-01T23:32:04.972Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/8bfed758e2efc2d6f28259634d94c09b10e50c62cd5914ac888ce268378d/pymavlink-2.4.49-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf4c13703bc6dcbc70083a12aaec71f3a36a6b607290e93f59f2b004ebd02784", size = 6226353, upload-time = "2025-08-01T23:32:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/78419c2ae5489fdd996f6af0c1e4bd6dceaa5a5b155a367851926da7b05f/pymavlink-2.4.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8522d652fef8fb03c7ee50abd2686ffe0262cbec06136ae230f3a88cccdff21c", size = 6222943, upload-time = "2025-08-01T23:32:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/c9ae05436ed219bb9f2b505d7c82474173c8ebcd28ff8f55833213d732a2/pymavlink-2.4.49-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1b6e29972792a1da3fafde911355631b8a62735297a2f3c5209aa985919917a", size = 6376049, upload-time = "2025-08-01T23:32:08.949Z" }, + { url = "https://files.pythonhosted.org/packages/36/6f/eb93cc44e2653044eb5bbfa7ce0f808611e42d56106a4d6d5de4db8bb211/pymavlink-2.4.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8b13b9ac195e9fa8f01cda21638e26af9c5a90e3475ddb43fd2b9e396913f6b", size = 6388174, upload-time = "2025-08-01T23:32:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/34099ab9e4e41db4b2ec9f05c3d8e7726ef3d5a2ae8cfb6f90596c4d82fb/pymavlink-2.4.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a87b508a9e9215afdb809189224731b4b34153f3879226fd94b8f485ac626ab", size = 6390472, upload-time = "2025-08-01T23:32:11.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/51/0146c0008feb5d8a7721870489b4c19fd30a1e49433be7a83624dc961f90/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31ca1c1e60a21f240abf35258df30e7b5ee954a055bbe7584f0ebabb48dd8c40", size = 6376189, upload-time = "2025-08-01T23:32:12.921Z" }, + { url = "https://files.pythonhosted.org/packages/c4/51/aa4b51cd9948eca7b63359ad392d8cd69b393bd781830c4a518a98aede33/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f854d1d730f40d4efa52d8901413af1b23d16187e941b76d55f0dcc0208d641d", size = 6378697, upload-time = "2025-08-01T23:32:14.471Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b6/dec8f9f7e1769894b7b11c8900b0a13cf13fb9cee2c45d7f9f5a785b3f39/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16c915365a21b7734c794ba97fa804ae6db52411bf62d21b877a51df2183dfab", size = 6384644, upload-time = "2025-08-01T23:32:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/3db53612dab0bfa31eca8e9162489f44c9f9e23c183a2b263d707eb5ddc7/pymavlink-2.4.49-cp312-cp312-win32.whl", hash = "sha256:af7e84aec82f00fd574c2a0dbe11fb1a4c3cbf26f294ca0ef3807dcc5670567e", size = 6230813, upload-time = "2025-08-01T23:32:17.732Z" }, + { url = "https://files.pythonhosted.org/packages/bf/47/fe857933a464b5a07bf72e2a1d2e92a87ad9d96915f48f86c9475333a63d/pymavlink-2.4.49-cp312-cp312-win_amd64.whl", hash = "sha256:246e227ca8535de98a4b93876a14b9ced7bfc82c70458e480725a715aa6b6bf3", size = 6242451, upload-time = "2025-08-01T23:32:19.063Z" }, + { url = "https://files.pythonhosted.org/packages/25/ca/995d1201925ad49fb6b174a9d488f1d90b77256b1088ebd3d7f192b0f65a/pymavlink-2.4.49-cp312-cp312-win_arm64.whl", hash = "sha256:c7415592166d9cbd4434775828b00c71bebf292c8367744d861e3ccd2dab9f3e", size = 6231742, upload-time = "2025-08-01T23:32:20.707Z" }, + { url = "https://files.pythonhosted.org/packages/26/10/67756d987b1aefd991664ce0a996ee3bf69ed7aaf8c7319ff6012a4dc8a2/pymavlink-2.4.49-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f7cfaf3cc1abd611c0757d4b7e56eaf5b4cfa54510a3178b26ebbd9d3443b9d7", size = 6290269, upload-time = "2025-08-01T23:32:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/c2/02/9e63467d65da78fed03981c86e5b7877fcf163a98372ba5ef03015e3798c/pymavlink-2.4.49-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db9c0d00e79946ecf1ac89847f32712ef546994342f44b3e9a68e59cfbc85bef", size = 6225761, upload-time = "2025-08-01T23:32:23.419Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/46a5512964043ada02914657610c885b083375dd169dea172870f4dd73b0/pymavlink-2.4.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37937d5dfd2ddc2a64ea64687380278ac9c49e1644ea125f1e8a5caf4e1f2ebd", size = 6222450, upload-time = "2025-08-01T23:32:24.803Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/1b63a8c4d35887edc979805b324240ff4b847e9d912b323d71613e8f1971/pymavlink-2.4.49-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8100f2f1f53b094611531df2cfb25f1c8e8fdee01f095eb8ee18976994663cf6", size = 6368072, upload-time = "2025-08-01T23:32:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/29/69/94348757424a94c5a3e87f41d4c05a168bc5de2549afdbea1d4424a318dc/pymavlink-2.4.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2db4b88f38aa1ba4c0653a8c5938364bfe78a008e8d02627534015142bf774", size = 6379869, upload-time = "2025-08-01T23:32:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/91/a7/792925eadc046ae580ab444181a06e8d51d38204a81a9274460f90009b88/pymavlink-2.4.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7fe9286fd5b2db05277d30d1ea6b9b3a9ea010a99aff04d451705cc4be6a7e6", size = 6382786, upload-time = "2025-08-01T23:32:28.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/d7b1d280dda800ac386fd54dcded6344b518a8266a918729512e46e39f6b/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d49309e00d4d434f2e414c166b18ef18496987a13a613864f89a19ca190ef0d0", size = 6368732, upload-time = "2025-08-01T23:32:29.794Z" }, + { url = "https://files.pythonhosted.org/packages/23/89/b75ef8eea1e31ec07f13fe71883b08cdc2bce0c33418218cebb03e55124a/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7104eef554b01d6c180e1a532dc494c4b1d74e48b0b725328ec39f042982e172", size = 6370950, upload-time = "2025-08-01T23:32:31.041Z" }, + { url = "https://files.pythonhosted.org/packages/f6/57/3cb77e3f593e27dc63bd74357b3c3b57075af74771c4446275097f0865f2/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:795e6628f9ecf0b06e3b7b65f8fcf477ec1603971d590cffd4640cff1852da23", size = 6376423, upload-time = "2025-08-01T23:32:32.345Z" }, + { url = "https://files.pythonhosted.org/packages/41/bb/49b83c6d212751c88a29cebe413c940ee1d0b7991a667710689eb0cd648e/pymavlink-2.4.49-cp313-cp313-win32.whl", hash = "sha256:9f14bbe1ce3d5c0af4994f0f76d1a8d0c2f915d7dcb7645c1ecba42eeff89536", size = 6230635, upload-time = "2025-08-01T23:32:33.613Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c4/d3e9e414dd7ba0124ef07d33d9492cc01db1b76ae3cec45443ec4d6a7935/pymavlink-2.4.49-cp313-cp313-win_amd64.whl", hash = "sha256:9777a0375ebcda0efda3f4eae6d8d2e5ce6de8e26c2f0ac7be1a016d0d386b82", size = 6242260, upload-time = "2025-08-01T23:32:35.256Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/52616b4fdd076177f1ba22e6ef40782b48e14efb47fce2c3bd4f8496ec23/pymavlink-2.4.49-cp313-cp313-win_arm64.whl", hash = "sha256:712ee4240a9489c6dab6158882c7e1f37516c5951db5841cd408ad7b4c6db0d4", size = 6231575, upload-time = "2025-08-01T23:32:36.845Z" }, +] + +[[package]] +name = "pyopengl" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/1d/4544708aaa89f26c97cc09450bb333a23724a320923e74d73e028b3560f9/PyOpenGL-3.1.0.tar.gz", hash = "sha256:9b47c5c3a094fa518ca88aeed35ae75834d53e4285512c61879f67a48c94ddaf", size = 1172688, upload-time = "2014-06-26T14:51:25.571Z" } + +[[package]] +name = "pyopenssl" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, +] + +[[package]] +name = "pypika" +version = "0.48.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/2c/94ed7b91db81d61d7096ac8f2d325ec562fc75e35f3baea8749c85b28784/PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378", size = 67259, upload-time = "2022-03-15T11:22:57.066Z" } + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pyquaternion" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/3d092aa20efaedacb89c3221a92c6491be5b28f618a2c36b52b53e7446c2/pyquaternion-0.9.9.tar.gz", hash = "sha256:b1f61af219cb2fe966b5fb79a192124f2e63a3f7a777ac3cadf2957b1a81bea8", size = 15530, upload-time = "2020-10-05T01:31:30.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b3/d8482e8cacc8ea15a356efea13d22ce1c5914a9ee36622ba250523240bf2/pyquaternion-0.9.9-py3-none-any.whl", hash = "sha256:e65f6e3f7b1fdf1a9e23f82434334a1ae84f14223eee835190cd2e841f8172ec", size = 14361, upload-time = "2020-10-05T01:31:37.575Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, +] + +[[package]] +name = "pyrender" +version = "0.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "freetype-py" }, + { name = "imageio" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "pyglet" }, + { name = "pyopengl" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "six" }, + { name = "trimesh" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/5a/2a3e5bfd83071a81e02291288391e0fa2c85d1c6765357f4de2dbc27bca6/pyrender-0.1.45.tar.gz", hash = "sha256:284b2432bf6832f05c5216c4b979ceb514ea78163bf53b8ce2bdf0069cb3b92e", size = 1202386, upload-time = "2021-02-18T18:56:28.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/88/174c28b9d3d03cf6d8edb6f637458f30f1cf1a2bd7a617cbd9dadb1740f6/pyrender-0.1.45-py3-none-any.whl", hash = "sha256:5cf751d1f21fba4640e830cef3a0b5a95ed0f05677bf92c6b8330056b4023aeb", size = 1214061, upload-time = "2021-02-18T18:56:27.275Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, +] + +[[package]] +name = "pytest-env" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911, upload-time = "2024-09-17T22:39:18.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/99/3323ee5c16b3637b4d941c362182d3e749c11e400bea31018c42219f3a98/pytest_mock-3.15.0.tar.gz", hash = "sha256:ab896bd190316b9d5d87b277569dfcdf718b2d049a2ccff5f7aca279c002a1cf", size = 33838, upload-time = "2025-09-04T20:57:48.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/b3/7fefc43fb706380144bcd293cc6e446e6f637ddfa8b83f48d1734156b529/pytest_mock-3.15.0-py3-none-any.whl", hash = "sha256:ef2219485fb1bd256b00e7ad7466ce26729b30eadfc7cbcdb4fa9a92ca68db6f", size = 10050, upload-time = "2025-09-04T20:57:47.274Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "python-can" +version = "4.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/f9/a9d99d36dd33be5badb747801c9255c3c526171a5542092eaacc73350fb8/python_can-4.6.1.tar.gz", hash = "sha256:290fea135d04b8504ebff33889cc6d301e2181a54099116609f940825ffe5005", size = 1206049, upload-time = "2025-08-12T07:44:58.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/34/e4ac153acdbcfba7f48bc73d6586a74c91cc919fcc2e29acbf81be329d1f/python_can-4.6.1-py3-none-any.whl", hash = "sha256:17f95255868a95108dcfcb90565a684dad32d5a3ebb35afd14f739e18c84ff6c", size = 276996, upload-time = "2025-08-12T07:44:56.55Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-engineio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/5a/349caac055e03ef9e56ed29fa304846063b1771ee54ab8132bf98b29491e/python_engineio-4.13.0.tar.gz", hash = "sha256:f9c51a8754d2742ba832c24b46ed425fdd3064356914edd5a1e8ffde76ab7709", size = 92194, upload-time = "2025-12-24T22:38:05.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl", hash = "sha256:57b94eac094fa07b050c6da59f48b12250ab1cd920765f4849963e3d89ad9de3", size = 59676, upload-time = "2025-12-24T22:38:03.56Z" }, +] + +[[package]] +name = "python-fcl" +version = "0.7.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/ff/5f095a3f8a4ba918b14f61d6566fd50dcad0beb0f8f8e7f9569f4fc70469/python_fcl-0.7.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9a2428768f6d1d3dab1e3f7ccbae3cd5e36287e74e1006773fdc5c1fc908b375", size = 2004230, upload-time = "2025-10-22T06:28:08.625Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e4/0e3a47dba337c66f68468a5dcc4737a83b055347783de25bf2f1cee8d3f6/python_fcl-0.7.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9877c731ad80971afa89d87e8adb2edcd80c2804ad214dc7767661c33a40c5c0", size = 1568886, upload-time = "2025-10-22T06:28:10.605Z" }, + { url = "https://files.pythonhosted.org/packages/b1/84/a13e09672d86eb12d6614537a30c649feedd143b56a2ce659723e64a3068/python_fcl-0.7.0.10-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3252c0e8420857e6a08a703be46a65b97d089bab8a571f4ddc3d3c9b604f665", size = 4626302, upload-time = "2025-10-22T06:28:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/9b/6f/4fc417d2e2ed7c2cc826ab992e06ce7297fec8966343be8d2f4ce74c4147/python_fcl-0.7.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:a25ffd460c1bfdcd296ad97bccff5bd7696cf5311e73260c1dcf46262cc84113", size = 1095153, upload-time = "2025-10-22T06:28:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/68/a6/62d3426e438991c1c97c6483045da8c22fd037972b9299fcf3e6e80b7c9e/python_fcl-0.7.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81eaf143c5fe478928c7012b26ab98f75deda2b01abff4f633b54a75bfa35eae", size = 2006706, upload-time = "2025-10-22T06:28:15.563Z" }, + { url = "https://files.pythonhosted.org/packages/32/16/c468f3b2a5bef5ae0662b4a44ec1baf660c383b229a7836e636d83568d02/python_fcl-0.7.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4a4218ca935ca2306ac0f43600ca159919470e0702532dff14bace3e06ef98c2", size = 1571152, upload-time = "2025-10-22T06:28:17.281Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/cfe8928d36463972a011afb127dfdf18f903dab4184f2cffdf818e592514/python_fcl-0.7.0.10-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f942fe9307287f9c4dd6288881883afe29c80730e670cd0ad83851d2d0e27fcd", size = 4690394, upload-time = "2025-10-22T06:28:19.035Z" }, + { url = "https://files.pythonhosted.org/packages/9b/63/d4b8b2735806710835e94614c57551564218c25900eb47f62652b8250ff3/python_fcl-0.7.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:b569acd01fc9e86f83b2c185a299301ab494143fdb46b0c57c81aa657696a6a5", size = 1095360, upload-time = "2025-10-22T06:28:20.708Z" }, + { url = "https://files.pythonhosted.org/packages/0d/19/9453f061ef50746c8e1bc0b15b3549d8ec599e8d1b13413d0b44b4307775/python_fcl-0.7.0.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:151112db1ab2cd9245046054cd632e6a6441a178704c69e40f2b4d040093be2c", size = 2004820, upload-time = "2025-10-22T06:28:21.938Z" }, + { url = "https://files.pythonhosted.org/packages/3d/71/9761bd7f2d89e45afc199c797a6e70c556134b56827983fb874231f0affb/python_fcl-0.7.0.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:471f7f5c2cac5397835f009bb7d8f4efed86ab5ac232cf85f35f99d15bbab832", size = 1571792, upload-time = "2025-10-22T06:28:23.353Z" }, + { url = "https://files.pythonhosted.org/packages/97/46/bda2b85d827b7c05effbac3563d8cd7635baa7e939fc8c183a0455ab973a/python_fcl-0.7.0.10-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ad9b66dedd1f267bb1cc27a8c27fdefb180e9f782b8e670ef3a7e59bc6aec8d", size = 4679032, upload-time = "2025-10-22T06:28:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/03/db/324bba54308477bac0c9b3c6b5b91cbb0c2b4c65c4dc08c9cabc8adb215a/python_fcl-0.7.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:10ef439be61b591928ae0081f749b34aa68ca5a60504f60adbcbe19106d4b2bd", size = 1095678, upload-time = "2025-10-22T06:28:26.637Z" }, + { url = "https://files.pythonhosted.org/packages/d7/af/28dd814aeeea6ca7ae7c6ceee3e8d44a9006158cec1b1b7da40cd68d562f/python_fcl-0.7.0.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bb37579eea7043cf8a2aa2ab7ee705e8438a08d22dcf7ebaf0c8ec6dfcf89b2", size = 2003577, upload-time = "2025-10-22T06:28:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/609147335621a9244bca472da4938a6145429803f67d1eb75722797058a7/python_fcl-0.7.0.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb9bf7e768a433c3eabd0cae73c1a9c411f590b49d73eb38c91bb88f44b782cc", size = 1570854, upload-time = "2025-10-22T06:28:29.493Z" }, + { url = "https://files.pythonhosted.org/packages/57/6d/344c46667901b4b0c64a44fa0f73ef4d9ce1757d86129083b820a27971b3/python_fcl-0.7.0.10-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c905bc8b6ebf86524d958b3f17e28f5f033331aed9dd8610c6896243a0003e4c", size = 4679836, upload-time = "2025-10-22T06:28:30.991Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/405b88ce34e2d05d4765b58b7f1f99b9afd91eef9bf4807ef6310669fed0/python_fcl-0.7.0.10-cp313-cp313-win_amd64.whl", hash = "sha256:7f2014f29a7ba65c9c4be2bd1ad1c80d91079b1e94f06fb59abbe4595b73d3a2", size = 1095886, upload-time = "2025-10-22T06:28:32.672Z" }, + { url = "https://files.pythonhosted.org/packages/45/db/220c122653624901fdd50bfdb4f4103f326b2d5438d208af286ae4b6bf26/python_fcl-0.7.0.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3791d32c35b50f7b8b3941ecf3b6f8435ede3db16cf9255ef5577a78291dd954", size = 2003205, upload-time = "2025-10-22T06:28:33.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/4964d80affcf581b2e55a068737448f46ca48ad07281913e450e55d793a3/python_fcl-0.7.0.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:288e60098004f41c458ac6835f00a87241ddcb2364476156f23cd040963c4e32", size = 1571231, upload-time = "2025-10-22T06:28:35.578Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c7/c3f9832eabdfbe597691f43e59ee50af024a2152f8ff8fa7b12d9fd1e15f/python_fcl-0.7.0.10-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:518391eee8033fdbae0e5de12644978c3ffe7c7c9ec0b2712fe9e501372022db", size = 4666617, upload-time = "2025-10-22T06:28:36.98Z" }, + { url = "https://files.pythonhosted.org/packages/03/77/68cd8914605a5d6657ba13c21d1d8c16000c4e8acc49237866c94a0a63ad/python_fcl-0.7.0.10-cp314-cp314-win_amd64.whl", hash = "sha256:978f4f187ed04dcacb2ed975c081899a587bcbd053eafffc40abc6d0aefd2269", size = 1116098, upload-time = "2025-10-22T06:28:38.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/fc/8c29bcbf7a0dc8419cec46e1081e4e5e981a018fce0669cc9cd5df824ee6/python_fcl-0.7.0.10-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e8f20a2e76c3728b4dc6741cb99dd2b0fdcb26e77bd24d3036b2d78fae398743", size = 2009882, upload-time = "2025-10-22T06:28:39.953Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3f/c664eb49a2370b0bdf6e98ec3927b45c2ded45b20db4bb325c606089bfbd/python_fcl-0.7.0.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c6dd8534085f48d41b5171ae0f397b6d34ca046194826ff4dfa17a2139f323fa", size = 1579024, upload-time = "2025-10-22T06:28:41.531Z" }, + { url = "https://files.pythonhosted.org/packages/03/27/59296f3280169d3e39d29cfe8170e8edeaecb38270dacf467571c2ee85d0/python_fcl-0.7.0.10-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bce8f0823bdf040a2b0668599afdcb7405ac7e6a272ffedf0c6acd6756d082e", size = 4653964, upload-time = "2025-10-22T06:28:43.337Z" }, + { url = "https://files.pythonhosted.org/packages/b9/06/a4ddfd46794c7d6e175c34e8c10554949d1c17aeb78c188050b4746d4b48/python_fcl-0.7.0.10-cp314-cp314t-win_amd64.whl", hash = "sha256:6ab961f459c294695385d518f7a6eb3a2577029ca008698045dac2b7253fa3f7", size = 1140958, upload-time = "2025-10-22T06:28:44.586Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "python-socketio" +version = "5.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/55/5d8af5884283b58e4405580bcd84af1d898c457173c708736e065f10ca4a/python_socketio-5.16.0.tar.gz", hash = "sha256:f79403c7f1ba8b84460aa8fe4c671414c8145b21a501b46b676f3740286356fd", size = 127120, upload-time = "2025-12-24T23:51:48.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl", hash = "sha256:d95802961e15c7bd54ecf884c6e7644f81be8460f0a02ee66b473df58088ee8a", size = 79607, upload-time = "2025-12-24T23:51:47.2Z" }, +] + +[[package]] +name = "pyturbojpeg" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/e8/0cbd6e4f086a3b9261b2539ab5ddb1e3ba0c94d45b47832594d4b4607586/PyTurboJPEG-1.8.2.tar.gz", hash = "sha256:b7d9625bbb2121b923228fc70d0c2b010b386687501f5b50acec4501222e152b", size = 12694, upload-time = "2025-06-22T07:26:45.861Z" } + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "reactivex" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/af/38a4b62468e4c5bd50acf511d86fe62e65a466aa6abb55b1d59a4a9e57f3/reactivex-4.1.0.tar.gz", hash = "sha256:c7499e3c802bccaa20839b3e17355a7d939573fded3f38ba3d4796278a169a3d", size = 113482, upload-time = "2025-11-05T21:44:24.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/9e/3c2f5d3abb6c5d82f7696e1e3c69b7279049e928596ce82ed25ca97a08f3/reactivex-4.1.0-py3-none-any.whl", hash = "sha256:485750ec8d9b34bcc8ff4318971d234dc4f595058a1b4435a74aefef4b2bc9bd", size = 218588, upload-time = "2025-11-05T21:44:23.015Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/d6/d788d52da01280a30a3f6268aef2aa71043bff359c618fea4c5b536654d5/regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", size = 488087, upload-time = "2025-11-03T21:30:47.317Z" }, + { url = "https://files.pythonhosted.org/packages/69/39/abec3bd688ec9bbea3562de0fd764ff802976185f5ff22807bf0a2697992/regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", size = 290544, upload-time = "2025-11-03T21:30:49.912Z" }, + { url = "https://files.pythonhosted.org/packages/39/b3/9a231475d5653e60002508f41205c61684bb2ffbf2401351ae2186897fc4/regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", size = 288408, upload-time = "2025-11-03T21:30:51.344Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c5/1929a0491bd5ac2d1539a866768b88965fa8c405f3e16a8cef84313098d6/regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", size = 781584, upload-time = "2025-11-03T21:30:52.596Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/16aa16cf5d497ef727ec966f74164fbe75d6516d3d58ac9aa989bc9cdaad/regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", size = 850733, upload-time = "2025-11-03T21:30:53.825Z" }, + { url = "https://files.pythonhosted.org/packages/e6/49/3294b988855a221cb6565189edf5dc43239957427df2d81d4a6b15244f64/regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", size = 898691, upload-time = "2025-11-03T21:30:55.575Z" }, + { url = "https://files.pythonhosted.org/packages/14/62/b56d29e70b03666193369bdbdedfdc23946dbe9f81dd78ce262c74d988ab/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", size = 791662, upload-time = "2025-11-03T21:30:57.262Z" }, + { url = "https://files.pythonhosted.org/packages/15/fc/e4c31d061eced63fbf1ce9d853975f912c61a7d406ea14eda2dd355f48e7/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", size = 782587, upload-time = "2025-11-03T21:30:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bb/5e30c7394bcf63f0537121c23e796be67b55a8847c3956ae6068f4c70702/regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", size = 774709, upload-time = "2025-11-03T21:31:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c4/fce773710af81b0cb37cb4ff0947e75d5d17dee304b93d940b87a67fc2f4/regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", size = 845773, upload-time = "2025-11-03T21:31:01.583Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/9466a7ec4b8ec282077095c6eb50a12a389d2e036581134d4919e8ca518c/regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", size = 836164, upload-time = "2025-11-03T21:31:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/95/18/82980a60e8ed1594eb3c89eb814fb276ef51b9af7caeab1340bfd8564af6/regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", size = 779832, upload-time = "2025-11-03T21:31:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/90ab0fdbe6dce064a42015433f9152710139fb04a8b81b4fb57a1cb63ffa/regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", size = 265802, upload-time = "2025-11-03T21:31:06.581Z" }, + { url = "https://files.pythonhosted.org/packages/34/9d/e9e8493a85f3b1ddc4a5014465f5c2b78c3ea1cbf238dcfde78956378041/regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", size = 277722, upload-time = "2025-11-03T21:31:08.144Z" }, + { url = "https://files.pythonhosted.org/packages/15/c4/b54b24f553966564506dbf873a3e080aef47b356a3b39b5d5aba992b50db/regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", size = 270289, upload-time = "2025-11-03T21:31:10.267Z" }, + { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" }, + { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" }, + { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" }, + { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, + { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" }, + { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + +[[package]] +name = "requests-mock" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rerun-sdk" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "pyarrow" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/4f/7105b8aa0cfb2afdef53ef34d82a043126b6a69e74fbc530c08362b756b5/rerun_sdk-0.28.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:efc4534ad2db8ec8c3b3c68f5883537d639c6758943e1ee10cfe6fb2022708e5", size = 108152262, upload-time = "2025-12-19T22:15:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c5/2a170f9d7c59888875cd60f9b756fc88eef6bf431ffbfe030291bb272754/rerun_sdk-0.28.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:af0631b2598c7edb1872b42ec67ff04ff07bda60db6a4f9987821e4628271cbb", size = 115949311, upload-time = "2025-12-19T22:15:56.449Z" }, + { url = "https://files.pythonhosted.org/packages/75/f7/ca2231395357d874f6e94daede9d726f9a5969654ae7efca4990cf3b3c6e/rerun_sdk-0.28.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:330ff7df6bcc31d45ccea84e8790b8755353731230c036bfa33fed53a48a02e7", size = 123828354, upload-time = "2025-12-19T22:16:02.755Z" }, + { url = "https://files.pythonhosted.org/packages/bb/36/81fda4823c56c492cc5dc0408acb3635f08b0b17b1471b90dfbc4bea2793/rerun_sdk-0.28.1-cp310-abi3-win_amd64.whl", hash = "sha256:04e70610090dee4128b404a7f010c3b25208708c9dd2b0a279dbd27a69ccf453", size = 105707482, upload-time = "2025-12-19T22:16:08.704Z" }, +] + +[[package]] +name = "retrying" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/5a/b17e1e257d3e6f2e7758930e1256832c9ddd576f8631781e6a072914befa/retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39", size = 11411, upload-time = "2025-08-03T03:35:25.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f3/6cd296376653270ac1b423bb30bd70942d9916b6978c6f40472d6ac038e7/retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59", size = 10859, upload-time = "2025-08-03T03:35:23.829Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "rtree" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/09/7302695875a019514de9a5dd17b8320e7a19d6e7bc8f85dcfb79a4ce2da3/rtree-1.4.1.tar.gz", hash = "sha256:c6b1b3550881e57ebe530cc6cffefc87cd9bf49c30b37b894065a9f810875e46", size = 52425, upload-time = "2025-08-13T19:32:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/d9/108cd989a4c0954e60b3cdc86fd2826407702b5375f6dfdab2802e5fed98/rtree-1.4.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d672184298527522d4914d8ae53bf76982b86ca420b0acde9298a7a87d81d4a4", size = 468484, upload-time = "2025-08-13T19:31:50.593Z" }, + { url = "https://files.pythonhosted.org/packages/f3/cf/2710b6fd6b07ea0aef317b29f335790ba6adf06a28ac236078ed9bd8a91d/rtree-1.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7e48d805e12011c2cf739a29d6a60ae852fb1de9fc84220bbcef67e6e595d7d", size = 436325, upload-time = "2025-08-13T19:31:52.367Z" }, + { url = "https://files.pythonhosted.org/packages/55/e1/4d075268a46e68db3cac51846eb6a3ab96ed481c585c5a1ad411b3c23aad/rtree-1.4.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa8c4496e31e9ad58ff6c7df89abceac7022d906cb64a3e18e4fceae6b77f65", size = 459789, upload-time = "2025-08-13T19:31:53.926Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/e5d44be90525cd28503e7f836d077ae6663ec0687a13ba7810b4114b3668/rtree-1.4.1-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c", size = 507644, upload-time = "2025-08-13T19:31:55.164Z" }, + { url = "https://files.pythonhosted.org/packages/fd/85/b8684f769a142163b52859a38a486493b05bafb4f2fb71d4f945de28ebf9/rtree-1.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b558edda52eca3e6d1ee629042192c65e6b7f2c150d6d6cd207ce82f85be3967", size = 1454478, upload-time = "2025-08-13T19:31:56.808Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/c2292b95246b9165cc43a0c3757e80995d58bc9b43da5cb47ad6e3535213/rtree-1.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f155bc8d6bac9dcd383481dee8c130947a4866db1d16cb6dff442329a038a0dc", size = 1555140, upload-time = "2025-08-13T19:31:58.031Z" }, + { url = "https://files.pythonhosted.org/packages/74/25/5282c8270bfcd620d3e73beb35b40ac4ab00f0a898d98ebeb41ef0989ec8/rtree-1.4.1-py3-none-win_amd64.whl", hash = "sha256:efe125f416fd27150197ab8521158662943a40f87acab8028a1aac4ad667a489", size = 389358, upload-time = "2025-08-13T19:31:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/3f/50/0a9e7e7afe7339bd5e36911f0ceb15fed51945836ed803ae5afd661057fd/rtree-1.4.1-py3-none-win_arm64.whl", hash = "sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0", size = 355253, upload-time = "2025-08-13T19:32:00.296Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/a1/64e7b111e753307ffb7c5b6d039c52d4a91a47fa32a7f5bc377a49b22402/sentence_transformers-5.2.0.tar.gz", hash = "sha256:acaeb38717de689f3dab45d5e5a02ebe2f75960a4764ea35fea65f58a4d3019f", size = 381004, upload-time = "2025-12-11T14:12:31.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/d0/3b2897ef6a0c0c801e9fecca26bcc77081648e38e8c772885ebdd8d7d252/sentence_transformers-5.2.0-py3-none-any.whl", hash = "sha256:aa57180f053687d29b08206766ae7db549be5074f61849def7b17bf0b8025ca2", size = 493748, upload-time = "2025-12-11T14:12:29.516Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, +] + +[[package]] +name = "simplejson" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/09/2bf3761de89ea2d91bdce6cf107dcd858892d0adc22c995684878826cc6b/simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5", size = 94039, upload-time = "2025-09-26T16:27:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/0f/33/c3277db8931f0ae9e54b9292668863365672d90fb0f632f4cf9829cb7d68/simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d", size = 75894, upload-time = "2025-09-26T16:27:30.378Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ea/ae47b04d03c7c8a7b7b1a8b39a6e27c3bd424e52f4988d70aca6293ff5e5/simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1", size = 76116, upload-time = "2025-09-26T16:27:31.42Z" }, + { url = "https://files.pythonhosted.org/packages/4b/42/6c9af551e5a8d0f171d6dce3d9d1260068927f7b80f1f09834e07887c8c4/simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6", size = 138827, upload-time = "2025-09-26T16:27:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/2b/22/5e268bbcbe9f75577491e406ec0a5536f5b2fa91a3b52031fea51cd83e1d/simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01", size = 146772, upload-time = "2025-09-26T16:27:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/b4/800f14728e2ad666f420dfdb57697ca128aeae7f991b35759c09356b829a/simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3", size = 134497, upload-time = "2025-09-26T16:27:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b9/c54eef4226c6ac8e9a389bbe5b21fef116768f97a2dc1a683c716ffe66ef/simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda", size = 138172, upload-time = "2025-09-26T16:27:36.44Z" }, + { url = "https://files.pythonhosted.org/packages/09/36/4e282f5211b34620f1b2e4b51d9ddaab5af82219b9b7b78360a33f7e5387/simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2", size = 140272, upload-time = "2025-09-26T16:27:37.605Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/94ad2cf32f477c449e1f63c863d8a513e2408d651c4e58fe4b6a7434e168/simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4", size = 140468, upload-time = "2025-09-26T16:27:39.015Z" }, + { url = "https://files.pythonhosted.org/packages/e5/46/827731e4163be3f987cb8ee90f5d444161db8f540b5e735355faa098d9bc/simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8", size = 148700, upload-time = "2025-09-26T16:27:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/c32121064b1ec2fb7b5d872d9a1abda62df064d35e0160eddfa907118343/simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7", size = 141323, upload-time = "2025-09-26T16:27:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/46/b6/c897c54326fe86dd12d101981171a49361949f4728294f418c3b86a1af77/simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53", size = 74377, upload-time = "2025-09-26T16:27:42.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/87/a6e03d4d80cca99c1fee4e960f3440e2f21be9470e537970f960ca5547f1/simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476", size = 76081, upload-time = "2025-09-26T16:27:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3e/96898c6c66d9dca3f9bd14d7487bf783b4acc77471b42f979babbb68d4ca/simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f", size = 92633, upload-time = "2025-09-26T16:27:45.028Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a2/cd2e10b880368305d89dd540685b8bdcc136df2b3c76b5ddd72596254539/simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8", size = 75309, upload-time = "2025-09-26T16:27:46.142Z" }, + { url = "https://files.pythonhosted.org/packages/5d/02/290f7282eaa6ebe945d35c47e6534348af97472446951dce0d144e013f4c/simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a", size = 75308, upload-time = "2025-09-26T16:27:47.542Z" }, + { url = "https://files.pythonhosted.org/packages/43/91/43695f17b69e70c4b0b03247aa47fb3989d338a70c4b726bbdc2da184160/simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51", size = 143733, upload-time = "2025-09-26T16:27:48.673Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4b/fdcaf444ac1c3cbf1c52bf00320c499e1cf05d373a58a3731ae627ba5e2d/simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd", size = 153397, upload-time = "2025-09-26T16:27:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/c4/83/21550f81a50cd03599f048a2d588ffb7f4c4d8064ae091511e8e5848eeaa/simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013", size = 141654, upload-time = "2025-09-26T16:27:51.168Z" }, + { url = "https://files.pythonhosted.org/packages/cf/54/d76c0e72ad02450a3e723b65b04f49001d0e73218ef6a220b158a64639cb/simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81", size = 144913, upload-time = "2025-09-26T16:27:52.331Z" }, + { url = "https://files.pythonhosted.org/packages/3f/49/976f59b42a6956d4aeb075ada16ad64448a985704bc69cd427a2245ce835/simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa", size = 144568, upload-time = "2025-09-26T16:27:53.41Z" }, + { url = "https://files.pythonhosted.org/packages/60/c7/30bae30424ace8cd791ca660fed454ed9479233810fe25c3f3eab3d9dc7b/simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb", size = 146239, upload-time = "2025-09-26T16:27:54.502Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/7f3b7b97351c53746e7b996fcd106986cda1954ab556fd665314756618d2/simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2", size = 154497, upload-time = "2025-09-26T16:27:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/1d/48/7241daa91d0bf19126589f6a8dcbe8287f4ed3d734e76fd4a092708947be/simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413", size = 148069, upload-time = "2025-09-26T16:27:57.039Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/ef18d2962fe53e7be5123d3784e623859eec7ed97060c9c8536c69d34836/simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961", size = 74158, upload-time = "2025-09-26T16:27:58.265Z" }, + { url = "https://files.pythonhosted.org/packages/35/fd/3d1158ecdc573fdad81bf3cc78df04522bf3959758bba6597ba4c956c74d/simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c", size = 75911, upload-time = "2025-09-26T16:27:59.292Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/1a91e7614db0416885eab4136d49b7303de20528860ffdd798ce04d054db/simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6", size = 93523, upload-time = "2025-09-26T16:28:00.356Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2b/d2413f5218fc25608739e3d63fe321dfa85c5f097aa6648dbe72513a5f12/simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259", size = 75844, upload-time = "2025-09-26T16:28:01.756Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f1/efd09efcc1e26629e120fef59be059ce7841cc6e1f949a4db94f1ae8a918/simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8", size = 75655, upload-time = "2025-09-26T16:28:03.037Z" }, + { url = "https://files.pythonhosted.org/packages/97/ec/5c6db08e42f380f005d03944be1af1a6bd501cc641175429a1cbe7fb23b9/simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9", size = 150335, upload-time = "2025-09-26T16:28:05.027Z" }, + { url = "https://files.pythonhosted.org/packages/81/f5/808a907485876a9242ec67054da7cbebefe0ee1522ef1c0be3bfc90f96f6/simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868", size = 158519, upload-time = "2025-09-26T16:28:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/66/af/b8a158246834645ea890c36136584b0cc1c0e4b83a73b11ebd9c2a12877c/simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b", size = 148571, upload-time = "2025-09-26T16:28:07.715Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/ed9b2571bbf38f1a2425391f18e3ac11cb1e91482c22d644a1640dea9da7/simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e", size = 152367, upload-time = "2025-09-26T16:28:08.921Z" }, + { url = "https://files.pythonhosted.org/packages/81/2c/bad68b05dd43e93f77994b920505634d31ed239418eb6a88997d06599983/simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c", size = 150205, upload-time = "2025-09-26T16:28:10.086Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/90c7fc878061adafcf298ce60cecdee17a027486e9dce507e87396d68255/simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970", size = 151823, upload-time = "2025-09-26T16:28:11.329Z" }, + { url = "https://files.pythonhosted.org/packages/ab/27/b85b03349f825ae0f5d4f780cdde0bbccd4f06c3d8433f6a3882df887481/simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e", size = 158997, upload-time = "2025-09-26T16:28:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/71/ad/d7f3c331fb930638420ac6d236db68e9f4c28dab9c03164c3cd0e7967e15/simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544", size = 154367, upload-time = "2025-09-26T16:28:14.393Z" }, + { url = "https://files.pythonhosted.org/packages/f0/46/5c67324addd40fa2966f6e886cacbbe0407c03a500db94fb8bb40333fcdf/simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54", size = 74285, upload-time = "2025-09-26T16:28:15.931Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/5cc2189f4acd3a6e30ffa9775bf09b354302dbebab713ca914d7134d0f29/simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab", size = 75969, upload-time = "2025-09-26T16:28:17.017Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9e/f326d43f6bf47f4e7704a4426c36e044c6bedfd24e072fb8e27589a373a5/simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86", size = 93530, upload-time = "2025-09-26T16:28:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/35/28/5a4b8f3483fbfb68f3f460bc002cef3a5735ef30950e7c4adce9c8da15c7/simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74", size = 75846, upload-time = "2025-09-26T16:28:19.12Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4d/30dfef83b9ac48afae1cf1ab19c2867e27b8d22b5d9f8ca7ce5a0a157d8c/simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726", size = 75661, upload-time = "2025-09-26T16:28:20.219Z" }, + { url = "https://files.pythonhosted.org/packages/09/1d/171009bd35c7099d72ef6afd4bb13527bab469965c968a17d69a203d62a6/simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5", size = 150579, upload-time = "2025-09-26T16:28:21.337Z" }, + { url = "https://files.pythonhosted.org/packages/61/ae/229bbcf90a702adc6bfa476e9f0a37e21d8c58e1059043038797cbe75b8c/simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d", size = 158797, upload-time = "2025-09-26T16:28:22.53Z" }, + { url = "https://files.pythonhosted.org/packages/90/c5/fefc0ac6b86b9108e302e0af1cf57518f46da0baedd60a12170791d56959/simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0", size = 148851, upload-time = "2025-09-26T16:28:23.733Z" }, + { url = "https://files.pythonhosted.org/packages/43/f1/b392952200f3393bb06fbc4dd975fc63a6843261705839355560b7264eb2/simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b", size = 152598, upload-time = "2025-09-26T16:28:24.962Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b4/d6b7279e52a3e9c0fa8c032ce6164e593e8d9cf390698ee981ed0864291b/simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f", size = 150498, upload-time = "2025-09-26T16:28:26.114Z" }, + { url = "https://files.pythonhosted.org/packages/62/22/ec2490dd859224326d10c2fac1353e8ad5c84121be4837a6dd6638ba4345/simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522", size = 152129, upload-time = "2025-09-26T16:28:27.552Z" }, + { url = "https://files.pythonhosted.org/packages/33/ce/b60214d013e93dd9e5a705dcb2b88b6c72bada442a97f79828332217f3eb/simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3", size = 159359, upload-time = "2025-09-26T16:28:28.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/21/603709455827cdf5b9d83abe726343f542491ca8dc6a2528eb08de0cf034/simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769", size = 154717, upload-time = "2025-09-26T16:28:30.288Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f9/dc7f7a4bac16cf7eb55a4df03ad93190e11826d2a8950052949d3dfc11e2/simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661", size = 74289, upload-time = "2025-09-26T16:28:31.809Z" }, + { url = "https://files.pythonhosted.org/packages/87/10/d42ad61230436735c68af1120622b28a782877146a83d714da7b6a2a1c4e/simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608", size = 75972, upload-time = "2025-09-26T16:28:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sounddevice" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/4f/28e734898b870db15b6474453f19813d3c81b91c806d9e6f867bd6e4dd03/sounddevice-0.5.3.tar.gz", hash = "sha256:cbac2b60198fbab84533697e7c4904cc895ec69d5fb3973556c9eb74a4629b2c", size = 53465, upload-time = "2025-10-19T13:23:57.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/e7/9020e9f0f3df00432728f4c4044387468a743e3d9a4f91123d77be10010e/sounddevice-0.5.3-py3-none-any.whl", hash = "sha256:ea7738baa0a9f9fef7390f649e41c9f2c8ada776180e56c2ffd217133c92a806", size = 32670, upload-time = "2025-10-19T13:23:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/2f/39/714118f8413e0e353436914f2b976665161f1be2b6483ac15a8f61484c14/sounddevice-0.5.3-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:278dc4451fff70934a176df048b77d80d7ce1623a6ec9db8b34b806f3112f9c2", size = 108306, upload-time = "2025-10-19T13:23:53.277Z" }, + { url = "https://files.pythonhosted.org/packages/f5/74/52186e3e5c833d00273f7949a9383adff93692c6e02406bf359cb4d3e921/sounddevice-0.5.3-py3-none-win32.whl", hash = "sha256:845d6927bcf14e84be5292a61ab3359cf8e6b9145819ec6f3ac2619ff089a69c", size = 312882, upload-time = "2025-10-19T13:23:54.829Z" }, + { url = "https://files.pythonhosted.org/packages/66/c7/16123d054aef6d445176c9122bfbe73c11087589b2413cab22aff5a7839a/sounddevice-0.5.3-py3-none-win_amd64.whl", hash = "sha256:f55ad20082efc2bdec06928e974fbcae07bc6c405409ae1334cefe7d377eb687", size = 364025, upload-time = "2025-10-19T13:23:56.362Z" }, +] + +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "structlog" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "tblib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/14c15ae154895cc131174f858c707790d416c444fc69f93918adfd8c4c0b/tblib-3.2.2.tar.gz", hash = "sha256:e9a652692d91bf4f743d4a15bc174c0b76afc750fe8c7b6d195cc1c1d6d2ccec", size = 35046, upload-time = "2025-11-12T12:21:16.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", size = 12893, upload-time = "2025-11-12T12:21:14.407Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "grpcio" }, + { name = "markdown" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "setuptools" }, + { name = "tensorboard-data-server" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, +] + +[[package]] +name = "tensorboard-data-server" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, +] + +[[package]] +name = "tensorboardx" +version = "2.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/c5/d4cc6e293fb837aaf9f76dd7745476aeba8ef7ef5146c3b3f9ee375fe7a5/tensorboardx-2.6.4.tar.gz", hash = "sha256:b163ccb7798b31100b9f5fa4d6bc22dad362d7065c2f24b51e50731adde86828", size = 4769801, upload-time = "2025-06-10T22:37:07.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/1d/b5d63f1a6b824282b57f7b581810d20b7a28ca951f2d5b59f1eb0782c12b/tensorboardx-2.6.4-py3-none-any.whl", hash = "sha256:5970cf3a1f0a6a6e8b180ccf46f3fe832b8a25a70b86e5a237048a7c0beb18e2", size = 87201, upload-time = "2025-06-10T22:37:05.44Z" }, +] + +[[package]] +name = "tensorstore" +version = "0.1.78" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "ml-dtypes", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/ee/05eb424437f4db63331c90e4605025eedc0f71da3faff97161d5d7b405af/tensorstore-0.1.78.tar.gz", hash = "sha256:e26074ffe462394cf54197eb76d6569b500f347573cd74da3f4dd5f510a4ad7c", size = 6913502, upload-time = "2025-10-06T17:44:29.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/1e/77eff7bb320f72a9cb6e9a19eee4d78bee4a6ac1c28ceef60df28b4ab670/tensorstore-0.1.78-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f1bc58164ad964d9cc298d20b62ca704ab6241639a21015e47ce6ea5b5cae27f", size = 15710776, upload-time = "2025-10-06T17:43:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/55/df/f74f8004b246006ae03c90c28e32d71eb8a86a5b325d2d84dda327babdcc/tensorstore-0.1.78-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1910101ea85b6507958da28628ef53712c5311df19a795f449604f82bae6a24b", size = 13771121, upload-time = "2025-10-06T17:43:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/be/b8/ab0d0b2afc53f47fbfd95c10d9ae21d393019aca45c8513657b8d7002f1f/tensorstore-0.1.78-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e92195db0c8c3ca749f24b1e930ab93382ac27430ac4ad2e3f53fc8f739323f", size = 18154513, upload-time = "2025-10-06T17:43:51.694Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/c1b4cc6a089a39f63e8d189a55c715e393995628b12b4c8560b3ae4874ba/tensorstore-0.1.78-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90570b867f9100f7405e4116c73910d0bd283a101500ea5680c5a8a881ea05c6", size = 20048971, upload-time = "2025-10-06T17:43:54.358Z" }, + { url = "https://files.pythonhosted.org/packages/58/2a/7167087885b12473f20ae4fddb9a8feeed6bd44ea8d42c73ae29ad3d1591/tensorstore-0.1.78-cp310-cp310-win_amd64.whl", hash = "sha256:4de9d4ee93d712cb665890af0738f4d74cac3b9b9a0492d477a3ee63fbbf445b", size = 12707793, upload-time = "2025-10-06T17:43:56.405Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/45070c393586306cef44c7bfc47ed2eddfb8930e648aaa847f615e3ae797/tensorstore-0.1.78-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1c91e7ff93561612bd9868f3ee56702b0e4fecb45079a4c152dff9a6aa751913", size = 15712387, upload-time = "2025-10-06T17:43:58.458Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d8/c045da71460301f37704e1ab1eec9e7e480dc711dbd281d86dc3d792c50e/tensorstore-0.1.78-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:781e123d392b2d9115e94b01849797a4540f54cd6d34c6ee32b9491f2f2a399c", size = 13773158, upload-time = "2025-10-06T17:44:00.285Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/2b0d48100816649ec516fca31d02ad8028c090324e77b1c309c09a172350/tensorstore-0.1.78-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e650d363ad43754626a828a242785e6359a59fedb171276e9a0c66c0bd963cd4", size = 18154388, upload-time = "2025-10-06T17:44:02.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a1/d9be82de18afe764c0fc7fb21b3d3bb0ad12845d202861fff7189afdb99d/tensorstore-0.1.78-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33fed0ffa7a42ad24ce203486cf039f81b211723b45bd54859ba237a9d3aedb9", size = 20050304, upload-time = "2025-10-06T17:44:04.673Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fc/b980958f91a9780e4dbc1038da723d2ad91307dbe30563359606f78926e5/tensorstore-0.1.78-cp311-cp311-win_amd64.whl", hash = "sha256:c02df3d8de4703d9ee42c8f620b2288f41c19a0fd5ffa907b72a736678e22188", size = 12708115, upload-time = "2025-10-06T17:44:06.574Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5f/5853c04bebaed2d3c0ada9245328ffe3fff8b0f0f1c64f4776f67b42033f/tensorstore-0.1.78-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:ce375a8f6621cdb94638b9cdc5266519db16a58353d4c6920e8b9d6bdd419e21", size = 15727539, upload-time = "2025-10-06T17:44:08.631Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e2/f67fcca8f90258c1cf1326aa366fe10f559f4c60102f53fdcc6614159c45/tensorstore-0.1.78-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82f68fa5a3b4c84365a667ea0a7465a53d5d969c4d3909ac990f314d1569ffc3", size = 13780753, upload-time = "2025-10-06T17:44:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/95013db6ef3b6a14b4237b95184c21becdf56d16605bf42903bb141f729e/tensorstore-0.1.78-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dc0bd6361d73e3f67d70980f96f4e8bcbd8e810b5475a01333ca9c37f0785a5", size = 18157446, upload-time = "2025-10-06T17:44:12.831Z" }, + { url = "https://files.pythonhosted.org/packages/e2/75/6e7cef68cab3a672c6668cc80c399ae6626a498a3ef04b35b3704b41e9cc/tensorstore-0.1.78-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75a17cef99f05fad9cc6fda37f1a1868d5f1502fd577af13174382931481c948", size = 20060211, upload-time = "2025-10-06T17:44:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/1e/46/4ff3e395c44348c7442523c8ddd8ccc72d9ac81838e7a8f6afdd92131c3e/tensorstore-0.1.78-cp312-cp312-win_amd64.whl", hash = "sha256:56271d4652a7cb445879089f620af47801c091765d35a005505d6bfb8d00c535", size = 12711274, upload-time = "2025-10-06T17:44:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/18/36/cfb5a2acf9005896c88f80b93c2aee42f00fab9d0045369fef6e1b297242/tensorstore-0.1.78-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:8a1d0ae7996c80f2e623be5b8cfbc32a307d08dfef3d2dcb455f592908ecd46d", size = 15727334, upload-time = "2025-10-06T17:44:19.93Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/d1bcc3aab5be4298616dbc060b5aa2012b686270aaa16a9579c7945d0a1c/tensorstore-0.1.78-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:311846cfb2d644cd4a7861005e521a79816093e76d7924c83de5d06ca323067e", size = 13780722, upload-time = "2025-10-06T17:44:21.822Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/b0bb4440a9d67859b1abb367e436c62b0a27991dd7109f20be9dabff488f/tensorstore-0.1.78-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630538a66eb9964bd2975c4e09ae83be9984f2e4ebd5f7969983137bfda92071", size = 18157269, upload-time = "2025-10-06T17:44:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/d6/d95cde18ca2475bf317051b2be168cc963c5cfcd67e9c59786326ccdca53/tensorstore-0.1.78-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6886bec93b8ba22f83c4dc9e7c1ee20b11025ea9a5a839de21d0cbf7fd7aada2", size = 20060053, upload-time = "2025-10-06T17:44:25.942Z" }, + { url = "https://files.pythonhosted.org/packages/db/a2/dbd1af0e97d5d549051309d72c6e3f2fe81fae636f9db3692d21adc9c731/tensorstore-0.1.78-cp313-cp313-win_amd64.whl", hash = "sha256:e0073de8fa3074bc4cc92ced0210310fd89851899faf42a5ba256f0ba87d095c", size = 12711250, upload-time = "2025-10-06T17:44:27.926Z" }, +] + +[[package]] +name = "tensorstore" +version = "0.1.80" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "ml-dtypes", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/18/7b91daa9cf29dbb6bfdd603154f355c9069a9cd8c757038fe52b0f613611/tensorstore-0.1.80.tar.gz", hash = "sha256:4158fe76b96f62d12a37d7868150d836e089b5280b2bdd363c43c5d651f10e26", size = 7090032, upload-time = "2025-12-10T21:35:10.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/1f/902d822626a6c2774229236440c85c17e384f53afb4d2c6fa4118a30c53a/tensorstore-0.1.80-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:246641a8780ee5e04e88bc95c8e31faac6471bab1180d1f5cdc9804b29a77c04", size = 16519587, upload-time = "2025-12-10T21:34:05.758Z" }, + { url = "https://files.pythonhosted.org/packages/21/c9/2ed6ed809946d7b0de08645800584937912c404b85900eea66361d5e2541/tensorstore-0.1.80-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7451b30f99d9f31a2b9d70e6ef61815713dc782c58c6d817f91781341e4dac05", size = 14550336, upload-time = "2025-12-10T21:34:08.394Z" }, + { url = "https://files.pythonhosted.org/packages/d6/50/d97acbc5a4d632590dd9053697181fa41cbcb09389e88acfa6958ab8ead5/tensorstore-0.1.80-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1113a6982fc0fa8dda8fcc0495715e647ac3360909a86ff13f2e04564f82d54a", size = 19004795, upload-time = "2025-12-10T21:34:11.14Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/fdbbf3cd6f08d41d3c1d8a2f6a67a4a2a07ac238fb6eeea852c2669184a3/tensorstore-0.1.80-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b193a7a1c4f455a61e60ed2dd67271a3daab0910ddb4bd9db51390d1b36d9996", size = 20996847, upload-time = "2025-12-10T21:34:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/4570fe93f0c5c339843042556a841cfe0073d3e7fa4dae7ba31417eb4fd3/tensorstore-0.1.80-cp311-cp311-win_amd64.whl", hash = "sha256:9c088e8c9f67c266ef4dae3703bd617f7c0cb0fd98e99c4500692e38a4328140", size = 13258296, upload-time = "2025-12-10T21:34:16.764Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/8733a99926caca2db6e8dbe22491c0623da2298a23bc649bfe6e6f645fa7/tensorstore-0.1.80-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:f65dfaf9e737a41389e29a5a2ea52ca5d14c8d6f48b402c723d800cd16d322b0", size = 16537887, upload-time = "2025-12-10T21:34:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/50/54/59a34fee963e46f9f401c54131bdc6a17d6cfb10e5a094d586d33ae273df/tensorstore-0.1.80-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f8b51d7e685bbb63f6becd7d2ac8634d5ab67ec7e53038e597182e2db2c7aa90", size = 14551674, upload-time = "2025-12-10T21:34:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/0734521f8b648e2c43a00f1bc99a7195646c9e4e31f64ab22a15ac84e75c/tensorstore-0.1.80-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acb8d52fadcefafef4ef8ecca3fc99b1d0e3c5c5a888766484c3e39f050be7f5", size = 19013402, upload-time = "2025-12-10T21:34:24.961Z" }, + { url = "https://files.pythonhosted.org/packages/48/85/55addd16896343ea2731388028945576060139dda3c68a15d6b00158ef6f/tensorstore-0.1.80-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc28a58c580253a526a4b6d239d18181ef96f1e285a502dbb03ff15eeec07a5b", size = 21007488, upload-time = "2025-12-10T21:34:28.093Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d2/5075cfea2ffd13c5bd2e91d76cdf87a355f617e40fa0b8fbfbbdc5e7bd23/tensorstore-0.1.80-cp312-cp312-win_amd64.whl", hash = "sha256:1b2b2ed0051dfab7e25295b14e6620520729e6e2ddf505f98c8d3917569614bf", size = 13263376, upload-time = "2025-12-10T21:34:30.797Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/34e64ef1e4573419671b9aa72b69e927702d84e1d95bcef3cc98a8d63ad5/tensorstore-0.1.80-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:46136fe42ee6dd835d957db37073058aea0b78fdfbe2975941640131b7740824", size = 16537403, upload-time = "2025-12-10T21:34:33.404Z" }, + { url = "https://files.pythonhosted.org/packages/94/03/19f45f6134bbb98d13f8de3160271aa4f49466e1a91000c6ab2eec7d9264/tensorstore-0.1.80-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a92505189731fcb03f1c69a84ea4460abb24204bfac1f339448a0621e7def77c", size = 14551401, upload-time = "2025-12-10T21:34:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d5de3f1b711773e33a329b5fe11de1265b77a13f2a2447fe685ee5d0c1bc/tensorstore-0.1.80-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de63843706fdfe9565a45567238c5b1e55a0b28bbde6524200b31d29043a9a16", size = 19013246, upload-time = "2025-12-10T21:34:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/e874b5a495a7aa14817772a91095971f3a965a4cef5b52ad06a8e15c924f/tensorstore-0.1.80-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c8dbbdd31cbb28eccfb23dbbd4218fe67bfc32e9cb452875a485b81031c949d", size = 21008391, upload-time = "2025-12-10T21:34:41.332Z" }, + { url = "https://files.pythonhosted.org/packages/2f/99/03bcc5da6a735ffa290f888af1f2c990edc9a375b373d04152d8b6fce3e8/tensorstore-0.1.80-cp313-cp313-win_amd64.whl", hash = "sha256:c0529afab3800749dd245843d3bf0d061a109a8edb77fb345f476e8bccda51b8", size = 13262770, upload-time = "2025-12-10T21:34:43.673Z" }, + { url = "https://files.pythonhosted.org/packages/ef/57/75f65d8ba5829768e67aa978d4c0856956b9bacb279c96f0ee28564b6c41/tensorstore-0.1.80-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:04c29d979eb8b8ee48f873dc13d2701bfd49425500ffc5b848e4ec55b2548281", size = 16543698, upload-time = "2025-12-10T21:34:46.095Z" }, + { url = "https://files.pythonhosted.org/packages/9c/92/17a18eac2cfdb019c36b4362d1a5c614d769a78d10cad0aae3d368fefa0e/tensorstore-0.1.80-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:189d924eaec394c9331e284a9c513ed583e336472a925823b5151cb26f41d091", size = 14552217, upload-time = "2025-12-10T21:34:48.539Z" }, + { url = "https://files.pythonhosted.org/packages/b6/df/71f317633a0cd5270b85d185ac5ce91a749930fc076205d3fae4f1f043ed/tensorstore-0.1.80-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07e4a84bacf70b78305831897068a9b5ad30326e63bbeb92c4bf7e565fcf5e9e", size = 19020675, upload-time = "2025-12-10T21:34:51.168Z" }, + { url = "https://files.pythonhosted.org/packages/2b/35/f03cdb5edf8e009ff73e48c0c3d0f692a70a7ffc5e393f2ea1761eff89b5/tensorstore-0.1.80-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2b353b0bd53fedd77fc5a12a1c1a91cacc3cf59e3dd785529c5a54b31d1c7b1", size = 21009171, upload-time = "2025-12-10T21:34:53.979Z" }, + { url = "https://files.pythonhosted.org/packages/51/a9/6cf5675a7d4214ae7fd114c5c7bcf09aa71a57fce6648e187576e60c0c08/tensorstore-0.1.80-cp314-cp314-win_amd64.whl", hash = "sha256:53fd121ccd332bc4cc397f7af45889360c668b43dc3ff6bc3264df0f9886c11a", size = 13653134, upload-time = "2025-12-10T21:34:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d0/8cd2725c6691387438491d0c1fbbe07235439084722f968c20f07de4119d/tensorstore-0.1.80-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4baee67fce95f29f593fbab4866119347115eaace887732aa92cfcbb9e6b0748", size = 16620211, upload-time = "2025-12-10T21:34:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c0/289b8979a08b477ce0622a6c13a59dbe8cda407e4c82c8b2ab0b4f8d1989/tensorstore-0.1.80-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cd11027b5a8b66db8d344085a31a1666c78621dac27039c4d571bc4974804a1", size = 14638072, upload-time = "2025-12-10T21:35:01.598Z" }, + { url = "https://files.pythonhosted.org/packages/42/47/5c63024ced48e3f440c131babedef2f5398f48ab81c1aeee6c6193491d1c/tensorstore-0.1.80-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b7c5dd434bba4ee08fe46bbbdb25c60dd3d47ccb4b8561a9751cf1526da52b8", size = 19024739, upload-time = "2025-12-10T21:35:04.324Z" }, + { url = "https://files.pythonhosted.org/packages/6e/16/d08ade819949e0622f27e949c15b09f7b86ac18f8ac7c4d8bdfb4a711076/tensorstore-0.1.80-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e93df6d34ff5f0f6be245f4d29b99a7c1eef8ad91b50686adf57a5eeea99cb74", size = 21024449, upload-time = "2025-12-10T21:35:08.149Z" }, +] + +[[package]] +name = "tensorzero" +version = "2025.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/63/188bb1f520123008be982f815b7c35234d924d90c1284016ecc6c95886d9/tensorzero-2025.7.5.tar.gz", hash = "sha256:cb366f3c355524e3e0a2a3a2a80e96454d2e5816e2789fb8b93de03874318383", size = 1218364, upload-time = "2025-07-30T16:24:04.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/2f/7d57e631f3c14c585dbefb779d11e441c2f22cd03748c75375b0ad08d1ea/tensorzero-2025.7.5-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:40b589770c86cea5942d144300f381d851351defa9efd0986a0d87b8735f7a07", size = 16389069, upload-time = "2025-07-30T16:23:57.282Z" }, + { url = "https://files.pythonhosted.org/packages/c4/73/3673c9f30e81f3107db3a6a600c8846c9b2edd57b9dcb15ea4c03182dd23/tensorzero-2025.7.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a24fed842f2485be39bcbf1c8280a2538e6bfdbd3ab615e2583ae9c86743dd9d", size = 15522191, upload-time = "2025-07-30T16:23:54.692Z" }, + { url = "https://files.pythonhosted.org/packages/94/0d/0d604dbe1089f482767fb8fc227b381d922df72108e6ace87f1884cb4db4/tensorzero-2025.7.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b32b47e1a5f1f6c769eb067698a8ad804f6189f1588e0f4e45445ee9dc329164", size = 16034337, upload-time = "2025-07-30T16:23:47.152Z" }, + { url = "https://files.pythonhosted.org/packages/fa/81/a6ad537839c874c9b03ce5473b4bcc4348f58fa7d6e63baba9f425d98c1c/tensorzero-2025.7.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9338617764a65d0be9482246d84ddc9a76d9c6524abd1e4d10db48f3a2abb180", size = 17233682, upload-time = "2025-07-30T16:23:50.139Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b4/4c43957672ad7bf4d49050c67ddf0ed3b31dfe2ccd990a1d9bc04241e61c/tensorzero-2025.7.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db6fbc8b522f43da219ab9f71c2177295fc6820e9168a98b94facb75317987ab", size = 16112384, upload-time = "2025-07-30T16:23:59.98Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a7/936433b56a6506c1b6ee0476c41e39539fb14dca54aefacb30179bc0b086/tensorzero-2025.7.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d4e17147f9449df8cf6aad0f18c936f1170c0cb59b07760dd09abb47a29b40", size = 17445354, upload-time = "2025-07-30T16:24:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fd/88f4368b71ae8c4bd1e3ed99c1660467760ca6cfbd31d9167f3a010f9d02/tensorzero-2025.7.5-cp39-abi3-win_amd64.whl", hash = "sha256:a80d9739c61c8d839f8d4f9f61d6333ca13b2bd7ea1bb021ea989dd15a8eb39e", size = 17174978, upload-time = "2025-07-30T16:24:08.122Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "terminaltexteffects" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/92/0eb3f0ad206bf449b7db75f061202dce27d8cb90e598ce3c7d32c0bd80b9/terminaltexteffects-0.12.2.tar.gz", hash = "sha256:4a5eef341d538743e7ac4341cd74d47afc9d0345acdad330ed03fd0a72e41f5f", size = 164321, upload-time = "2025-10-20T20:58:26.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/93/a588ab8b15ceeef23042aa52660fb4891a0e955e92cd3aa97dcafe621720/terminaltexteffects-0.12.2-py3-none-any.whl", hash = "sha256:4b986034094007aa9a31cb1bd16d5d8fcac9755fb6a5da8f74ee7b70c0fa2d63", size = 189344, upload-time = "2025-10-20T20:58:24.425Z" }, +] + +[[package]] +name = "textual" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify", "plugins"] }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/83/c99c252c3fad2f7010ceb476a31af042eec71da441ffeef75bb590bc2e9e/textual-3.7.1.tar.gz", hash = "sha256:a76ba0c8a6c194ef24fd5c3681ebfddca55e7127c064a014128c84fbd7f5d271", size = 1604038, upload-time = "2025-07-09T09:04:45.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/f1/8929fcce6dc983f7a260d0f3ddd2a69b74ba17383dbe57a7e0a9e085e8be/textual-3.7.1-py3-none-any.whl", hash = "sha256:ab5d153f4f65e77017977fa150d0376409e0acf5f1d2e25e2e4ab9de6c0d61ff", size = 691472, upload-time = "2025-07-09T09:04:43.626Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "timm" +version = "1.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "torchvision" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/9d/0ea45640be447445c8664ce2b10c74f763b0b0b9ed11620d41a4d4baa10c/timm-1.0.24.tar.gz", hash = "sha256:c7b909f43fe2ef8fe62c505e270cd4f1af230dfbc37f2ee93e3608492b9d9a40", size = 2412239, upload-time = "2026-01-07T00:26:17.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/dd/c1f5b0890f7b5db661bde0864b41cb0275be76851047e5f7e085fe0b455a/timm-1.0.24-py3-none-any.whl", hash = "sha256:8301ac783410c6ad72c73c49326af6d71a9e4d1558238552796e825c2464913f", size = 2560563, upload-time = "2026-01-07T00:26:13.956Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.21.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, + { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, + { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, + { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, + { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, + { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "torch" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", version = "12.8.90", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/56/9577683b23072075ed2e40d725c52c2019d71a972fab8e083763da8e707e/torch-2.9.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1cc208435f6c379f9b8fdfd5ceb5be1e3b72a6bdf1cb46c0d2812aa73472db9e", size = 104207681, upload-time = "2025-11-12T15:19:56.48Z" }, + { url = "https://files.pythonhosted.org/packages/38/45/be5a74f221df8f4b609b78ff79dc789b0cc9017624544ac4dd1c03973150/torch-2.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:9fd35c68b3679378c11f5eb73220fdcb4e6f4592295277fbb657d31fd053237c", size = 899794036, upload-time = "2025-11-12T15:21:01.886Z" }, + { url = "https://files.pythonhosted.org/packages/67/95/a581e8a382596b69385a44bab2733f1273d45c842f5d4a504c0edc3133b6/torch-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:2af70e3be4a13becba4655d6cc07dcfec7ae844db6ac38d6c1dafeb245d17d65", size = 110969861, upload-time = "2025-11-12T15:21:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/ad/51/1756dc128d2bf6ea4e0a915cb89ea5e730315ff33d60c1ff56fd626ba3eb/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a83b0e84cc375e3318a808d032510dde99d696a85fe9473fc8575612b63ae951", size = 74452222, upload-time = "2025-11-12T15:20:46.223Z" }, + { url = "https://files.pythonhosted.org/packages/15/db/c064112ac0089af3d2f7a2b5bfbabf4aa407a78b74f87889e524b91c5402/torch-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d", size = 104220430, upload-time = "2025-11-12T15:20:31.705Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/76eaa36c9cd032d3b01b001e2c5a05943df75f26211f68fae79e62f87734/torch-2.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b", size = 899821446, upload-time = "2025-11-12T15:20:15.544Z" }, + { url = "https://files.pythonhosted.org/packages/47/cc/7a2949e38dfe3244c4df21f0e1c27bce8aedd6c604a587dd44fc21017cb4/torch-2.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb", size = 110973074, upload-time = "2025-11-12T15:21:39.958Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/7d251155a783fb2c1bb6837b2b7023c622a2070a0a72726ca1df47e7ea34/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475", size = 74463887, upload-time = "2025-11-12T15:20:36.611Z" }, + { url = "https://files.pythonhosted.org/packages/0f/27/07c645c7673e73e53ded71705045d6cb5bae94c4b021b03aa8d03eee90ab/torch-2.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6", size = 104126592, upload-time = "2025-11-12T15:20:41.62Z" }, + { url = "https://files.pythonhosted.org/packages/19/17/e377a460603132b00760511299fceba4102bd95db1a0ee788da21298ccff/torch-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4", size = 899742281, upload-time = "2025-11-12T15:22:17.602Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1a/64f5769025db846a82567fa5b7d21dba4558a7234ee631712ee4771c436c/torch-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083", size = 110940568, upload-time = "2025-11-12T15:21:18.689Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/07739fd776618e5882661d04c43f5b5586323e2f6a2d7d84aac20d8f20bd/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:c0d25d1d8e531b8343bea0ed811d5d528958f1dcbd37e7245bc686273177ad7e", size = 74479191, upload-time = "2025-11-12T15:21:25.816Z" }, + { url = "https://files.pythonhosted.org/packages/20/60/8fc5e828d050bddfab469b3fe78e5ab9a7e53dda9c3bdc6a43d17ce99e63/torch-2.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb", size = 104135743, upload-time = "2025-11-12T15:21:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/6d3f80e6918213babddb2a37b46dbb14c15b14c5f473e347869a51f40e1f/torch-2.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9", size = 899749493, upload-time = "2025-11-12T15:24:36.356Z" }, + { url = "https://files.pythonhosted.org/packages/a6/47/c7843d69d6de8938c1cbb1eba426b1d48ddf375f101473d3e31a5fc52b74/torch-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2", size = 110944162, upload-time = "2025-11-12T15:21:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/28/0e/2a37247957e72c12151b33a01e4df651d9d155dd74d8cfcbfad15a79b44a/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e", size = 74830751, upload-time = "2025-11-12T15:21:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f7/7a18745edcd7b9ca2381aa03353647bca8aace91683c4975f19ac233809d/torch-2.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a", size = 104142929, upload-time = "2025-11-12T15:21:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/f4/dd/f1c0d879f2863ef209e18823a988dc7a1bf40470750e3ebe927efdb9407f/torch-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2", size = 899748978, upload-time = "2025-11-12T15:23:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9f/6986b83a53b4d043e36f3f898b798ab51f7f20fdf1a9b01a2720f445043d/torch-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db", size = 111176995, upload-time = "2025-11-12T15:22:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587", size = 74480347, upload-time = "2025-11-12T15:21:57.648Z" }, + { url = "https://files.pythonhosted.org/packages/48/50/c4b5112546d0d13cc9eaa1c732b823d676a9f49ae8b6f97772f795874a03/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245, upload-time = "2025-11-12T15:22:39.027Z" }, + { url = "https://files.pythonhosted.org/packages/81/c9/2628f408f0518b3bae49c95f5af3728b6ab498c8624ab1e03a43dd53d650/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804, upload-time = "2025-11-12T15:22:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/28/fc/5bc91d6d831ae41bf6e9e6da6468f25330522e92347c9156eb3f1cb95956/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132, upload-time = "2025-11-12T15:23:36.068Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/e8d4e009e52b6b2cf1684bde2a6be157b96fb873732542fb2a9a99e85a83/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845, upload-time = "2025-11-12T15:22:48.367Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b2/2d15a52516b2ea3f414643b8de68fa4cb220d3877ac8b1028c83dc8ca1c4/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558, upload-time = "2025-11-12T15:22:43.392Z" }, + { url = "https://files.pythonhosted.org/packages/86/5c/5b2e5d84f5b9850cd1e71af07524d8cbb74cba19379800f1f9f7c997fc70/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788, upload-time = "2025-11-12T15:23:52.109Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8c/3da60787bcf70add986c4ad485993026ac0ca74f2fc21410bc4eb1bb7695/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500, upload-time = "2025-11-12T15:24:08.788Z" }, + { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" }, +] + +[[package]] +name = "torchreid" +version = "0.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/9a/d3d8da1d1a8a189b2b2d6f191b21cd7fbdb91a587a9c992bcd9666895284/torchreid-0.2.5.tar.gz", hash = "sha256:bc1055c6fb8444968798708dd13fdad00148e9d7cf3cb18cf52f4b949857fe08", size = 92656, upload-time = "2022-10-16T12:33:29.693Z" } + +[[package]] +name = "torchvision" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/09/d51aadf8591138e08b74c64a6eb783630c7a31ca2634416277115a9c3a2b/torchvision-0.24.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ded5e625788572e4e1c4d155d1bbc48805c113794100d70e19c76e39e4d53465", size = 1891441, upload-time = "2025-11-12T15:25:01.687Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/a35df863e7c153aad82af7505abd8264a5b510306689712ef86bea862822/torchvision-0.24.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:54ed17c3d30e718e08d8da3fd5b30ea44b0311317e55647cb97077a29ecbc25b", size = 2386226, upload-time = "2025-11-12T15:25:05.449Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/f2d7cd1eea052887c1083afff0b8df5228ec93b53e03759f20b1a3c6d22a/torchvision-0.24.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f476da4e085b7307aaab6f540219617d46d5926aeda24be33e1359771c83778f", size = 8046093, upload-time = "2025-11-12T15:25:09.425Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/0ff4007c09903199307da5f53a192ff5d62b45447069e9ef3a19bdc5ff12/torchvision-0.24.1-cp310-cp310-win_amd64.whl", hash = "sha256:fbdbdae5e540b868a681240b7dbd6473986c862445ee8a138680a6a97d6c34ff", size = 3696202, upload-time = "2025-11-12T15:25:10.657Z" }, + { url = "https://files.pythonhosted.org/packages/e7/69/30f5f03752aa1a7c23931d2519b31e557f3f10af5089d787cddf3b903ecf/torchvision-0.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:056c525dc875f18fe8e9c27079ada166a7b2755cea5a2199b0bc7f1f8364e600", size = 1891436, upload-time = "2025-11-12T15:25:04.3Z" }, + { url = "https://files.pythonhosted.org/packages/0c/69/49aae86edb75fe16460b59a191fcc0f568c2378f780bb063850db0fe007a/torchvision-0.24.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1e39619de698e2821d71976c92c8a9e50cdfd1e993507dfb340f2688bfdd8283", size = 2387757, upload-time = "2025-11-12T15:25:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/1dfc3db98797b326f1d0c3f3bb61c83b167a813fc7eab6fcd2edb8c7eb9d/torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a0f106663e60332aa4fcb1ca2159ef8c3f2ed266b0e6df88de261048a840e0df", size = 8047682, upload-time = "2025-11-12T15:25:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bb/cfc6a6f6ccc84a534ed1fdf029ae5716dd6ff04e57ed9dc2dab38bf652d5/torchvision-0.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:a9308cdd37d8a42e14a3e7fd9d271830c7fecb150dd929b642f3c1460514599a", size = 4037588, upload-time = "2025-11-12T15:25:14.402Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/18e2c6b9538a045f60718a0c5a058908ccb24f88fde8e6f0fc12d5ff7bd3/torchvision-0.24.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e48bf6a8ec95872eb45763f06499f87bd2fb246b9b96cb00aae260fda2f96193", size = 1891433, upload-time = "2025-11-12T15:25:03.232Z" }, + { url = "https://files.pythonhosted.org/packages/9d/43/600e5cfb0643d10d633124f5982d7abc2170dfd7ce985584ff16edab3e76/torchvision-0.24.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7fb7590c737ebe3e1c077ad60c0e5e2e56bb26e7bccc3b9d04dbfc34fd09f050", size = 2386737, upload-time = "2025-11-12T15:25:08.288Z" }, + { url = "https://files.pythonhosted.org/packages/93/b1/db2941526ecddd84884132e2742a55c9311296a6a38627f9e2627f5ac889/torchvision-0.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:66a98471fc18cad9064123106d810a75f57f0838eee20edc56233fd8484b0cc7", size = 8049868, upload-time = "2025-11-12T15:25:13.058Z" }, + { url = "https://files.pythonhosted.org/packages/69/98/16e583f59f86cd59949f59d52bfa8fc286f86341a229a9d15cbe7a694f0c/torchvision-0.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:4aa6cb806eb8541e92c9b313e96192c6b826e9eb0042720e2fa250d021079952", size = 4302006, upload-time = "2025-11-12T15:25:16.184Z" }, + { url = "https://files.pythonhosted.org/packages/e4/97/ab40550f482577f2788304c27220e8ba02c63313bd74cf2f8920526aac20/torchvision-0.24.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:8a6696db7fb71eadb2c6a48602106e136c785642e598eb1533e0b27744f2cce6", size = 1891435, upload-time = "2025-11-12T15:25:28.642Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/ac0a3f9be6abdbe4e1d82c915d7e20de97e7fd0e9a277970508b015309f3/torchvision-0.24.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:db2125c46f9cb25dc740be831ce3ce99303cfe60439249a41b04fd9f373be671", size = 2338718, upload-time = "2025-11-12T15:25:26.19Z" }, + { url = "https://files.pythonhosted.org/packages/10/b5/5bba24ff9d325181508501ed7f0c3de8ed3dd2edca0784d48b144b6c5252/torchvision-0.24.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:f035f0cacd1f44a8ff6cb7ca3627d84c54d685055961d73a1a9fb9827a5414c8", size = 8049661, upload-time = "2025-11-12T15:25:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ec/54a96ae9ab6a0dd66d4bba27771f892e36478a9c3489fa56e51c70abcc4d/torchvision-0.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:16274823b93048e0a29d83415166a2e9e0bf4e1b432668357b657612a4802864", size = 4319808, upload-time = "2025-11-12T15:25:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f3/a90a389a7e547f3eb8821b13f96ea7c0563cdefbbbb60a10e08dda9720ff/torchvision-0.24.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e3f96208b4bef54cd60e415545f5200346a65024e04f29a26cd0006dbf9e8e66", size = 2005342, upload-time = "2025-11-12T15:25:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/a9/fe/ff27d2ed1b524078164bea1062f23d2618a5fc3208e247d6153c18c91a76/torchvision-0.24.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f231f6a4f2aa6522713326d0d2563538fa72d613741ae364f9913027fa52ea35", size = 2341708, upload-time = "2025-11-12T15:25:25.08Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b9/d6c903495cbdfd2533b3ef6f7b5643ff589ea062f8feb5c206ee79b9d9e5/torchvision-0.24.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:1540a9e7f8cf55fe17554482f5a125a7e426347b71de07327d5de6bfd8d17caa", size = 8177239, upload-time = "2025-11-12T15:25:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2b/ba02e4261369c3798310483028495cf507e6cb3f394f42e4796981ecf3a7/torchvision-0.24.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d83e16d70ea85d2f196d678bfb702c36be7a655b003abed84e465988b6128938", size = 4251604, upload-time = "2025-11-12T15:25:34.069Z" }, + { url = "https://files.pythonhosted.org/packages/42/84/577b2cef8f32094add5f52887867da4c2a3e6b4261538447e9b48eb25812/torchvision-0.24.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cccf4b4fec7fdfcd3431b9ea75d1588c0a8596d0333245dafebee0462abe3388", size = 2005319, upload-time = "2025-11-12T15:25:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/ecb786bffe0159a3b49941a61caaae089853132f3cd1e8f555e3621f7e6f/torchvision-0.24.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:1b495edd3a8f9911292424117544f0b4ab780452e998649425d1f4b2bed6695f", size = 2338844, upload-time = "2025-11-12T15:25:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/51/99/a84623786a6969504c87f2dc3892200f586ee13503f519d282faab0bb4f0/torchvision-0.24.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ab211e1807dc3e53acf8f6638df9a7444c80c0ad050466e8d652b3e83776987b", size = 8175144, upload-time = "2025-11-12T15:25:31.355Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ba/8fae3525b233e109317ce6a9c1de922ab2881737b029a7e88021f81e068f/torchvision-0.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:18f9cb60e64b37b551cd605a3d62c15730c086362b40682d23e24b616a697d41", size = 4234459, upload-time = "2025-11-12T15:25:19.859Z" }, + { url = "https://files.pythonhosted.org/packages/50/33/481602c1c72d0485d4b3a6b48c9534b71c2957c9d83bf860eb837bf5a620/torchvision-0.24.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec9d7379c519428395e4ffda4dbb99ec56be64b0a75b95989e00f9ec7ae0b2d7", size = 2005336, upload-time = "2025-11-12T15:25:27.225Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/372de60bf3dd8f5593bd0d03f4aecf0d1fd58f5bc6943618d9d913f5e6d5/torchvision-0.24.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:af9201184c2712d808bd4eb656899011afdfce1e83721c7cb08000034df353fe", size = 2341704, upload-time = "2025-11-12T15:25:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/36/9b/0f3b9ff3d0225ee2324ec663de0e7fb3eb855615ca958ac1875f22f1f8e5/torchvision-0.24.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9ef95d819fd6df81bc7cc97b8f21a15d2c0d3ac5dbfaab5cbc2d2ce57114b19e", size = 8177422, upload-time = "2025-11-12T15:25:37.357Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ab/e2bcc7c2f13d882a58f8b30ff86f794210b075736587ea50f8c545834f8a/torchvision-0.24.1-cp314-cp314t-win_amd64.whl", hash = "sha256:480b271d6edff83ac2e8d69bbb4cf2073f93366516a50d48f140ccfceedb002e", size = 4335190, upload-time = "2025-11-12T15:25:35.745Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "transformers" +version = "4.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/50/46573150944f46df8ec968eda854023165a84470b42f69f67c7d475dabc5/transformers-4.49.0.tar.gz", hash = "sha256:7e40e640b5b8dc3f48743f5f5adbdce3660c82baafbd3afdfc04143cdbd2089e", size = 8610952, upload-time = "2025-02-17T15:19:03.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/37/1f29af63e9c30156a3ed6ebc2754077016577c094f31de7b2631e5d379eb/transformers-4.49.0-py3-none-any.whl", hash = "sha256:6b4fded1c5fee04d384b1014495b4235a2b53c87503d7d592423c06128cbbe03", size = 9970275, upload-time = "2025-02-17T15:18:58.814Z" }, +] + +[package.optional-dependencies] +torch = [ + { name = "accelerate" }, + { name = "torch" }, +] + +[[package]] +name = "treescope" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/2a/d13d3c38862632742d2fe2f7ae307c431db06538fd05ca03020d207b5dcc/treescope-0.1.10.tar.gz", hash = "sha256:20f74656f34ab2d8716715013e8163a0da79bdc2554c16d5023172c50d27ea95", size = 138870, upload-time = "2025-08-08T05:43:48.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/2b/36e984399089c026a6499ac8f7401d38487cf0183839a4aa78140d373771/treescope-0.1.10-py3-none-any.whl", hash = "sha256:dde52f5314f4c29d22157a6fe4d3bd103f9cae02791c9e672eefa32c9aa1da51", size = 182255, upload-time = "2025-08-08T05:43:46.673Z" }, +] + +[[package]] +name = "trimesh" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/69/eedfeb084460d429368e03db83ed41b18d6de4fd4945de7eb8874b9fae36/trimesh-4.10.1.tar.gz", hash = "sha256:2067ebb8dcde0d7f00c2a85bfcae4aa891c40898e5f14232592429025ee2c593", size = 831998, upload-time = "2025-12-07T00:39:05.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/f08f0d16b4f97ec2ea6d542b9a70472a344384382fa3543a12ec417cc063/trimesh-4.10.1-py3-none-any.whl", hash = "sha256:4e81fae696683dfe912ef54ce124869487d35d267b87e10fe07fc05ab62aaadb", size = 737037, upload-time = "2025-12-07T00:39:04.086Z" }, +] + +[[package]] +name = "triton" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/6e/676ab5019b4dde8b9b7bab71245102fc02778ef3df48218b298686b9ffd6/triton-3.5.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fc53d849f879911ea13f4a877243afc513187bc7ee92d1f2c0f1ba3169e3c94", size = 170320692, upload-time = "2025-11-11T17:40:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/72/ec90c3519eaf168f22cb1757ad412f3a2add4782ad3a92861c9ad135d886/triton-3.5.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579", size = 170425802, upload-time = "2025-11-11T17:40:53.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/50/9a8358d3ef58162c0a415d173cfb45b67de60176e1024f71fbc4d24c0b6d/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207, upload-time = "2025-11-11T17:41:00.253Z" }, + { url = "https://files.pythonhosted.org/packages/27/46/8c3bbb5b0a19313f50edcaa363b599e5a1a5ac9683ead82b9b80fe497c8d/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410, upload-time = "2025-11-11T17:41:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/37/92/e97fcc6b2c27cdb87ce5ee063d77f8f26f19f06916aa680464c8104ef0f6/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924, upload-time = "2025-11-11T17:41:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e6/c595c35e5c50c4bc56a7bac96493dad321e9e29b953b526bbbe20f9911d0/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488, upload-time = "2025-11-11T17:41:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192, upload-time = "2025-11-11T17:41:23.963Z" }, +] + +[[package]] +name = "typeguard" +version = "4.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/68/71c1a15b5f65f40e91b65da23b8224dad41349894535a97f63a52e462196/typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", size = 75203, upload-time = "2025-06-18T09:56:07.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874, upload-time = "2025-06-18T09:56:05.999Z" }, +] + +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + +[[package]] +name = "types-colorama" +version = "0.4.15.20250801" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/37/af713e7d73ca44738c68814cbacf7a655aa40ddd2e8513d431ba78ace7b3/types_colorama-0.4.15.20250801.tar.gz", hash = "sha256:02565d13d68963d12237d3f330f5ecd622a3179f7b5b14ee7f16146270c357f5", size = 10437, upload-time = "2025-08-01T03:48:22.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3a/44ccbbfef6235aeea84c74041dc6dfee6c17ff3ddba782a0250e41687ec7/types_colorama-0.4.15.20250801-py3-none-any.whl", hash = "sha256:b6e89bd3b250fdad13a8b6a465c933f4a5afe485ea2e2f104d739be50b13eea9", size = 10743, upload-time = "2025-08-01T03:48:21.774Z" }, +] + +[[package]] +name = "types-defusedxml" +version = "0.7.0.20250822" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/4a/5b997ae87bf301d1796f72637baa4e0e10d7db17704a8a71878a9f77f0c0/types_defusedxml-0.7.0.20250822.tar.gz", hash = "sha256:ba6c395105f800c973bba8a25e41b215483e55ec79c8ca82b6fe90ba0bc3f8b2", size = 10590, upload-time = "2025-08-22T03:02:59.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/73/8a36998cee9d7c9702ed64a31f0866c7f192ecffc22771d44dbcc7878f18/types_defusedxml-0.7.0.20250822-py3-none-any.whl", hash = "sha256:5ee219f8a9a79c184773599ad216123aedc62a969533ec36737ec98601f20dcf", size = 13430, upload-time = "2025-08-22T03:02:58.466Z" }, +] + +[[package]] +name = "types-gevent" +version = "25.9.0.20251228" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-greenlet" }, + { name = "types-psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/85/c5043c4472f82c8ee3d9e0673eb4093c7d16770a26541a137a53a1d096f6/types_gevent-25.9.0.20251228.tar.gz", hash = "sha256:423ef9891d25c5a3af236c3e9aace4c444c86ff773fe13ef22731bc61d59abef", size = 38063, upload-time = "2025-12-28T03:28:28.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/b7/a2d6b652ab5a26318b68cafd58c46fafb9b15c5313d2d76a70b838febb4b/types_gevent-25.9.0.20251228-py3-none-any.whl", hash = "sha256:e2e225af4fface9241c16044983eb2fc3993f2d13d801f55c2932848649b7f2f", size = 55486, upload-time = "2025-12-28T03:28:27.382Z" }, +] + +[[package]] +name = "types-greenlet" +version = "3.3.0.20251206" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/d3/23f4ab29a5ce239935bb3c157defcf50df8648c16c65965fae03980d67f3/types_greenlet-3.3.0.20251206.tar.gz", hash = "sha256:3e1ab312ab7154c08edc2e8110fbf00d9920323edc1144ad459b7b0052063055", size = 8901, upload-time = "2025-12-06T03:01:38.634Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/8f/aabde1b6e49b25a6804c12a707829e44ba0f5520563c09271f05d3196142/types_greenlet-3.3.0.20251206-py3-none-any.whl", hash = "sha256:8d11041c0b0db545619e8c8a1266aa4aaa4ebeae8ae6b4b7049917a6045a5590", size = 8809, upload-time = "2025-12-06T03:01:37.651Z" }, +] + +[[package]] +name = "types-jmespath" +version = "1.0.2.20250809" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/ff/6848b1603ca47fff317b44dfff78cc1fb0828262f840b3ab951b619d5a22/types_jmespath-1.0.2.20250809.tar.gz", hash = "sha256:e194efec21c0aeae789f701ae25f17c57c25908e789b1123a5c6f8d915b4adff", size = 10248, upload-time = "2025-08-09T03:14:57.996Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/6a/65c8be6b6555beaf1a654ae1c2308c2e19a610c0b318a9730e691b79ac79/types_jmespath-1.0.2.20250809-py3-none-any.whl", hash = "sha256:4147d17cc33454f0dac7e78b4e18e532a1330c518d85f7f6d19e5818ab83da21", size = 11494, upload-time = "2025-08-09T03:14:57.292Z" }, +] + +[[package]] +name = "types-jsonschema" +version = "4.25.1.20251009" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/da/5b901088da5f710690b422137e8ae74197fb1ca471e4aa84dd3ef0d6e295/types_jsonschema-4.25.1.20251009.tar.gz", hash = "sha256:75d0f5c5dd18dc23b664437a0c1a625743e8d2e665ceaf3aecb29841f3a5f97f", size = 15661, upload-time = "2025-10-09T02:54:36.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/6a/e5146754c0dfc272f176db9c245bc43cc19030262d891a5a85d472797e60/types_jsonschema-4.25.1.20251009-py3-none-any.whl", hash = "sha256:f30b329037b78e7a60146b1146feb0b6fb0b71628637584409bada83968dad3e", size = 15925, upload-time = "2025-10-09T02:54:35.847Z" }, +] + +[[package]] +name = "types-networkx" +version = "3.6.1.20251220" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/e3/dcc20d645dc0631b0df263959b8dde49dc47ad3c0537d8958bfefe692380/types_networkx-3.6.1.20251220.tar.gz", hash = "sha256:caf95e0d7777b969e50ceeb2c430d9d4dfe6b7bdee43c42dc9879a2d4408a790", size = 73500, upload-time = "2025-12-20T03:07:47.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/e7/fe40cfe7ba384d1f46fee835eb7727a4ee2fd80021a69add9553197b69a1/types_networkx-3.6.1.20251220-py3-none-any.whl", hash = "sha256:417ccbe7841f335a4c2b8e7515c3bc97a00fb5f686f399a763ef64392b209eac", size = 162715, upload-time = "2025-12-20T03:07:46.882Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20251210" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" }, +] + +[[package]] +name = "types-psutil" +version = "7.2.1.20251231" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/e0/f4881668da3fcc9473b3fb4b3dc028840cf57374d72b798c0912a183163a/types_psutil-7.2.1.20251231.tar.gz", hash = "sha256:dbf9df530b1130e131e4211ed8cea62c08007bfa69faf2883d296bd241d30e4a", size = 25620, upload-time = "2025-12-31T03:18:29.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/61/81f180ffbcd0b3516fa3e0e95588dcd48200b6a08e3df53c6c0941a688fe/types_psutil-7.2.1.20251231-py3-none-any.whl", hash = "sha256:40735ca2fc818aed9dcbff7acb3317a774896615e3f4a7bd356afa224b9178e3", size = 32426, upload-time = "2025-12-31T03:18:28.14Z" }, +] + +[[package]] +name = "types-pysocks" +version = "1.7.1.20251001" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/d7/421deaee04ffe69dc1449cbf57dc4d4d92e8f966f4a35b482ea3811b7980/types_pysocks-1.7.1.20251001.tar.gz", hash = "sha256:50a0e737d42527abbec09e891c64f76a9f66f302e673cd149bc112c15764869f", size = 8785, upload-time = "2025-10-01T03:04:13.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/07/6a8aafa0fa5fc0880a37c98b41348bf91bc28f76577bdac68f78bcf8a124/types_pysocks-1.7.1.20251001-py3-none-any.whl", hash = "sha256:dd9abcfc7747aeddf1bab270c8daab3a1309c3af9e07c8c2c52038ab8539f06c", size = 9620, upload-time = "2025-10-01T03:04:13.042Z" }, +] + +[[package]] +name = "types-pytz" +version = "2025.2.0.20251108" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-simplejson" +version = "3.20.0.20250822" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/6b/96d43a90cd202bd552cdd871858a11c138fe5ef11aeb4ed8e8dc51389257/types_simplejson-3.20.0.20250822.tar.gz", hash = "sha256:2b0bfd57a6beed3b932fd2c3c7f8e2f48a7df3978c9bba43023a32b3741a95b0", size = 10608, upload-time = "2025-08-22T03:03:35.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/9f/8e2c9e6aee9a2ff34f2ffce6ccd9c26edeef6dfd366fde611dc2e2c00ab9/types_simplejson-3.20.0.20250822-py3-none-any.whl", hash = "sha256:b5e63ae220ac7a1b0bb9af43b9cb8652237c947981b2708b0c776d3b5d8fa169", size = 10417, upload-time = "2025-08-22T03:03:34.485Z" }, +] + +[[package]] +name = "types-tabulate" +version = "0.9.0.20241207" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/43/16030404a327e4ff8c692f2273854019ed36718667b2993609dc37d14dd4/types_tabulate-0.9.0.20241207.tar.gz", hash = "sha256:ac1ac174750c0a385dfd248edc6279fa328aaf4ea317915ab879a2ec47833230", size = 8195, upload-time = "2024-12-07T02:54:42.554Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/86/a9ebfd509cbe74471106dffed320e208c72537f9aeb0a55eaa6b1b5e4d17/types_tabulate-0.9.0.20241207-py3-none-any.whl", hash = "sha256:b8dad1343c2a8ba5861c5441370c3e35908edd234ff036d4298708a1d4cf8a85", size = 8307, upload-time = "2024-12-07T02:54:41.031Z" }, +] + +[[package]] +name = "types-tensorflow" +version = "2.18.0.20251008" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "types-protobuf" }, + { name = "types-requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0a/13bde03fb5a23faaadcca2d6914f865e444334133902310ea05e6ade780c/types_tensorflow-2.18.0.20251008.tar.gz", hash = "sha256:8db03d4dd391a362e2ea796ffdbccb03c082127606d4d852edb7ed9504745933", size = 257550, upload-time = "2025-10-08T02:51:51.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/cc/e50e49db621b0cf03c1f3d10be47389de41a02dc9924c3a83a9c1a55bf28/types_tensorflow-2.18.0.20251008-py3-none-any.whl", hash = "sha256:d6b0dd4d81ac6d9c5af803ebcc8ce0f65c5850c063e8b9789dc828898944b5f4", size = 329023, upload-time = "2025-10-08T02:51:50.024Z" }, +] + +[[package]] +name = "types-tqdm" +version = "4.67.0.20250809" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d0/cf498fc630d9fdaf2428b93e60b0e67b08008fec22b78716b8323cf644dc/types_tqdm-4.67.0.20250809.tar.gz", hash = "sha256:02bf7ab91256080b9c4c63f9f11b519c27baaf52718e5fdab9e9606da168d500", size = 17200, upload-time = "2025-08-09T03:17:43.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/13/3ff0781445d7c12730befce0fddbbc7a76e56eb0e7029446f2853238360a/types_tqdm-4.67.0.20250809-py3-none-any.whl", hash = "sha256:1a73053b31fcabf3c1f3e2a9d5ecdba0f301bde47a418cd0e0bdf774827c5c57", size = 24020, upload-time = "2025-08-09T03:17:42.453Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + +[[package]] +name = "ultralytics" +version = "8.3.248" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "polars" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "ultralytics-thop" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/74/dc3628b3ff540e496174179d66f7b3fee411401096f676f10b9375e1b524/ultralytics-8.3.248.tar.gz", hash = "sha256:a543cfd2a043dc94010bc6e7e05d07bec80d5b00eda94f5e95ea93e5c75de652", size = 988736, upload-time = "2026-01-04T17:21:46.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/5e/ec74c9c1d362dea75ed776ead732695482a84d0a902faa33cce0f9f884b4/ultralytics-8.3.248-py3-none-any.whl", hash = "sha256:1dc3ed47bbd7e3cdaace14a2d1e94d1977a1b7276f9ee92574446fd8bb1390db", size = 1152202, upload-time = "2026-01-04T17:21:42.817Z" }, +] + +[[package]] +name = "ultralytics-thop" +version = "2.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/63/21a32e1facfeee245dbdfb7b4669faf7a36ff7c00b50987932bdab126f4b/ultralytics_thop-2.0.18.tar.gz", hash = "sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf", size = 34557, upload-time = "2025-10-29T16:58:13.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/c7/fb42228bb05473d248c110218ffb8b1ad2f76728ed8699856e5af21112ad/ultralytics_thop-2.0.18-py3-none-any.whl", hash = "sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec", size = 28941, upload-time = "2025-10-29T16:58:12.093Z" }, +] + +[[package]] +name = "unitree-webrtc-connect-leshy" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiortc" }, + { name = "flask-socketio" }, + { name = "lz4" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opencv-python" }, + { name = "packaging" }, + { name = "pyaudio" }, + { name = "pycryptodome" }, + { name = "pydub" }, + { name = "requests" }, + { name = "sounddevice" }, + { name = "wasmtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/20/a92ceb094188fcf176da5609878c923273d414e93b8b5bbbb32c4f6ffd7c/unitree_webrtc_connect_leshy-2.0.7.tar.gz", hash = "sha256:9eeddab68e42e286cd9ba1e520303a56fd0920dce2bd4ef0cdec1d21669fda3b", size = 34362, upload-time = "2025-12-31T01:08:12.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/1b/e34448851e1cdad620175e048f58c177ac853564d4d7fdb9aa9cfc21eae7/unitree_webrtc_connect_leshy-2.0.7-py3-none-any.whl", hash = "sha256:82e2b9d842bf58288ec40e0bfd685780e20af9a2d0495aa9330950afde1d8ce4", size = 39989, upload-time = "2025-12-31T01:08:09.787Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/0e/512fb221e4970c2f75ca9dae412d320b7d9ddc9f2b15e04ea8e44710396c/uuid_utils-0.12.0.tar.gz", hash = "sha256:252bd3d311b5d6b7f5dfce7a5857e27bb4458f222586bb439463231e5a9cbd64", size = 20889, upload-time = "2025-12-01T17:29:55.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/43/de5cd49a57b6293b911b6a9a62fc03e55db9f964da7d5882d9edbee1e9d2/uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3b9b30707659292f207b98f294b0e081f6d77e1fbc760ba5b41331a39045f514", size = 603197, upload-time = "2025-12-01T17:29:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/02/fa/5fd1d8c9234e44f0c223910808cde0de43bb69f7df1349e49b1afa7f2baa/uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:add3d820c7ec14ed37317375bea30249699c5d08ff4ae4dbee9fc9bce3bfbf65", size = 305168, upload-time = "2025-12-01T17:29:31.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c6/8633ac9942bf9dc97a897b5154e5dcffa58816ec4dd780b3b12b559ff05c/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8fce83ecb3b16af29c7809669056c4b6e7cc912cab8c6d07361645de12dd79", size = 340580, upload-time = "2025-12-01T17:29:32.362Z" }, + { url = "https://files.pythonhosted.org/packages/f3/88/8a61307b04b4da1c576373003e6d857a04dade52ab035151d62cb84d5cb5/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec921769afcb905035d785582b0791d02304a7850fbd6ce924c1a8976380dfc6", size = 346771, upload-time = "2025-12-01T17:29:33.708Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fb/aab2dcf94b991e62aa167457c7825b9b01055b884b888af926562864398c/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f3b060330f5899a92d5c723547dc6a95adef42433e9748f14c66859a7396664", size = 474781, upload-time = "2025-12-01T17:29:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7a/dbd5e49c91d6c86dba57158bbfa0e559e1ddf377bb46dcfd58aea4f0d567/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:908dfef7f0bfcf98d406e5dc570c25d2f2473e49b376de41792b6e96c1d5d291", size = 343685, upload-time = "2025-12-01T17:29:36.677Z" }, + { url = "https://files.pythonhosted.org/packages/1a/19/8c4b1d9f450159733b8be421a4e1fb03533709b80ed3546800102d085572/uuid_utils-0.12.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c6a24148926bd0ca63e8a2dabf4cc9dc329a62325b3ad6578ecd60fbf926506", size = 366482, upload-time = "2025-12-01T17:29:37.979Z" }, + { url = "https://files.pythonhosted.org/packages/82/43/c79a6e45687647f80a159c8ba34346f287b065452cc419d07d2212d38420/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:64a91e632669f059ef605f1771d28490b1d310c26198e46f754e8846dddf12f4", size = 523132, upload-time = "2025-12-01T17:29:39.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a2/b2d75a621260a40c438aa88593827dfea596d18316520a99e839f7a5fb9d/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:93c082212470bb4603ca3975916c205a9d7ef1443c0acde8fbd1e0f5b36673c7", size = 614218, upload-time = "2025-12-01T17:29:40.315Z" }, + { url = "https://files.pythonhosted.org/packages/13/6b/ba071101626edd5a6dabf8525c9a1537ff3d885dbc210540574a03901fef/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:431b1fb7283ba974811b22abd365f2726f8f821ab33f0f715be389640e18d039", size = 546241, upload-time = "2025-12-01T17:29:41.656Z" }, + { url = "https://files.pythonhosted.org/packages/01/12/9a942b81c0923268e6d85bf98d8f0a61fcbcd5e432fef94fdf4ce2ef8748/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd7838c40149100299fa37cbd8bab5ee382372e8e65a148002a37d380df7c8", size = 511842, upload-time = "2025-12-01T17:29:43.107Z" }, + { url = "https://files.pythonhosted.org/packages/a9/a7/c326f5163dd48b79368b87d8a05f5da4668dd228a3f5ca9d79d5fee2fc40/uuid_utils-0.12.0-cp39-abi3-win32.whl", hash = "sha256:487f17c0fee6cbc1d8b90fe811874174a9b1b5683bf2251549e302906a50fed3", size = 179088, upload-time = "2025-12-01T17:29:44.492Z" }, + { url = "https://files.pythonhosted.org/packages/38/92/41c8734dd97213ee1d5ae435cf4499705dc4f2751e3b957fd12376f61784/uuid_utils-0.12.0-cp39-abi3-win_amd64.whl", hash = "sha256:9598e7c9da40357ae8fffc5d6938b1a7017f09a1acbcc95e14af8c65d48c655a", size = 183003, upload-time = "2025-12-01T17:29:45.47Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/52ab0359618987331a1f739af837d26168a4b16281c9c3ab46519940c628/uuid_utils-0.12.0-cp39-abi3-win_arm64.whl", hash = "sha256:c9bea7c5b2aa6f57937ebebeee4d4ef2baad10f86f1b97b58a3f6f34c14b4e84", size = 182975, upload-time = "2025-12-01T17:29:46.444Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f7/6c55b7722cede3b424df02ed5cddb25c19543abda2f95fa4cfc34a892ae5/uuid_utils-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e2209d361f2996966ab7114f49919eb6aaeabc6041672abbbbf4fdbb8ec1acc0", size = 593065, upload-time = "2025-12-01T17:29:47.507Z" }, + { url = "https://files.pythonhosted.org/packages/b8/40/ce5fe8e9137dbd5570e0016c2584fca43ad81b11a1cef809a1a1b4952ab7/uuid_utils-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d9636bcdbd6cfcad2b549c352b669412d0d1eb09be72044a2f13e498974863cd", size = 300047, upload-time = "2025-12-01T17:29:48.596Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9b/31c5d0736d7b118f302c50214e581f40e904305d8872eb0f0c921d50e138/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd8543a3419251fb78e703ce3b15fdfafe1b7c542cf40caf0775e01db7e7674", size = 335165, upload-time = "2025-12-01T17:29:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/d80b4d08691c9d7446d0ad58fd41503081a662cfd2c7640faf68c64d8098/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e98db2d8977c052cb307ae1cb5cc37a21715e8d415dbc65863b039397495a013", size = 341437, upload-time = "2025-12-01T17:29:51.112Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b3/9dccdc6f3c22f6ef5bd381ae559173f8a1ae185ae89ed1f39f499d9d8b02/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8f2bdf5e4ffeb259ef6d15edae92aed60a1d6f07cbfab465d836f6b12b48da8", size = 469123, upload-time = "2025-12-01T17:29:52.389Z" }, + { url = "https://files.pythonhosted.org/packages/fd/90/6c35ef65fbc49f8189729839b793a4a74a7dd8c5aa5eb56caa93f8c97732/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c3ec53c0cb15e1835870c139317cc5ec06e35aa22843e3ed7d9c74f23f23898", size = 335892, upload-time = "2025-12-01T17:29:53.44Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c7/e3f3ce05c5af2bf86a0938d22165affe635f4dcbfd5687b1dacc042d3e0e/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:84e5c0eba209356f7f389946a3a47b2cc2effd711b3fc7c7f155ad9f7d45e8a3", size = 360693, upload-time = "2025-12-01T17:29:54.558Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "wasmtime" +version = "40.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/96/3e7e9b4c5b9d3071b469502d0c4418d1492e5ce52bbf5b985703b08c6892/wasmtime-40.0.0.tar.gz", hash = "sha256:48417c59f13be145184cff61fef61bb52556ea0e7417c25bec09af2d859745ab", size = 117370, upload-time = "2025-12-22T16:30:39.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/da6935d495d5bf5a1defa261c183af3e624b6684bfe8d54a0aa4caf238b6/wasmtime-40.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:f81dcd8850c66bbe8da53774515bd255a18fce595899e9d851f9969d48d7f592", size = 6894176, upload-time = "2025-12-22T16:30:23.962Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/2d6afa0e102e85745a3f637e399151f725e836e91c1cd8304bf8cda6eb8f/wasmtime-40.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:b462e868f9af4bc69ee353e2cebb3ea5c14984f07b703e3dfc208697ac798fc9", size = 7735017, upload-time = "2025-12-22T16:30:25.709Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fa/4d061d3b54d8b550c1a043d197380dd54fb1954c58363b914c061fa7a86e/wasmtime-40.0.0-py3-none-any.whl", hash = "sha256:b7532706094f768fcab15fa0cf8c57278f7bc2770a32a74b98e3be7db0984e56", size = 6297908, upload-time = "2025-12-22T16:30:27.164Z" }, + { url = "https://files.pythonhosted.org/packages/64/33/10a68779d53557a7d441b40106a7ea0085e9b0af9d82466082cafa890258/wasmtime-40.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:e2f374948982a749e5c64de04d1a322ecc79ffd633e0f269c47567c3834c4836", size = 7507846, upload-time = "2025-12-22T16:30:28.312Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5f/ef035900032a5012aad368017abf2a7b626aed38b31e8f35c3266a3a3676/wasmtime-40.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd2d37071c493377b7c4b27e5d1fe2154f4434fbb6af70f1dce9969f287dac62", size = 6533509, upload-time = "2025-12-22T16:30:29.99Z" }, + { url = "https://files.pythonhosted.org/packages/01/bb/8f6dd6a213706a101c7c598609015648fbd82bd34455cabdec300c304d8c/wasmtime-40.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:d1ad1be984bea3f2325e67258bc9d6d2d4520cfdbcc3b0ae752c8b4817d0212c", size = 7798564, upload-time = "2025-12-22T16:30:31.649Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/d6f1b1f22da240c14bc60459677fbc13cd630260a2c5eac9737dbde63bb5/wasmtime-40.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3e711b1049ac95f5f34c945b827311c5d382236af5e535a880a26b8861e85aae", size = 6815182, upload-time = "2025-12-22T16:30:33.154Z" }, + { url = "https://files.pythonhosted.org/packages/be/9f/401934f38c6a6559d2be12180793f18c7c726938a1d207fcfc20a8d4091b/wasmtime-40.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:504a903099b0518d589db5c17ee6c95b207392c58a272081dc59c33d7000d11f", size = 6893582, upload-time = "2025-12-22T16:30:34.344Z" }, + { url = "https://files.pythonhosted.org/packages/9f/60/c9300d1146f577847aab879ec90d9da3bc7e20f62150386f08adc4aacf41/wasmtime-40.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fee1be5dea191e8350db2e00ff0e57205b9c2d552c6537d736c5c9c75b1470da", size = 7831639, upload-time = "2025-12-22T16:30:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/85/be/2f81a31430f02f57602ae1b4ff0e369b3cbd07c2fcdd0b696b75e9bfc30a/wasmtime-40.0.0-py3-none-win_amd64.whl", hash = "sha256:ebce72e82d1d18726ce3e769094fd8b1d9fc9a1d310cd87c6a85d3ce48fa6567", size = 6297915, upload-time = "2025-12-22T16:30:36.878Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/35cbfdf9619c958a8b48f2ad083b88abc1521d771bfab668002e4405a1da/wasmtime-40.0.0-py3-none-win_arm64.whl", hash = "sha256:7667966236bba5e80a1c454553e566a1fa700328bc3e65b5ca970bee7e177e57", size = 5398931, upload-time = "2025-12-22T16:30:38.047Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "wsproto" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, +] + +[[package]] +name = "xarm-python-sdk" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/dd/073cf64fa9e74cfb97c9ded97750ed4652ada1b4921cd0e7d895ff242f7c/xarm_python_sdk-1.17.3.tar.gz", hash = "sha256:e61b988bc3be684c15f8e686958c00e619e14725130ee94148074b8ef5bd9ec3", size = 215842, upload-time = "2025-12-02T03:09:49.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/0a/85b3df0fa6ddd4f1a9d23cd63948aa145797631c9e20d165ada8e13a058c/xarm_python_sdk-1.17.3-py3-none-any.whl", hash = "sha256:3dee2f9819d54f0ba476ea51ff63f2d1eb248e0658da1b1dcab8c519008955bb", size = 186790, upload-time = "2025-12-02T03:09:47.606Z" }, +] + +[[package]] +name = "xformers" +version = "0.0.33.post2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/69/403e963d35f1b0c52a1b3127e0bc4e94e7e50ecee8c6684a8abe40e6638e/xformers-0.0.33.post2.tar.gz", hash = "sha256:647ddf26578d2b8643230467ef1f0fbfef0bbe556a546bd27a70d4855d3433e1", size = 14783914, upload-time = "2025-12-04T18:52:42.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/c8/2957d8a8bf089a4e57f046867d4c9b31fc2e1d16013bc57cd7ae651a65b5/xformers-0.0.33.post2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9ea6032defa60395559b6a446c2ae945236707e98daabd88fea57cd08671c174", size = 122883631, upload-time = "2025-12-04T18:52:35.318Z" }, + { url = "https://files.pythonhosted.org/packages/b3/72/057e48a3c2187f74202b3cca97e9f8a844342122909c93314fd641daa5d0/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl", hash = "sha256:4a0a59a0c698a483f13ecad967dbbe71386827985e80cc373bec4cdf9aed59cd", size = 105088221, upload-time = "2025-12-04T18:52:39.699Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, + { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, + { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "xyzservices" +version = "2025.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/0f/022795fc1201e7c29e742a509913badb53ce0b38f64b6db859e2f6339da9/xyzservices-2025.11.0.tar.gz", hash = "sha256:2fc72b49502b25023fd71e8f532fb4beddbbf0aa124d90ea25dba44f545e17ce", size = 1135703, upload-time = "2025-11-22T11:31:51.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/5c/2c189d18d495dd0fa3f27ccc60762bbc787eed95b9b0147266e72bb76585/xyzservices-2025.11.0-py3-none-any.whl", hash = "sha256:de66a7599a8d6dad63980b77defd1d8f5a5a9cb5fc8774ea1c6e89ca7c2a3d2f", size = 93916, upload-time = "2025-11-22T11:31:50.525Z" }, +] + +[[package]] +name = "yacs" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/3e/4a45cb0738da6565f134c01d82ba291c746551b5bc82e781ec876eb20909/yacs-0.1.8.tar.gz", hash = "sha256:efc4c732942b3103bea904ee89af98bcd27d01f0ac12d8d4d369f1e7a2914384", size = 11100, upload-time = "2020-08-10T16:37:47.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/4f/fe9a4d472aa867878ce3bb7efb16654c5d63672b86dc0e6e953a67018433/yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32", size = 14747, upload-time = "2020-08-10T16:37:46.4Z" }, +] + +[[package]] +name = "yapf" +version = "0.40.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "platformdirs" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/14/c1f0ebd083fddd38a7c832d5ffde343150bd465689d12c549c303fbcd0f5/yapf-0.40.2.tar.gz", hash = "sha256:4dab8a5ed7134e26d57c1647c7483afb3f136878b579062b786c9ba16b94637b", size = 252068, upload-time = "2023-09-22T18:40:46.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/c9/d4b03b2490107f13ebd68fe9496d41ae41a7de6275ead56d0d4621b11ffd/yapf-0.40.2-py3-none-any.whl", hash = "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b", size = 254707, upload-time = "2023-09-22T18:40:43.297Z" }, +] + +[[package]] +name = "zict" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/ac/3c494dd7ec5122cff8252c1a209b282c0867af029f805ae9befd73ae37eb/zict-3.0.0.tar.gz", hash = "sha256:e321e263b6a97aafc0790c3cfb3c04656b7066e6738c37fffcca95d803c9fba5", size = 33238, upload-time = "2023-04-17T21:41:16.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/ab/11a76c1e2126084fde2639514f24e6111b789b0bfa4fc6264a8975c7e1f1/zict-3.0.0-py2.py3-none-any.whl", hash = "sha256:5796e36bd0e0cc8cf0fbc1ace6a68912611c1dbd74750a3f3026b9b9d6a327ae", size = 43332, upload-time = "2023-04-17T21:41:13.444Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]